Skip to content

Commit

Permalink
Registration flow with OAuth
Browse files Browse the repository at this point in the history
  • Loading branch information
Iulia Stana committed Mar 6, 2023
1 parent 89b1aa5 commit be8807e
Show file tree
Hide file tree
Showing 17 changed files with 404 additions and 352 deletions.
17 changes: 15 additions & 2 deletions app/src/main/kotlin/nl/eduid/MainComposeActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,33 @@ package nl.eduid
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.core.view.WindowCompat
import androidx.navigation.compose.rememberNavController
import dagger.hilt.android.AndroidEntryPoint
import nl.eduid.graphs.MainGraph
import nl.eduid.screens.homepage.HomePageViewModel
import nl.eduid.screens.splash.SplashScreen
import nl.eduid.ui.theme.EduidAppAndroidTheme

@AndroidEntryPoint
class MainComposeActivity : ComponentActivity() {
private val viewModel by viewModels<HomePageViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)

setContent {
EduidAppAndroidTheme {
MainGraph(navController = rememberNavController())
val knownState by viewModel.knownState.observeAsState(initial = null)
val navController = rememberNavController()
if (knownState == null) {
SplashScreen()
} else {
MainGraph(navController = navController, viewModel)
}
}
}
}
Expand Down
18 changes: 18 additions & 0 deletions app/src/main/kotlin/nl/eduid/di/assist/AuthenticationAssistant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,22 @@ class AuthenticationAssistant {
}
}

