Anonymous
Проблема с реализацией входа в Google в ViewModel
Сообщение
Anonymous » 16 сен 2025, 17:31
Я сталкиваюсь с проблемой моей реализации входа в Google в приложении Android. У меня есть две части кода: первый работает правильно, в то время как второй не удается во время вызова в credentialmanager.getcredential (context = context, request = request) .
Первый код (работающий)
Код: Выделить всё
class GoogleAuthClient(
private val context: Context,
private val credentialManager: CredentialManager,
private val onSignInComplete: () -> Unit, // 로그인 완료 콜백 추가
private val onSignOutComplete: () -> Unit // 로그아웃 완료 콜백 추가
) {
companion object {
private const val TAG = "GoogleAuthClient"
}
fun handleSignIn(result: GetCredentialResponse) {
val credential = result.credential
when {
credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL -> {
try {
val googleIdTokenCredential =
GoogleIdTokenCredential.createFrom(credential.data)
val idToken = googleIdTokenCredential.idToken
val email = googleIdTokenCredential.id
val displayName = googleIdTokenCredential.displayName
Log.d(TAG, "Login successful: $email")
Toast.makeText(context, "Welcome, $displayName!", Toast.LENGTH_SHORT).show()
onSignInComplete() // 로그인 완료 알림
} catch (e: GoogleIdTokenParsingException) {
Log.e(TAG, "Invalid Google ID token", e)
Toast.makeText(context, "Login failed: Invalid token", Toast.LENGTH_SHORT)
.show()
}
}
else -> {
Log.e(TAG, "Unexpected credential type")
Toast.makeText(context, "Login failed: Unexpected error", Toast.LENGTH_SHORT).show()
}
}
}
fun signOut() {
CoroutineScope(Dispatchers.Main).launch {
try {
credentialManager.clearCredentialState(
ClearCredentialStateRequest(
ClearCredentialStateRequest.TYPE_CLEAR_CREDENTIAL_STATE
)
)
Toast.makeText(context, "Signed out successfully", Toast.LENGTH_SHORT).show()
onSignOutComplete() // 로그아웃 완료 알림
} catch (e: Exception) {
Log.e(TAG, "Logout failed", e)
Toast.makeText(context, "Logout failed", Toast.LENGTH_SHORT).show()
}
}
}
}
class MainActivity : ComponentActivity() {
private lateinit var googleAuthClient: GoogleAuthClient
private lateinit var credentialManager: CredentialManager
private var isSignedIn = false
private lateinit var signInOutText: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
signInOutText = TextView(this).apply {
text = "Sign In"
textSize = 18f
setPadding(32, 32, 32, 32)
setOnClickListener { handleSignInOutClick() }
}
setContentView(signInOutText)
credentialManager = CredentialManager.create(this)
googleAuthClient = GoogleAuthClient(
this,
credentialManager,
onSignInComplete = {
isSignedIn = true
signInOutText.text = "Sign Out"
},
onSignOutComplete = {
isSignedIn = false
signInOutText.text = "Sign In"
}
)
}
private fun handleSignInOutClick() {
if (!isSignedIn) {
signInWithGoogle()
} else {
googleAuthClient.signOut()
}
}
private fun signInWithGoogle() {
val nonce = generateNonce()
val signInWithGoogleOption = GetSignInWithGoogleOption.Builder(
"909240456210-gomip46dc5200lbd8vdrs2ii94clss4t.apps.googleusercontent.com")
.setNonce(nonce)
.build()
Log.d("haha", "signInWithGoogleOption 성공")
val request = GetCredentialRequest.Builder()
.addCredentialOption(signInWithGoogleOption)
.build()
Log.i("haha", "GetCredentialRequest 성공")
CoroutineScope(Dispatchers.Main).launch {
try {
val result = credentialManager.getCredential(
request = request,
context = this@MainActivity
)
Log.d("haha", "getCredential 성공")
googleAuthClient.handleSignIn(result)
} catch (e: Exception) {
println("Login failed: ${e.message}")
}
}
}
private fun generateNonce(): String {
val randomBytes = ByteArray(32)
SecureRandom().nextBytes(randomBytes)
return randomBytes.joinToString("") { "%02x".format(it) }
}
}
< /code>
второй код (не работает) < /p>
fun NavGraphBuilder.loginComposable(
onNavigateToGenderAndAgeGroup: (Map) -> Unit,
onNavigateToQuote: () -> Unit
) {
composable(route = "login") {
val loginViewModel: LoginViewModel = hiltViewModel()
val pendingUserMap = loginViewModel.pendingUserMap.collectAsState().value
LoginScreen(
loginViewModel = loginViewModel,
onNavigateToGenderAndAgeGroup = {
pendingUserMap?.let { onNavigateToGenderAndAgeGroup(it) }
},
onNavigateToQuote = onNavigateToQuote
)
}
}
----------------
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideContext(@ApplicationContext context: Context): Context {
return context
}
@Provides
@Singleton
fun provideClientId(@ApplicationContext context: Context): String {
return context.getString(R.string.client_id)
}
@Provides
@Singleton
fun provideCredentialManager(@ApplicationContext context: Context): CredentialManager {
return CredentialManager.create(context)
}
}
--------------------
@Composable
fun LoginScreen(
loginViewModel: LoginViewModel,
onNavigateToGenderAndAgeGroup: () -> Unit,
onNavigateToQuote: () -> Unit
) {
val context = LocalContext.current
val authState by loginViewModel.authState.collectAsState()
when (authState) {
is AuthState.Loading -> {
CircularProgressIndicator(modifier = Modifier.fillMaxSize())
}
is AuthState.SignedOut -> {
LoginScreenContent(onGoogleLoginClick = {
loginViewModel.onGoogleLoginClicked()
})
}
is AuthState.SignedIn -> {
LaunchedEffect(Unit) {
onNavigateToQuote()
}
}
is AuthState.SignUpRequired -> {
LaunchedEffect(Unit) {
onNavigateToGenderAndAgeGroup()
}
}
is AuthState.Error -> {
val errorMessage = (authState as AuthState.Error).message
Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show()
}
}
}
@Composable
fun LoginScreenContent(
onGoogleLoginClick: () -> Unit
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(BlackBackground),
) {
Column(
modifier = Modifier
.offset(x = 34.dp, y = 99.dp)
.align(Alignment.TopStart),
) {
}
Column(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 80.dp),
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
horizontalAlignment = Alignment.Start,
) {
GoogleLoginButton(onGoogleLoginClick)
}
}
}
@Composable
private fun GoogleLoginButton(
onGoogleLoginClick: () -> Unit
) {
Row(
modifier = Modifier
.width(345.dp)
.height(54.dp)
.background(color = GrayScaleWhite, shape = RoundedCornerShape(size = 10.dp))
.padding(start = 17.dp)
.clickable(onClick = onGoogleLoginClick),
horizontalArrangement = Arrangement.spacedBy(79.dp, Alignment.Start),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
modifier = Modifier
.width(24.dp)
.height(24.dp)
.background(color = GrayScaleWhite),
painter = painterResource(id = R.drawable.google_icon),
contentDescription = "google_icon",
contentScale = ContentScale.None,
)
Text(
text = stringResource(id = R.string.google_login),
style = TextStyle(
fontSize = 18.sp,
fontFamily = FontFamily(Font(R.font.inter_18pt_regular)),
fontWeight = FontWeight(400),
color = Color(0xFF000000),
),
)
}
}
---------------------------
@HiltViewModel
class LoginViewModel @Inject constructor(
private val credentialManager: CredentialManager,
private val clientId: String,
@ApplicationContext private val applicationContext: Context // 애플리케이션 컨텍스트 추가
) : ViewModel() {
private val tag = "LoginViewModel: "
private val firebaseAuth: FirebaseAuth = Firebase.auth
private val firestore: FirebaseFirestore = Firebase.firestore
private val _authState = MutableStateFlow(AuthState.SignedOut)
val authState: StateFlow = _authState.asStateFlow()
private val _pendingUserMap = MutableStateFlow(null)
val pendingUserMap: StateFlow = _pendingUserMap.asStateFlow()
init {
checkInitialAuthState()
}
private fun checkInitialAuthState() {
viewModelScope.launch {
_authState.value = if (isSignedIn()) {
val currentUser = getCurrentUser()
if (currentUser != null) AuthState.SignedIn(currentUser) else AuthState.SignedOut
} else {
AuthState.SignedOut
}
}
}
fun isSignedIn(): Boolean = firebaseAuth.currentUser != null
suspend fun getCurrentUser(): User? {
val currentUser = firebaseAuth.currentUser ?: return null
return try {
val userDoc = firestore.collection("users")
.document(currentUser.uid)
.get()
.await()
userDoc.toObject(User::class.java)
} catch (e: Exception) {
if (e is CancellationException) throw e
logError("Error fetching current user: ${e.message}")
null
}
}
fun onGoogleLoginClicked() {
viewModelScope.launch {
val state = signIn()
_authState.value = state
}
}
private suspend fun signIn(): AuthState {
return try {
if (isSignedIn()) {
val currentUser = getCurrentUser()
Log.d(tag, "이미 로그인 상태입니다. 사용자: $currentUser")
return AuthState.SignedIn(currentUser)
}
val result = buildCredentialRequest()
Log.d(tag, "Credential 요청 성공: $result")
handleSignIn(result)
} catch (e: Exception) {
Log.e(tag, "Sign-in failed: ${e.message}", e)
AuthState.Error("Sign-in failed: ${e.message}")
}
}
private suspend fun buildCredentialRequest(): GetCredentialResponse {
val nonce = generateNonce()
val signInWithGoogleOption = GetSignInWithGoogleOption.Builder(clientId)
.setNonce(nonce)
.build()
val request = GetCredentialRequest.Builder()
.addCredentialOption(signInWithGoogleOption)
.build()
return credentialManager.getCredential(context = applicationContext, request = request)
}
private fun generateNonce(): String {
val randomBytes = ByteArray(32)
SecureRandom().nextBytes(randomBytes)
return randomBytes.joinToString("") { "%02x".format(it) }
}
private fun logError(message: String) {
Log.e(tag, message)
}
private suspend fun handleSignIn(result: GetCredentialResponse): AuthState {
Log.d(tag, "{handleSignIn 진입 완료}")
val credential = result.credential
if (credential is CustomCredential &&
credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL
) {
try {
val tokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
val authCredential = GoogleAuthProvider.getCredential(
tokenCredential.idToken, null
)
val authResult = firebaseAuth.signInWithCredential(authCredential).await()
val firebaseUser = authResult.user
return if (firebaseUser != null) {
val userExists = checkUserExistsInFirestore(firebaseUser.uid)
if (!userExists) {
val initialProfile = createInitialProfile(firebaseUser)
_pendingUserMap.value = initialProfile
AuthState.SignUpRequired
} else {
val userDocument = firestore.collection("users")
.document(firebaseUser.uid).get().await()
val user = userDocument.toObject(User::class.java)
AuthState.SignedIn(user)
}
} else {
AuthState.SignedOut
}
} catch (e: Exception) {
Log.e(tag, "Sign-in error: ${e.message}", e)
return AuthState.Error("Sign-in failed: ${e.message}")
}
} else {
Log.e(tag, "credential is not GoogleIdTokenCredential")
return AuthState.SignedOut
}
}
private suspend fun checkUserExistsInFirestore(userId: String): Boolean {
return try {
firestore.collection("users").document(userId).get().await().exists()
} catch (e: Exception) {
e.printStackTrace()
false
}
}
private fun createInitialProfile(firebaseUser: FirebaseUser): Map {
return mapOf(
"uid" to firebaseUser.uid,
"email" to firebaseUser.email,
"displayName" to firebaseUser.displayName
)
}
}
< /code>
Во втором коде, когда я вызову cutreatentalmanager.getcredential (context = context, request = request), он бросает ошибку. [b] Тем не менее, я подтвердил, что настройки GCP и конфигурации SHA-1 являются правильными, поскольку первый код отлично работает в одном и том же проекте. [/b]
Что может привести к тому, что CredentialManager.getCredential вызов не выполняется во втором коде? /> Любые идеи или предложения о том, как устранить этот вопрос>
Подробнее здесь:
https://stackoverflow.com/questions/792 ... -viewmodel
1758033083
Anonymous
Я сталкиваюсь с проблемой моей реализации входа в Google в приложении Android. У меня есть две части кода: первый работает правильно, в то время как второй не удается во время вызова в credentialmanager.getcredential (context = context, request = request) . Первый код (работающий) [code]class GoogleAuthClient( private val context: Context, private val credentialManager: CredentialManager, private val onSignInComplete: () -> Unit, // 로그인 완료 콜백 추가 private val onSignOutComplete: () -> Unit // 로그아웃 완료 콜백 추가 ) { companion object { private const val TAG = "GoogleAuthClient" } fun handleSignIn(result: GetCredentialResponse) { val credential = result.credential when { credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL -> { try { val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data) val idToken = googleIdTokenCredential.idToken val email = googleIdTokenCredential.id val displayName = googleIdTokenCredential.displayName Log.d(TAG, "Login successful: $email") Toast.makeText(context, "Welcome, $displayName!", Toast.LENGTH_SHORT).show() onSignInComplete() // 로그인 완료 알림 } catch (e: GoogleIdTokenParsingException) { Log.e(TAG, "Invalid Google ID token", e) Toast.makeText(context, "Login failed: Invalid token", Toast.LENGTH_SHORT) .show() } } else -> { Log.e(TAG, "Unexpected credential type") Toast.makeText(context, "Login failed: Unexpected error", Toast.LENGTH_SHORT).show() } } } fun signOut() { CoroutineScope(Dispatchers.Main).launch { try { credentialManager.clearCredentialState( ClearCredentialStateRequest( ClearCredentialStateRequest.TYPE_CLEAR_CREDENTIAL_STATE ) ) Toast.makeText(context, "Signed out successfully", Toast.LENGTH_SHORT).show() onSignOutComplete() // 로그아웃 완료 알림 } catch (e: Exception) { Log.e(TAG, "Logout failed", e) Toast.makeText(context, "Logout failed", Toast.LENGTH_SHORT).show() } } } } class MainActivity : ComponentActivity() { private lateinit var googleAuthClient: GoogleAuthClient private lateinit var credentialManager: CredentialManager private var isSignedIn = false private lateinit var signInOutText: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) signInOutText = TextView(this).apply { text = "Sign In" textSize = 18f setPadding(32, 32, 32, 32) setOnClickListener { handleSignInOutClick() } } setContentView(signInOutText) credentialManager = CredentialManager.create(this) googleAuthClient = GoogleAuthClient( this, credentialManager, onSignInComplete = { isSignedIn = true signInOutText.text = "Sign Out" }, onSignOutComplete = { isSignedIn = false signInOutText.text = "Sign In" } ) } private fun handleSignInOutClick() { if (!isSignedIn) { signInWithGoogle() } else { googleAuthClient.signOut() } } private fun signInWithGoogle() { val nonce = generateNonce() val signInWithGoogleOption = GetSignInWithGoogleOption.Builder( "909240456210-gomip46dc5200lbd8vdrs2ii94clss4t.apps.googleusercontent.com") .setNonce(nonce) .build() Log.d("haha", "signInWithGoogleOption 성공") val request = GetCredentialRequest.Builder() .addCredentialOption(signInWithGoogleOption) .build() Log.i("haha", "GetCredentialRequest 성공") CoroutineScope(Dispatchers.Main).launch { try { val result = credentialManager.getCredential( request = request, context = this@MainActivity ) Log.d("haha", "getCredential 성공") googleAuthClient.handleSignIn(result) } catch (e: Exception) { println("Login failed: ${e.message}") } } } private fun generateNonce(): String { val randomBytes = ByteArray(32) SecureRandom().nextBytes(randomBytes) return randomBytes.joinToString("") { "%02x".format(it) } } } < /code> второй код (не работает) < /p> fun NavGraphBuilder.loginComposable( onNavigateToGenderAndAgeGroup: (Map) -> Unit, onNavigateToQuote: () -> Unit ) { composable(route = "login") { val loginViewModel: LoginViewModel = hiltViewModel() val pendingUserMap = loginViewModel.pendingUserMap.collectAsState().value LoginScreen( loginViewModel = loginViewModel, onNavigateToGenderAndAgeGroup = { pendingUserMap?.let { onNavigateToGenderAndAgeGroup(it) } }, onNavigateToQuote = onNavigateToQuote ) } } ---------------- @Module @InstallIn(SingletonComponent::class) object AppModule { @Provides @Singleton fun provideContext(@ApplicationContext context: Context): Context { return context } @Provides @Singleton fun provideClientId(@ApplicationContext context: Context): String { return context.getString(R.string.client_id) } @Provides @Singleton fun provideCredentialManager(@ApplicationContext context: Context): CredentialManager { return CredentialManager.create(context) } } -------------------- @Composable fun LoginScreen( loginViewModel: LoginViewModel, onNavigateToGenderAndAgeGroup: () -> Unit, onNavigateToQuote: () -> Unit ) { val context = LocalContext.current val authState by loginViewModel.authState.collectAsState() when (authState) { is AuthState.Loading -> { CircularProgressIndicator(modifier = Modifier.fillMaxSize()) } is AuthState.SignedOut -> { LoginScreenContent(onGoogleLoginClick = { loginViewModel.onGoogleLoginClicked() }) } is AuthState.SignedIn -> { LaunchedEffect(Unit) { onNavigateToQuote() } } is AuthState.SignUpRequired -> { LaunchedEffect(Unit) { onNavigateToGenderAndAgeGroup() } } is AuthState.Error -> { val errorMessage = (authState as AuthState.Error).message Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show() } } } @Composable fun LoginScreenContent( onGoogleLoginClick: () -> Unit ) { Box( modifier = Modifier .fillMaxSize() .background(BlackBackground), ) { Column( modifier = Modifier .offset(x = 34.dp, y = 99.dp) .align(Alignment.TopStart), ) { } Column( modifier = Modifier .align(Alignment.BottomCenter) .padding(bottom = 80.dp), verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top), horizontalAlignment = Alignment.Start, ) { GoogleLoginButton(onGoogleLoginClick) } } } @Composable private fun GoogleLoginButton( onGoogleLoginClick: () -> Unit ) { Row( modifier = Modifier .width(345.dp) .height(54.dp) .background(color = GrayScaleWhite, shape = RoundedCornerShape(size = 10.dp)) .padding(start = 17.dp) .clickable(onClick = onGoogleLoginClick), horizontalArrangement = Arrangement.spacedBy(79.dp, Alignment.Start), verticalAlignment = Alignment.CenterVertically, ) { Image( modifier = Modifier .width(24.dp) .height(24.dp) .background(color = GrayScaleWhite), painter = painterResource(id = R.drawable.google_icon), contentDescription = "google_icon", contentScale = ContentScale.None, ) Text( text = stringResource(id = R.string.google_login), style = TextStyle( fontSize = 18.sp, fontFamily = FontFamily(Font(R.font.inter_18pt_regular)), fontWeight = FontWeight(400), color = Color(0xFF000000), ), ) } } --------------------------- @HiltViewModel class LoginViewModel @Inject constructor( private val credentialManager: CredentialManager, private val clientId: String, @ApplicationContext private val applicationContext: Context // 애플리케이션 컨텍스트 추가 ) : ViewModel() { private val tag = "LoginViewModel: " private val firebaseAuth: FirebaseAuth = Firebase.auth private val firestore: FirebaseFirestore = Firebase.firestore private val _authState = MutableStateFlow(AuthState.SignedOut) val authState: StateFlow = _authState.asStateFlow() private val _pendingUserMap = MutableStateFlow(null) val pendingUserMap: StateFlow = _pendingUserMap.asStateFlow() init { checkInitialAuthState() } private fun checkInitialAuthState() { viewModelScope.launch { _authState.value = if (isSignedIn()) { val currentUser = getCurrentUser() if (currentUser != null) AuthState.SignedIn(currentUser) else AuthState.SignedOut } else { AuthState.SignedOut } } } fun isSignedIn(): Boolean = firebaseAuth.currentUser != null suspend fun getCurrentUser(): User? { val currentUser = firebaseAuth.currentUser ?: return null return try { val userDoc = firestore.collection("users") .document(currentUser.uid) .get() .await() userDoc.toObject(User::class.java) } catch (e: Exception) { if (e is CancellationException) throw e logError("Error fetching current user: ${e.message}") null } } fun onGoogleLoginClicked() { viewModelScope.launch { val state = signIn() _authState.value = state } } private suspend fun signIn(): AuthState { return try { if (isSignedIn()) { val currentUser = getCurrentUser() Log.d(tag, "이미 로그인 상태입니다. 사용자: $currentUser") return AuthState.SignedIn(currentUser) } val result = buildCredentialRequest() Log.d(tag, "Credential 요청 성공: $result") handleSignIn(result) } catch (e: Exception) { Log.e(tag, "Sign-in failed: ${e.message}", e) AuthState.Error("Sign-in failed: ${e.message}") } } private suspend fun buildCredentialRequest(): GetCredentialResponse { val nonce = generateNonce() val signInWithGoogleOption = GetSignInWithGoogleOption.Builder(clientId) .setNonce(nonce) .build() val request = GetCredentialRequest.Builder() .addCredentialOption(signInWithGoogleOption) .build() return credentialManager.getCredential(context = applicationContext, request = request) } private fun generateNonce(): String { val randomBytes = ByteArray(32) SecureRandom().nextBytes(randomBytes) return randomBytes.joinToString("") { "%02x".format(it) } } private fun logError(message: String) { Log.e(tag, message) } private suspend fun handleSignIn(result: GetCredentialResponse): AuthState { Log.d(tag, "{handleSignIn 진입 완료}") val credential = result.credential if (credential is CustomCredential && credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL ) { try { val tokenCredential = GoogleIdTokenCredential.createFrom(credential.data) val authCredential = GoogleAuthProvider.getCredential( tokenCredential.idToken, null ) val authResult = firebaseAuth.signInWithCredential(authCredential).await() val firebaseUser = authResult.user return if (firebaseUser != null) { val userExists = checkUserExistsInFirestore(firebaseUser.uid) if (!userExists) { val initialProfile = createInitialProfile(firebaseUser) _pendingUserMap.value = initialProfile AuthState.SignUpRequired } else { val userDocument = firestore.collection("users") .document(firebaseUser.uid).get().await() val user = userDocument.toObject(User::class.java) AuthState.SignedIn(user) } } else { AuthState.SignedOut } } catch (e: Exception) { Log.e(tag, "Sign-in error: ${e.message}", e) return AuthState.Error("Sign-in failed: ${e.message}") } } else { Log.e(tag, "credential is not GoogleIdTokenCredential") return AuthState.SignedOut } } private suspend fun checkUserExistsInFirestore(userId: String): Boolean { return try { firestore.collection("users").document(userId).get().await().exists() } catch (e: Exception) { e.printStackTrace() false } } private fun createInitialProfile(firebaseUser: FirebaseUser): Map { return mapOf( "uid" to firebaseUser.uid, "email" to firebaseUser.email, "displayName" to firebaseUser.displayName ) } } < /code> Во втором коде, когда я вызову cutreatentalmanager.getcredential (context = context, request = request), он бросает ошибку. [b] Тем не менее, я подтвердил, что настройки GCP и конфигурации SHA-1 являются правильными, поскольку первый код отлично работает в одном и том же проекте. [/b] Что может привести к тому, что CredentialManager.getCredential [/code] вызов не выполняется во втором коде? /> Любые идеи или предложения о том, как устранить этот вопрос> Подробнее здесь: [url]https://stackoverflow.com/questions/79257545/issue-with-google-sign-in-implementation-in-viewmodel[/url]