From 3933cea9e8ac16efeba0c6ea3fb0dc415eeba722 Mon Sep 17 00:00:00 2001 From: Ic-ks <> Date: Sun, 19 May 2024 22:31:22 +0200 Subject: [PATCH] wasmJs prototype fix --- firebase-app/build.gradle.kts | 2 + firebase-app/package.json | 4 +- .../dev/gitlive/firebase/externals/app.kt | 31 ++ .../kotlin/dev/gitlive/firebase/firebase.kt | 101 ++++++ firebase-auth/build.gradle.kts | 2 + firebase-auth/package.json | 4 +- .../kotlin/dev/gitlive/firebase/auth/auth.kt | 210 ++++++++++++ .../dev/gitlive/firebase/auth/credentials.kt | 100 ++++++ .../gitlive/firebase/auth/externals/auth.kt | 298 ++++++++++++++++++ .../dev/gitlive/firebase/auth/multifactor.kt | 47 +++ .../kotlin/dev/gitlive/firebase/auth/user.kt | 77 +++++ firebase-common-internal/package.json | 2 +- firebase-common/build.gradle.kts | 8 + firebase-common/package.json | 2 +- .../dev/gitlive/firebase/Unsubscribe.kt | 7 + firebase-config/package.json | 2 +- firebase-crashlytics/package.json | 2 +- firebase-database/package.json | 2 +- firebase-firestore/package.json | 2 +- firebase-functions/package.json | 2 +- firebase-installations/package.json | 2 +- firebase-perf/package.json | 2 +- firebase-storage/package.json | 2 +- gradle.properties | 10 +- 24 files changed, 902 insertions(+), 19 deletions(-) create mode 100644 firebase-app/src/wasmJsMain/kotlin/dev/gitlive/firebase/externals/app.kt create mode 100644 firebase-app/src/wasmJsMain/kotlin/dev/gitlive/firebase/firebase.kt create mode 100644 firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/auth.kt create mode 100644 firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/credentials.kt create mode 100644 firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/externals/auth.kt create mode 100644 firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/multifactor.kt create mode 100644 firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/user.kt create mode 100644 firebase-common/src/wasmJsMain/kotlin/dev/gitlive/firebase/Unsubscribe.kt diff --git a/firebase-app/build.gradle.kts b/firebase-app/build.gradle.kts index 635e9cd76..f0c1717a2 100644 --- a/firebase-app/build.gradle.kts +++ b/firebase-app/build.gradle.kts @@ -92,6 +92,8 @@ kotlin { } } + wasmJs() + js(IR) { useCommonJs() nodejs { diff --git a/firebase-app/package.json b/firebase-app/package.json index 397e36723..e2900b77b 100644 --- a/firebase-app/package.json +++ b/firebase-app/package.json @@ -1,6 +1,6 @@ { "name": "@gitlive/firebase-app", - "version": "1.12.0", + "version": "1.13.0-wasmJsFix", "description": "Wrapper around firebase for usage in Kotlin Multiplatform projects", "main": "firebase-app.js", "scripts": { @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-common": "1.12.0", + "@gitlive/firebase-common": "1.13.0-wasmJsFix", "firebase": "9.19.1", "kotlin": "1.8.20", "kotlinx-coroutines-core": "1.6.4" diff --git a/firebase-app/src/wasmJsMain/kotlin/dev/gitlive/firebase/externals/app.kt b/firebase-app/src/wasmJsMain/kotlin/dev/gitlive/firebase/externals/app.kt new file mode 100644 index 000000000..2b9cb8bd7 --- /dev/null +++ b/firebase-app/src/wasmJsMain/kotlin/dev/gitlive/firebase/externals/app.kt @@ -0,0 +1,31 @@ +@file:JsModule("firebase/app") + +package dev.gitlive.firebase.externals + +import kotlin.js.Promise + +external fun initializeApp(options: JsAny, name: String = definedExternally): FirebaseApp + +external fun getApp(name: String = definedExternally): FirebaseApp + +external fun getApps(): JsArray + +external fun deleteApp(app: FirebaseApp): Promise + +external interface FirebaseApp: JsAny { + val automaticDataCollectionEnabled: Boolean + val name: String + val options: FirebaseOptions +} + +external interface FirebaseOptions { + val apiKey: String + val appId : String + val authDomain: String? + val databaseURL: String? + val measurementId: String? + val messagingSenderId: String? + val gaTrackingId: String? + val projectId: String? + val storageBucket: String? +} diff --git a/firebase-app/src/wasmJsMain/kotlin/dev/gitlive/firebase/firebase.kt b/firebase-app/src/wasmJsMain/kotlin/dev/gitlive/firebase/firebase.kt new file mode 100644 index 000000000..641ec64c3 --- /dev/null +++ b/firebase-app/src/wasmJsMain/kotlin/dev/gitlive/firebase/firebase.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.gitlive.firebase + +import dev.gitlive.firebase.externals.deleteApp +import dev.gitlive.firebase.externals.getApp +import dev.gitlive.firebase.externals.getApps +import dev.gitlive.firebase.externals.initializeApp +import dev.gitlive.firebase.externals.FirebaseApp as JsFirebaseApp + +actual val Firebase.app: FirebaseApp + get() = FirebaseApp(getApp()) + +actual fun Firebase.app(name: String): FirebaseApp = + FirebaseApp(getApp(name)) + +actual fun Firebase.initialize(context: Any?): FirebaseApp? = + throw UnsupportedOperationException("Cannot initialize firebase without options in JS") + +actual fun Firebase.initialize(context: Any?, options: FirebaseOptions, name: String): FirebaseApp = + FirebaseApp(initializeApp(options.toJson(), name)) + +actual fun Firebase.initialize(context: Any?, options: FirebaseOptions) = + FirebaseApp(initializeApp(options.toJson())) + +actual class FirebaseApp internal constructor(val js: JsFirebaseApp) { + actual val name: String + get() = js.name + actual val options: FirebaseOptions + get() = js.options.run { + FirebaseOptions(appId, apiKey, databaseURL, gaTrackingId, storageBucket, projectId, messagingSenderId, authDomain) + } + + actual suspend fun delete() { + "".toJsReference() + deleteApp(js) + } +} + +actual fun Firebase.apps(context: Any?) = getApps().asSequence().filterNotNull().map { FirebaseApp(it) }.toList() + +@JsName("Object") +external class JsObject : JsAny { + operator fun get(key: JsString): JsAny? + operator fun set(key: JsString, value: JsAny?) +} + +fun json(params: List>): JsObject { + return json(*params.toTypedArray()) +} + +fun json(params: Map): JsObject { + return json(*params.entries.map { it.key to it.value }.toTypedArray()) +} + +fun json(vararg params: Pair): JsObject { + return JsObject().apply { + params.forEach { + val key = it.first.toJsString() + when (val value = it.second) { + is String -> set(key, value.toJsString()) + is Boolean -> set(key, value.toJsBoolean()) + is Int -> set(key, value.toJsNumber()) + is JsObject -> set(key, value) + is JsString -> set(key, value) + is JsBoolean -> set(key, value) + is JsNumber -> set(key, value) + is JsArray<*> -> set(key, value) + else -> error("Unknown param $it") + } + } + } +} + +private fun FirebaseOptions.toJson() = JsObject().apply { + set("apiKey".toJsString(), apiKey.toJsString()) + set("appId".toJsString(), applicationId.toJsString()) + set("databaseURL".toJsString(), (databaseUrl?.toJsString())) + set("storageBucket".toJsString(), (storageBucket?.toJsString())) + set("projectId".toJsString(), (projectId?.toJsString())) + set("gaTrackingId".toJsString(), (gaTrackingId?.toJsString())) + set("messagingSenderId".toJsString(), (gcmSenderId?.toJsString())) + set("authDomain".toJsString(), (authDomain?.toJsString())) +} + +actual open class FirebaseException(code: String?, cause: Throwable) : Exception("$code: ${cause.message}", cause) +actual open class FirebaseNetworkException(code: String?, cause: Throwable) : FirebaseException(code, cause) +actual open class FirebaseTooManyRequestsException(code: String?, cause: Throwable) : FirebaseException(code, cause) +actual open class FirebaseApiNotAvailableException(code: String?, cause: Throwable) : FirebaseException(code, cause) + + +fun JsArray.asSequence(): Sequence { + var i = 0 + return sequence { + while (i++ < length) { + yield(get(i)) + } + } +} \ No newline at end of file diff --git a/firebase-auth/build.gradle.kts b/firebase-auth/build.gradle.kts index 03cb349a3..e0d4a4f8b 100644 --- a/firebase-auth/build.gradle.kts +++ b/firebase-auth/build.gradle.kts @@ -118,6 +118,8 @@ kotlin { } } + wasmJs() + jvm { compilations.getByName("main") { kotlinOptions { diff --git a/firebase-auth/package.json b/firebase-auth/package.json index 7f21dc747..be072d1f2 100644 --- a/firebase-auth/package.json +++ b/firebase-auth/package.json @@ -1,6 +1,6 @@ { "name": "@gitlive/firebase-auth", - "version": "1.12.0", + "version": "1.13.0-wasmJsFix", "description": "Wrapper around firebase for usage in Kotlin Multiplatform projects", "main": "firebase-auth.js", "scripts": { @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-app": "1.12.0", + "@gitlive/firebase-app": "1.13.0-wasmJsFix", "firebase": "9.19.1", "kotlin": "1.8.20", "kotlinx-coroutines-core": "1.6.4" diff --git a/firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/auth.kt b/firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/auth.kt new file mode 100644 index 000000000..09fb3c65e --- /dev/null +++ b/firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/auth.kt @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.gitlive.firebase.auth + +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.FirebaseApp +import dev.gitlive.firebase.FirebaseException +import dev.gitlive.firebase.FirebaseNetworkException +import dev.gitlive.firebase.JsObject +import dev.gitlive.firebase.asSequence +import dev.gitlive.firebase.auth.externals.* +import dev.gitlive.firebase.json +import kotlinx.coroutines.await +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import dev.gitlive.firebase.auth.externals.AuthResult as JsAuthResult + +actual val Firebase.auth + get() = rethrow { FirebaseAuth(getAuth()) } + +actual fun Firebase.auth(app: FirebaseApp) = + rethrow { FirebaseAuth(getAuth(app.js)) } + +actual class FirebaseAuth internal constructor(val js: Auth) { + + actual val currentUser: FirebaseUser? + get() = rethrow { js.currentUser?.let { FirebaseUser(it) } } + + actual val authStateChanged get() = callbackFlow { + val unsubscribe = js.onAuthStateChanged { + trySend(it?.let { FirebaseUser(it) }) + } + awaitClose { unsubscribe() } + } + + actual val idTokenChanged get() = callbackFlow { + val unsubscribe = js.onIdTokenChanged { + trySend(it?.let { FirebaseUser(it) }) + } + awaitClose { unsubscribe() } + } + + actual var languageCode: String + get() = js.languageCode ?: "" + set(value) { js.languageCode = value } + + actual suspend fun applyActionCode(code: String): Unit = rethrow { applyActionCode(js, code).await() } + actual suspend fun confirmPasswordReset(code: String, newPassword: String): Unit = rethrow { confirmPasswordReset(js, code, newPassword).await() } + + actual suspend fun createUserWithEmailAndPassword(email: String, password: String) = + rethrow { AuthResult(createUserWithEmailAndPassword(js, email, password).await()) } + + actual suspend fun fetchSignInMethodsForEmail(email: String): List = rethrow { fetchSignInMethodsForEmail(js, email).await>().asSequence().map { it.toString() }.toList() } + + actual suspend fun sendPasswordResetEmail(email: String, actionCodeSettings: ActionCodeSettings?): Unit = + rethrow { sendPasswordResetEmail(js, email, actionCodeSettings?.toJson()).await() } + + actual suspend fun sendSignInLinkToEmail(email: String, actionCodeSettings: ActionCodeSettings): Unit = + rethrow { sendSignInLinkToEmail(js, email, actionCodeSettings.toJson()).await() } + + actual fun isSignInWithEmailLink(link: String) = rethrow { isSignInWithEmailLink(js, link) } + + actual suspend fun signInWithEmailAndPassword(email: String, password: String) = + rethrow { AuthResult(signInWithEmailAndPassword(js, email, password).await()) } + + actual suspend fun signInWithCustomToken(token: String) = + rethrow { AuthResult(signInWithCustomToken(js, token).await()) } + + actual suspend fun signInAnonymously() = + rethrow { AuthResult(signInAnonymously(js).await()) } + + actual suspend fun signInWithCredential(authCredential: AuthCredential) = + rethrow { AuthResult(signInWithCredential(js, authCredential.js).await()) } + + actual suspend fun signInWithEmailLink(email: String, link: String) = + rethrow { AuthResult(signInWithEmailLink(js, email, link).await()) } + + actual suspend fun signOut(): Unit = rethrow { signOut(js).await() } + + actual suspend fun updateCurrentUser(user: FirebaseUser): Unit = + rethrow { updateCurrentUser(js, user.js).await() } + + actual suspend fun verifyPasswordResetCode(code: String): String = + rethrow { verifyPasswordResetCode(js, code).await() } + + actual suspend fun checkActionCode(code: String): T = rethrow { + val result: ActionCodeInfo = checkActionCode(js, code).await() + @Suppress("UNCHECKED_CAST") + return when(result.operation) { + "EMAIL_SIGNIN" -> ActionCodeResult.SignInWithEmailLink + "VERIFY_EMAIL" -> ActionCodeResult.VerifyEmail(result.data.email!!) + "PASSWORD_RESET" -> ActionCodeResult.PasswordReset(result.data.email!!) + "RECOVER_EMAIL" -> ActionCodeResult.RecoverEmail(result.data.email!!, result.data.previousEmail!!) + "VERIFY_AND_CHANGE_EMAIL" -> ActionCodeResult.VerifyBeforeChangeEmail( + result.data.email!!, + result.data.previousEmail!! + ) + "REVERT_SECOND_FACTOR_ADDITION" -> ActionCodeResult.RevertSecondFactorAddition( + result.data.email!!, + result.data.multiFactorInfo?.let { MultiFactorInfo(it) } + ) + else -> throw UnsupportedOperationException(result.operation) + } as T + } + + actual fun useEmulator(host: String, port: Int) = rethrow { connectAuthEmulator(js, "http://$host:$port") } +} + +actual class AuthResult internal constructor(val js: JsAuthResult) { + actual val user: FirebaseUser? + get() = rethrow { js.user?.let { FirebaseUser(it) } } +} + +actual class AuthTokenResult(val tokenResult: IdTokenResult) { +// actual val authTimestamp: Long +// get() = js.authTime + actual val claims: Map + get() = JsObjectKeys(tokenResult.claims).asSequence().mapNotNull { key -> + val keyJsString = key as? JsString ?: return@mapNotNull null + val value = tokenResult.claims[keyJsString] ?: return@mapNotNull null + val keyString = keyJsString.toString() + keyString to value + }.toList().toMap() +// actual val expirationTimestamp: Long +// get() = android.expirationTime +// actual val issuedAtTimestamp: Long +// get() = js.issuedAtTime + actual val signInProvider: String? + get() = tokenResult.signInProvider + actual val token: String? + get() = tokenResult.token +} + +internal fun ActionCodeSettings.toJson() = json( + "url" to url, + "android" to (androidPackageName?.run { json("installApp" to installIfNotAvailable, "minimumVersion" to minimumVersion, "packageName" to packageName) }), + "dynamicLinkDomain" to (dynamicLinkDomain ?: null), + "handleCodeInApp" to canHandleCodeInApp, + "ios" to (iOSBundleId?.run { json("bundleId" to iOSBundleId) } ?: null) +) + +actual open class FirebaseAuthException(code: String?, cause: Throwable): FirebaseException(code, cause) +actual open class FirebaseAuthActionCodeException(code: String?, cause: Throwable): FirebaseAuthException(code, cause) +actual open class FirebaseAuthEmailException(code: String?, cause: Throwable): FirebaseAuthException(code, cause) +actual open class FirebaseAuthInvalidCredentialsException(code: String?, cause: Throwable): FirebaseAuthException(code, cause) +actual open class FirebaseAuthWeakPasswordException(code: String?, cause: Throwable): FirebaseAuthInvalidCredentialsException(code, cause) +actual open class FirebaseAuthInvalidUserException(code: String?, cause: Throwable): FirebaseAuthException(code, cause) +actual open class FirebaseAuthMultiFactorException(code: String?, cause: Throwable): FirebaseAuthException(code, cause) +actual open class FirebaseAuthRecentLoginRequiredException(code: String?, cause: Throwable): FirebaseAuthException(code, cause) +actual open class FirebaseAuthUserCollisionException(code: String?, cause: Throwable): FirebaseAuthException(code, cause) +actual open class FirebaseAuthWebException(code: String?, cause: Throwable): FirebaseAuthException(code, cause) + + +internal inline fun T.rethrow(function: T.() -> R): R = dev.gitlive.firebase.auth.rethrow { function() } + +private inline fun rethrow(function: () -> R): R { + try { + return function() + } catch (e: Exception) { + throw e + } +} + +private fun errorToException(o: JsObject): FirebaseException { + val cause = Exception(o.toString()) + return when(val code = o["code".toJsString()]?.toString()?.lowercase()) { + "auth/invalid-user-token" -> FirebaseAuthInvalidUserException(code, cause) + "auth/requires-recent-login" -> FirebaseAuthRecentLoginRequiredException(code, cause) + "auth/user-disabled" -> FirebaseAuthInvalidUserException(code, cause) + "auth/user-token-expired" -> FirebaseAuthInvalidUserException(code, cause) + "auth/web-storage-unsupported" -> FirebaseAuthWebException(code, cause) + "auth/network-request-failed" -> FirebaseNetworkException(code, cause) + "auth/timeout" -> FirebaseNetworkException(code, cause) + "auth/weak-password" -> FirebaseAuthWeakPasswordException(code, cause) + "auth/invalid-credential", + "auth/invalid-verification-code", + "auth/missing-verification-code", + "auth/invalid-verification-id", + "auth/missing-verification-id" -> FirebaseAuthInvalidCredentialsException(code, cause) + "auth/maximum-second-factor-count-exceeded", + "auth/second-factor-already-in-use" -> FirebaseAuthMultiFactorException(code, cause) + "auth/credential-already-in-use" -> FirebaseAuthUserCollisionException(code, cause) + "auth/email-already-in-use" -> FirebaseAuthUserCollisionException(code, cause) + "auth/invalid-email" -> FirebaseAuthEmailException(code, cause) +// "auth/app-deleted" -> +// "auth/app-not-authorized" -> +// "auth/argument-error" -> +// "auth/invalid-api-key" -> +// "auth/operation-not-allowed" -> +// "auth/too-many-arguments" -> +// "auth/unauthorized-domain" -> + else -> { + FirebaseAuthException(code, cause) + } + } +} + +@JsFun("(output) => Object.keys(output)") +external fun JsObjectKeys(vararg output: JsObject?): JsArray + +fun JsArray.asSequence(): Sequence { + var i = 0 + return sequence { + while (i++ < length) { + yield(get(i)) + } + } +} diff --git a/firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/credentials.kt b/firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/credentials.kt new file mode 100644 index 000000000..db5dc1251 --- /dev/null +++ b/firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/credentials.kt @@ -0,0 +1,100 @@ +package dev.gitlive.firebase.auth + +import dev.gitlive.firebase.auth.externals.ApplicationVerifier +import dev.gitlive.firebase.auth.externals.EmailAuthProvider +import dev.gitlive.firebase.auth.externals.FacebookAuthProvider +import dev.gitlive.firebase.auth.externals.GithubAuthProvider +import dev.gitlive.firebase.auth.externals.GoogleAuthProvider +import dev.gitlive.firebase.auth.externals.PhoneAuthProvider +import dev.gitlive.firebase.auth.externals.TwitterAuthProvider +import dev.gitlive.firebase.json +import kotlinx.coroutines.await +import dev.gitlive.firebase.auth.externals.AuthCredential as JsAuthCredential +import dev.gitlive.firebase.auth.externals.OAuthProvider as JsOAuthProvider + +actual open class AuthCredential(val js: JsAuthCredential) { + actual val providerId: String + get() = js.providerId +} + +actual class PhoneAuthCredential(js: JsAuthCredential) : AuthCredential(js) +actual class OAuthCredential(js: JsAuthCredential) : AuthCredential(js) + +actual object EmailAuthProvider { + actual fun credential(email: String, password: String): AuthCredential = + AuthCredential(EmailAuthProvider.credential(email, password)) + + actual fun credentialWithLink( + email: String, + emailLink: String + ): AuthCredential = AuthCredential(EmailAuthProvider.credentialWithLink(email, emailLink)) +} + +actual object FacebookAuthProvider { + actual fun credential(accessToken: String): AuthCredential = + AuthCredential(FacebookAuthProvider.credential(accessToken)) +} + +actual object GithubAuthProvider { + actual fun credential(token: String): AuthCredential = + AuthCredential(GithubAuthProvider.credential(token)) +} + +actual object GoogleAuthProvider { + actual fun credential(idToken: String?, accessToken: String?): AuthCredential { + require(idToken != null || accessToken != null) { + "Both parameters are optional but at least one must be present." + } + return AuthCredential(GoogleAuthProvider.credential(idToken, accessToken)) + } +} + +actual class OAuthProvider(val js: JsOAuthProvider) { + + actual constructor( + provider: String, + scopes: List, + customParameters: Map, + auth: FirebaseAuth + ) : this(JsOAuthProvider(provider)) { + rethrow { + scopes.forEach { js.addScope(it) } + js.setCustomParameters(json(customParameters)) + } + } + actual companion object { + actual fun credential(providerId: String, accessToken: String?, idToken: String?, rawNonce: String?): OAuthCredential = rethrow { + JsOAuthProvider(providerId) + .credential( + json( + "accessToken" to (accessToken), + "idToken" to (idToken), + "rawNonce" to (rawNonce) + ), + accessToken + ) + .let { OAuthCredential(it) } + } + } +} + +actual class PhoneAuthProvider(val js: PhoneAuthProvider) { + + actual constructor(auth: FirebaseAuth) : this(PhoneAuthProvider(auth.js)) + + actual fun credential(verificationId: String, smsCode: String): PhoneAuthCredential = PhoneAuthCredential(PhoneAuthProvider.credential(verificationId, smsCode)) + actual suspend fun verifyPhoneNumber(phoneNumber: String, verificationProvider: PhoneVerificationProvider): AuthCredential = rethrow { + val verificationId: JsString = js.verifyPhoneNumber(phoneNumber, verificationProvider.verifier).await() + val verificationCode = verificationProvider.getVerificationCode(verificationId.toString()) + credential(verificationId.toString(), verificationCode) + } +} + +actual interface PhoneVerificationProvider { + val verifier: ApplicationVerifier + suspend fun getVerificationCode(verificationId: String): String +} + +actual object TwitterAuthProvider { + actual fun credential(token: String, secret: String): AuthCredential = AuthCredential(TwitterAuthProvider.credential(token, secret)) +} diff --git a/firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/externals/auth.kt b/firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/externals/auth.kt new file mode 100644 index 000000000..f1a392ec9 --- /dev/null +++ b/firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/externals/auth.kt @@ -0,0 +1,298 @@ +@file:JsModule("firebase/auth") + +package dev.gitlive.firebase.auth.externals + +import dev.gitlive.firebase.JsObject +import dev.gitlive.firebase.Unsubscribe +import dev.gitlive.firebase.externals.FirebaseApp +import kotlin.js.Promise + +external fun applyActionCode(auth: Auth, code: String): Promise + +external fun checkActionCode(auth: Auth, code: String): Promise + +external fun confirmPasswordReset(auth: Auth, code: String, newPassword: String): Promise + +external fun connectAuthEmulator(auth: Auth, url: String, options: JsAny? = definedExternally) + +external fun createUserWithEmailAndPassword( + auth: Auth, + email: String, + password: String +): Promise + +external fun deleteUser(user: User): Promise + +external fun fetchSignInMethodsForEmail(auth: Auth, email: String): Promise> + +external fun getAuth(app: FirebaseApp? = definedExternally): Auth + +external fun initializeAuth(app: FirebaseApp? = definedExternally, deps: JsAny = definedExternally): Auth + +external fun getIdToken(user: User, forceRefresh: Boolean?): Promise + +external fun getIdTokenResult(user: User, forceRefresh: Boolean?): Promise + +external fun isSignInWithEmailLink(auth: Auth, link: String): Boolean + +external fun linkWithCredential(user: User, credential: AuthCredential): Promise + +external fun multiFactor(user: User): MultiFactorUser + +external fun onAuthStateChanged(auth: Auth, nextOrObserver: (User?) -> Unit): Unsubscribe + +external fun onIdTokenChanged(auth: Auth, nextOrObserver: (User?) -> Unit): Unsubscribe + +external fun sendEmailVerification(user: User, actionCodeSettings: JsAny?): Promise + +external fun reauthenticateWithCredential( + user: User, + credential: AuthCredential +): Promise + +external fun reload(user: User): Promise + +external fun sendPasswordResetEmail( + auth: Auth, + email: String, + actionCodeSettings: JsAny? +): Promise + +external fun sendSignInLinkToEmail( + auth: Auth, + email: String, + actionCodeSettings: JsAny? +): Promise + +external fun signInAnonymously(auth: Auth): Promise + +external fun signInWithCredential(auth: Auth, authCredential: AuthCredential): Promise + +external fun signInWithCustomToken(auth: Auth, token: String): Promise + +external fun signInWithEmailAndPassword( + auth: Auth, + email: String, + password: String +): Promise + +external fun signInWithEmailLink(auth: Auth, email: String, link: String): Promise + +external fun signInWithPopup(auth: Auth, provider: AuthProvider): Promise + +external fun signInWithRedirect(auth: Auth, provider: AuthProvider): Promise + +external fun getRedirectResult(auth: Auth): Promise + +external fun signOut(auth: Auth): Promise + +external fun unlink(user: User, providerId: String): Promise + +external fun updateCurrentUser(auth: Auth, user: User?): Promise + +external fun updateEmail(user: User, newEmail: String): Promise + +external fun updatePassword(user: User, newPassword: String): Promise + +external fun updatePhoneNumber(user: User, phoneCredential: AuthCredential): Promise + +external fun updateProfile(user: User, profile: JsAny): Promise + +external fun verifyBeforeUpdateEmail( + user: User, + newEmail: String, + actionCodeSettings: JsAny? +): Promise + +external fun verifyPasswordResetCode(auth: Auth, code: String): Promise + +external interface Auth { + val currentUser: User? + var languageCode: String? + + fun onAuthStateChanged(nextOrObserver: (User?) -> Unit): Unsubscribe + fun onIdTokenChanged(nextOrObserver: (User?) -> Unit): Unsubscribe + fun signOut(): Promise + fun updateCurrentUser(user: User?): Promise +} + +external interface UserInfo: JsAny { + val displayName: String? + val email: String? + val phoneNumber: String? + val photoURL: String? + val providerId: String + val uid: String +} + +external interface User : UserInfo, JsAny { + val emailVerified: Boolean + val isAnonymous: Boolean + val metadata: UserMetadata + val providerData: JsArray + val refreshToken: String + val tenantId: String? + + fun delete(): Promise + fun getIdToken(forceRefresh: Boolean?): Promise + fun getIdTokenResult(forceRefresh: Boolean?): Promise + fun reload(): Promise +} + +external interface UserMetadata { + val creationTime: String? + val lastSignInTime: String? +} + +external interface IdTokenResult: JsAny { + val authTime: String + val claims: JsObject + val expirationTime: String + val issuedAtTime: String + val signInProvider: String? + val signInSecondFactor: String? + val token: String +} + +external interface ActionCodeInfo: JsAny { + val operation: String + val data: ActionCodeData +} + +external interface ActionCodeData { + val email: String? + val multiFactorInfo: MultiFactorInfo? + val previousEmail: String? +} + +external interface AuthResult: JsAny { + val credential: AuthCredential? + val operationType: String? + val user: User? +} + +external interface AuthCredential { + val providerId: String + val signInMethod: String +} + +external interface OAuthCredential : AuthCredential { + val accessToken: String? + val idToken: String? + val secret: String? +} + +external interface UserCredential: JsAny { + val operationType: String + val providerId: String? + val user: User +} + +external interface ProfileUpdateRequest { + val displayName: String? + val photoURL: String? +} + +external interface MultiFactorUser { + val enrolledFactors: JsArray + + fun enroll(assertion: MultiFactorAssertion, displayName: String?): Promise + fun getSession(): Promise + fun unenroll(option: MultiFactorInfo): Promise + fun unenroll(option: String): Promise +} + +external interface MultiFactorInfo: JsAny { + val displayName: String? + val enrollmentTime: String + val factorId: String + val uid: String +} + +external interface MultiFactorAssertion { + val factorId: String +} + +external interface MultiFactorSession: JsAny + +external interface MultiFactorResolver { + val auth: Auth + val hints: JsArray + val session: MultiFactorSession + + fun resolveSignIn(assertion: MultiFactorAssertion): Promise +} + +external interface AuthProvider + +external interface AuthError + +external object EmailAuthProvider : AuthProvider { + fun credential(email: String, password: String): AuthCredential + fun credentialWithLink(email: String, emailLink: String): AuthCredential +} + +external object FacebookAuthProvider : AuthProvider { + fun credential(token: String): AuthCredential +} + +external object GithubAuthProvider : AuthProvider { + fun credential(token: String): AuthCredential +} + +external class GoogleAuthProvider : AuthProvider { + fun addScope(scope: String) + companion object { + fun credential(idToken: String?, accessToken: String?): AuthCredential + fun credentialFromResult(userCredential: UserCredential): OAuthCredential? + fun credentialFromError(error: AuthError): OAuthCredential? + } +} + +external class OAuthProvider(providerId: String) : AuthProvider { + val providerId: String + fun credential(optionsOrIdToken: JsObject?, accessToken: String?): AuthCredential + + fun addScope(scope: String) + fun setCustomParameters(customOAuthParameters: JsObject) +} + +external interface OAuthCredentialOptions { + val accessToken: String? + val idToken: String? + val rawNonce: String? +} + +external class PhoneAuthProvider(auth: Auth?) : AuthProvider { + companion object { + fun credential( + verificationId: String, + verificationCode: String + ): AuthCredential + } + + fun verifyPhoneNumber( + phoneInfoOptions: String, + applicationVerifier: ApplicationVerifier + ): Promise +} + +external interface ApplicationVerifier { + val type: String + fun verify(): Promise +} + +external object TwitterAuthProvider : AuthProvider { + fun credential(token: String, secret: String): AuthCredential +} + +external interface Persistence { + val type: String +} + +external val browserLocalPersistence: Persistence +external val browserSessionPersistence: Persistence +external val indexedDBLocalPersistence: Persistence +external val inMemoryPersistence: Persistence + +external fun setPersistence(auth: Auth, persistence: Persistence): Promise; diff --git a/firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/multifactor.kt b/firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/multifactor.kt new file mode 100644 index 000000000..a79d29252 --- /dev/null +++ b/firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/multifactor.kt @@ -0,0 +1,47 @@ +package dev.gitlive.firebase.auth + +import dev.gitlive.firebase.auth.externals.MultiFactorUser +import kotlinx.coroutines.await +import dev.gitlive.firebase.auth.externals.MultiFactorAssertion as JsMultiFactorAssertion +import dev.gitlive.firebase.auth.externals.MultiFactorInfo as JsMultiFactorInfo +import dev.gitlive.firebase.auth.externals.MultiFactorResolver as JsMultiFactorResolver +import dev.gitlive.firebase.auth.externals.MultiFactorSession as JsMultiFactorSession + +actual class MultiFactor(val js: MultiFactorUser) { + actual val enrolledFactors: List + get() = rethrow { js.enrolledFactors.asSequence().filterNotNull().map { MultiFactorInfo(it) }.toList() } + actual suspend fun enroll(multiFactorAssertion: MultiFactorAssertion, displayName: String?): Unit = + rethrow { js.enroll(multiFactorAssertion.js, displayName).await() } + actual suspend fun getSession(): MultiFactorSession = + rethrow { MultiFactorSession(js.getSession().await()) } + actual suspend fun unenroll(multiFactorInfo: MultiFactorInfo): Unit = + rethrow { js.unenroll(multiFactorInfo.js).await() } + actual suspend fun unenroll(factorUid: String): Unit = + rethrow { js.unenroll(factorUid).await() } +} + +actual class MultiFactorInfo(val js: JsMultiFactorInfo) { + actual val displayName: String? + get() = rethrow { js.displayName } + actual val enrollmentTime: Double + get() = error("Date error: ${js.enrollmentTime}") //rethrow { ((js.enrollmentTime).getTime() / 1000.0) } + actual val factorId: String + get() = rethrow { js.factorId } + actual val uid: String + get() = rethrow { js.uid } +} + +actual class MultiFactorAssertion(val js: JsMultiFactorAssertion) { + actual val factorId: String + get() = rethrow { js.factorId } +} + +actual class MultiFactorSession(val js: JsMultiFactorSession) + +actual class MultiFactorResolver(val js: JsMultiFactorResolver) { + actual val auth: FirebaseAuth = rethrow { FirebaseAuth(js.auth) } + actual val hints: List = rethrow { js.hints.asSequence().filterNotNull().map { MultiFactorInfo(it) }.toList() } + actual val session: MultiFactorSession = rethrow { MultiFactorSession(js.session) } + + actual suspend fun resolveSignIn(assertion: MultiFactorAssertion): AuthResult = rethrow { AuthResult(js.resolveSignIn(assertion.js).await()) } +} diff --git a/firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/user.kt b/firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/user.kt new file mode 100644 index 000000000..43b6335c9 --- /dev/null +++ b/firebase-auth/src/wasmJsMain/kotlin/dev/gitlive/firebase/auth/user.kt @@ -0,0 +1,77 @@ +package dev.gitlive.firebase.auth + +import dev.gitlive.firebase.auth.externals.* +import dev.gitlive.firebase.json +import kotlinx.coroutines.await +import dev.gitlive.firebase.auth.externals.UserInfo as JsUserInfo + +actual class FirebaseUser internal constructor(val js: User) { + actual val uid: String + get() = rethrow { js.uid } + actual val displayName: String? + get() = rethrow { js.displayName } + actual val email: String? + get() = rethrow { js.email } + actual val phoneNumber: String? + get() = rethrow { js.phoneNumber } + actual val photoURL: String? + get() = rethrow { js.photoURL } + actual val isAnonymous: Boolean + get() = rethrow { js.isAnonymous } + actual val isEmailVerified: Boolean + get() = rethrow { js.emailVerified } + actual val metaData: UserMetaData? + get() = rethrow { UserMetaData(js.metadata) } + actual val multiFactor: MultiFactor + get() = rethrow { MultiFactor(multiFactor(js)) } + actual val providerData: List + get() = rethrow { js.providerData.asSequence().filterNotNull().map { UserInfo(it) }.toList() } + actual val providerId: String + get() = rethrow { js.providerId } + actual suspend fun delete(): Unit = rethrow { js.delete().await() } + actual suspend fun reload(): Unit = rethrow { js.reload().await() } + actual suspend fun getIdToken(forceRefresh: Boolean): String? = rethrow { js.getIdToken(forceRefresh).await() } + actual suspend fun getIdTokenResult(forceRefresh: Boolean): AuthTokenResult = rethrow { AuthTokenResult(getIdTokenResult(js, forceRefresh).await()) } + actual suspend fun linkWithCredential(credential: AuthCredential): AuthResult = rethrow { AuthResult( linkWithCredential(js, credential.js).await()) } + actual suspend fun reauthenticate(credential: AuthCredential) = rethrow { + reauthenticateWithCredential(js, credential.js).await() + Unit + } + actual suspend fun reauthenticateAndRetrieveData(credential: AuthCredential): AuthResult = rethrow { AuthResult(reauthenticateWithCredential(js, credential.js).await()) } + + actual suspend fun sendEmailVerification(actionCodeSettings: ActionCodeSettings?): Unit = rethrow { sendEmailVerification(js, actionCodeSettings?.toJson()).await() } + actual suspend fun unlink(provider: String): FirebaseUser? = rethrow { FirebaseUser(unlink(js, provider).await()) } + actual suspend fun updateEmail(email: String): Unit = rethrow { updateEmail(js, email).await() } + actual suspend fun updatePassword(password: String): Unit = rethrow { updatePassword(js, password).await() } + actual suspend fun updatePhoneNumber(credential: PhoneAuthCredential): Unit = rethrow { updatePhoneNumber(js, credential.js).await() } + actual suspend fun updateProfile(displayName: String?, photoUrl: String?): Unit = rethrow { + val request = listOfNotNull( + displayName.takeUnless { it === UNCHANGED }?.let { "displayName" to it }, + photoUrl.takeUnless { it === UNCHANGED }?.let { "photoURL" to it } + ) + updateProfile(js, json(*request.toTypedArray())).await() + } + actual suspend fun verifyBeforeUpdateEmail(newEmail: String, actionCodeSettings: ActionCodeSettings?): Unit = rethrow { verifyBeforeUpdateEmail(js, newEmail, actionCodeSettings?.toJson()).await() } +} + +actual class UserInfo(val js: JsUserInfo) { + actual val displayName: String? + get() = rethrow { js.displayName } + actual val email: String? + get() = rethrow { js.email } + actual val phoneNumber: String? + get() = rethrow { js.phoneNumber } + actual val photoURL: String? + get() = rethrow { js.photoURL } + actual val providerId: String + get() = rethrow { js.providerId } + actual val uid: String + get() = rethrow { js.uid } +} + +actual class UserMetaData(val js: UserMetadata) { + actual val creationTime: Double? + get() = error("Unimplemented ${js.creationTime}")// rethrow {js.creationTime?.let { (Date(it).getTime() / 1000.0) } } + actual val lastSignInTime: Double? + get() = error("Unimplemented ${js.lastSignInTime}") //rethrow {js.lastSignInTime?.let { (Date(it).getTime() / 1000.0) } } +} diff --git a/firebase-common-internal/package.json b/firebase-common-internal/package.json index e5bfd8389..01147dd84 100644 --- a/firebase-common-internal/package.json +++ b/firebase-common-internal/package.json @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-multiplatform-sdk", "dependencies": { - "@gitlive/firebase-common": "1.12.0", + "@gitlive/firebase-common": "1.13.0-wasmJsFix", "firebase": "9.19.1", "kotlin": "1.8.20", "kotlinx-coroutines-core": "1.6.4", diff --git a/firebase-common/build.gradle.kts b/firebase-common/build.gradle.kts index 0586bfd3f..d28ad8b90 100644 --- a/firebase-common/build.gradle.kts +++ b/firebase-common/build.gradle.kts @@ -107,6 +107,8 @@ kotlin { } } + wasmJs() + sourceSets { all { languageSettings.apply { @@ -147,6 +149,12 @@ kotlin { } } + getByName("wasmJsMain") { + dependencies { + api(npm("firebase", "10.6.0")) + } + } + getByName("jvmMain") { kotlin.srcDir("src/androidMain/kotlin") } diff --git a/firebase-common/package.json b/firebase-common/package.json index 5723b5918..b75929789 100644 --- a/firebase-common/package.json +++ b/firebase-common/package.json @@ -1,6 +1,6 @@ { "name": "@gitlive/firebase-common", - "version": "1.12.0", + "version": "1.13.0-wasmJsFix", "description": "Wrapper around firebase for usage in Kotlin Multiplatform projects", "main": "firebase-common.js", "scripts": { diff --git a/firebase-common/src/wasmJsMain/kotlin/dev/gitlive/firebase/Unsubscribe.kt b/firebase-common/src/wasmJsMain/kotlin/dev/gitlive/firebase/Unsubscribe.kt new file mode 100644 index 000000000..087d4f86b --- /dev/null +++ b/firebase-common/src/wasmJsMain/kotlin/dev/gitlive/firebase/Unsubscribe.kt @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.gitlive.firebase + +typealias Unsubscribe = () -> Unit diff --git a/firebase-config/package.json b/firebase-config/package.json index e6c4057d0..24db5ade8 100644 --- a/firebase-config/package.json +++ b/firebase-config/package.json @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-app": "1.12.0", + "@gitlive/firebase-app": "1.13.0-wasmJsFix", "firebase": "9.19.1", "kotlin": "1.8.20", "kotlinx-coroutines-core": "1.6.4" diff --git a/firebase-crashlytics/package.json b/firebase-crashlytics/package.json index 435166e4f..23f12e68f 100644 --- a/firebase-crashlytics/package.json +++ b/firebase-crashlytics/package.json @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-app": "1.12.0", + "@gitlive/firebase-app": "1.13.0-wasmJsFix", "firebase": "9.19.1", "kotlin": "1.6.10", "kotlinx-coroutines-core": "1.6.1-native-mt" diff --git a/firebase-database/package.json b/firebase-database/package.json index a2910b27a..0748a41c5 100644 --- a/firebase-database/package.json +++ b/firebase-database/package.json @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-app": "1.12.0", + "@gitlive/firebase-app": "1.13.0-wasmJsFix", "firebase": "9.19.1", "kotlin": "1.8.20", "kotlinx-coroutines-core": "1.6.4" diff --git a/firebase-firestore/package.json b/firebase-firestore/package.json index 1a5e44bd0..5aacf168c 100644 --- a/firebase-firestore/package.json +++ b/firebase-firestore/package.json @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-app": "1.12.0", + "@gitlive/firebase-app": "1.13.0-wasmJsFix", "firebase": "9.19.1", "kotlin": "1.8.20", "kotlinx-coroutines-core": "1.6.4" diff --git a/firebase-functions/package.json b/firebase-functions/package.json index 585e2773f..09cd37965 100644 --- a/firebase-functions/package.json +++ b/firebase-functions/package.json @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-app": "1.12.0", + "@gitlive/firebase-app": "1.13.0-wasmJsFix", "firebase": "9.19.1", "kotlin": "1.8.20", "kotlinx-coroutines-core": "1.6.4" diff --git a/firebase-installations/package.json b/firebase-installations/package.json index d4b77bb64..769f55b9e 100644 --- a/firebase-installations/package.json +++ b/firebase-installations/package.json @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-app": "1.12.0", + "@gitlive/firebase-app": "1.13.0-wasmJsFix", "firebase": "9.19.1", "kotlin": "1.8.20", "kotlinx-coroutines-core": "1.6.4" diff --git a/firebase-perf/package.json b/firebase-perf/package.json index c51f39a2c..6aae7e478 100644 --- a/firebase-perf/package.json +++ b/firebase-perf/package.json @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-app": "1.12.0", + "@gitlive/firebase-app": "1.13.0-wasmJsFix", "firebase": "9.19.1", "kotlin": "1.6.10", "kotlinx-coroutines-core": "1.6.1-native-mt" diff --git a/firebase-storage/package.json b/firebase-storage/package.json index db6ab752b..9b3de28f5 100644 --- a/firebase-storage/package.json +++ b/firebase-storage/package.json @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/GitLiveApp/firebase-kotlin-sdk", "dependencies": { - "@gitlive/firebase-app": "1.12.0", + "@gitlive/firebase-app": "1.13.0-wasmJsFix", "firebase": "9.19.1", "kotlin": "1.6.10", "kotlinx-coroutines-core": "1.6.1-native-mt" diff --git a/gradle.properties b/gradle.properties index 19068e9f3..77a6b3ebe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -49,9 +49,9 @@ firebase-perf.skipJsTests=false firebase-storage.skipJsTests=false # Versions: -firebase-app.version=1.12.0 -firebase-auth.version=1.12.0 -firebase-common.version=1.12.0 +firebase-app.version=1.13.0-wasmJsFix +firebase-auth.version=1.13.0-wasmJsFix +firebase-common.version=1.13.0-wasmJsFix firebase-common-internal.version=1.12.0 firebase-config.version=1.12.0 firebase-database.version=1.12.0 @@ -65,8 +65,8 @@ firebase-storage.version=1.12.0 # Dependencies Versions: gradlePluginVersion=8.2.0 kotlinVersion=1.9.23 -coroutinesVersion=1.7.3 -serializationVersion=1.6.0 +coroutinesVersion=1.8.1 +serializationVersion=1.6.2 firebaseBoMVersion=33.0.0 apiVersion=1.8 languageVersion=1.9