commonMain.com.plusmobileapps.firebase.auth.FirebaseAuthImpl.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of auth-jvm Show documentation
Show all versions of auth-jvm Show documentation
A kotlin multiplatform mobile library for authenticating with Firebase for Android, iOS, and JVM
The newest version!
package com.plusmobileapps.firebase.auth
import com.plusmobileapps.firebase.auth.model.FirebaseUser
import com.plusmobileapps.firebase.auth.model.FirebaseUserEntity
import com.plusmobileapps.firebase.auth.model.network.ApiResponse
import com.plusmobileapps.firebase.auth.model.network.FirebaseApiErrorDetails
import com.plusmobileapps.firebase.auth.model.network.models.ChangePasswordError
import com.plusmobileapps.firebase.auth.model.network.models.ChangePasswordModel
import com.plusmobileapps.firebase.auth.model.network.models.ChangePasswordResult
import com.plusmobileapps.firebase.auth.model.network.models.DeleteUserDTO
import com.plusmobileapps.firebase.auth.model.network.models.ForgotPasswordError
import com.plusmobileapps.firebase.auth.model.network.models.ForgotPasswordResult
import com.plusmobileapps.firebase.auth.model.network.models.GetUserDataModel
import com.plusmobileapps.firebase.auth.model.network.models.IdTokenError
import com.plusmobileapps.firebase.auth.model.network.models.IdTokenModel
import com.plusmobileapps.firebase.auth.model.network.models.SendPasswordResetModel
import com.plusmobileapps.firebase.auth.model.network.models.SignInError
import com.plusmobileapps.firebase.auth.model.network.models.SignInModel
import com.plusmobileapps.firebase.auth.model.network.models.SignInResult
import com.plusmobileapps.firebase.auth.model.network.models.SignUpError
import com.plusmobileapps.firebase.auth.model.network.models.SignUpModel
import com.plusmobileapps.firebase.auth.model.network.models.SignUpResult
import com.plusmobileapps.firebase.auth.util.DateTimeUtil
import com.plusmobileapps.firebase.auth.util.mapState
import com.russhwolf.settings.Settings
import com.russhwolf.settings.set
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.datetime.Instant
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlin.time.Duration.Companion.seconds
internal class FirebaseAuthImpl(
private val apiClient: ApiClient,
private val dateTimeUtil: DateTimeUtil,
private val cache: Cache,
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
) : FirebaseAuth {
override val authState: StateFlow = cache.user.mapState { it.toAuthState() }
override val isAuthenticated: Flow =
authState.map { it is AuthState.Authenticated }
.distinctUntilChanged()
override fun signOut() {
cache.clear()
}
override suspend fun signUp(email: String, password: String): SignUpResult {
val response = apiClient.signUp(SignUpModel.Request(email = email, password = password))
return when (response) {
is ApiResponse.Error.HttpError -> {
val errorBody = response.errorBody ?: return SignUpResult.Error.Unknown
val error = SignUpError.values()
.find { it.apiName == errorBody.error.message }
?: return SignUpResult.Error.Unknown
SignUpResult.Error.ApiError(error)
}
ApiResponse.Error.NetworkError -> SignUpResult.Error.NetworkError
ApiResponse.Error.SerializationError -> SignUpResult.Error.Unknown
is ApiResponse.Success -> {
val user = onUserAuthenticated(
idToken = response.body.idToken,
email = email,
refreshToken = response.body.refreshToken,
expiresAt = response.body.expiresIn,
localId = response.body.localId
)
SignUpResult.Success(user)
}
}
}
override suspend fun signIn(email: String, password: String): SignInResult {
val response = apiClient.signIn(
SignInModel.Request(
email = email,
password = password,
returnSecureToken = true
)
)
return when (response) {
is ApiResponse.Success -> SignInResult.Success(
onUserAuthenticated(
idToken = response.body.idToken,
email = email,
refreshToken = response.body.refreshToken ?: "",
expiresAt = response.body.expiresIn ?: "",
localId = response.body.localId
)
)
is ApiResponse.Error.HttpError -> {
val body = response.errorBody?.error ?: return SignInResult.Error.Unknown
val error = SignInError.values()
.find { body.message == it.apiName }
?: return SignInResult.Error.Unknown
SignInResult.Error.ApiError(error)
}
ApiResponse.Error.NetworkError -> SignInResult.Error.NetworkError
ApiResponse.Error.SerializationError -> SignInResult.Error.Unknown
}
}
override suspend fun forgotPassword(email: String): ForgotPasswordResult {
val response = apiClient.forgotPassword(SendPasswordResetModel.Request(email = email))
return when (response) {
is ApiResponse.Error.HttpError -> {
val error =
response.errorBody?.error?.message ?: return ForgotPasswordResult.Error.Unknown
val apiError = ForgotPasswordError.values()
.find { it.apiName == error }
?: return ForgotPasswordResult.Error.Unknown
ForgotPasswordResult.Error.ApiError(apiError)
}
ApiResponse.Error.NetworkError -> ForgotPasswordResult.Error.NetworkConnection
ApiResponse.Error.SerializationError -> ForgotPasswordResult.Error.Unknown
is ApiResponse.Success -> ForgotPasswordResult.Success
}
}
override suspend fun changePassword(
idToken: String,
newPassword: String
): ChangePasswordResult {
val response = apiClient.changePassword(
ChangePasswordModel.Request(
idToken = idToken, password = newPassword, returnSecureToken = true
)
)
return when (response) {
is ApiResponse.Error.HttpError -> {
val error =
response.errorBody?.error?.message ?: return ChangePasswordResult.Error.Unknown
val apiError = ChangePasswordError.values()
.find { it.apiName == error }
?: return ChangePasswordResult.Error.Unknown
ChangePasswordResult.Error.ApiError(apiError)
}
ApiResponse.Error.NetworkError -> ChangePasswordResult.Error.NetworkConnection
ApiResponse.Error.SerializationError -> ChangePasswordResult.Error.Unknown
is ApiResponse.Success -> ChangePasswordResult.Success
}
}
override suspend fun updateProfile(displayName: String?, photoUrl: String?) {
val entity = cache.user.value ?: return
cache.saveUser(
entity.copy(
displayName = displayName,
photoUrl = photoUrl,
)
)
}
override suspend fun refreshProfile(): Result {
val response = apiClient.getUserData(
GetUserDataModel.Request(
idToken = getJWT()
?: return Result.failure(IllegalStateException("User not authenticated"))
)
)
return when (response) {
is ApiResponse.Success -> {
val entity = cache.user.value
?: return Result.failure(IllegalStateException("User not authenticated"))
val user = response.body.users.firstOrNull()
?: return Result.success(entity.toFirebaseUser())
val newEntity =
entity.copy(displayName = user.displayName, photoUrl = user.photoUrl)
cache.saveUser(newEntity)
Result.success(newEntity.toFirebaseUser())
}
is ApiResponse.Error -> Result.failure(Exception())
}
}
override suspend fun getJWT(): String? {
val userEntity = cache.user.value ?: return null
val now = dateTimeUtil.now
val expiresAt = Instant.parse(userEntity.expiresAt)
if (now.epochSeconds < expiresAt.epochSeconds) {
return userEntity.idToken
}
return refreshJWT()
}
override suspend fun refreshJWT(): String? {
val user = cache.user.value
val refreshToken = user?.refreshToken ?: return null
val response = apiClient.getAccessToken(
IdTokenModel.Request(
refresh_token = refreshToken
)
)
return when (response) {
is ApiResponse.Success -> {
val expiresInSeconds = response.body.expires_in.toIntOrNull() ?: 3600
val newUser = user.copy(
idToken = response.body.id_token,
refreshToken = response.body.refresh_token,
expiresAt = dateTimeUtil.now.plus(expiresInSeconds.seconds).toString()
)
cache.saveUser(newUser)
newUser.idToken
}
is ApiResponse.Error.HttpError -> {
val error = IdTokenError.values().find {
it.apiName == response.errorBody?.error?.message
}
if (error?.isSignOutError == true) {
signOut()
}
null
}
else -> null
}
}
override suspend fun deleteAccount(): Result {
val idToken = getJWT()
?: return Result.failure(IllegalArgumentException("Missing an id token to delete the account"))
val response = apiClient.deleteAccount(DeleteUserDTO.Request(idToken))
signOut()
return Result.success(Unit)
}
private fun onUserAuthenticated(
idToken: String,
email: String,
refreshToken: String,
expiresAt: String,
localId: String
): FirebaseUser {
val expiresInSeconds = expiresAt.toIntOrNull() ?: 3600
val firebaseAuthStoreUser = FirebaseUserEntity(
idToken = idToken,
email = email,
refreshToken = refreshToken,
expiresAt = dateTimeUtil.now.plus(expiresInSeconds.seconds).toString(),
localId = localId,
)
cache.saveUser(firebaseAuthStoreUser)
return firebaseAuthStoreUser.toFirebaseUser().also {
scope.launch {
fetchProfilePicture(idToken)
}
}
}
private suspend fun fetchProfilePicture(idToken: String) {
val profileResponse = apiClient.getUserData(GetUserDataModel.Request(idToken))
when (profileResponse) {
is ApiResponse.Error.HttpError,
ApiResponse.Error.NetworkError,
ApiResponse.Error.SerializationError -> {
// TODO: Handle error
}
is ApiResponse.Success -> {
val user = profileResponse.body.users.firstOrNull() ?: return
val entity = cache.user.value ?: return
cache.saveUser(
entity.copy(photoUrl = user.photoUrl, displayName = user.displayName)
)
}
}
}
}
private fun FirebaseUserEntity?.toAuthState(): AuthState {
this ?: return AuthState.Unauthenticated
return AuthState.Authenticated(
firebaseUser = toFirebaseUser(),
idToken = idToken,
expiresAt = Instant.parse(expiresAt).toString(),
)
}
private fun FirebaseUserEntity.toFirebaseUser(): FirebaseUser = FirebaseUser(
uid = localId,
email = email,
photoUrl = photoUrl,
displayName = displayName,
)
© 2015 - 2025 Weber Informatics LLC | Privacy Policy