From f63e1b392a612cbcd1c04b4434e2644e674c744f Mon Sep 17 00:00:00 2001 From: Jakub Kostka Date: Tue, 10 Sep 2024 14:40:38 +0200 Subject: [PATCH 01/10] signupNewUser implementation --- .../com/google/firebase/auth/FirebaseAuth.kt | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt index 3a41a23..c0dd4d2 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt @@ -245,6 +245,38 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { return source.task } + fun createUserWithEmailAndPassword(email: String, password: String): Task { + val source = TaskCompletionSource() + val body = RequestBody.create( + json, + JsonObject(mapOf("email" to JsonPrimitive(email), "password" to JsonPrimitive(password), "returnSecureToken" to JsonPrimitive(true))).toString() + ) + val request = Request.Builder() + .url("https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser?key=" + app.options.apiKey) + .post(body) + .build() + client.newCall(request).enqueue(object : Callback { + + override fun onFailure(call: Call, e: IOException) { + source.setException(FirebaseException(e.toString(), e)) + } + + @Throws(IOException::class) + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + source.setException( + createAuthInvalidUserException("signupNewUser", request, response) + ) + } else { + val responseBody = response.body()?.use { it.string() } ?: "" + val user = FirebaseUserImpl(app, jsonParser.parseToJsonElement(responseBody).jsonObject) + refreshToken(user, source) { AuthResult { it } } + } + } + }) + return source.task + } + fun signInWithEmailAndPassword(email: String, password: String): Task { val source = TaskCompletionSource() val body = RequestBody.create( @@ -425,7 +457,6 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { } fun sendPasswordResetEmail(email: String, settings: ActionCodeSettings?): Task = TODO() - fun createUserWithEmailAndPassword(email: String, password: String): Task = TODO() fun signInWithCredential(authCredential: AuthCredential): Task = TODO() fun checkActionCode(code: String): Task = TODO() fun confirmPasswordReset(code: String, newPassword: String): Task = TODO() From 0972225904fc1d5873a77f0303e49ac05f037204 Mon Sep 17 00:00:00 2001 From: Jakub Kostka Date: Fri, 13 Sep 2024 19:23:17 +0200 Subject: [PATCH 02/10] email property partially implemented - not all EPs return it simple tests added for createUserWithEmailAndPassword, signInWithEmailAndPassword, and signInAnonymously. --- .../com/google/firebase/auth/FirebaseAuth.kt | 27 +++++--- .../com/google/firebase/auth/FirebaseUser.kt | 2 +- src/test/kotlin/FirebaseAuthTest.kt | 68 +++++++++++++++++++ 3 files changed, 86 insertions(+), 11 deletions(-) create mode 100644 src/test/kotlin/FirebaseAuthTest.kt diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt index c0dd4d2..95bd2e7 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt @@ -52,17 +52,24 @@ class FirebaseUserImpl private constructor( val idToken: String, val refreshToken: String, val expiresIn: Int, - val createdAt: Long + val createdAt: Long, + override val email: String ) : FirebaseUser() { - constructor(app: FirebaseApp, data: JsonObject, isAnonymous: Boolean = data["isAnonymous"]?.jsonPrimitive?.booleanOrNull ?: false) : this( - app, - isAnonymous, - data["uid"]?.jsonPrimitive?.contentOrNull ?: data["user_id"]?.jsonPrimitive?.contentOrNull ?: data["localId"]?.jsonPrimitive?.contentOrNull ?: "", - data["idToken"]?.jsonPrimitive?.contentOrNull ?: data.getValue("id_token").jsonPrimitive.content, - data["refreshToken"]?.jsonPrimitive?.contentOrNull ?: data.getValue("refresh_token").jsonPrimitive.content, - data["expiresIn"]?.jsonPrimitive?.intOrNull ?: data.getValue("expires_in").jsonPrimitive.int, - data["createdAt"]?.jsonPrimitive?.longOrNull ?: System.currentTimeMillis() + constructor( + app: FirebaseApp, + data: JsonObject, + isAnonymous: Boolean = data["isAnonymous"]?.jsonPrimitive?.booleanOrNull ?: false, + email: String = data.getOrElse("email"){ null }?.jsonPrimitive?.contentOrNull ?: "" + ): this( + app = app, + isAnonymous = isAnonymous, + uid = data["uid"]?.jsonPrimitive?.contentOrNull ?: data["user_id"]?.jsonPrimitive?.contentOrNull ?: data["localId"]?.jsonPrimitive?.contentOrNull ?: "", + idToken = data["idToken"]?.jsonPrimitive?.contentOrNull ?: data.getValue("id_token").jsonPrimitive.content, + refreshToken = data["refreshToken"]?.jsonPrimitive?.contentOrNull ?: data.getValue("refresh_token").jsonPrimitive.content, + expiresIn = data["expiresIn"]?.jsonPrimitive?.intOrNull ?: data.getValue("expires_in").jsonPrimitive.int, + createdAt = data["createdAt"]?.jsonPrimitive?.longOrNull ?: System.currentTimeMillis(), + email = email ) val claims: Map by lazy { @@ -385,7 +392,7 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { signOutAndThrowInvalidUserException(body.orEmpty(), "token API returned an error: $body") } else { jsonParser.parseToJsonElement(body!!).jsonObject.apply { - val user = FirebaseUserImpl(app, this, user.isAnonymous) + val user = FirebaseUserImpl(app, this, user.isAnonymous, user.email) if (user.claims["aud"] != app.options.projectId) { signOutAndThrowInvalidUserException( user.claims.toString(), diff --git a/src/main/java/com/google/firebase/auth/FirebaseUser.kt b/src/main/java/com/google/firebase/auth/FirebaseUser.kt index 3f61dea..c76c62b 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUser.kt +++ b/src/main/java/com/google/firebase/auth/FirebaseUser.kt @@ -7,8 +7,8 @@ abstract class FirebaseUser { abstract val isAnonymous: Boolean abstract fun delete(): Task abstract fun reload(): Task + abstract val email: String - val email: String get() = TODO() val displayName: String get() = TODO() val phoneNumber: String get() = TODO() val photoUrl: String? get() = TODO() diff --git a/src/test/kotlin/FirebaseAuthTest.kt b/src/test/kotlin/FirebaseAuthTest.kt new file mode 100644 index 0000000..68b1574 --- /dev/null +++ b/src/test/kotlin/FirebaseAuthTest.kt @@ -0,0 +1,68 @@ +import android.app.Application +import com.google.firebase.Firebase +import com.google.firebase.FirebaseOptions +import com.google.firebase.FirebasePlatform +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.firestore +import com.google.firebase.initialize +import kotlinx.coroutines.tasks.await +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Before +import org.junit.Test +import java.io.File +import kotlin.random.Random + +internal class FirebaseAuthTest: FirebaseTest() { + + private lateinit var auth: FirebaseAuth + + @Before + fun initialize() { + FirebasePlatform.initializeFirebasePlatform(object : FirebasePlatform() { + val storage = mutableMapOf() + override fun store(key: String, value: String) = storage.set(key, value) + override fun retrieve(key: String) = storage[key] + override fun clear(key: String) { storage.remove(key) } + override fun log(msg: String) = println(msg) + override fun getDatabasePath(name: String) = File("./build/$name") + }) + val options = FirebaseOptions.Builder() + .setProjectId("my-firebase-project") + .setApplicationId("1:27992087142:android:ce3b6448250083d1") + .setApiKey("AIzaSyADUe90ULnQDuGShD9W23RDP0xmeDc6Mvw") + // setDatabaseURL(...) + // setStorageBucket(...) + .build() + val firebaseApp = Firebase.initialize(Application(), options) + auth = FirebaseAuth.getInstance(app = firebaseApp) + } + + @Test + fun testCreateUserWithEmailAndPassword() = runTest { + val email = "test+${Random.nextInt(100000)}@test.com" + val createResult = auth.createUserWithEmailAndPassword( + email, + "test123" + ).await() + assertNotEquals(null, createResult.user?.uid) + //assertEquals(null, createResult.user?.displayName) + //assertEquals(null, createResult.user?.phoneNumber) + assertEquals(false, createResult.user?.isAnonymous) + assertEquals(email, createResult.user?.email) + + val signInResult = auth.signInWithEmailAndPassword(email, "test123").await() + assertEquals(createResult.user?.uid, signInResult.user?.uid) + + signInResult.user!!.delete() + } + + @Test + fun testSignInAnonymously() = runTest { + val signInResult = auth.signInAnonymously().await() + assertEquals(true, signInResult.user?.isAnonymous) + signInResult.user!!.delete() + } +} \ No newline at end of file From 1ac38d7151b45a954840afa921f788752c2f13fe Mon Sep 17 00:00:00 2001 From: Jakub Kostka Date: Mon, 30 Sep 2024 09:58:23 +0200 Subject: [PATCH 03/10] "File must end with a newline (\n)" fix --- src/test/kotlin/FirebaseAuthTest.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/test/kotlin/FirebaseAuthTest.kt b/src/test/kotlin/FirebaseAuthTest.kt index 68b1574..6211481 100644 --- a/src/test/kotlin/FirebaseAuthTest.kt +++ b/src/test/kotlin/FirebaseAuthTest.kt @@ -3,11 +3,9 @@ import com.google.firebase.Firebase import com.google.firebase.FirebaseOptions import com.google.firebase.FirebasePlatform import com.google.firebase.auth.FirebaseAuth -import com.google.firebase.firestore.firestore import com.google.firebase.initialize import kotlinx.coroutines.tasks.await import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Before @@ -17,7 +15,7 @@ import kotlin.random.Random internal class FirebaseAuthTest: FirebaseTest() { - private lateinit var auth: FirebaseAuth + private lateinit var auth : FirebaseAuth @Before fun initialize() { @@ -65,4 +63,4 @@ internal class FirebaseAuthTest: FirebaseTest() { assertEquals(true, signInResult.user?.isAnonymous) signInResult.user!!.delete() } -} \ No newline at end of file +} From 6e663f0171eb1176367d35e5825576517706abed Mon Sep 17 00:00:00 2001 From: nbransby Date: Sat, 5 Oct 2024 13:28:47 +0800 Subject: [PATCH 04/10] ktlintFormat --- .../java/com/google/firebase/auth/FirebaseAuth.kt | 14 ++++---------- src/test/kotlin/FirebaseAuthTest.kt | 8 ++++---- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt index 95bd2e7..152963e 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt @@ -29,15 +29,9 @@ import kotlinx.serialization.json.intOrNull import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.longOrNull -import okhttp3.Call -import okhttp3.Callback -import okhttp3.MediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody -import okhttp3.Response +import okhttp3.* import java.io.IOException -import java.util.Base64 +import java.util.* import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.TimeUnit @@ -60,8 +54,8 @@ class FirebaseUserImpl private constructor( app: FirebaseApp, data: JsonObject, isAnonymous: Boolean = data["isAnonymous"]?.jsonPrimitive?.booleanOrNull ?: false, - email: String = data.getOrElse("email"){ null }?.jsonPrimitive?.contentOrNull ?: "" - ): this( + email: String = data.getOrElse("email") { null }?.jsonPrimitive?.contentOrNull ?: "" + ) : this( app = app, isAnonymous = isAnonymous, uid = data["uid"]?.jsonPrimitive?.contentOrNull ?: data["user_id"]?.jsonPrimitive?.contentOrNull ?: data["localId"]?.jsonPrimitive?.contentOrNull ?: "", diff --git a/src/test/kotlin/FirebaseAuthTest.kt b/src/test/kotlin/FirebaseAuthTest.kt index 6211481..6e8965a 100644 --- a/src/test/kotlin/FirebaseAuthTest.kt +++ b/src/test/kotlin/FirebaseAuthTest.kt @@ -13,9 +13,9 @@ import org.junit.Test import java.io.File import kotlin.random.Random -internal class FirebaseAuthTest: FirebaseTest() { +internal class FirebaseAuthTest : FirebaseTest() { - private lateinit var auth : FirebaseAuth + private lateinit var auth: FirebaseAuth @Before fun initialize() { @@ -46,8 +46,8 @@ internal class FirebaseAuthTest: FirebaseTest() { "test123" ).await() assertNotEquals(null, createResult.user?.uid) - //assertEquals(null, createResult.user?.displayName) - //assertEquals(null, createResult.user?.phoneNumber) + // assertEquals(null, createResult.user?.displayName) + // assertEquals(null, createResult.user?.phoneNumber) assertEquals(false, createResult.user?.isAnonymous) assertEquals(email, createResult.user?.email) From ab943b6d7b8b252940da9fdaaefac774d29803c3 Mon Sep 17 00:00:00 2001 From: nbransby Date: Sat, 5 Oct 2024 13:47:29 +0800 Subject: [PATCH 05/10] remove * imports --- src/main/java/com/google/firebase/auth/FirebaseAuth.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt index 152963e..ee51e32 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt @@ -29,9 +29,15 @@ import kotlinx.serialization.json.intOrNull import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.longOrNull -import okhttp3.* +import okhttp3.Call +import okhttp3.Callback +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response import java.io.IOException -import java.util.* +import java.util.Base64 import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.TimeUnit From d4967982ddcc4ec83bcfc22da542c892333efb11 Mon Sep 17 00:00:00 2001 From: Jakub Kostka Date: Mon, 21 Oct 2024 11:45:12 +0200 Subject: [PATCH 06/10] correct mock Firebase project implementation --- src/test/kotlin/FirebaseAuthTest.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/test/kotlin/FirebaseAuthTest.kt b/src/test/kotlin/FirebaseAuthTest.kt index 6211481..ccbaff3 100644 --- a/src/test/kotlin/FirebaseAuthTest.kt +++ b/src/test/kotlin/FirebaseAuthTest.kt @@ -28,12 +28,14 @@ internal class FirebaseAuthTest: FirebaseTest() { override fun getDatabasePath(name: String) = File("./build/$name") }) val options = FirebaseOptions.Builder() - .setProjectId("my-firebase-project") - .setApplicationId("1:27992087142:android:ce3b6448250083d1") - .setApiKey("AIzaSyADUe90ULnQDuGShD9W23RDP0xmeDc6Mvw") - // setDatabaseURL(...) - // setStorageBucket(...) + .setProjectId("fir-java-sdk") + .setApplicationId("1:341458593155:web:bf8e1aa37efe01f32d42b6") + .setApiKey("AIzaSyCvVHjTJHyeStnzIE7J9LLtHqWk6reGM08") + .setDatabaseUrl("https://fir-java-sdk-default-rtdb.firebaseio.com") + .setStorageBucket("fir-java-sdk.appspot.com") + .setGcmSenderId("341458593155") .build() + val firebaseApp = Firebase.initialize(Application(), options) auth = FirebaseAuth.getInstance(app = firebaseApp) } From e8a723e291d612b6806d3fdae44e6af790496b87 Mon Sep 17 00:00:00 2001 From: Jakub Kostka Date: Mon, 21 Oct 2024 15:53:14 +0200 Subject: [PATCH 07/10] updateEmail and verifyBeforeUpdateEmail implementation email is nullable again, check for empty string cancelation of token refresher after signout --- .../com/google/firebase/auth/FirebaseAuth.kt | 332 +++++++++++------- .../com/google/firebase/auth/FirebaseUser.kt | 6 +- .../google/firebase/auth/OobRequestType.kt | 8 + src/test/kotlin/FirebaseAuthTest.kt | 21 +- 4 files changed, 234 insertions(+), 133 deletions(-) create mode 100644 src/main/java/com/google/firebase/auth/OobRequestType.kt diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt index ee51e32..0e94f5f 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt @@ -41,10 +41,10 @@ import java.util.Base64 import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.TimeUnit -val jsonParser = Json { ignoreUnknownKeys = true } +internal val jsonParser = Json { ignoreUnknownKeys = true } @Serializable -class FirebaseUserImpl private constructor( +class FirebaseUserImpl internal constructor( @Transient private val app: FirebaseApp = FirebaseApp.getInstance(), override val isAnonymous: Boolean, @@ -53,14 +53,14 @@ class FirebaseUserImpl private constructor( val refreshToken: String, val expiresIn: Int, val createdAt: Long, - override val email: String + override val email: String? ) : FirebaseUser() { constructor( app: FirebaseApp, data: JsonObject, isAnonymous: Boolean = data["isAnonymous"]?.jsonPrimitive?.booleanOrNull ?: false, - email: String = data.getOrElse("email") { null }?.jsonPrimitive?.contentOrNull ?: "" + email: String? = data.getOrElse("email") { null }?.jsonPrimitive?.contentOrNull ) : this( app = app, isAnonymous = isAnonymous, @@ -120,24 +120,109 @@ class FirebaseUserImpl private constructor( return source.task } + override fun updateEmail(email: String): Task = FirebaseAuth.getInstance(app).updateEmail(email) + override fun reload(): Task { val source = TaskCompletionSource() FirebaseAuth.getInstance(app).refreshToken(this, source) { null } return source.task } + override fun verifyBeforeUpdateEmail( + newEmail: String, + actionCodeSettings: ActionCodeSettings? + ): Task { + val source = TaskCompletionSource() + val body = RequestBody.create( + FirebaseAuth.getInstance(app).json, + JsonObject( + mapOf( + "idToken" to JsonPrimitive(idToken), + "email" to JsonPrimitive(email), + "newEmail" to JsonPrimitive(newEmail), + "requestType" to JsonPrimitive(OobRequestType.VERIFY_AND_CHANGE_EMAIL.name) + ) + ).toString() + ) + val request = Request.Builder() + .url("https://identitytoolkit.googleapis.com/v1/accounts:sendOobCode?key=" + app.options.apiKey) + .post(body) + .build() + FirebaseAuth.getInstance(app).client.newCall(request).enqueue(object : Callback { + + override fun onFailure(call: Call, e: IOException) { + source.setException(FirebaseException(e.toString(), e)) + e.printStackTrace() + } + + @Throws(IOException::class) + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + FirebaseAuth.getInstance(app).signOut() + source.setException( + FirebaseAuth.getInstance(app).createAuthInvalidUserException( + "verifyEmail", + request, + response + ) + ) + } else { + source.setResult(null) + } + } + }) + return source.task + } + override fun getIdToken(forceRefresh: Boolean) = FirebaseAuth.getInstance(app).getAccessToken(forceRefresh) } class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { - val json = MediaType.parse("application/json; charset=utf-8") - val client: OkHttpClient = OkHttpClient.Builder() + internal val json = MediaType.parse("application/json; charset=utf-8") + internal val client: OkHttpClient = OkHttpClient.Builder() .connectTimeout(60, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .writeTimeout(60, TimeUnit.SECONDS) .build() + private fun enqueueAuthPost( + url: String, + body: RequestBody, + setResult: (responseBody: String) -> FirebaseUserImpl? + ): TaskCompletionSource { + val source = TaskCompletionSource() + val request = Request.Builder() + .url("$url?key=" + app.options.apiKey) + .post(body) + .build() + + client.newCall(request).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + source.setException(FirebaseException(e.toString(), e)) + } + + @Throws(IOException::class) + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + source.setException( + createAuthInvalidUserException("accounts", request, response) + ) + } else { + if(response.body()?.use { it.string() }?.also { responseBody -> + user = setResult(responseBody) + source.setResult(AuthResult { user }) + } == null) { + source.setException( + createAuthInvalidUserException("accounts", request, response) + ) + } + } + } + }) + return source + } + companion object { @JvmStatic @@ -145,6 +230,8 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { @JvmStatic fun getInstance(app: FirebaseApp): FirebaseAuth = app.get(FirebaseAuth::class.java) + + private const val REFRESH_TOKEN_TAG = "refresh_token_tag" } private val internalIdTokenListeners = CopyOnWriteArrayList() @@ -192,127 +279,67 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { } fun signInAnonymously(): Task { - val source = TaskCompletionSource() - val body = RequestBody.create(json, JsonObject(mapOf("returnSecureToken" to JsonPrimitive(true))).toString()) - val request = Request.Builder() - .url("https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=" + app.options.apiKey) - .post(body) - .build() - client.newCall(request).enqueue(object : Callback { - - override fun onFailure(call: Call, e: IOException) { - source.setException(FirebaseException(e.toString(), e)) + val source = enqueueAuthPost( + url = "https://identitytoolkit.googleapis.com/v1/accounts:signUp", + body = RequestBody.create(json, JsonObject(mapOf("returnSecureToken" to JsonPrimitive(true))).toString()), + setResult = { responseBody -> + FirebaseUserImpl(app, jsonParser.parseToJsonElement(responseBody).jsonObject, isAnonymous = true) } - - @Throws(IOException::class) - override fun onResponse(call: Call, response: Response) { - if (!response.isSuccessful) { - source.setException( - createAuthInvalidUserException("accounts:signUp", request, response) - ) - } else { - val body = response.body()!!.use { it.string() } - user = FirebaseUserImpl(app, jsonParser.parseToJsonElement(body).jsonObject, true) - source.setResult(AuthResult { user }) - } - } - }) + ) return source.task } fun signInWithCustomToken(customToken: String): Task { - val source = TaskCompletionSource() - val body = RequestBody.create( - json, - JsonObject(mapOf("token" to JsonPrimitive(customToken), "returnSecureToken" to JsonPrimitive(true))).toString() - ) - val request = Request.Builder() - .url("https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key=" + app.options.apiKey) - .post(body) - .build() - client.newCall(request).enqueue(object : Callback { - - override fun onFailure(call: Call, e: IOException) { - source.setException(FirebaseException(e.toString(), e)) + val source = enqueueAuthPost( + url = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken", + body = RequestBody.create( + json, + JsonObject(mapOf("token" to JsonPrimitive(customToken), "returnSecureToken" to JsonPrimitive(true))).toString() + ), + setResult = { responseBody -> + FirebaseUserImpl(app, jsonParser.parseToJsonElement(responseBody).jsonObject) } - - @Throws(IOException::class) - override fun onResponse(call: Call, response: Response) { - if (!response.isSuccessful) { - source.setException( - createAuthInvalidUserException("verifyCustomToken", request, response) - ) - } else { - val body = response.body()!!.use { it.string() } - val user = FirebaseUserImpl(app, jsonParser.parseToJsonElement(body).jsonObject) - refreshToken(user, source) { AuthResult { it } } - } - } - }) + ) return source.task } fun createUserWithEmailAndPassword(email: String, password: String): Task { - val source = TaskCompletionSource() - val body = RequestBody.create( - json, - JsonObject(mapOf("email" to JsonPrimitive(email), "password" to JsonPrimitive(password), "returnSecureToken" to JsonPrimitive(true))).toString() - ) - val request = Request.Builder() - .url("https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser?key=" + app.options.apiKey) - .post(body) - .build() - client.newCall(request).enqueue(object : Callback { - - override fun onFailure(call: Call, e: IOException) { - source.setException(FirebaseException(e.toString(), e)) - } - - @Throws(IOException::class) - override fun onResponse(call: Call, response: Response) { - if (!response.isSuccessful) { - source.setException( - createAuthInvalidUserException("signupNewUser", request, response) + val source = enqueueAuthPost( + url = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser", + body = RequestBody.create( + json, + JsonObject( + mapOf( + "email" to JsonPrimitive(email), + "password" to JsonPrimitive(password), + "returnSecureToken" to JsonPrimitive(true) ) - } else { - val responseBody = response.body()?.use { it.string() } ?: "" - val user = FirebaseUserImpl(app, jsonParser.parseToJsonElement(responseBody).jsonObject) - refreshToken(user, source) { AuthResult { it } } - } + ).toString() + ), + setResult = { responseBody -> + FirebaseUserImpl(app, jsonParser.parseToJsonElement(responseBody).jsonObject) } - }) + ) return source.task } fun signInWithEmailAndPassword(email: String, password: String): Task { - val source = TaskCompletionSource() - val body = RequestBody.create( - json, - JsonObject(mapOf("email" to JsonPrimitive(email), "password" to JsonPrimitive(password), "returnSecureToken" to JsonPrimitive(true))).toString() - ) - val request = Request.Builder() - .url("https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=" + app.options.apiKey) - .post(body) - .build() - client.newCall(request).enqueue(object : Callback { - - override fun onFailure(call: Call, e: IOException) { - source.setException(FirebaseException(e.toString(), e)) - } - - @Throws(IOException::class) - override fun onResponse(call: Call, response: Response) { - if (!response.isSuccessful) { - source.setException( - createAuthInvalidUserException("verifyPassword", request, response) + val source = enqueueAuthPost( + url = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword", + body = RequestBody.create( + json, + JsonObject( + mapOf( + "email" to JsonPrimitive(email), + "password" to JsonPrimitive(password), + "returnSecureToken" to JsonPrimitive(true) ) - } else { - val body = response.body()!!.use { it.string() } - val user = FirebaseUserImpl(app, jsonParser.parseToJsonElement(body).jsonObject) - refreshToken(user, source) { AuthResult { it } } - } + ).toString() + ), + setResult = { responseBody -> + FirebaseUserImpl(app, jsonParser.parseToJsonElement(responseBody).jsonObject) } - }) + ) return source.task } @@ -336,7 +363,10 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { } fun signOut() { - // todo cancel token refresher + // cancel token refresher + client.dispatcher().queuedCalls().find { it.request().tag() == REFRESH_TOKEN_TAG }?.cancel() ?: { + client.dispatcher().runningCalls().find { it.request().tag() == REFRESH_TOKEN_TAG }?.cancel() + } user = null } @@ -377,6 +407,7 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { val request = Request.Builder() .url("https://securetoken.googleapis.com/v1/token?key=" + app.options.apiKey) .post(body) + .tag(REFRESH_TOKEN_TAG) .build() client.newCall(request).enqueue(object : Callback { @@ -387,20 +418,21 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { @Throws(IOException::class) override fun onResponse(call: Call, response: Response) { - val body = response.body()?.use { it.string() } - if (!response.isSuccessful) { - signOutAndThrowInvalidUserException(body.orEmpty(), "token API returned an error: $body") + val responseBody = response.body()?.use { it.string() } + + if (!response.isSuccessful || responseBody == null) { + signOutAndThrowInvalidUserException(responseBody.orEmpty(), "token API returned an error: $body") } else { - jsonParser.parseToJsonElement(body!!).jsonObject.apply { - val user = FirebaseUserImpl(app, this, user.isAnonymous, user.email) - if (user.claims["aud"] != app.options.projectId) { + jsonParser.parseToJsonElement(responseBody).jsonObject.apply { + val newUser = FirebaseUserImpl(app, this, user.isAnonymous, user.email) + if (newUser.claims["aud"] != app.options.projectId) { signOutAndThrowInvalidUserException( - user.claims.toString(), - "Project ID's do not match ${user.claims["aud"]} != ${app.options.projectId}" + newUser.claims.toString(), + "Project ID's do not match ${newUser.claims["aud"]} != ${app.options.projectId}" ) } else { - this@FirebaseAuth.user = user - source.setResult(user) + this@FirebaseAuth.user = newUser + source.setResult(newUser) } } } @@ -414,6 +446,65 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { return source } + internal fun updateEmail(email: String): Task { + val source = TaskCompletionSource() + + val body = RequestBody.create( + json, + JsonObject( + mapOf( + "idToken" to JsonPrimitive(user?.idToken), + "email" to JsonPrimitive(email), + "returnSecureToken" to JsonPrimitive(true) + ) + ).toString() + ) + val request = Request.Builder() + .url("https://identitytoolkit.googleapis.com/v1/accounts:update?key=" + app.options.apiKey) + .post(body) + .build() + + client.newCall(request).enqueue(object : Callback { + + override fun onFailure(call: Call, e: IOException) { + source.setException(FirebaseException(e.toString(), e)) + } + + @Throws(IOException::class) + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + signOut() + source.setException( + createAuthInvalidUserException( + "updateEmail", + request, + response + ) + ) + } else { + val newBody = jsonParser.parseToJsonElement( + response.body()?.use { it.string() } ?: "" + ).jsonObject + + user?.let { prev -> + user = FirebaseUserImpl( + app = app, + isAnonymous = prev.isAnonymous, + uid = prev.uid, + idToken = newBody["idToken"]?.jsonPrimitive?.contentOrNull ?: prev.idToken, + refreshToken = newBody["refreshToken"]?.jsonPrimitive?.contentOrNull ?: prev.refreshToken, + expiresIn = newBody["expiresIn"]?.jsonPrimitive?.intOrNull ?: prev.expiresIn, + createdAt = prev.createdAt, + email = newBody["newEmail"]?.jsonPrimitive?.contentOrNull ?: prev.email + ) + } + source.setResult(null) + } + } + }) + return source.task + } + override fun getUid(): String? { return user?.uid } @@ -464,7 +555,6 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { } fun sendPasswordResetEmail(email: String, settings: ActionCodeSettings?): Task = TODO() - fun signInWithCredential(authCredential: AuthCredential): Task = TODO() fun checkActionCode(code: String): Task = TODO() fun confirmPasswordReset(code: String, newPassword: String): Task = TODO() fun fetchSignInMethodsForEmail(email: String): Task = TODO() diff --git a/src/main/java/com/google/firebase/auth/FirebaseUser.kt b/src/main/java/com/google/firebase/auth/FirebaseUser.kt index c76c62b..50e4e52 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUser.kt +++ b/src/main/java/com/google/firebase/auth/FirebaseUser.kt @@ -4,10 +4,12 @@ import com.google.android.gms.tasks.Task abstract class FirebaseUser { abstract val uid: String + abstract val email: String? abstract val isAnonymous: Boolean abstract fun delete(): Task abstract fun reload(): Task - abstract val email: String + abstract fun verifyBeforeUpdateEmail(newEmail: String, actionCodeSettings: ActionCodeSettings?): Task + abstract fun updateEmail(email: String): Task val displayName: String get() = TODO() val phoneNumber: String get() = TODO() @@ -22,11 +24,9 @@ abstract class FirebaseUser { fun sendEmailVerification(): Task = TODO() fun sendEmailVerification(actionCodeSettings: ActionCodeSettings): Task = TODO() fun unlink(provider: String): Task = TODO() - fun updateEmail(email: String): Task = TODO() fun updatePassword(password: String): Task = TODO() fun updatePhoneNumber(credential: AuthCredential): Task = TODO() fun updateProfile(request: UserProfileChangeRequest): Task = TODO() - fun verifyBeforeUpdateEmail(newEmail: String, actionCodeSettings: ActionCodeSettings?): Task = TODO() fun reauthenticate(credential: AuthCredential): Task = TODO() fun reauthenticateAndRetrieveData(credential: AuthCredential): Task = TODO() } diff --git a/src/main/java/com/google/firebase/auth/OobRequestType.kt b/src/main/java/com/google/firebase/auth/OobRequestType.kt new file mode 100644 index 0000000..f0c31c3 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/OobRequestType.kt @@ -0,0 +1,8 @@ +package com.google.firebase.auth + +internal enum class OobRequestType { + PASSWORD_RESET, + EMAIL_SIGNIN, + VERIFY_EMAIL, + VERIFY_AND_CHANGE_EMAIL +} \ No newline at end of file diff --git a/src/test/kotlin/FirebaseAuthTest.kt b/src/test/kotlin/FirebaseAuthTest.kt index 2925db4..029cd11 100644 --- a/src/test/kotlin/FirebaseAuthTest.kt +++ b/src/test/kotlin/FirebaseAuthTest.kt @@ -1,3 +1,4 @@ + import android.app.Application import com.google.firebase.Firebase import com.google.firebase.FirebaseOptions @@ -6,12 +7,13 @@ import com.google.firebase.auth.FirebaseAuth import com.google.firebase.initialize import kotlinx.coroutines.tasks.await import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Before import org.junit.Test import java.io.File -import kotlin.random.Random +import java.util.UUID internal class FirebaseAuthTest : FirebaseTest() { @@ -40,29 +42,30 @@ internal class FirebaseAuthTest : FirebaseTest() { auth = FirebaseAuth.getInstance(app = firebaseApp) } + @After + fun clear() { + auth.currentUser?.delete() + } + @Test fun testCreateUserWithEmailAndPassword() = runTest { - val email = "test+${Random.nextInt(100000)}@test.com" - val createResult = auth.createUserWithEmailAndPassword( - email, - "test123" - ).await() + val email = "test+${UUID.randomUUID()}@test.com" + val createResult = auth.createUserWithEmailAndPassword(email, "test123").await() assertNotEquals(null, createResult.user?.uid) // assertEquals(null, createResult.user?.displayName) // assertEquals(null, createResult.user?.phoneNumber) assertEquals(false, createResult.user?.isAnonymous) assertEquals(email, createResult.user?.email) + assertNotEquals("", createResult.user!!.email) val signInResult = auth.signInWithEmailAndPassword(email, "test123").await() assertEquals(createResult.user?.uid, signInResult.user?.uid) - - signInResult.user!!.delete() } @Test fun testSignInAnonymously() = runTest { val signInResult = auth.signInAnonymously().await() + assertNotEquals("", signInResult.user!!.email) assertEquals(true, signInResult.user?.isAnonymous) - signInResult.user!!.delete() } } From e51997d384cafe9024eae7e59a73e9d544c77306 Mon Sep 17 00:00:00 2001 From: Jakub Kostka Date: Mon, 21 Oct 2024 16:02:26 +0200 Subject: [PATCH 08/10] added todo --- src/main/java/com/google/firebase/auth/FirebaseAuth.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt index 0e94f5f..ba7543b 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt @@ -128,6 +128,7 @@ class FirebaseUserImpl internal constructor( return source.task } + //TODO implement ActionCodeSettings and pass it to the url override fun verifyBeforeUpdateEmail( newEmail: String, actionCodeSettings: ActionCodeSettings? From 25efe1cfd9e6d76db84688f8e7cae50b9e95d350 Mon Sep 17 00:00:00 2001 From: Jakub Kostka Date: Thu, 7 Nov 2024 16:11:35 +0100 Subject: [PATCH 09/10] ktlint formatting + update of user data after signInWithCustomToken + photoUrl and displayName implementation --- src/main/java/android/net/Uri.kt | 7 +- .../com/google/firebase/auth/FirebaseAuth.kt | 808 ++++++++++++------ .../com/google/firebase/auth/FirebaseUser.kt | 27 +- .../google/firebase/auth/OobRequestType.kt | 2 +- .../auth/UserProfileChangeRequest.java | 18 - .../firebase/auth/UserProfileChangeRequest.kt | 47 + src/test/kotlin/AppTest.kt | 42 +- src/test/kotlin/AuthTest.kt | 92 +- src/test/kotlin/FirebaseAuthTest.kt | 100 +-- src/test/kotlin/FirebaseTest.kt | 67 +- 10 files changed, 814 insertions(+), 396 deletions(-) delete mode 100644 src/main/java/com/google/firebase/auth/UserProfileChangeRequest.java create mode 100644 src/main/java/com/google/firebase/auth/UserProfileChangeRequest.kt diff --git a/src/main/java/android/net/Uri.kt b/src/main/java/android/net/Uri.kt index 851faa2..c14bc76 100644 --- a/src/main/java/android/net/Uri.kt +++ b/src/main/java/android/net/Uri.kt @@ -3,13 +3,16 @@ package android.net import java.net.URI import java.util.Collections -class Uri(private val uri: URI) { - +class Uri( + private val uri: URI, +) { companion object { @JvmStatic fun parse(uriString: String) = Uri(URI.create(uriString)) } + override fun toString(): String = uri.toString() + val scheme get() = uri.scheme val port get() = uri.port val host get() = uri.host diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt index 8442c1e..5492c67 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt @@ -1,5 +1,6 @@ package com.google.firebase.auth +import android.net.Uri import android.util.Log import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.TaskCompletionSource @@ -45,11 +46,9 @@ internal val jsonParser = Json { ignoreUnknownKeys = true } class UrlFactory( private val app: FirebaseApp, - private val emulatorUrl: String? = null + private val emulatorUrl: String? = null, ) { - fun buildUrl(uri: String): String { - return "${emulatorUrl ?: "https://"}$uri?key=${app.options.apiKey}" - } + fun buildUrl(uri: String): String = "${emulatorUrl ?: "https://"}$uri?key=${app.options.apiKey}" } @Serializable @@ -63,26 +62,34 @@ class FirebaseUserImpl internal constructor( val expiresIn: Int, val createdAt: Long, override val email: String?, + override val photoUrl: String?, + override val displayName: String?, @Transient - private val urlFactory: UrlFactory = UrlFactory(app) + private val urlFactory: UrlFactory = UrlFactory(app), ) : FirebaseUser() { - constructor( app: FirebaseApp, data: JsonObject, isAnonymous: Boolean = data["isAnonymous"]?.jsonPrimitive?.booleanOrNull ?: false, email: String? = data.getOrElse("email") { null }?.jsonPrimitive?.contentOrNull, - urlFactory: UrlFactory = UrlFactory(app) + photoUrl: String? = data.getOrElse("photoUrl") { null }?.jsonPrimitive?.contentOrNull, + displayName: String? = data.getOrElse("displayName") { null }?.jsonPrimitive?.contentOrNull, + urlFactory: UrlFactory = UrlFactory(app), ) : this( app = app, isAnonymous = isAnonymous, - uid = data["uid"]?.jsonPrimitive?.contentOrNull ?: data["user_id"]?.jsonPrimitive?.contentOrNull ?: data["localId"]?.jsonPrimitive?.contentOrNull ?: "", + uid = + data["uid"]?.jsonPrimitive?.contentOrNull ?: data["user_id"]?.jsonPrimitive?.contentOrNull + ?: data["localId"]?.jsonPrimitive?.contentOrNull + ?: "", idToken = data["idToken"]?.jsonPrimitive?.contentOrNull ?: data.getValue("id_token").jsonPrimitive.content, refreshToken = data["refreshToken"]?.jsonPrimitive?.contentOrNull ?: data.getValue("refresh_token").jsonPrimitive.content, expiresIn = data["expiresIn"]?.jsonPrimitive?.intOrNull ?: data.getValue("expires_in").jsonPrimitive.int, createdAt = data["createdAt"]?.jsonPrimitive?.longOrNull ?: System.currentTimeMillis(), email = email, - urlFactory = urlFactory + photoUrl = photoUrl ?: data["photo_url"]?.jsonPrimitive?.contentOrNull, + displayName = displayName ?: data["display_name"]?.jsonPrimitive?.contentOrNull, + urlFactory = urlFactory, ) internal val claims: Map by lazy { @@ -93,43 +100,53 @@ class FirebaseUserImpl internal constructor( .orEmpty() } - internal val JsonElement.value get(): Any? = when (this) { - is JsonNull -> null - is JsonArray -> map { it.value } - is JsonObject -> jsonObject.mapValues { (_, it) -> it.value } - is JsonPrimitive -> booleanOrNull ?: doubleOrNull ?: content - else -> TODO() - } + internal val JsonElement.value get(): Any? = + when (this) { + is JsonNull -> null + is JsonArray -> map { it.value } + is JsonObject -> jsonObject.mapValues { (_, it) -> it.value } + is JsonPrimitive -> booleanOrNull ?: doubleOrNull ?: content + else -> TODO() + } override fun delete(): Task { val source = TaskCompletionSource() val body = RequestBody.create(FirebaseAuth.getInstance(app).json, JsonObject(mapOf("idToken" to JsonPrimitive(idToken))).toString()) - val request = Request.Builder() - .url(urlFactory.buildUrl("www.googleapis.com/identitytoolkit/v3/relyingparty/deleteAccount")) - .post(body) - .build() - FirebaseAuth.getInstance(app).client.newCall(request).enqueue(object : Callback { - - override fun onFailure(call: Call, e: IOException) { - source.setException(FirebaseException(e.toString(), e)) - } + val request = + Request + .Builder() + .url(urlFactory.buildUrl("www.googleapis.com/identitytoolkit/v3/relyingparty/deleteAccount")) + .post(body) + .build() + FirebaseAuth.getInstance(app).client.newCall(request).enqueue( + object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + source.setException(FirebaseException(e.toString(), e)) + } - @Throws(IOException::class) - override fun onResponse(call: Call, response: Response) { - if (!response.isSuccessful) { - FirebaseAuth.getInstance(app).signOut() - source.setException( - FirebaseAuth.getInstance(app).createAuthInvalidUserException( - "deleteAccount", - request, - response + @Throws(IOException::class) + override fun onResponse( + call: Call, + response: Response, + ) { + if (!response.isSuccessful) { + FirebaseAuth.getInstance(app).signOut() + source.setException( + FirebaseAuth.getInstance(app).createAuthInvalidUserException( + "deleteAccount", + request, + response, + ), ) - ) - } else { - source.setResult(null) + } else { + source.setResult(null) + } } - } - }) + }, + ) return source.task } @@ -141,104 +158,142 @@ class FirebaseUserImpl internal constructor( return source.task } - //TODO implement ActionCodeSettings and pass it to the url + // TODO implement ActionCodeSettings and pass it to the url override fun verifyBeforeUpdateEmail( newEmail: String, - actionCodeSettings: ActionCodeSettings? + actionCodeSettings: ActionCodeSettings?, ): Task { val source = TaskCompletionSource() - val body = RequestBody.create( - FirebaseAuth.getInstance(app).json, - JsonObject( - mapOf( - "idToken" to JsonPrimitive(idToken), - "email" to JsonPrimitive(email), - "newEmail" to JsonPrimitive(newEmail), - "requestType" to JsonPrimitive(OobRequestType.VERIFY_AND_CHANGE_EMAIL.name) - ) - ).toString() - ) - val request = Request.Builder() - .url("https://identitytoolkit.googleapis.com/v1/accounts:sendOobCode?key=" + app.options.apiKey) - .post(body) - .build() - FirebaseAuth.getInstance(app).client.newCall(request).enqueue(object : Callback { - - override fun onFailure(call: Call, e: IOException) { - source.setException(FirebaseException(e.toString(), e)) - e.printStackTrace() - } + val body = + RequestBody.create( + FirebaseAuth.getInstance(app).json, + JsonObject( + mapOf( + "idToken" to JsonPrimitive(idToken), + "email" to JsonPrimitive(email), + "newEmail" to JsonPrimitive(newEmail), + "requestType" to JsonPrimitive(OobRequestType.VERIFY_AND_CHANGE_EMAIL.name), + ), + ).toString(), + ) + val request = + Request + .Builder() + .url(urlFactory.buildUrl("identitytoolkit.googleapis.com/v1/accounts:sendOobCode")) + .post(body) + .build() + FirebaseAuth.getInstance(app).client.newCall(request).enqueue( + object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + source.setException(FirebaseException(e.toString(), e)) + e.printStackTrace() + } - @Throws(IOException::class) - override fun onResponse(call: Call, response: Response) { - if (!response.isSuccessful) { - FirebaseAuth.getInstance(app).signOut() - source.setException( - FirebaseAuth.getInstance(app).createAuthInvalidUserException( - "verifyEmail", - request, - response + @Throws(IOException::class) + override fun onResponse( + call: Call, + response: Response, + ) { + if (!response.isSuccessful) { + FirebaseAuth.getInstance(app).signOut() + source.setException( + FirebaseAuth.getInstance(app).createAuthInvalidUserException( + "verifyEmail", + request, + response, + ), ) - ) - } else { - source.setResult(null) + } else { + source.setResult(null) + } } - } - }) + }, + ) return source.task } override fun getIdToken(forceRefresh: Boolean) = FirebaseAuth.getInstance(app).getAccessToken(forceRefresh) -} -class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { + override fun updateProfile(request: UserProfileChangeRequest): Task = FirebaseAuth.getInstance(app).updateProfile(request) + + fun updateProfile( + displayName: String?, + photoUrl: String?, + ): Task { + val request = + UserProfileChangeRequest + .Builder() + .apply { setDisplayName(displayName) } + .apply { setPhotoUri(photoUrl?.let { Uri.parse(it) }) } + .build() + return FirebaseAuth.getInstance(app).updateProfile(request) + } +} +class FirebaseAuth constructor( + val app: FirebaseApp, +) : InternalAuthProvider { internal val json = MediaType.parse("application/json; charset=utf-8") - internal val client: OkHttpClient = OkHttpClient.Builder() - .connectTimeout(60, TimeUnit.SECONDS) - .readTimeout(60, TimeUnit.SECONDS) - .writeTimeout(60, TimeUnit.SECONDS) - .build() + internal val client: OkHttpClient = + OkHttpClient + .Builder() + .connectTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) + .build() private fun enqueueAuthPost( url: String, body: RequestBody, - setResult: (responseBody: String) -> FirebaseUserImpl? + setResult: (responseBody: String) -> FirebaseUserImpl?, ): TaskCompletionSource { val source = TaskCompletionSource() - val request = Request.Builder() - .url("$url?key=" + app.options.apiKey) - .post(body) - .build() - - client.newCall(request).enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) { - source.setException(FirebaseException(e.toString(), e)) - } + val request = + Request + .Builder() + .url(urlFactory.buildUrl(url)) + .post(body) + .build() + + client.newCall(request).enqueue( + object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + source.setException(FirebaseException(e.toString(), e)) + } - @Throws(IOException::class) - override fun onResponse(call: Call, response: Response) { - if (!response.isSuccessful) { - source.setException( - createAuthInvalidUserException("accounts", request, response) - ) - } else { - if(response.body()?.use { it.string() }?.also { responseBody -> - user = setResult(responseBody) - source.setResult(AuthResult { user }) - } == null) { + @Throws(IOException::class) + override fun onResponse( + call: Call, + response: Response, + ) { + if (!response.isSuccessful) { source.setException( - createAuthInvalidUserException("accounts", request, response) + createAuthInvalidUserException("accounts", request, response), ) + } else { + if (response.body()?.use { it.string() }?.also { responseBody -> + user = setResult(responseBody) + source.setResult(AuthResult { user }) + } == null + ) { + source.setException( + createAuthInvalidUserException("accounts", request, response), + ) + } } } - } - }) + }, + ) return source } companion object { - @JvmStatic fun getInstance(): FirebaseAuth = getInstance(FirebaseApp.getInstance()) @@ -257,10 +312,11 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { val FirebaseApp.key get() = "com.google.firebase.auth.FIREBASE_USER${"[$name]".takeUnless { isDefaultApp }.orEmpty()}" - private var user: FirebaseUserImpl? = FirebasePlatform.firebasePlatform - .runCatching { retrieve(app.key)?.let { FirebaseUserImpl(app, jsonParser.parseToJsonElement(it).jsonObject) } } - .onFailure { it.printStackTrace() } - .getOrNull() + private var user: FirebaseUserImpl? = + FirebasePlatform.firebasePlatform + .runCatching { retrieve(app.key)?.let { FirebaseUserImpl(app, data = jsonParser.parseToJsonElement(it).jsonObject) } } + .onFailure { it.printStackTrace() } + .getOrNull() private set(value) { if (field != value) { @@ -295,93 +351,185 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { private var urlFactory = UrlFactory(app) fun signInAnonymously(): Task { - val source = enqueueAuthPost( - url = "https://identitytoolkit.googleapis.com/v1/accounts:signUp", - body = RequestBody.create(json, JsonObject(mapOf("returnSecureToken" to JsonPrimitive(true))).toString()), - setResult = { responseBody -> - FirebaseUserImpl(app, jsonParser.parseToJsonElement(responseBody).jsonObject, isAnonymous = true) - } - ) + val source = + enqueueAuthPost( + url = "identitytoolkit.googleapis.com/v1/accounts:signUp", + body = RequestBody.create(json, JsonObject(mapOf("returnSecureToken" to JsonPrimitive(true))).toString()), + setResult = { responseBody -> + FirebaseUserImpl(app, jsonParser.parseToJsonElement(responseBody).jsonObject, isAnonymous = true) + }, + ) return source.task } fun signInWithCustomToken(customToken: String): Task { - val source = enqueueAuthPost( - url = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken", - body = RequestBody.create( - json, - JsonObject(mapOf("token" to JsonPrimitive(customToken), "returnSecureToken" to JsonPrimitive(true))).toString() - ), - setResult = { responseBody -> - FirebaseUserImpl(app, jsonParser.parseToJsonElement(responseBody).jsonObject) + val source = + enqueueAuthPost( + url = "www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken", + body = + RequestBody.create( + json, + JsonObject(mapOf("token" to JsonPrimitive(customToken), "returnSecureToken" to JsonPrimitive(true))).toString(), + ), + setResult = { responseBody -> + FirebaseUserImpl(app, jsonParser.parseToJsonElement(responseBody).jsonObject) + }, + ).task.continueWith { + updateByGetAccountInfo() } - ) - return source.task + return source.result } - fun createUserWithEmailAndPassword(email: String, password: String): Task { - val source = enqueueAuthPost( - url = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser", - body = RequestBody.create( + internal fun updateByGetAccountInfo(): Task { + val source = TaskCompletionSource() + + val body = + RequestBody.create( json, - JsonObject( - mapOf( - "email" to JsonPrimitive(email), - "password" to JsonPrimitive(password), - "returnSecureToken" to JsonPrimitive(true) - ) - ).toString() - ), - setResult = { responseBody -> - FirebaseUserImpl(app, jsonParser.parseToJsonElement(responseBody).jsonObject) - } + JsonObject(mapOf("idToken" to JsonPrimitive(user?.idToken))).toString(), + ) + val request = + Request + .Builder() + .url(urlFactory.buildUrl("www.googleapis.com/identitytoolkit/v3/relyingparty/getAccountInfo")) + .post(body) + .build() + + client.newCall(request).enqueue( + object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + source.setException(FirebaseException(e.toString(), e)) + } + + @Throws(IOException::class) + override fun onResponse( + call: Call, + response: Response, + ) { + if (!response.isSuccessful) { + source.setException( + createAuthInvalidUserException("updateWithAccountInfo", request, response), + ) + } else { + val newBody = + jsonParser + .parseToJsonElement( + response.body()?.use { it.string() } ?: "", + ).jsonObject + + user?.let { prev -> + user = + FirebaseUserImpl( + app = app, + isAnonymous = prev.isAnonymous, + uid = prev.uid, + idToken = prev.idToken, + refreshToken = prev.refreshToken, + expiresIn = prev.expiresIn, + createdAt = newBody["createdAt"]?.jsonPrimitive?.longOrNull ?: prev.createdAt, + email = newBody["email"]?.jsonPrimitive?.contentOrNull ?: prev.email, + photoUrl = newBody["photoUrl"]?.jsonPrimitive?.contentOrNull ?: prev.photoUrl, + displayName = newBody["displayName"]?.jsonPrimitive?.contentOrNull ?: prev.displayName, + ) + source.setResult(AuthResult { user }) + } + source.setResult(null) + } + } + }, ) return source.task } - fun signInWithEmailAndPassword(email: String, password: String): Task { - val source = enqueueAuthPost( - url = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword", - body = RequestBody.create( - json, - JsonObject( - mapOf( - "email" to JsonPrimitive(email), - "password" to JsonPrimitive(password), - "returnSecureToken" to JsonPrimitive(true) + fun createUserWithEmailAndPassword( + email: String, + password: String, + ): Task { + val source = + enqueueAuthPost( + url = "www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser", + body = + RequestBody.create( + json, + JsonObject( + mapOf( + "email" to JsonPrimitive(email), + "password" to JsonPrimitive(password), + "returnSecureToken" to JsonPrimitive(true), + ), + ).toString(), + ), + setResult = { responseBody -> + FirebaseUserImpl( + app = app, + data = jsonParser.parseToJsonElement(responseBody).jsonObject, ) - ).toString() - ), - setResult = { responseBody -> - FirebaseUserImpl(app, jsonParser.parseToJsonElement(responseBody).jsonObject) - } - ) + }, + ) + return source.task + } + + fun signInWithEmailAndPassword( + email: String, + password: String, + ): Task { + val source = + enqueueAuthPost( + url = "www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword", + body = + RequestBody.create( + json, + JsonObject( + mapOf( + "email" to JsonPrimitive(email), + "password" to JsonPrimitive(password), + "returnSecureToken" to JsonPrimitive(true), + ), + ).toString(), + ), + setResult = { responseBody -> + FirebaseUserImpl(app, jsonParser.parseToJsonElement(responseBody).jsonObject) + }, + ) return source.task } internal fun createAuthInvalidUserException( action: String, request: Request, - response: Response + response: Response, ): FirebaseAuthInvalidUserException { val body = response.body()!!.use { it.string() } val jsonObject = jsonParser.parseToJsonElement(body).jsonObject return FirebaseAuthInvalidUserException( - jsonObject["error"]?.jsonObject - ?.get("message")?.jsonPrimitive + jsonObject["error"] + ?.jsonObject + ?.get("message") + ?.jsonPrimitive ?.contentOrNull ?: "UNKNOWN_ERROR", "$action API returned an error, " + - "with url [${request.method()}] ${request.url()} ${request.body()} -- " + - "response [${response.code()}] ${response.message()} $body" + "with url [${request.method()}] ${request.url()} ${request.body()} -- " + + "response [${response.code()}] ${response.message()} $body", ) } fun signOut() { // cancel token refresher - client.dispatcher().queuedCalls().find { it.request().tag() == REFRESH_TOKEN_TAG }?.cancel() ?: { - client.dispatcher().runningCalls().find { it.request().tag() == REFRESH_TOKEN_TAG }?.cancel() + client + .dispatcher() + .queuedCalls() + .find { it.request().tag() == REFRESH_TOKEN_TAG } + ?.cancel() ?: { + client + .dispatcher() + .runningCalls() + .find { it.request().tag() == REFRESH_TOKEN_TAG } + ?.cancel() } user = null } @@ -401,7 +549,11 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { private var refreshSource = TaskCompletionSource().apply { setException(Exception()) } - internal fun refreshToken(user: FirebaseUserImpl, source: TaskCompletionSource, map: (user: FirebaseUserImpl) -> T?) { + internal fun refreshToken( + user: FirebaseUserImpl, + source: TaskCompletionSource, + map: (user: FirebaseUserImpl) -> T?, + ) { refreshSource = refreshSource .takeUnless { it.task.isComplete } ?: enqueueRefreshTokenCall(user) @@ -411,119 +563,220 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { private fun enqueueRefreshTokenCall(user: FirebaseUserImpl): TaskCompletionSource { val source = TaskCompletionSource() - val body = RequestBody.create( - json, - JsonObject( - mapOf( - "refresh_token" to JsonPrimitive(user.refreshToken), - "grant_type" to JsonPrimitive("refresh_token") - ) - ).toString() - ) - val request = Request.Builder() - .url(urlFactory.buildUrl("securetoken.googleapis.com/v1/token")) - .post(body) - .tag(REFRESH_TOKEN_TAG) - .build() - - client.newCall(request).enqueue(object : Callback { - - override fun onFailure(call: Call, e: IOException) { - source.setException(e) - } - - @Throws(IOException::class) - override fun onResponse(call: Call, response: Response) { - val responseBody = response.body()?.use { it.string() } + val body = + RequestBody.create( + json, + JsonObject( + mapOf( + "refresh_token" to JsonPrimitive(user.refreshToken), + "grant_type" to JsonPrimitive("refresh_token"), + ), + ).toString(), + ) + val request = + Request + .Builder() + .url(urlFactory.buildUrl("securetoken.googleapis.com/v1/token")) + .post(body) + .tag(REFRESH_TOKEN_TAG) + .build() + + client.newCall(request).enqueue( + object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + source.setException(e) + } - if (!response.isSuccessful || responseBody == null) { - signOutAndThrowInvalidUserException(responseBody.orEmpty(), "token API returned an error: $body") - } else { - jsonParser.parseToJsonElement(responseBody).jsonObject.apply { - val newUser = FirebaseUserImpl(app, this, user.isAnonymous, user.email) - if (newUser.claims["aud"] != app.options.projectId) { - signOutAndThrowInvalidUserException( - newUser.claims.toString(), - "Project ID's do not match ${newUser.claims["aud"]} != ${app.options.projectId}" - ) - } else { - this@FirebaseAuth.user = newUser - source.setResult(newUser) + @Throws(IOException::class) + override fun onResponse( + call: Call, + response: Response, + ) { + val responseBody = response.body()?.use { it.string() } + + if (!response.isSuccessful || responseBody == null) { + signOutAndThrowInvalidUserException(responseBody.orEmpty(), "token API returned an error: $body") + } else { + jsonParser.parseToJsonElement(responseBody).jsonObject.apply { + val newUser = FirebaseUserImpl(app, this, user.isAnonymous, user.email) + if (newUser.claims["aud"] != app.options.projectId) { + signOutAndThrowInvalidUserException( + newUser.claims.toString(), + "Project ID's do not match ${newUser.claims["aud"]} != ${app.options.projectId}", + ) + } else { + this@FirebaseAuth.user = newUser + source.setResult(newUser) + } } } } - } - private fun signOutAndThrowInvalidUserException(body: String, message: String) { - signOut() - source.setException(FirebaseAuthInvalidUserException(body, message)) - } - }) + private fun signOutAndThrowInvalidUserException( + body: String, + message: String, + ) { + signOut() + source.setException(FirebaseAuthInvalidUserException(body, message)) + } + }, + ) return source } internal fun updateEmail(email: String): Task { val source = TaskCompletionSource() - val body = RequestBody.create( - json, - JsonObject( - mapOf( - "idToken" to JsonPrimitive(user?.idToken), - "email" to JsonPrimitive(email), - "returnSecureToken" to JsonPrimitive(true) - ) - ).toString() + val body = + RequestBody.create( + json, + JsonObject( + mapOf( + "idToken" to JsonPrimitive(user?.idToken), + "email" to JsonPrimitive(email), + "returnSecureToken" to JsonPrimitive(true), + ), + ).toString(), + ) + val request = + Request + .Builder() + .url(urlFactory.buildUrl("identitytoolkit.googleapis.com/v1/accounts:update")) + .post(body) + .build() + + client.newCall(request).enqueue( + object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + source.setException(FirebaseException(e.toString(), e)) + } + + @Throws(IOException::class) + override fun onResponse( + call: Call, + response: Response, + ) { + if (!response.isSuccessful) { + signOut() + source.setException( + createAuthInvalidUserException( + "updateEmail", + request, + response, + ), + ) + } else { + val newBody = + jsonParser + .parseToJsonElement( + response.body()?.use { it.string() } ?: "", + ).jsonObject + + user?.let { prev -> + user = + FirebaseUserImpl( + app = app, + isAnonymous = prev.isAnonymous, + uid = prev.uid, + idToken = newBody["idToken"]?.jsonPrimitive?.contentOrNull ?: prev.idToken, + refreshToken = newBody["refreshToken"]?.jsonPrimitive?.contentOrNull ?: prev.refreshToken, + expiresIn = newBody["expiresIn"]?.jsonPrimitive?.intOrNull ?: prev.expiresIn, + createdAt = prev.createdAt, + email = newBody["newEmail"]?.jsonPrimitive?.contentOrNull ?: prev.email, + photoUrl = newBody["photoUrl"]?.jsonPrimitive?.contentOrNull ?: prev.photoUrl, + displayName = newBody["displayName"]?.jsonPrimitive?.contentOrNull ?: prev.displayName, + ) + } + source.setResult(null) + } + } + }, ) - val request = Request.Builder() - .url("https://identitytoolkit.googleapis.com/v1/accounts:update?key=" + app.options.apiKey) - .post(body) - .build() + return source.task + } - client.newCall(request).enqueue(object : Callback { + internal fun updateProfile(request: UserProfileChangeRequest): Task { + val source = TaskCompletionSource() - override fun onFailure(call: Call, e: IOException) { - source.setException(FirebaseException(e.toString(), e)) - } + val body = + RequestBody.create( + json, + JsonObject( + mapOf( + "idToken" to JsonPrimitive(user?.idToken), + "displayName" to JsonPrimitive(request.displayName), + "photoUrl" to JsonPrimitive(request.photoUrl), + "returnSecureToken" to JsonPrimitive(true), + ), + ).toString(), + ) + val req = + Request + .Builder() + .url(urlFactory.buildUrl("identitytoolkit.googleapis.com/v1/accounts:update")) + .post(body) + .build() + + client.newCall(req).enqueue( + object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + source.setException(FirebaseException(e.toString(), e)) + } - @Throws(IOException::class) - override fun onResponse(call: Call, response: Response) { - if (!response.isSuccessful) { - signOut() - source.setException( - createAuthInvalidUserException( - "updateEmail", - request, - response - ) - ) - } else { - val newBody = jsonParser.parseToJsonElement( - response.body()?.use { it.string() } ?: "" - ).jsonObject - - user?.let { prev -> - user = FirebaseUserImpl( - app = app, - isAnonymous = prev.isAnonymous, - uid = prev.uid, - idToken = newBody["idToken"]?.jsonPrimitive?.contentOrNull ?: prev.idToken, - refreshToken = newBody["refreshToken"]?.jsonPrimitive?.contentOrNull ?: prev.refreshToken, - expiresIn = newBody["expiresIn"]?.jsonPrimitive?.intOrNull ?: prev.expiresIn, - createdAt = prev.createdAt, - email = newBody["newEmail"]?.jsonPrimitive?.contentOrNull ?: prev.email + @Throws(IOException::class) + override fun onResponse( + call: Call, + response: Response, + ) { + if (!response.isSuccessful) { + signOut() + source.setException( + createAuthInvalidUserException( + "updateProfile", + req, + response, + ), ) + } else { + val newBody = + jsonParser + .parseToJsonElement( + response.body()?.use { it.string() } ?: "", + ).jsonObject + + user?.let { prev -> + user = + FirebaseUserImpl( + app = app, + isAnonymous = prev.isAnonymous, + uid = prev.uid, + idToken = newBody["idToken"]?.jsonPrimitive?.contentOrNull ?: prev.idToken, + refreshToken = newBody["refreshToken"]?.jsonPrimitive?.contentOrNull ?: prev.refreshToken, + expiresIn = newBody["expiresIn"]?.jsonPrimitive?.intOrNull ?: prev.expiresIn, + createdAt = prev.createdAt, + email = newBody["newEmail"]?.jsonPrimitive?.contentOrNull ?: prev.email, + photoUrl = newBody["photoUrl"]?.jsonPrimitive?.contentOrNull ?: prev.photoUrl, + displayName = newBody["displayName"]?.jsonPrimitive?.contentOrNull ?: prev.displayName, + ) + } + source.setResult(null) } - source.setResult(null) } - } - }) + }, + ) return source.task } - override fun getUid(): String? { - return user?.uid - } + override fun getUid(): String? = user?.uid override fun addIdTokenListener(listener: com.google.firebase.auth.internal.IdTokenListener) { internalIdTokenListeners.addIfAbsent(listener) @@ -570,21 +823,46 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { idTokenListeners.remove(listener) } - fun sendPasswordResetEmail(email: String, settings: ActionCodeSettings?): Task = TODO() + fun sendPasswordResetEmail( + email: String, + settings: ActionCodeSettings?, + ): Task = TODO() + fun checkActionCode(code: String): Task = TODO() - fun confirmPasswordReset(code: String, newPassword: String): Task = TODO() + + fun confirmPasswordReset( + code: String, + newPassword: String, + ): Task = TODO() + fun fetchSignInMethodsForEmail(email: String): Task = TODO() - fun sendSignInLinkToEmail(email: String, actionCodeSettings: ActionCodeSettings): Task = TODO() + + fun sendSignInLinkToEmail( + email: String, + actionCodeSettings: ActionCodeSettings, + ): Task = TODO() + fun verifyPasswordResetCode(code: String): Task = TODO() + fun updateCurrentUser(user: FirebaseUser): Task = TODO() + fun applyActionCode(code: String): Task = TODO() + val languageCode: String get() = TODO() + fun isSignInWithEmailLink(link: String): Boolean = TODO() - fun signInWithEmailLink(email: String, link: String): Task = TODO() + + fun signInWithEmailLink( + email: String, + link: String, + ): Task = TODO() fun setLanguageCode(value: String): Nothing = TODO() - fun useEmulator(host: String, port: Int) { + fun useEmulator( + host: String, + port: Int, + ) { urlFactory = UrlFactory(app, "http://$host:$port/") } } diff --git a/src/main/java/com/google/firebase/auth/FirebaseUser.kt b/src/main/java/com/google/firebase/auth/FirebaseUser.kt index 50e4e52..0932518 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUser.kt +++ b/src/main/java/com/google/firebase/auth/FirebaseUser.kt @@ -5,28 +5,45 @@ import com.google.android.gms.tasks.Task abstract class FirebaseUser { abstract val uid: String abstract val email: String? + abstract val photoUrl: String? + abstract val displayName: String? abstract val isAnonymous: Boolean + abstract fun delete(): Task + abstract fun reload(): Task - abstract fun verifyBeforeUpdateEmail(newEmail: String, actionCodeSettings: ActionCodeSettings?): Task + + abstract fun verifyBeforeUpdateEmail( + newEmail: String, + actionCodeSettings: ActionCodeSettings?, + ): Task + abstract fun updateEmail(email: String): Task - val displayName: String get() = TODO() + abstract fun getIdToken(forceRefresh: Boolean): Task + + abstract fun updateProfile(request: UserProfileChangeRequest): Task + val phoneNumber: String get() = TODO() - val photoUrl: String? get() = TODO() val isEmailVerified: Boolean get() = TODO() val metadata: FirebaseUserMetadata get() = TODO() val multiFactor: MultiFactor get() = TODO() val providerData: List get() = TODO() val providerId: String get() = TODO() - abstract fun getIdToken(forceRefresh: Boolean): Task + fun linkWithCredential(credential: AuthCredential): Task = TODO() + fun sendEmailVerification(): Task = TODO() + fun sendEmailVerification(actionCodeSettings: ActionCodeSettings): Task = TODO() + fun unlink(provider: String): Task = TODO() + fun updatePassword(password: String): Task = TODO() + fun updatePhoneNumber(credential: AuthCredential): Task = TODO() - fun updateProfile(request: UserProfileChangeRequest): Task = TODO() + fun reauthenticate(credential: AuthCredential): Task = TODO() + fun reauthenticateAndRetrieveData(credential: AuthCredential): Task = TODO() } diff --git a/src/main/java/com/google/firebase/auth/OobRequestType.kt b/src/main/java/com/google/firebase/auth/OobRequestType.kt index f0c31c3..51a77e2 100644 --- a/src/main/java/com/google/firebase/auth/OobRequestType.kt +++ b/src/main/java/com/google/firebase/auth/OobRequestType.kt @@ -5,4 +5,4 @@ internal enum class OobRequestType { EMAIL_SIGNIN, VERIFY_EMAIL, VERIFY_AND_CHANGE_EMAIL -} \ No newline at end of file +} diff --git a/src/main/java/com/google/firebase/auth/UserProfileChangeRequest.java b/src/main/java/com/google/firebase/auth/UserProfileChangeRequest.java deleted file mode 100644 index 437f98d..0000000 --- a/src/main/java/com/google/firebase/auth/UserProfileChangeRequest.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.google.firebase.auth; - -import android.net.Uri; -import kotlin.NotImplementedError; - -public class UserProfileChangeRequest { - public static class Builder { - public Builder setDisplayName(String name) { - throw new NotImplementedError(); - } - public Builder setPhotoUri(Uri uri) { - throw new NotImplementedError(); - } - public UserProfileChangeRequest build() { - throw new NotImplementedError(); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/google/firebase/auth/UserProfileChangeRequest.kt b/src/main/java/com/google/firebase/auth/UserProfileChangeRequest.kt new file mode 100644 index 0000000..c60bb83 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/UserProfileChangeRequest.kt @@ -0,0 +1,47 @@ +package com.google.firebase.auth + +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable + +class UserProfileChangeRequest private constructor( + internal val displayName: String?, + internal val photoUrl: String?, +) : Parcelable { + override fun describeContents(): Int = displayName.hashCode() + photoUrl.hashCode() + + override fun writeToParcel( + dest: Parcel, + flags: Int, + ) { + dest.writeString(displayName) + dest.writeString(photoUrl) + } + + internal companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): UserProfileChangeRequest { + val displayName = parcel.readString() + val photoUri = parcel.readString() + return UserProfileChangeRequest(displayName, photoUri) + } + + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + + class Builder { + private var displayName: String? = null + private var photoUri: Uri? = null + + fun setDisplayName(name: String?): Builder { + this.displayName = name + return this + } + + fun setPhotoUri(uri: Uri?): Builder { + this.photoUri = uri + return this + } + + fun build(): UserProfileChangeRequest = UserProfileChangeRequest(displayName, photoUri?.toString()) + } +} diff --git a/src/test/kotlin/AppTest.kt b/src/test/kotlin/AppTest.kt index d017e38..2e5697f 100644 --- a/src/test/kotlin/AppTest.kt +++ b/src/test/kotlin/AppTest.kt @@ -8,20 +8,34 @@ import org.junit.Test class AppTest : FirebaseTest() { @Test fun testInitialize() { - FirebasePlatform.initializeFirebasePlatform(object : FirebasePlatform() { - val storage = mutableMapOf() - override fun store(key: String, value: String) = storage.set(key, value) - override fun retrieve(key: String) = storage[key] - override fun clear(key: String) { storage.remove(key) } - override fun log(msg: String) = println(msg) - }) - val options = FirebaseOptions.Builder() - .setProjectId("my-firebase-project") - .setApplicationId("1:27992087142:android:ce3b6448250083d1") - .setApiKey("AIzaSyADUe90ULnQDuGShD9W23RDP0xmeDc6Mvw") - // setDatabaseURL(...) - // setStorageBucket(...) - .build() + FirebasePlatform.initializeFirebasePlatform( + object : FirebasePlatform() { + val storage = mutableMapOf() + + override fun store( + key: String, + value: String, + ) = storage.set(key, value) + + override fun retrieve(key: String) = storage[key] + + override fun clear(key: String) { + storage.remove(key) + } + + override fun log(msg: String) = println(msg) + }, + ) + val options = + FirebaseOptions + .Builder() + .setProjectId("fir-java-sdk") + .setApplicationId("1:341458593155:web:bf8e1aa37efe01f32d42b6") + .setApiKey("AIzaSyCvVHjTJHyeStnzIE7J9LLtHqWk6reGM08") + .setDatabaseUrl("https://fir-java-sdk-default-rtdb.firebaseio.com") + .setStorageBucket("fir-java-sdk.appspot.com") + .setGcmSenderId("341458593155") + .build() val app = Firebase.initialize(Application(), options) } } diff --git a/src/test/kotlin/AuthTest.kt b/src/test/kotlin/AuthTest.kt index 7bdb90c..080c784 100644 --- a/src/test/kotlin/AuthTest.kt +++ b/src/test/kotlin/AuthTest.kt @@ -1,47 +1,97 @@ -import com.google.firebase.auth.FirebaseAuth +import android.net.Uri import com.google.firebase.auth.FirebaseAuthInvalidUserException import kotlinx.coroutines.runBlocking import kotlinx.coroutines.tasks.await import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals import org.junit.Assert.assertThrows +import org.junit.Before import org.junit.Test +import java.util.UUID class AuthTest : FirebaseTest() { - private fun createAuth(): FirebaseAuth { - return FirebaseAuth(app).apply { + private val email = "email${UUID.randomUUID()}@example.com" + + @Before + fun initialize() { + auth.apply { useEmulator("localhost", 9099) } } @Test - fun `should authenticate via anonymous auth`() = runTest { - val auth = createAuth() + fun `should authenticate via anonymous auth`() = + runTest { + auth.signInAnonymously().await() - auth.signInAnonymously().await() + assertEquals(true, auth.currentUser?.isAnonymous) + } - assertEquals(true, auth.currentUser?.isAnonymous) - } + @Test + fun `should create user via email and password`() = + runTest { + val createResult = auth.createUserWithEmailAndPassword(email, "test123").await() + assertNotEquals(null, createResult.user?.uid) + assertEquals(null, createResult.user?.displayName) + // assertEquals(null, createResult.user?.phoneNumber) + assertEquals(false, createResult.user?.isAnonymous) + assertEquals(email, createResult.user?.email) + assertNotEquals("", createResult.user!!.email) + + val signInResult = auth.signInWithEmailAndPassword(email, "test123").await() + assertEquals(createResult.user?.uid, signInResult.user?.uid) + } @Test - fun `should authenticate via email and password`() = runTest { - val auth = createAuth() + fun `should authenticate via email and password`() = + runTest { + auth.createUserWithEmailAndPassword(email, "test123").await() - auth.signInWithEmailAndPassword("email@example.com", "securepassword").await() + auth.signInWithEmailAndPassword(email, "test123").await() - assertEquals(false, auth.currentUser?.isAnonymous) - } + assertEquals(false, auth.currentUser?.isAnonymous) + } @Test - fun `should throw exception on invalid password`() { - val auth = createAuth() + fun `should update displayName and photoUrl`() = + runTest { + auth + .createUserWithEmailAndPassword(email, "test123") + .await() + .user + auth.currentUser + ?.updateProfile( + com.google.firebase.auth.UserProfileChangeRequest + .Builder() + .setDisplayName("testDisplayName") + .setPhotoUri(Uri.parse("https://picsum.photos/100")) + .build(), + )?.await() + assertEquals("testDisplayName", auth.currentUser?.displayName) + assertEquals("https://picsum.photos/100", auth.currentUser?.photoUrl) + } - val exception = assertThrows(FirebaseAuthInvalidUserException::class.java) { - runBlocking { - auth.signInWithEmailAndPassword("email@example.com", "wrongpassword").await() - } + @Test + fun `should sign in anonymously`() = + runTest { + val signInResult = auth.signInAnonymously().await() + assertNotEquals("", signInResult.user!!.email) + assertEquals(true, signInResult.user?.isAnonymous) } - assertEquals("INVALID_PASSWORD", exception.errorCode) - } + @Test + fun `should throw exception on invalid password`() = + runTest { + auth.createUserWithEmailAndPassword(email, "test123").await() + + val exception = + assertThrows(FirebaseAuthInvalidUserException::class.java) { + runBlocking { + auth.signInWithEmailAndPassword(email, "wrongpassword").await() + } + } + + assertEquals("INVALID_PASSWORD", exception.errorCode) + } } diff --git a/src/test/kotlin/FirebaseAuthTest.kt b/src/test/kotlin/FirebaseAuthTest.kt index 029cd11..b2000f2 100644 --- a/src/test/kotlin/FirebaseAuthTest.kt +++ b/src/test/kotlin/FirebaseAuthTest.kt @@ -1,71 +1,57 @@ -import android.app.Application -import com.google.firebase.Firebase -import com.google.firebase.FirebaseOptions -import com.google.firebase.FirebasePlatform -import com.google.firebase.auth.FirebaseAuth -import com.google.firebase.initialize +import android.net.Uri +import com.google.firebase.auth.FirebaseUser import kotlinx.coroutines.tasks.await import kotlinx.coroutines.test.runTest -import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals -import org.junit.Before import org.junit.Test -import java.io.File import java.util.UUID internal class FirebaseAuthTest : FirebaseTest() { - - private lateinit var auth: FirebaseAuth - - @Before - fun initialize() { - FirebasePlatform.initializeFirebasePlatform(object : FirebasePlatform() { - val storage = mutableMapOf() - override fun store(key: String, value: String) = storage.set(key, value) - override fun retrieve(key: String) = storage[key] - override fun clear(key: String) { storage.remove(key) } - override fun log(msg: String) = println(msg) - override fun getDatabasePath(name: String) = File("./build/$name") - }) - val options = FirebaseOptions.Builder() - .setProjectId("fir-java-sdk") - .setApplicationId("1:341458593155:web:bf8e1aa37efe01f32d42b6") - .setApiKey("AIzaSyCvVHjTJHyeStnzIE7J9LLtHqWk6reGM08") - .setDatabaseUrl("https://fir-java-sdk-default-rtdb.firebaseio.com") - .setStorageBucket("fir-java-sdk.appspot.com") - .setGcmSenderId("341458593155") - .build() - - val firebaseApp = Firebase.initialize(Application(), options) - auth = FirebaseAuth.getInstance(app = firebaseApp) - } - - @After - fun clear() { - auth.currentUser?.delete() - } - @Test - fun testCreateUserWithEmailAndPassword() = runTest { - val email = "test+${UUID.randomUUID()}@test.com" - val createResult = auth.createUserWithEmailAndPassword(email, "test123").await() - assertNotEquals(null, createResult.user?.uid) - // assertEquals(null, createResult.user?.displayName) - // assertEquals(null, createResult.user?.phoneNumber) - assertEquals(false, createResult.user?.isAnonymous) - assertEquals(email, createResult.user?.email) - assertNotEquals("", createResult.user!!.email) + fun testCreateUserWithEmailAndPassword() = + runTest { + val email = "test+${UUID.randomUUID()}@test.com" + val createResult = auth.createUserWithEmailAndPassword(email, "test123").await() + assertNotEquals(null, createResult.user?.uid) + // assertEquals(null, createResult.user?.displayName) + // assertEquals(null, createResult.user?.phoneNumber) + assertEquals(false, createResult.user?.isAnonymous) + assertEquals(email, createResult.user?.email) + assertNotEquals("", createResult.user!!.email) + + val signInResult = auth.signInWithEmailAndPassword(email, "test123").await() + assertEquals(createResult.user?.uid, signInResult.user?.uid) + } - val signInResult = auth.signInWithEmailAndPassword(email, "test123").await() - assertEquals(createResult.user?.uid, signInResult.user?.uid) - } + @Test + fun testUpdateProfile() = + runTest { + val user = createUser() + user + ?.updateProfile( + com.google.firebase.auth.UserProfileChangeRequest + .Builder() + .setDisplayName("testDisplayName") + .setPhotoUri(Uri.parse("https://picsum.photos/100")) + .build(), + )?.await() + assertEquals("testDisplayName", auth.currentUser?.displayName) + assertEquals("https://picsum.photos/100", auth.currentUser?.photoUrl) + } @Test - fun testSignInAnonymously() = runTest { - val signInResult = auth.signInAnonymously().await() - assertNotEquals("", signInResult.user!!.email) - assertEquals(true, signInResult.user?.isAnonymous) - } + fun testSignInAnonymously() = + runTest { + val signInResult = auth.signInAnonymously().await() + assertNotEquals("", signInResult.user!!.email) + assertEquals(true, signInResult.user?.isAnonymous) + } + + private suspend fun createUser(email: String = "test+${UUID.randomUUID()}@test.com"): FirebaseUser? = + auth + .createUserWithEmailAndPassword(email, "test123") + .await() + .user } diff --git a/src/test/kotlin/FirebaseTest.kt b/src/test/kotlin/FirebaseTest.kt index 77aa858..a714f8c 100644 --- a/src/test/kotlin/FirebaseTest.kt +++ b/src/test/kotlin/FirebaseTest.kt @@ -3,31 +3,72 @@ import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseOptions import com.google.firebase.FirebasePlatform +import com.google.firebase.auth.FirebaseAuth import com.google.firebase.initialize +import com.google.firebase.ktx.initialize +import org.junit.After import org.junit.Before import java.io.File abstract class FirebaseTest { + protected lateinit var auth: FirebaseAuth + protected val app: FirebaseApp get() { - val options = FirebaseOptions.Builder() - .setProjectId("my-firebase-project") - .setApplicationId("1:27992087142:android:ce3b6448250083d1") - .setApiKey("AIzaSyADUe90ULnQDuGShD9W23RDP0xmeDc6Mvw") - .build() + val options = + FirebaseOptions + .Builder() + .setProjectId("fir-java-sdk") + .setApplicationId("1:341458593155:web:bf8e1aa37efe01f32d42b6") + .setApiKey("AIzaSyCvVHjTJHyeStnzIE7J9LLtHqWk6reGM08") + .setDatabaseUrl("https://fir-java-sdk-default-rtdb.firebaseio.com") + .setStorageBucket("fir-java-sdk.appspot.com") + .setGcmSenderId("341458593155") + .build() return Firebase.initialize(Application(), options) } @Before fun beforeEach() { - FirebasePlatform.initializeFirebasePlatform(object : FirebasePlatform() { - val storage = mutableMapOf() - override fun store(key: String, value: String) = storage.set(key, value) - override fun retrieve(key: String) = storage[key] - override fun clear(key: String) { storage.remove(key) } - override fun log(msg: String) = println(msg) - override fun getDatabasePath(name: String) = File("./build/$name") - }) + FirebasePlatform.initializeFirebasePlatform( + object : FirebasePlatform() { + val storage = mutableMapOf() + + override fun store( + key: String, + value: String, + ) = storage.set(key, value) + + override fun retrieve(key: String) = storage[key] + + override fun clear(key: String) { + storage.remove(key) + } + + override fun log(msg: String) = println(msg) + + override fun getDatabasePath(name: String) = File("./build/$name") + }, + ) + val options = + FirebaseOptions + .Builder() + .setProjectId("fir-java-sdk") + .setApplicationId("1:341458593155:web:bf8e1aa37efe01f32d42b6") + .setApiKey("AIzaSyCvVHjTJHyeStnzIE7J9LLtHqWk6reGM08") + .setDatabaseUrl("https://fir-java-sdk-default-rtdb.firebaseio.com") + .setStorageBucket("fir-java-sdk.appspot.com") + .setGcmSenderId("341458593155") + .build() + + val firebaseApp = Firebase.initialize(Application(), options) + auth = FirebaseAuth.getInstance(app = firebaseApp) + FirebaseApp.clearInstancesForTest() } + + @After + fun clear() { + auth.currentUser?.delete() + } } From 040414cbcf34a20b790c2110bfb78f82fc831836 Mon Sep 17 00:00:00 2001 From: Jakub Kostka Date: Thu, 7 Nov 2024 16:38:29 +0100 Subject: [PATCH 10/10] refactor --- .../java/com/google/firebase/auth/FirebaseAuth.kt | 4 ++-- src/test/kotlin/AuthTest.kt | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt index 5492c67..484afc6 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt @@ -374,10 +374,10 @@ class FirebaseAuth constructor( setResult = { responseBody -> FirebaseUserImpl(app, jsonParser.parseToJsonElement(responseBody).jsonObject) }, - ).task.continueWith { + ).task.addOnSuccessListener { updateByGetAccountInfo() } - return source.result + return source } internal fun updateByGetAccountInfo(): Task { diff --git a/src/test/kotlin/AuthTest.kt b/src/test/kotlin/AuthTest.kt index 080c784..d603d5f 100644 --- a/src/test/kotlin/AuthTest.kt +++ b/src/test/kotlin/AuthTest.kt @@ -53,6 +53,21 @@ class AuthTest : FirebaseTest() { assertEquals(false, auth.currentUser?.isAnonymous) } + /*@Test + fun `should authenticate via custom token`() = + runTest { + val user = auth.createUserWithEmailAndPassword(email, "test123").await() + auth + .signInWithCustomToken( + user.user + .getIdToken(false) + .await() + .token ?: "", + ).await() + + assertEquals(false, auth.currentUser?.isAnonymous) + }*/ + @Test fun `should update displayName and photoUrl`() = runTest {