suspend fun refreshToken(authState: AuthState, service: AuthorizationService): TokenResponse =
suspendCoroutine { continuation ->
val refreshTokenRequest = authState.createTokenRefreshRequest()
service.performTokenRequest(refreshTokenRequest) { tokenResponse, ex ->
when {
ex != null -> {
Timber.e(ex, "Failed to refresh token")
continuation.resumeWith(Result.failure(ex))
}
tokenResponse != null -> {
continuation.resumeWith(Result.success(tokenResponse))
}
else -> {
continuation.resumeWith(Result.failure(RuntimeException("Could not complete token refresh")))
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
package nl.eduid
package nl.eduid.graphs

import androidx.compose.runtime.Composable
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import nl.eduid.screens.biometric.EnableBiometricScreen
import nl.eduid.screens.biometric.EnableBiometricViewModel
import nl.eduid.screens.enroll.EnrollScreen
import nl.eduid.screens.firsttimedialog.FirstTimeDialogScreen
import nl.eduid.screens.homepage.HomePageScreen
import nl.eduid.screens.homepage.HomePageViewModel
import nl.eduid.screens.login.LoginScreen
import nl.eduid.screens.login.LoginViewModel
import nl.eduid.screens.oauth.OAuthScreen
import nl.eduid.screens.oauth.OAuthViewModel
import nl.eduid.screens.personalinfo.PersonalInfoScreen
Expand All @@ -22,66 +20,52 @@ import nl.eduid.screens.pinsetup.RegistrationPinSetupViewModel
import nl.eduid.screens.requestiddetails.RequestIdDetailsScreen
import nl.eduid.screens.requestiddetails.RequestIdDetailsViewModel
import nl.eduid.screens.requestidlinksent.RequestIdLinkSentScreen
import nl.eduid.screens.requestidpin.RequestIdPinScreen
import nl.eduid.screens.requestidpin.RequestIdConfirmPhoneNumber
import nl.eduid.screens.requestidpin.RequestIdPinViewModel
import nl.eduid.screens.requestidrecovery.RequestIdRecoveryScreen
import nl.eduid.screens.requestidrecovery.RequestIdRecoveryViewModel
import nl.eduid.screens.requestidstart.RequestIdStartScreen
import nl.eduid.screens.scan.ScanScreen
import nl.eduid.screens.scan.StatelessScanViewModel
import nl.eduid.screens.splash.SplashScreen
import nl.eduid.screens.splash.SplashViewModel
import nl.eduid.screens.start.StartScreen

@Composable
fun MainGraph(navController: NavHostController) = NavHost(
navController = navController, route = Graph.MAIN, startDestination = Graph.SPLASH
fun MainGraph(navController: NavHostController, homePageViewModel: HomePageViewModel) = NavHost(
navController = navController, route = Graph.MAIN, startDestination = Graph.HOME_PAGE
) {
composable(Graph.SPLASH) {
val viewModel = hiltViewModel<SplashViewModel>(it)
SplashScreen(viewModel = viewModel, onFirstTimeUser = {
navController.navigate(Graph.ENROLL) {
popUpTo(Graph.SPLASH) {
inclusive = true
}
}
}) {
navController.navigate(Graph.HOME_PAGE) {
popUpTo(Graph.SPLASH) {
inclusive = true
}
}
composable(Graph.HOME_PAGE) {
HomePageScreen(viewModel = homePageViewModel,
onScanForAuthorization = { /*QR authorization for 3rd party*/ },
onActivityClicked = { },
onPersonalInfoClicked = { navController.navigate(Graph.PERSONAL_INFO) },
onSecurityClicked = {},
onEnrollWithQR = { navController.navigate(ExistingAccount.EnrollWithQR.route) }) {
navController.navigate(
Graph.REQUEST_EDU_ID_ACCOUNT
)
}
}
composable(Graph.ENROLL) {
EnrollScreen(onLogin = { navController.navigate(Graph.LOGIN) },
onScan = { navController.navigate(Graph.SCAN_REGISTRATION) },
onRequestEduId = { navController.navigate(Graph.REQUEST_EDU_ID_START) })
}
composable(Graph.LOGIN) {
val viewModel = hiltViewModel<LoginViewModel>(it)
LoginScreen(viewModel = viewModel,
onLoginDone = {},
goBack = { navController.popBackStack() })
}
composable(Graph.SCAN_REGISTRATION) {

composable(ExistingAccount.EnrollWithQR.route) {
val viewModel = hiltViewModel<StatelessScanViewModel>(it)
ScanScreen(viewModel = viewModel,
isRegistration = true,
goBack = { navController.popBackStack() },
goToRegistrationPinSetup = { challenge ->
navController.navigate(
RegistrationPinSetup.buildRouteWithEncodedChallenge(
encodedChallenge = viewModel.encodeChallenge(
"${ExistingAccount.RegistrationPinSetup.route}/${
viewModel.encodeChallenge(
challenge
)
)
}"
)
},
goToAuthentication = {})
}

composable(
route = RegistrationPinSetup.routeWithArgs, arguments = RegistrationPinSetup.arguments
route = ExistingAccount.RegistrationPinSetup.routeWithArgs,
arguments = ExistingAccount.RegistrationPinSetup.arguments
) { entry ->
val viewModel = hiltViewModel<RegistrationPinSetupViewModel>(entry)
RegistrationPinSetupScreen(viewModel = viewModel,
Expand All @@ -92,37 +76,48 @@ fun MainGraph(navController: NavHostController) = NavHost(
encodedChallenge = viewModel.encodeChallenge(challenge), pin = pin
)
) {
popUpTo(Graph.ENROLL) {
inclusive = true
}
popUpTo(navController.graph.findStartDestination().id)
}
}) {
navController.navigate(Graph.OAUTH_MOBILE) {
popUpTo(Graph.ENROLL) {
inclusive = true
},
onRegistrationDone = {
navController.navigate(OAuth.routeWithPhone) {
popUpTo(navController.graph.findStartDestination().id)
}
}
}
})
}
composable(
route = WithChallenge.EnableBiometric.routeWithArgs, arguments = WithChallenge.arguments
) { entry ->
val viewModel = hiltViewModel<EnableBiometricViewModel>(entry)
EnableBiometricScreen(viewModel = viewModel) {
navController.navigate(Graph.OAUTH_MOBILE) {
popUpTo(Graph.ENROLL) {
inclusive = true
}
val isEnroll = entry.arguments?.getBoolean(WithChallenge.isEnrolmentArg, true) ?: true
val authRoute = if (isEnroll) OAuth.routeWithPhone else OAuth.routeWithoutPhone
EnableBiometricScreen(viewModel = viewModel, goToOauth = {
val currentRouteId = navController.currentDestination?.id ?: 0
navController.navigate(authRoute) {
popUpTo(currentRouteId) { inclusive = true }
}
}
}) { navController.popBackStack() }
}
composable(Graph.OAUTH_MOBILE) { entry ->
composable(route = OAuth.routeWithArgs, arguments = OAuth.arguments) { entry ->
val viewModel = hiltViewModel<OAuthViewModel>(entry)
OAuthScreen(viewModel = viewModel) {
navController.navigate(Graph.HOME_PAGE)
val isEnroll = entry.arguments?.getBoolean(OAuth.withPhoneConfirmArg, true) ?: true
OAuthScreen(viewModel = viewModel, continueWith = {
val currentRouteId = navController.currentDestination?.id ?: 0
if (isEnroll) {
navController.navigate(PhoneNumberRecovery.RequestCode.route) {
popUpTo(currentRouteId) { inclusive = true }
}
} else {
navController.navigate(Graph.MAIN) {
popUpTo(currentRouteId) { inclusive = true }
}
}
}) {
navController.popBackStack()
}
}
composable(Graph.REQUEST_EDU_ID_START) {

composable(Graph.REQUEST_EDU_ID_ACCOUNT) {
RequestIdStartScreen(requestId = { navController.navigate(Graph.REQUEST_EDU_ID_DETAILS) },
onBackClicked = { navController.popBackStack() })
}
Expand All @@ -136,23 +131,25 @@ fun MainGraph(navController: NavHostController) = NavHost(
composable(Graph.REQUEST_EDU_ID_LINK_SENT + "/{userId}") { backStackEntry ->
RequestIdLinkSentScreen(userEmail = backStackEntry.arguments?.getString("userId")
?: "your email address",
requestId = { navController.navigate(Graph.REQUEST_EDU_ID_RECOVERY) },
requestId = { navController.navigate(PhoneNumberRecovery.RequestCode.route) },
onBackClicked = { navController.popBackStack() })
}

composable(Graph.REQUEST_EDU_ID_RECOVERY) {
composable(
PhoneNumberRecovery.RequestCode.route,
) {
val viewModel = hiltViewModel<RequestIdRecoveryViewModel>(it)
RequestIdRecoveryScreen(
onVerifyPhoneNumberClicked = { navController.navigate(Graph.REQUEST_EDU_ID_PIN) },
onVerifyPhoneNumberClicked = { navController.navigate(PhoneNumberRecovery.ConfirmCode.route) },
onBackClicked = { navController.popBackStack() },
viewModel = viewModel,
)
}

composable(Graph.REQUEST_EDU_ID_PIN) {
composable(PhoneNumberRecovery.ConfirmCode.route) {
val viewModel = hiltViewModel<RequestIdPinViewModel>(it)
RequestIdPinScreen(viewModel = viewModel,
onPinVerified = { navController.navigate(Graph.START) },
RequestIdConfirmPhoneNumber(viewModel = viewModel,
onCodeVerified = { navController.navigate(Graph.START) },
goBack = { navController.popBackStack() })
}

Expand All @@ -166,16 +163,6 @@ fun MainGraph(navController: NavHostController) = NavHost(
FirstTimeDialogScreen()
}

composable(Graph.HOME_PAGE) {
val viewModel = hiltViewModel<HomePageViewModel>(it)
HomePageScreen(
viewModel = viewModel,
onActivityClicked = { },
onPersonalInfoClicked = { navController.navigate(Graph.PERSONAL_INFO) },
onSecurityClicked = {},
)
}

composable(Graph.PERSONAL_INFO) {
val viewModel = hiltViewModel<PersonalInfoViewModel>(it)
PersonalInfoScreen(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,49 @@
package nl.eduid
package nl.eduid.graphs

import androidx.navigation.NavType
import androidx.navigation.navArgument

object Graph {
const val MAIN = "main_graph"
const val SPLASH = "splash"
const val ENROLL = "enroll"
const val LOGIN = "login"
const val SCAN_REGISTRATION = "scan_registration"
const val OAUTH_MOBILE = "oauth_mobile_eduid"

const val REQUEST_EDU_ID_START = "request_edu_id_start"
const val HOME_PAGE = "home_page"
const val REQUEST_EDU_ID_ACCOUNT = "request_edu_id_account"
const val REQUEST_EDU_ID_DETAILS = "request_edu_id_details"
const val REQUEST_EDU_ID_LINK_SENT = "request_edu_id_link_sent"
const val REQUEST_EDU_ID_RECOVERY = "request_edu_id_recovery"
const val REQUEST_EDU_ID_PIN = "request_edu_id_pin"

const val START = "start"
const val FIRST_TIME_DIALOG = "first_time_dialog"
const val HOME_PAGE = "home_page"
const val PERSONAL_INFO = "personal_info"
}

object RegistrationPinSetup {
private const val route: String = "registration_pin_setup"
const val registrationChallengeArg = "registration_challenge_arg"

const val routeWithArgs = "${route}/{${registrationChallengeArg}}"
val arguments = listOf(navArgument(registrationChallengeArg) {
type = NavType.StringType
object OAuth {
private const val route = "oauth_mobile_eduid"
const val withPhoneConfirmArg = "confirm_phone_arg"
val routeWithPhone = "$route/true"
val routeWithoutPhone = "$route/false"
val routeWithArgs = "$route/{$withPhoneConfirmArg}"
val arguments = listOf(navArgument(withPhoneConfirmArg) {
type = NavType.BoolType
nullable = false
defaultValue = ""
defaultValue = true
})
}

fun buildRouteWithEncodedChallenge(encodedChallenge: String?): String {
return "$route/$encodedChallenge"
sealed class PhoneNumberRecovery(val route: String) {
object RequestCode : PhoneNumberRecovery("phone_number_recover")
object ConfirmCode : PhoneNumberRecovery("phone_number_confirm_code")
}

sealed class ExistingAccount(val route: String) {
object EnrollWithQR : ExistingAccount("scan_registration")
object RegistrationPinSetup : ExistingAccount("registration_pin_setup") {
const val registrationChallengeArg = "registration_challenge_arg"

val routeWithArgs = "$route/{$registrationChallengeArg}"
val arguments = listOf(navArgument(registrationChallengeArg) {
type = NavType.StringType
nullable = false
defaultValue = ""
})
}
}

Expand All @@ -44,7 +53,7 @@ sealed class WithChallenge(val route: String) {
const val pinArg = "pin_arg"
const val isEnrolmentArg = "is_enrolment_arg"

const val args = "{$challengeArg}/{${pinArg}}/{${isEnrolmentArg}}"
const val args = "{$challengeArg}/{$pinArg}/{$isEnrolmentArg}"
val arguments = listOf(navArgument(challengeArg) {
type = NavType.StringType
nullable = false
Expand Down
Loading

0 comments on commit be8807e

Please sign in to comment.