diff --git a/CredentialManager/app/build.gradle b/CredentialManager/app/build.gradle index ff3fde0b..3096c095 100644 --- a/CredentialManager/app/build.gradle +++ b/CredentialManager/app/build.gradle @@ -35,6 +35,7 @@ android { defaultConfig { applicationId "com.google.credentialmanager.sample" minSdk 21 + targetSdk 35 versionCode 1 versionName "1.0" @@ -76,7 +77,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = "1.4.2" + kotlinCompilerExtensionVersion = "1.5.15" } packagingOptions { resources { @@ -86,6 +87,7 @@ android { buildFeatures { viewBinding true + compose true } } @@ -107,7 +109,15 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" - implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.2" - + // Integration with activities + implementation 'androidx.activity:activity-compose:1.9.0' + // Compose Bill of Materials + implementation platform('androidx.compose:compose-bom:2024.05.00') + implementation 'androidx.compose.ui:ui' + implementation 'androidx.compose.ui:ui-graphics' + implementation 'androidx.compose.ui:ui-tooling-preview' + implementation 'androidx.compose.material3:material3' + implementation "androidx.navigation:navigation-compose:2.7.7" + // Android Studio Preview support + debugImplementation 'androidx.compose.ui:ui-tooling' } diff --git a/CredentialManager/app/src/main/assets/AuthFromServer b/CredentialManager/app/src/main/assets/AuthFromServer index 2c7e860c..fcf86d40 100644 --- a/CredentialManager/app/src/main/assets/AuthFromServer +++ b/CredentialManager/app/src/main/assets/AuthFromServer @@ -2,5 +2,5 @@ "challenge": "HjBbH__fbLuzy95AGR31yEARA0EMtKlY0NrV5oy3NQw", "timeout": 1800000, "userVerification": "required", - "rpId": "passkeys-codelab.glitch.me" + "rpId": "project-sesame-426206.appspot.com" } diff --git a/CredentialManager/app/src/main/assets/RegFromServer b/CredentialManager/app/src/main/assets/RegFromServer index 469c45ae..abefdb6a 100644 --- a/CredentialManager/app/src/main/assets/RegFromServer +++ b/CredentialManager/app/src/main/assets/RegFromServer @@ -1,8 +1,8 @@ { "challenge": "", "rp": { - "id": "passkeys-codelab.glitch.me", - "name": "CredMan App Test" + "id": "project-sesame-426206.appspot.com", + "name": "project-sesame-426206" }, "pubKeyCredParams": [ { diff --git a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/AppNavigation.kt b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/AppNavigation.kt new file mode 100644 index 00000000..734f27ae --- /dev/null +++ b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/AppNavigation.kt @@ -0,0 +1,40 @@ +package com.google.credentialmanager.sample + +import androidx.compose.runtime.Composable +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController + +// 1. Define navigation routes +sealed class Screen(val route: String) { + object Main : Screen("main") + object SignUp : Screen("signup") + object SignIn : Screen("signin") + object Home : Screen("home") +} + +// 2. Create a Composable function for the navigation host +@Composable +fun AppNavHost( + startDestination: String = Screen.Main.route +) { + val navController = rememberNavController() + + NavHost( + navController = navController, + startDestination = startDestination + ) { + composable(Screen.Main.route) { + MainScreen(navController = navController) + } + composable(Screen.SignUp.route) { + SignUpScreen(navController = navController) + } + composable(Screen.SignIn.route) { + SignInScreen(navController = navController) + } + composable(Screen.Home.route) { + HomeScreen(navController = navController) + } + } +} diff --git a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/HomeFragment.kt b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/HomeFragment.kt deleted file mode 100644 index 6843c613..00000000 --- a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/HomeFragment.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.credentialmanager.sample - -import android.content.Context -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import com.google.credentialmanager.sample.databinding.FragmentHomeBinding - -class HomeFragment : Fragment() { - - private lateinit var binding: FragmentHomeBinding - private lateinit var listener: HomeFragmentCallback - - companion object { - private const val LOGGED_IN_THROUGH_PASSWORD = "Logged in successfully through password" - private const val LOGGED_IN_THROUGH_PASSKEYS = "Logged in successfully through passkeys" - } - - override fun onAttach(context: Context) { - super.onAttach(context) - try { - listener = context as HomeFragmentCallback - } catch (castException: ClassCastException) { - /** The activity does not implement the listener. */ - } - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View { - binding = FragmentHomeBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - configureSignedInText() - - binding.logout.setOnClickListener { - listener.logout() - } - } - - private fun configureSignedInText() { - if (DataProvider.isSignedInThroughPasskeys()) { - binding.signedInText.text = LOGGED_IN_THROUGH_PASSKEYS - } else { - binding.signedInText.text = LOGGED_IN_THROUGH_PASSWORD - } - } - - interface HomeFragmentCallback { - fun logout() - } -} diff --git a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/HomeScreen.kt b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/HomeScreen.kt new file mode 100644 index 00000000..0e46d228 --- /dev/null +++ b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/HomeScreen.kt @@ -0,0 +1,73 @@ +package com.google.credentialmanager.sample + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.google.credentialmanager.sample.ui.theme.CredentialManagerSampleTheme + +@Composable +fun HomeScreen(navController: NavController) { + CredentialManagerSampleTheme { + val isSignedInThroughPasskeys = DataProvider.isSignedInThroughPasskeys() + val message = if (isSignedInThroughPasskeys) { + "Logged in successfully through passkeys" + } else { + "Logged in successfully through password" + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(20.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = message, + modifier = Modifier.padding(16.dp), + textAlign = TextAlign.Center, + fontSize = 24.sp, + fontWeight = FontWeight.Bold + ) + Button( + onClick = { + DataProvider.configureSignedInPref(false) + navController.navigate(Screen.Main.route) { + popUpTo(Screen.Home.route) { inclusive = true } + launchSingleTop = true + } + }, + shape = RoundedCornerShape(4.dp), + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp) + ) { + Text("Sign out and try again") + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun HomeScreenPreview() { + CredentialManagerSampleTheme { + HomeScreen(navController = rememberNavController()) + } +} diff --git a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/MainActivity.kt b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/MainActivity.kt index 8c5c51ff..498c2895 100644 --- a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/MainActivity.kt +++ b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/MainActivity.kt @@ -17,70 +17,23 @@ package com.google.credentialmanager.sample import android.os.Bundle +import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import com.google.credentialmanager.sample.HomeFragment.HomeFragmentCallback -import com.google.credentialmanager.sample.MainFragment.MainFragmentCallback -import com.google.credentialmanager.sample.R.id -import com.google.credentialmanager.sample.SignInFragment.SignInFragmentCallback -import com.google.credentialmanager.sample.SignUpFragment.SignUpFragmentCallback -import com.google.credentialmanager.sample.databinding.ActivityMainBinding -class MainActivity : AppCompatActivity(), MainFragmentCallback, HomeFragmentCallback, - SignInFragmentCallback, SignUpFragmentCallback { - - private lateinit var binding: ActivityMainBinding +class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) - DataProvider.initSharedPref(applicationContext) - if (DataProvider.isSignedIn()) { - showHome() - } else { - loadMainFragment() - } - } - - override fun signup() { - loadFragment(SignUpFragment(), false) - } - - override fun signIn() { - loadFragment(SignInFragment(), false) - } - - override fun logout() { - supportFragmentManager.popBackStack("home", FragmentManager.POP_BACK_STACK_INCLUSIVE) - loadMainFragment() - } - - private fun loadMainFragment() { - supportFragmentManager.popBackStack() - loadFragment(MainFragment(), false) - } - - override fun showHome() { - supportFragmentManager.popBackStack() - loadFragment(HomeFragment(), true, "home") - } - - private fun loadFragment(fragment: Fragment, flag: Boolean, backstackString: String? = null) { - DataProvider.configureSignedInPref(flag) - supportFragmentManager.beginTransaction().replace(id.fragment_container, fragment) - .addToBackStack(backstackString).commit() - } - - override fun onBackPressed() { - if (DataProvider.isSignedIn() || supportFragmentManager.backStackEntryCount == 1) { - finish() - } else { - super.onBackPressed() + setContent { + val startDestination = if (DataProvider.isSignedIn()) { + Screen.Home.route + } else { + Screen.Main.route + } + AppNavHost(startDestination = startDestination) } } } diff --git a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/MainFragment.kt b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/MainFragment.kt deleted file mode 100644 index 5a366635..00000000 --- a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/MainFragment.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.credentialmanager.sample - -import android.content.Context -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import com.google.credentialmanager.sample.databinding.FragmentMainBinding - -class MainFragment : Fragment() { - - private lateinit var listener: MainFragmentCallback - private var _binding: FragmentMainBinding? = null - private val binding get() = _binding!! - - override fun onAttach(context: Context) { - super.onAttach(context) - try { - listener = context as MainFragmentCallback - } catch (castException: ClassCastException) { - /** The activity does not implement the listener. */ - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentMainBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.signUp.setOnClickListener { - listener.signup() - } - - binding.signIn.setOnClickListener { - listener.signIn() - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - interface MainFragmentCallback { - fun signup() - fun signIn() - } -} diff --git a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/MainScreen.kt b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/MainScreen.kt new file mode 100644 index 00000000..81d3b073 --- /dev/null +++ b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/MainScreen.kt @@ -0,0 +1,71 @@ +package com.google.credentialmanager.sample + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.google.credentialmanager.sample.ui.theme.CredentialManagerSampleTheme + +@Composable +fun MainScreen(navController: NavController) { + CredentialManagerSampleTheme { + Column( + modifier = Modifier + .fillMaxSize() + .padding(20.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Try passkeys demo", + modifier = Modifier + .fillMaxWidth() + .padding(top = 128.dp) + .padding(16.dp), + textAlign = TextAlign.Center, + fontSize = 24.sp, + fontWeight = FontWeight.Bold + ) + Button( + onClick = { navController.navigate(Screen.SignUp.route) }, + shape = RoundedCornerShape(4.dp), + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp) + ) { + Text("Sign up") + } + Button( + onClick = { navController.navigate(Screen.SignIn.route) }, + shape = RoundedCornerShape(4.dp), + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp) + ) { + Text("Sign in") + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun MainScreenPreview() { + CredentialManagerSampleTheme { + MainScreen(navController = rememberNavController()) + } +} diff --git a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/NavigationEvents.kt b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/NavigationEvents.kt new file mode 100644 index 00000000..9f1dea92 --- /dev/null +++ b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/NavigationEvents.kt @@ -0,0 +1,5 @@ +package com.google.credentialmanager.sample + +sealed class NavigationEvent { + data class NavigateToHome(val signedInWithPasskeys: Boolean) : NavigationEvent() +} diff --git a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignInFragment.kt b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignInFragment.kt deleted file mode 100644 index 09277b0b..00000000 --- a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignInFragment.kt +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.credentialmanager.sample - -import android.content.Context -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.credentials.CredentialManager -import androidx.credentials.CustomCredential -import androidx.credentials.GetCredentialRequest -import androidx.credentials.GetPasswordOption -import androidx.credentials.GetPublicKeyCredentialOption -import androidx.credentials.PasswordCredential -import androidx.credentials.PendingGetCredentialRequest -import androidx.credentials.PublicKeyCredential -import androidx.credentials.pendingGetCredentialRequest -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import com.google.credentialmanager.sample.databinding.FragmentSignInBinding -import kotlinx.coroutines.launch - -class SignInFragment : Fragment() { - - private lateinit var credentialManager: CredentialManager - private var _binding: FragmentSignInBinding? = null - private val binding get() = _binding!! - private lateinit var listener: SignInFragmentCallback - - override fun onAttach(context: Context) { - super.onAttach(context) - try { - listener = context as SignInFragmentCallback - } catch (castException: ClassCastException) { - /** The activity does not implement the listener. */ - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentSignInBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - credentialManager = CredentialManager.create(requireActivity()) - - val getCredentialRequest = configureGetCredentialRequest() - - configureAutofill(getCredentialRequest) - - binding.signInWithSavedCredentials.setOnClickListener( - signInWithSavedCredentials( - getCredentialRequest - ) - ) - } - - private fun configureAutofill(getCredentialRequest: GetCredentialRequest) { - binding.textUsername - .pendingGetCredentialRequest = PendingGetCredentialRequest( - getCredentialRequest - ) { response -> - if (response.credential is PublicKeyCredential) { - DataProvider.setSignedInThroughPasskeys(true) - } - if (response.credential is PasswordCredential) { - DataProvider.setSignedInThroughPasskeys(false) - } - showHome() - } - } - - private fun configureGetCredentialRequest(): GetCredentialRequest { - val getPublicKeyCredentialOption = - GetPublicKeyCredentialOption(fetchAuthJsonFromServer(), null) - val getPasswordOption = GetPasswordOption() - val getCredentialRequest = GetCredentialRequest( - listOf( - getPublicKeyCredentialOption, - getPasswordOption - ) - ) - return getCredentialRequest - } - - private fun signInWithSavedCredentials(getCredentialRequest: GetCredentialRequest): View.OnClickListener { - return View.OnClickListener { - - lifecycleScope.launch { - configureViews(View.VISIBLE, false) - - val data = getSavedCredentials(getCredentialRequest) - - configureViews(View.INVISIBLE, true) - - data?.let { - showHome() - } - } - } - } - - private fun showHome() { - sendSignInResponseToServer() - listener.showHome() - } - - private fun configureViews(visibility: Int, flag: Boolean) { - configureProgress(visibility) - binding.signInWithSavedCredentials.isEnabled = flag - } - - private fun configureProgress(visibility: Int) { - binding.textProgress.visibility = visibility - binding.circularProgressIndicator.visibility = visibility - } - - private fun fetchAuthJsonFromServer(): String { - return requireContext().readFromAsset("AuthFromServer") - } - - private fun sendSignInResponseToServer(): Boolean { - return true - } - - private suspend fun getSavedCredentials(getCredentialRequest: GetCredentialRequest): String? { - - val result = try { - credentialManager.getCredential( - requireActivity(), - getCredentialRequest, - ) - } catch (e: Exception) { - configureViews(View.INVISIBLE, true) - Log.e("Auth", "getCredential failed with exception: " + e.message.toString()) - activity?.showErrorAlert( - "An error occurred while authenticating through saved credentials. Check logs for additional details" - ) - return null - } - - if (result.credential is PublicKeyCredential) { - val cred = result.credential as PublicKeyCredential - DataProvider.setSignedInThroughPasskeys(true) - return "Passkey: ${cred.authenticationResponseJson}" - } - if (result.credential is PasswordCredential) { - val cred = result.credential as PasswordCredential - DataProvider.setSignedInThroughPasskeys(false) - return "Got Password - User:${cred.id} Password: ${cred.password}" - } - if (result.credential is CustomCredential) { - //If you are also using any external sign-in libraries, parse them here with the - // utility functions provided. - } - return null - } - - override fun onDestroyView() { - super.onDestroyView() - configureProgress(View.INVISIBLE) - _binding = null - } - - interface SignInFragmentCallback { - fun showHome() - } -} diff --git a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignInScreen.kt b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignInScreen.kt new file mode 100644 index 00000000..d3431fdc --- /dev/null +++ b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignInScreen.kt @@ -0,0 +1,100 @@ +package com.google.credentialmanager.sample + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.google.credentialmanager.sample.ui.theme.CredentialManagerSampleTheme +import kotlinx.coroutines.flow.collectLatest + +@Composable +fun SignInScreen(navController: NavController) { + CredentialManagerSampleTheme { + val viewModel: SignInViewModel = viewModel() + + val isLoading by viewModel.isLoading.collectAsState() + val signInError by viewModel.signInError.collectAsState() + + val context = LocalContext.current + val activity = context.findActivity() + + LaunchedEffect(Unit) { + viewModel.navigationEvent.collectLatest { + event -> + when (event) { + is NavigationEvent.NavigateToHome -> { + DataProvider.setSignedInThroughPasskeys(event.signedInWithPasskeys) + navController.navigate(Screen.Home.route) { + popUpTo(Screen.SignIn.route) { inclusive = true } + } + } + } + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "Sign in", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(top = 20.dp, bottom = 16.dp) + ) + + if (isLoading) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 8.dp)) { + CircularProgressIndicator(modifier = Modifier.size(24.dp)) // Consider using theme color: color = MaterialTheme.colorScheme.primary + Spacer(modifier = Modifier.width(8.dp)) + Text("operation in progress...") + } + } + + signInError?.let { + Text( + it, + color = MaterialTheme.colorScheme.error, + fontSize = 12.sp, + textAlign = TextAlign.Center, + modifier = Modifier.padding(vertical = 4.dp) + ) + } + + Button( + onClick = { + if (activity != null) { + viewModel.signIn(activity, context) + } + }, + shape = RoundedCornerShape(4.dp), + enabled = !isLoading && activity != null, + modifier = Modifier.fillMaxWidth() + ) { + Text("Sign in with passkey/saved password") + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun SignInScreenPreview() { + CredentialManagerSampleTheme { + SignInScreen(navController = rememberNavController()) + } +} diff --git a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignInViewModel.kt b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignInViewModel.kt new file mode 100644 index 00000000..cced7ec4 --- /dev/null +++ b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignInViewModel.kt @@ -0,0 +1,89 @@ +package com.google.credentialmanager.sample + +import android.app.Activity +import android.content.Context +import android.util.Log +import androidx.credentials.* +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class SignInViewModel : ViewModel() { + + private val _isLoading = MutableStateFlow(false) + val isLoading = _isLoading.asStateFlow() + + private val _signInError = MutableStateFlow(null) + val signInError = _signInError.asStateFlow() + + private val _navigationEvent = MutableSharedFlow() + val navigationEvent = _navigationEvent.asSharedFlow() + + fun signIn(activity: Activity, context: Context) { + _isLoading.value = true + _signInError.value = null + + viewModelScope.launch { + val data = getSavedCredentials(activity, context) + + if(data != null) { + sendSignInResponseToServer() + _navigationEvent.emit(NavigationEvent.NavigateToHome(signedInWithPasskeys = DataProvider.isSignedInThroughPasskeys())) + } + } + } + + private suspend fun getSavedCredentials(activity: Activity, context: Context): String? { + val getPublicKeyCredentialOption = + GetPublicKeyCredentialOption(fetchAuthJsonFromServer(context), null) + + val getPasswordOption = GetPasswordOption() + + val credentialManager = CredentialManager.create(activity) + val result = try { + credentialManager.getCredential( + activity, + GetCredentialRequest( + listOf( + getPublicKeyCredentialOption, + getPasswordOption + ) + ) + ) + } catch (e: Exception) { + _isLoading.value = false + Log.e("Auth", "getCredential failed with exception: " + e.message.toString()) + _signInError.value = + "An error occurred while authenticating: " + e.message.toString() + return null + } + + if (result.credential is PublicKeyCredential) { + val cred = result.credential as PublicKeyCredential + DataProvider.setSignedInThroughPasskeys(true) + return "Passkey: ${cred.authenticationResponseJson}" + } + if (result.credential is PasswordCredential) { + val cred = result.credential as PasswordCredential + DataProvider.setSignedInThroughPasskeys(false) + return "Got Password - User:${cred.id} Password: ${cred.password}" + } + if (result.credential is CustomCredential) { + //If you are also using any external sign-in libraries, parse them here with the utility functions provided. + } + + return null + } + + private fun fetchAuthJsonFromServer(context: Context): String { + return context.assets.open("AuthFromServer").bufferedReader().use { it.readText() } + } + + private fun sendSignInResponseToServer(): Boolean { + return true + } +} diff --git a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignUpFragment.kt b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignUpFragment.kt deleted file mode 100644 index 93974c15..00000000 --- a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignUpFragment.kt +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.credentialmanager.sample - -import android.content.Context -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.util.Base64 -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.credentials.CreatePasswordRequest -import androidx.credentials.CreatePasswordResponse -import androidx.credentials.CreatePublicKeyCredentialRequest -import androidx.credentials.CreatePublicKeyCredentialResponse -import androidx.credentials.CredentialManager -import androidx.credentials.exceptions.CreateCredentialCancellationException -import androidx.credentials.exceptions.CreateCredentialCustomException -import androidx.credentials.exceptions.CreateCredentialException -import androidx.credentials.exceptions.CreateCredentialInterruptedException -import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException -import androidx.credentials.exceptions.CreateCredentialUnknownException -import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import com.google.credentialmanager.sample.databinding.FragmentSignUpBinding -import kotlinx.coroutines.launch -import java.security.SecureRandom - -class SignUpFragment : Fragment() { - - private lateinit var credentialManager: CredentialManager - private var _binding: FragmentSignUpBinding? = null - private val binding get() = _binding!! - private lateinit var listener: SignUpFragmentCallback - - override fun onAttach(context: Context) { - super.onAttach(context) - try { - listener = context as SignUpFragmentCallback - } catch (castException: ClassCastException) { - /** The activity does not implement the listener. */ - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentSignUpBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - credentialManager = CredentialManager.create(requireActivity()) - - binding.signUp.setOnClickListener(signUpWithPasskeys()) - binding.signUpWithPassword.setOnClickListener(signUpWithPassword()) - } - - private fun signUpWithPassword(): View.OnClickListener { - return View.OnClickListener { - binding.password.visibility = View.VISIBLE - - if (binding.username.text.isNullOrEmpty()) { - binding.username.error = "User name required" - binding.username.requestFocus() - } else if (binding.password.text.isNullOrEmpty()) { - binding.password.error = "Password required" - binding.password.requestFocus() - } else { - lifecycleScope.launch { - - configureViews(View.VISIBLE, false) - - createPassword() - - simulateServerDelayAndLogIn() - - } - } - } - } - - private fun simulateServerDelayAndLogIn() { - Handler(Looper.getMainLooper()).postDelayed({ - - DataProvider.setSignedInThroughPasskeys(false) - - configureViews(View.INVISIBLE, true) - - listener.showHome() - }, 2000) - } - - private fun signUpWithPasskeys(): View.OnClickListener { - return View.OnClickListener { - - binding.password.visibility = View.GONE - - if (binding.username.text.isNullOrEmpty()) { - binding.username.error = "User name required" - binding.username.requestFocus() - } else { - lifecycleScope.launch { - configureViews(View.VISIBLE, false) - - val data = createPasskey() - - configureViews(View.INVISIBLE, true) - - data?.let { - registerResponse() - DataProvider.setSignedInThroughPasskeys(true) - listener.showHome() - } - } - } - } - } - - private fun fetchRegistrationJsonFromServer(): String { - - val response = requireContext().readFromAsset("RegFromServer") - - //Update userId, name and Display name in the mock - return response.replace("", getEncodedUserId()) - .replace("", binding.username.text.toString()) - .replace("", binding.username.text.toString()) - .replace("", getEncodedChallenge()) - } - - private fun getEncodedUserId(): String { - val random = SecureRandom() - val bytes = ByteArray(64) - random.nextBytes(bytes) - return Base64.encodeToString( - bytes, - Base64.NO_WRAP or Base64.URL_SAFE or Base64.NO_PADDING - ) - } - - private fun getEncodedChallenge(): String { - val random = SecureRandom() - val bytes = ByteArray(32) - random.nextBytes(bytes) - return Base64.encodeToString( - bytes, - Base64.NO_WRAP or Base64.URL_SAFE or Base64.NO_PADDING - ) - } - - private suspend fun createPassword() { - val request = CreatePasswordRequest( - binding.username.text.toString(), - binding.password.text.toString() - ) - try { - credentialManager.createCredential(requireActivity(), request) as CreatePasswordResponse - } catch (e: Exception) { - Log.e("Auth", "createPassword failed with exception: " + e.message) - } - } - - private suspend fun createPasskey(): CreatePublicKeyCredentialResponse? { - val request = CreatePublicKeyCredentialRequest(fetchRegistrationJsonFromServer()) - var response: CreatePublicKeyCredentialResponse? = null - try { - response = credentialManager.createCredential( - requireActivity(), - request - ) as CreatePublicKeyCredentialResponse - } catch (e: CreateCredentialException) { - configureProgress(View.INVISIBLE) - handlePasskeyFailure(e) - } - return response - } - - private fun configureViews(visibility: Int, flag: Boolean) { - configureProgress(visibility) - binding.signUp.isEnabled = flag - binding.signUpWithPassword.isEnabled = flag - } - - private fun configureProgress(visibility: Int) { - binding.textProgress.visibility = visibility - binding.circularProgressIndicator.visibility = visibility - } - - // These are types of errors that can occur during passkey creation. - private fun handlePasskeyFailure(e: CreateCredentialException) { - val msg = when (e) { - is CreatePublicKeyCredentialDomException -> { - // Handle the passkey DOM errors thrown according to the - // WebAuthn spec using e.domError - "An error occurred while creating a passkey, please check logs for additional details." - } - - is CreateCredentialCancellationException -> { - // The user intentionally canceled the operation and chose not - // to register the credential. - "The user intentionally canceled the operation and chose not to register the credential. Check logs for additional details." - } - - is CreateCredentialInterruptedException -> { - // Retry-able error. Consider retrying the call. - "The operation was interrupted, please retry the call. Check logs for additional details." - } - - is CreateCredentialProviderConfigurationException -> { - // Your app is missing the provider configuration dependency. - // Most likely, you're missing "credentials-play-services-auth". - "Your app is missing the provider configuration dependency. Check logs for additional details." - } - - is CreateCredentialUnknownException -> { - "An unknown error occurred while creating passkey. Check logs for additional details." - } - - is CreateCredentialCustomException -> { - // You have encountered an error from a 3rd-party SDK. If you - // make the API call with a request object that's a subclass of - // CreateCustomCredentialRequest using a 3rd-party SDK, then you - // should check for any custom exception type constants within - // that SDK to match with e.type. Otherwise, drop or log the - // exception. - "An unknown error occurred from a 3rd party SDK. Check logs for additional details." - } - - else -> { - Log.w("Auth", "Unexpected exception type ${e::class.java.name}") - "An unknown error occurred." - } - } - Log.e("Auth", "createPasskey failed with exception: " + e.message.toString()) - activity?.showErrorAlert(msg) - } - - private fun registerResponse(): Boolean { - return true - } - - override fun onDestroyView() { - super.onDestroyView() - configureProgress(View.INVISIBLE) - _binding = null - } - - interface SignUpFragmentCallback { - fun showHome() - } -} diff --git a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignUpScreen.kt b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignUpScreen.kt new file mode 100644 index 00000000..8ba52983 --- /dev/null +++ b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignUpScreen.kt @@ -0,0 +1,179 @@ +package com.google.credentialmanager.sample + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.google.credentialmanager.sample.ui.theme.CredentialManagerSampleTheme +import kotlinx.coroutines.flow.collectLatest + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SignUpScreen(navController: NavController) { + CredentialManagerSampleTheme { + val viewModel: SignUpViewModel = viewModel() + + val username by viewModel.username.collectAsState() + val password by viewModel.password.collectAsState() + val isPasswordInputVisible by viewModel.isPasswordInputVisible.collectAsState() + val isLoading by viewModel.isLoading.collectAsState() + val usernameError by viewModel.usernameError.collectAsState() + val passwordError by viewModel.passwordError.collectAsState() + val passkeyCreationError by viewModel.passkeyCreationError.collectAsState() + val passwordCreationError by viewModel.passwordCreationError.collectAsState() + + val context = LocalContext.current + val activity = context.findActivity() + + LaunchedEffect(Unit) { + viewModel.navigationEvent.collectLatest { event -> + when (event) { + is NavigationEvent.NavigateToHome -> { + navController.navigate(Screen.Home.route) { + popUpTo(Screen.SignUp.route) { inclusive = true } + } + } + } + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text( + text = "Create New Account", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(top = 20.dp) + ) + + OutlinedTextField( + value = username, + onValueChange = viewModel::onUsernameChange, + label = { Text("Enter Username") }, + singleLine = true, + isError = usernameError != null, + modifier = Modifier.fillMaxWidth() + ) + if (usernameError != null) { + Text(usernameError!!, color = MaterialTheme.colorScheme.error, fontSize = 12.sp) + } + + if (isPasswordInputVisible) { + OutlinedTextField( + value = password, + onValueChange = viewModel::onPasswordChange, + label = { Text("Enter Password") }, + singleLine = true, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + isError = passwordError != null, + modifier = Modifier.fillMaxWidth() + ) + if (passwordError != null) { + Text(passwordError!!, color = MaterialTheme.colorScheme.error, fontSize = 12.sp) + } + } + + if (isLoading) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 8.dp)) { + CircularProgressIndicator(modifier = Modifier.size(24.dp)) + Spacer(modifier = Modifier.width(8.dp)) + Text("operation in progress...") + } + } + if (passkeyCreationError != null) { + Text( + passkeyCreationError!!, + color = MaterialTheme.colorScheme.error, + fontSize = 12.sp, + textAlign = TextAlign.Center, + modifier = Modifier.padding(vertical = 4.dp) + ) + } + if (passwordCreationError != null) { + Text( + passwordCreationError!!, + color = MaterialTheme.colorScheme.error, + fontSize = 12.sp, + textAlign = TextAlign.Center, + modifier = Modifier.padding(vertical = 4.dp) + ) + } + + Spacer(modifier = Modifier.height(0.dp)) + + Text( + text = "Sign in to your account easily and securely with a passkey. Note: Your biometric data is only stored on your devices and will never be shared with anyone.", + textAlign = TextAlign.Center, + fontSize = 12.sp, + modifier = Modifier.padding(bottom = 8.dp) + ) + + Button( + onClick = { + if (activity != null) { + viewModel.signUpWithPasskey(activity, context) + } + }, + shape = RoundedCornerShape(4.dp), + enabled = !isLoading && activity != null, + modifier = Modifier.fillMaxWidth() + ) { + Text("Sign Up with passkey") + } + + Text( + text = "-------------------- OR --------------------", + textAlign = TextAlign.Center, + modifier = Modifier.padding(vertical = 8.dp) + ) + + Text( + text = "Sign up to your account with a password. Your password will be saved securely with your password provider.", + textAlign = TextAlign.Center, + fontSize = 12.sp, + modifier = Modifier.padding(bottom = 8.dp) + ) + + Button( + onClick = { + if (activity != null) { + viewModel.signUpWithPassword(activity) + } + }, + shape = RoundedCornerShape(4.dp), + enabled = !isLoading && activity != null, + modifier = Modifier.fillMaxWidth() + ) { + Text(if (isPasswordInputVisible) "Sign up with Password" else "Sign up with a password instead") + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun SignUpScreenPreview() { + CredentialManagerSampleTheme { + SignUpScreen(navController = rememberNavController()) + } +} diff --git a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignUpViewModel.kt b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignUpViewModel.kt new file mode 100644 index 00000000..820f736c --- /dev/null +++ b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/SignUpViewModel.kt @@ -0,0 +1,242 @@ +package com.google.credentialmanager.sample + +import android.app.Activity +import android.content.Context +import android.util.Base64 +import android.util.Log +import androidx.credentials.CreatePasswordRequest +import androidx.credentials.CreatePublicKeyCredentialRequest +import androidx.credentials.CreatePublicKeyCredentialResponse +import androidx.credentials.CredentialManager +import androidx.credentials.exceptions.CreateCredentialCancellationException +import androidx.credentials.exceptions.CreateCredentialCustomException +import androidx.credentials.exceptions.CreateCredentialException +import androidx.credentials.exceptions.CreateCredentialInterruptedException +import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException +import androidx.credentials.exceptions.CreateCredentialUnknownException +import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import java.security.SecureRandom + +class SignUpViewModel : ViewModel() { + private val _username = MutableStateFlow("") + val username = _username.asStateFlow() + + private val _password = MutableStateFlow("") + val password = _password.asStateFlow() + + private val _isPasswordInputVisible = MutableStateFlow(false) + val isPasswordInputVisible = _isPasswordInputVisible.asStateFlow() + + private val _isLoading = MutableStateFlow(false) + val isLoading = _isLoading.asStateFlow() + + private val _usernameError = MutableStateFlow(null) + val usernameError = _usernameError.asStateFlow() + + private val _passwordError = MutableStateFlow(null) + val passwordError = _passwordError.asStateFlow() + + private val _passkeyCreationError = MutableStateFlow(null) + val passkeyCreationError = _passkeyCreationError.asStateFlow() + + private val _passwordCreationError = MutableStateFlow(null) + val passwordCreationError = _passwordCreationError.asStateFlow() + + private val _navigationEvent = MutableSharedFlow() + val navigationEvent = _navigationEvent.asSharedFlow() + + fun onUsernameChange(newUsername: String) { + _username.value = newUsername + _usernameError.value = null + _passkeyCreationError.value = null + _passwordCreationError.value = null + } + + fun onPasswordChange(newPassword: String) { + _password.value = newPassword + _passwordError.value = null + } + + fun signUpWithPasskey(activity: Activity, context: Context) { + if (_username.value.isBlank()) { + _usernameError.value = "Username cannot be blank" + return + } + clearErrors() + + viewModelScope.launch { + _isLoading.value = true + + val data = createPasskey(activity, context) + + _isLoading.value = false + if (data != null) { + registerResponse() + DataProvider.setSignedInThroughPasskeys(true) + _navigationEvent.emit(NavigationEvent.NavigateToHome(signedInWithPasskeys = true)) + } + } + } + + suspend fun createPasskey( + activity: Activity, + context: Context + ): CreatePublicKeyCredentialResponse? { + var credentialManager = CredentialManager.create(activity) + + var response: CreatePublicKeyCredentialResponse? = null + val request = CreatePublicKeyCredentialRequest(fetchRegistrationJsonFromServer(context)) + try { + response = credentialManager.createCredential( + activity, + request + ) as CreatePublicKeyCredentialResponse + } catch (e: CreateCredentialException) { + _isLoading.value = false + handlePasskeyFailure(e) + } + + return response + } + + private fun fetchRegistrationJsonFromServer(context: Context): String { + val response = context.assets.open("RegFromServer").bufferedReader().use { it.readText() } + + // Update userId,challenge, name and Display name in the mock + return response.replace("", getEncodedUserId()) + .replace("", _username.value) + .replace("", _username.value) + .replace("", getEncodedChallenge()) + } + + // These are types of errors that can occur during passkey creation. + private fun handlePasskeyFailure(e: CreateCredentialException) { + val msg = when (e) { + is CreatePublicKeyCredentialDomException -> { + // Handle the passkey DOM errors thrown according to the + // WebAuthn spec using e.domError + "An error occurred while creating a passkey, please check logs for additional details." + } + + is CreateCredentialCancellationException -> { + // The user intentionally canceled the operation and chose not + // to register the credential. + "The user intentionally canceled the operation and chose not to register the credential. Check logs for additional details." + } + + is CreateCredentialInterruptedException -> { + // Retry-able error. Consider retrying the call. + "The operation was interrupted, please retry the call. Check logs for additional details." + } + + is CreateCredentialProviderConfigurationException -> { + // Your app is missing the provider configuration dependency. + // Most likely, you're missing "credentials-play-services-auth". + "Your app is missing the provider configuration dependency. Check logs for additional details." + } + + is CreateCredentialUnknownException -> { + "An unknown error occurred while creating passkey. Check logs for additional details." + } + + is CreateCredentialCustomException -> { + // You have encountered an error from a 3rd-party SDK. If you + // make the API call with a request object that's a subclass of + // CreateCustomCredentialRequest using a 3rd-party SDK, then you + // should check for any custom exception type constants within + // that SDK to match with e.type. Otherwise, drop or log the + // exception. + "An unknown error occurred from a 3rd party SDK. Check logs for additional details." + } + + else -> { + Log.w("Auth", "Unexpected exception type ${e::class.java.name}") + "An unknown error occurred." + } + } + Log.e("Auth", "createPasskey failed with exception: " + e.message.toString()) + _passkeyCreationError.value = msg + } + + // Dummy function to simulate server passkey registration. + private fun registerResponse(): Boolean { + return true + } + + fun signUpWithPassword(activity: Activity) { + if (!_isPasswordInputVisible.value) { + _isPasswordInputVisible.value = true + clearErrors() + } else { + var valid = true + if (_username.value.isBlank()) { + _usernameError.value = "User name required" + valid = false + } + if (_password.value.isBlank()) { + _passwordError.value = "Password required" + valid = false + } + if (!valid) return + + clearErrors() + _isLoading.value = true + val credentialManager = CredentialManager.create(activity) + + viewModelScope.launch { + val passwordRequest = CreatePasswordRequest(_username.value, _password.value) + try { + credentialManager.createCredential(activity, passwordRequest) + simulateServerDelayAndLogIn() + } catch (e: Exception) { + val errorMessage = "Exception Message : " + e.message + Log.e("Auth", errorMessage) + _passwordCreationError.value = errorMessage + _isLoading.value = false + } + } + } + } + + private suspend fun simulateServerDelayAndLogIn() { + delay(1000) // Simulate server delay + DataProvider.setSignedInThroughPasskeys(false) + _isLoading.value = false + _navigationEvent.emit(NavigationEvent.NavigateToHome(signedInWithPasskeys = false)) + } + + private fun getEncodedUserId(): String { + val random = SecureRandom() + val bytes = ByteArray(64) + random.nextBytes(bytes) + return Base64.encodeToString( + bytes, + Base64.NO_WRAP or Base64.URL_SAFE or Base64.NO_PADDING + ) + } + + private fun getEncodedChallenge(): String { + val random = SecureRandom() + val bytes = ByteArray(32) + random.nextBytes(bytes) + return Base64.encodeToString( + bytes, + Base64.NO_WRAP or Base64.URL_SAFE or Base64.NO_PADDING + ) + } + + private fun clearErrors() { + _usernameError.value = null + _passwordError.value = null + _passkeyCreationError.value = null + _passwordCreationError.value = null + } +} diff --git a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/Utils.kt b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/Utils.kt new file mode 100644 index 00000000..24323e06 --- /dev/null +++ b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/Utils.kt @@ -0,0 +1,12 @@ +package com.google.credentialmanager.sample + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper + +// Helper extension function to find the activity from LocalContext +fun Context.findActivity(): Activity? = when (this) { + is Activity -> this + is ContextWrapper -> baseContext.findActivity() + else -> null +} \ No newline at end of file diff --git a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/ui/theme/Color.kt b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/ui/theme/Color.kt new file mode 100644 index 00000000..c1b170aa --- /dev/null +++ b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/ui/theme/Color.kt @@ -0,0 +1,17 @@ +package com.google.credentialmanager.sample.ui.theme + +import androidx.compose.ui.graphics.Color + +val Pink200 = Color(0xFFF48FB1) +val Pink500 = Color(0xFFE91E63) +val Pink700 = Color(0xFFC2185B) +val Yellow200 = Color(0xFFFFF59D) +val Yellow700 = Color(0xFFFBC02D) + +// Defined from themes.xml +val Primary = Pink500 +val PrimaryVariant = Pink700 +val OnPrimary = Color.White +val Secondary = Yellow200 +val SecondaryVariant = Yellow700 +val OnSecondary = Color.Black diff --git a/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/ui/theme/Theme.kt b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/ui/theme/Theme.kt new file mode 100644 index 00000000..6ef6b5ce --- /dev/null +++ b/CredentialManager/app/src/main/java/com/google/credentialmanager/sample/ui/theme/Theme.kt @@ -0,0 +1,24 @@ +package com.google.credentialmanager.sample.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +private val SampleColorScheme = lightColorScheme( + primary = Primary, + onPrimary = OnPrimary, + primaryContainer = PrimaryVariant, + secondary = Secondary, + onSecondary = OnSecondary, + secondaryContainer = SecondaryVariant, +) + +@Composable +fun CredentialManagerSampleTheme( + content: @Composable () -> Unit +) { + MaterialTheme( + colorScheme = SampleColorScheme, + content = content + ) +} diff --git a/CredentialManager/app/src/main/res/layout/activity_main.xml b/CredentialManager/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 8b688640..00000000 --- a/CredentialManager/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - diff --git a/CredentialManager/app/src/main/res/layout/fragment_home.xml b/CredentialManager/app/src/main/res/layout/fragment_home.xml deleted file mode 100644 index 79ad3623..00000000 --- a/CredentialManager/app/src/main/res/layout/fragment_home.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - -