fix(Android): add alert dialog for errors in auth flow (#4835)

fixes #4723
This commit is contained in:
Jason Elie Bou Kheir
2024-05-05 07:01:21 -07:00
committed by GitHub
parent 61a2d3b78a
commit 8f977593a4
3 changed files with 119 additions and 22 deletions

View File

@@ -0,0 +1,71 @@
/* Licensed under Apache 2.0 (C) 2024 Firezone, Inc. */
package dev.firezone.android.features.customuri.notifications
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Context.NOTIFICATION_SERVICE
import android.content.Intent
import android.util.Log
import androidx.core.app.NotificationCompat
import dev.firezone.android.R
import dev.firezone.android.core.presentation.MainActivity
object CustomUriNotification {
private const val CHANNEL_ID = "firezone-authentication-status"
private const val CHANNEL_NAME = "firezone-authentication-status"
private const val CHANNEL_DESCRIPTION = "Firezone authentication status"
const val ID = 1338
private const val TAG: String = "CustomUriNotification"
fun update(
context: Context,
status: StatusType,
): NotificationCompat.Builder {
Log.d(TAG, "update")
val manager = context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val chan =
NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT,
)
chan.description = CHANNEL_DESCRIPTION
manager.createNotificationChannel(chan)
val notificationBuilder =
NotificationCompat.Builder(context, CHANNEL_ID)
.setContentIntent(configIntent(context))
return status.applySettings(notificationBuilder)
}
private fun configIntent(context: Context): PendingIntent {
Log.d(TAG, "configIntent")
val intent = Intent(context, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
return PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
}
data class Error(val message: String) : StatusType() {
override fun applySettings(builder: NotificationCompat.Builder) =
builder.apply {
setSmallIcon(R.drawable.ic_firezone_logo)
setContentTitle("Authentication Error")
setContentText(message)
setStyle(NotificationCompat.BigTextStyle().bigText(message))
setPriority(NotificationCompat.PRIORITY_HIGH)
setAutoCancel(true)
}
}
sealed class StatusType {
abstract fun applySettings(builder: NotificationCompat.Builder): NotificationCompat.Builder
}
}

View File

@@ -1,6 +1,8 @@
/* Licensed under Apache 2.0 (C) 2024 Firezone, Inc. */
package dev.firezone.android.features.customuri.ui
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
@@ -9,6 +11,7 @@ import dagger.hilt.android.AndroidEntryPoint
import dev.firezone.android.R
import dev.firezone.android.core.presentation.MainActivity
import dev.firezone.android.databinding.ActivityCustomUriHandlerBinding
import dev.firezone.android.features.customuri.notifications.CustomUriNotification
import dev.firezone.android.tunnel.TunnelService
@AndroidEntryPoint
@@ -33,6 +36,10 @@ class CustomUriHandlerActivity : AppCompatActivity(R.layout.activity_custom_uri_
Intent(this, MainActivity::class.java),
)
}
is CustomUriViewModel.ViewAction.AuthFlowError -> {
notifyError("Errors occurred during authentication:\n${action.errors.joinToString(separator = "\n")}")
startActivity(Intent(this, MainActivity::class.java))
}
else -> {
throw IllegalStateException("Unknown action: $action")
}
@@ -41,4 +48,10 @@ class CustomUriHandlerActivity : AppCompatActivity(R.layout.activity_custom_uri_
finish()
}
}
private fun notifyError(message: String) {
val notification = CustomUriNotification.update(this, CustomUriNotification.Error(message)).build()
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.notify(CustomUriNotification.ID, notification)
}
}

View File

@@ -14,7 +14,6 @@ import dev.firezone.android.core.data.Repository
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import java.lang.IllegalStateException
import javax.inject.Inject
@HiltViewModel
@@ -28,39 +27,47 @@ internal class CustomUriViewModel
fun parseCustomUri(intent: Intent) {
viewModelScope.launch {
val accumulatedErrors = mutableListOf<String>()
val error = { msg: String ->
accumulatedErrors += msg
Firebase.crashlytics.log(msg)
Log.e(TAG, msg)
}
when (intent.data?.host) {
PATH_CALLBACK -> {
intent.data?.getQueryParameter(QUERY_ACTOR_NAME)?.let { actorName ->
Log.d("CustomUriViewModel", "Found actor name: $actorName")
Log.d(TAG, "Found actor name: $actorName")
repo.saveActorName(actorName).collect()
}
intent.data?.getQueryParameter(QUERY_CLIENT_STATE)?.let { state ->
if (repo.validateState(state).firstOrNull() == true) {
Log.d("CustomUriViewModel", "Valid state parameter. Continuing to save state...")
Log.d(TAG, "Valid state parameter. Continuing to save state...")
} else {
throw IllegalStateException("Invalid state parameter $state! Authentication will not succeed...")
error("Invalid state parameter $state")
}
intent.data?.getQueryParameter(QUERY_CLIENT_AUTH_FRAGMENT)?.let { fragment ->
if (fragment.isNotBlank()) {
Log.d("CustomUriViewModel", "Found valid auth fragment in response")
}
intent.data?.getQueryParameter(QUERY_CLIENT_AUTH_FRAGMENT)?.let { fragment ->
if (fragment.isNotBlank()) {
Log.d(TAG, "Found valid auth fragment in response")
// Save token, then clear nonce and state since we don't
// need to keep them around anymore
repo.saveToken(fragment).collect()
repo.clearNonce()
repo.clearState()
actionMutableLiveData.postValue(ViewAction.AuthFlowComplete)
} else {
throw IllegalStateException("Invalid auth fragment $fragment! Authentication will not succeed...")
}
// Save token, then clear nonce and state since we don't
// need to keep them around anymore
repo.saveToken(fragment).collect()
repo.clearNonce()
repo.clearState()
} else {
error("Auth fragment was empty")
}
}
}
else -> {
Firebase.crashlytics.log("Unknown path segment: ${intent.data?.lastPathSegment}")
Log.e("CustomUriViewModel", "Unknown path segment: ${intent.data?.lastPathSegment}")
}
else -> error("Unknown path segment: ${intent.data?.lastPathSegment}")
}
if (accumulatedErrors.isNotEmpty()) {
actionMutableLiveData.postValue(ViewAction.AuthFlowError(accumulatedErrors))
} else {
Log.d(TAG, "Auth flow complete")
actionMutableLiveData.postValue(ViewAction.AuthFlowComplete)
}
}
}
@@ -70,9 +77,15 @@ internal class CustomUriViewModel
private const val QUERY_CLIENT_STATE = "state"
private const val QUERY_CLIENT_AUTH_FRAGMENT = "fragment"
private const val QUERY_ACTOR_NAME = "actor_name"
private const val TAG = "CustomUriViewModel"
}
internal sealed class ViewAction {
object AuthFlowComplete : ViewAction()
data object AuthFlowComplete : ViewAction()
data class AuthFlowError(val errors: Iterable<String>) : ViewAction() {
constructor(vararg errors: String) : this(errors.toList())
}
}
}