├── .gitignore ├── .idea ├── .gitignore ├── .name ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src └── jsMain └── kotlin └── dev └── bitspittle └── firebase ├── analytics └── Bindings.kt ├── app └── Bindings.kt ├── auth ├── Bindings.ActionCode.kt ├── Bindings.Errors.kt ├── Bindings.Providers.kt └── Bindings.kt ├── database └── Bindings.kt ├── externals ├── analytics │ └── Externals.kt ├── app │ └── Externals.kt ├── auth │ └── Externals.kt ├── database │ └── Externals.kt └── util │ └── Externals.kt └── util ├── FirebaseError.kt ├── JsonSupport.kt └── StringExtensions.kt /.gitignore: -------------------------------------------------------------------------------- 1 | # General ignores 2 | .DS_Store 3 | build 4 | out 5 | kotlin-js-store 6 | 7 | # IntelliJ ignores 8 | *.iml 9 | /*.ipr 10 | 11 | /.idea/caches 12 | /.idea/libraries 13 | /.idea/modules.xml 14 | /.idea/workspace.xml 15 | /.idea/gradle.xml 16 | /.idea/navEditor.xml 17 | /.idea/assetWizardSettings.xml 18 | /.idea/artifacts 19 | /.idea/compiler.xml 20 | /.idea/jarRepositories.xml 21 | /.idea/*.iml 22 | /.idea/modules 23 | /.idea/libraries-with-intellij-classes.xml 24 | 25 | # Gradle ignores 26 | .gradle 27 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | firebase-kotlin-bindings -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project is a collection of bindings for working with various Firebase services in Kotlin/JS. 2 | 3 | *THIS PROJECT IS STILL VERY EXPERIMENTAL AND NOT READY FOR PUBLIC USE.* 4 | 5 | The goal of these bindings are to provide a clean, Kotlin-idiomatic view of Firebase web services: 6 | * JavaScript methods that return `Promise`s are converted to `suspend fun`s in Kotlin 7 | * Class design is updated for an API that is more object-oriented. 8 | * Some methods which take in / return JSON-objects in JavaScript are changed to typed objects in Kotlin. 9 | * Constructor (factory) methods are added when possible to make constructing JavaScript interfaces with optional 10 | parameters easier. 11 | 12 | ## Gradle 13 | 14 | At the moment, this library is not published anywhere. To use it, clone this project, and then run 15 | 16 | ```bash 17 | $ ./gradlew publishToMavenLocal 18 | ``` 19 | 20 | Then, in your own project: 21 | 22 | ```kotlin 23 | // build.gradle.kts 24 | 25 | repositories { 26 | /* ... other repositories ... */ 27 | mavenLocal() 28 | } 29 | 30 | kotlin { 31 | js(IR) { /* ... */ } 32 | sourceSets { 33 | val jsMain by getting { 34 | dependencies { 35 | // TODO: Replace with a real version when this library gets more mature 36 | implementation("dev.bitspittle:firebase-kotlin-bindings:+") 37 | } 38 | } 39 | } 40 | } 41 | ``` 42 | 43 | ## Background 44 | 45 | This project contains bindings I need for using various Firebase services, wrapping API calls from 46 | [the web API](https://firebase.google.com/docs/reference/js) and porting them to Kotlin. 47 | 48 | Honestly, I probably should have used an existing solution (for example, 49 | [GitLiveApp/firebase-kotlin-sdk](https://github.com/GitLiveApp/firebase-kotlin-sdk)), but I wanted to see what it's like 50 | to wrap a JS API with a Kotlin one. This is, at the moment, a learning project. 51 | 52 | ## Usage 53 | 54 | This library provides a handcrafted Kotlin layer backed by external JS APIs that it delegates to. 55 | 56 | These custom classes are provided instead of the underlying JS APIs as those are designed somewhat inconsistently and 57 | occasionally using features that don't map cleanly to Kotlin concepts. 58 | 59 | To get started, initialize a `FirebaseApp` class, and use that to access the remaining APIs. 60 | 61 | So for example, this JavaScript code (taken from 62 | [the tutorials](https://firebase.google.com/docs/database/web/read-and-write#basic_write)): 63 | 64 | ```javascript 65 | import { initializeApp } from "firebase/app"; 66 | import { getDatabase, ref, set } from "firebase/database"; 67 | 68 | const firebaseOptions = { /* ... */ }; 69 | const app = initializeApp(firebaseOptions) 70 | 71 | function writeUserData(userId, name, email, imageUrl) { 72 | const db = getDatabase(app); 73 | set(ref(db, 'users/' + userId), { 74 | username: name, 75 | email: email, 76 | profile_picture : imageUrl 77 | }); 78 | } 79 | ``` 80 | 81 | translates to the following Kotlin code: 82 | 83 | ```kotlin 84 | val app = FirebaseApp.initialize(FirebaseOptions(/*...*/)) 85 | 86 | fun writeUserData(userId: String, name: String, email: String, imageUrl: String) { 87 | val db = app.getDatabase() 88 | db.ref("users/$userId").set(json( 89 | "username" to name, 90 | "email" to email, 91 | "profile_picture" to imageUrl 92 | )) 93 | } 94 | ``` 95 | 96 | ## API support 97 | 98 | - @firebase/analytics (low) 99 | - @firebase/app (low) 100 | - @firebase/app-check (none) 101 | - @firebase/auth (low) 102 | - @firebase/database (low) 103 | - @firebase/firestore (none) 104 | - @firebase/functions (none) 105 | - @firebase/installations (none) 106 | - @firebase/messaging (none) 107 | - @firebase/performance (none) 108 | - @firebase/remote-config (none) 109 | - @firebase/storage (none) -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.multiplatform) 3 | `maven-publish` 4 | } 5 | 6 | repositories { 7 | mavenCentral() 8 | google() 9 | } 10 | 11 | 12 | group = "dev.bitspittle" 13 | version = libs.versions.firebase.bindings.get() 14 | 15 | kotlin { 16 | js(IR) { 17 | browser() 18 | } 19 | 20 | @Suppress("UNUSED_VARIABLE") // Suppress spurious warnings about sourceset variables not being used 21 | sourceSets { 22 | val commonMain by getting { 23 | dependencies { 24 | } 25 | } 26 | 27 | val jsMain by getting { 28 | dependencies { 29 | implementation(npm("firebase", libs.versions.firebase.web.get())) 30 | implementation(libs.kotlinx.coroutines) 31 | } 32 | } 33 | } 34 | } 35 | 36 | publishing { 37 | repositories { 38 | maven { 39 | group = project.group 40 | version = project.version.toString() 41 | } 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlin.mpp.stability.nowarn=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | firebase-web = "9.15.0" # from https://www.npmjs.com/package/firebase 3 | firebase-bindings = "0.1-SNAPSHOT" 4 | kotlin = "1.8.20" 5 | kotlinx-coroutines = "1.6.0" 6 | 7 | [libraries] 8 | kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } 9 | 10 | [plugins] 11 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitspittle/firebase-kotlin-bindings/7a38a16b1c6be612e09ffa8d81f0ce7530119dc7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Use the maximum available, or set MAX_FD != -1 to use that value. 89 | MAX_FD=maximum 90 | 91 | warn () { 92 | echo "$*" 93 | } >&2 94 | 95 | die () { 96 | echo 97 | echo "$*" 98 | echo 99 | exit 1 100 | } >&2 101 | 102 | # OS specific support (must be 'true' or 'false'). 103 | cygwin=false 104 | msys=false 105 | darwin=false 106 | nonstop=false 107 | case "$( uname )" in #( 108 | CYGWIN* ) cygwin=true ;; #( 109 | Darwin* ) darwin=true ;; #( 110 | MSYS* | MINGW* ) msys=true ;; #( 111 | NONSTOP* ) nonstop=true ;; 112 | esac 113 | 114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 115 | 116 | 117 | # Determine the Java command to use to start the JVM. 118 | if [ -n "$JAVA_HOME" ] ; then 119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 120 | # IBM's JDK on AIX uses strange locations for the executables 121 | JAVACMD=$JAVA_HOME/jre/sh/java 122 | else 123 | JAVACMD=$JAVA_HOME/bin/java 124 | fi 125 | if [ ! -x "$JAVACMD" ] ; then 126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 127 | 128 | Please set the JAVA_HOME variable in your environment to match the 129 | location of your Java installation." 130 | fi 131 | else 132 | JAVACMD=java 133 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 134 | 135 | Please set the JAVA_HOME variable in your environment to match the 136 | location of your Java installation." 137 | fi 138 | 139 | # Increase the maximum file descriptors if we can. 140 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 141 | case $MAX_FD in #( 142 | max*) 143 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 144 | # shellcheck disable=SC3045 145 | MAX_FD=$( ulimit -H -n ) || 146 | warn "Could not query maximum file descriptor limit" 147 | esac 148 | case $MAX_FD in #( 149 | '' | soft) :;; #( 150 | *) 151 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 152 | # shellcheck disable=SC3045 153 | ulimit -n "$MAX_FD" || 154 | warn "Could not set maximum file descriptor limit to $MAX_FD" 155 | esac 156 | fi 157 | 158 | # Collect all arguments for the java command, stacking in reverse order: 159 | # * args from the command line 160 | # * the main class name 161 | # * -classpath 162 | # * -D...appname settings 163 | # * --module-path (only if needed) 164 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 165 | 166 | # For Cygwin or MSYS, switch paths to Windows format before running java 167 | if "$cygwin" || "$msys" ; then 168 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 169 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 170 | 171 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 172 | 173 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 174 | for arg do 175 | if 176 | case $arg in #( 177 | -*) false ;; # don't mess with options #( 178 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 179 | [ -e "$t" ] ;; #( 180 | *) false ;; 181 | esac 182 | then 183 | arg=$( cygpath --path --ignore --mixed "$arg" ) 184 | fi 185 | # Roll the args list around exactly as many times as the number of 186 | # args, so each arg winds up back in the position where it started, but 187 | # possibly modified. 188 | # 189 | # NB: a `for` loop captures its iteration list before it begins, so 190 | # changing the positional parameters here affects neither the number of 191 | # iterations, nor the values presented in `arg`. 192 | shift # remove old arg 193 | set -- "$@" "$arg" # push replacement arg 194 | done 195 | fi 196 | 197 | 198 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 199 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 200 | 201 | # Collect all arguments for the java command; 202 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 203 | # shell script including quotes and variable substitutions, so put them in 204 | # double quotes to make sure that they get re-expanded; and 205 | # * put everything else in single quotes, so that it's not re-expanded. 206 | 207 | set -- \ 208 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 209 | -classpath "$CLASSPATH" \ 210 | org.gradle.wrapper.GradleWrapperMain \ 211 | "$@" 212 | 213 | # Stop when "xargs" is not available. 214 | if ! command -v xargs >/dev/null 2>&1 215 | then 216 | die "xargs is not available" 217 | fi 218 | 219 | # Use "xargs" to parse quoted args. 220 | # 221 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 222 | # 223 | # In Bash we could simply go: 224 | # 225 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 226 | # set -- "${ARGS[@]}" "$@" 227 | # 228 | # but POSIX shell has neither arrays nor command substitution, so instead we 229 | # post-process each arg (as a line of input to sed) to backslash-escape any 230 | # character that might be a shell metacharacter, then use eval to reverse 231 | # that process (while maintaining the separation between arguments), and wrap 232 | # the whole thing up as a single "set" statement. 233 | # 234 | # This will of course break if any of these variables contains a newline or 235 | # an unmatched quote. 236 | # 237 | 238 | eval "set -- $( 239 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 240 | xargs -n1 | 241 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 242 | tr '\n' ' ' 243 | )" '"$@"' 244 | 245 | exec "$JAVACMD" "$@" 246 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | } 5 | } 6 | 7 | rootProject.name = "firebase-kotlin-bindings" -------------------------------------------------------------------------------- /src/jsMain/kotlin/dev/bitspittle/firebase/analytics/Bindings.kt: -------------------------------------------------------------------------------- 1 | package dev.bitspittle.firebase.analytics 2 | 3 | import dev.bitspittle.firebase.util.jsonWithoutNullValues 4 | import kotlin.js.Json 5 | 6 | class Analytics internal constructor(private val wrapped: dev.bitspittle.firebase.externals.analytics.Analytics) { 7 | sealed class Event(val name: String) { 8 | internal abstract fun toParams(): Json 9 | 10 | /** Represents a screen within an app; one page might host multiple screens */ 11 | // https://firebase.google.com/docs/reference/js/analytics#logevent_3 12 | class PageView(private val title: String? = null, private val location: String? = null, private val path: String? = null) : Event("page_view") { 13 | override fun toParams() = jsonWithoutNullValues( 14 | "title" to title, 15 | "location" to location, 16 | "path" to path, 17 | ) 18 | } 19 | 20 | // region games 21 | 22 | // https://developers.google.com/analytics/devguides/collection/ga4/reference/events?client_type=gtag#level_start 23 | class LevelStart(private val levelName: String? = null) : Event("level_start") { 24 | override fun toParams() = jsonWithoutNullValues( 25 | "level_name" to levelName, 26 | ) 27 | } 28 | 29 | // https://developers.google.com/analytics/devguides/collection/ga4/reference/events?client_type=gtag#level_start 30 | class LevelEnd(private val levelName: String? = null, private val success: Boolean? = null) : Event("level_end") { 31 | override fun toParams() = jsonWithoutNullValues( 32 | "level_name" to levelName, 33 | "success" to success, 34 | ) 35 | } 36 | 37 | // endregion 38 | } 39 | 40 | fun log(event: Event, options: AnalyticsCallOptions? = null) { 41 | dev.bitspittle.firebase.externals.analytics.logEvent( 42 | wrapped, 43 | event.name, 44 | event.toParams(), 45 | @Suppress("NAME_SHADOWING") 46 | options?.let { options -> 47 | object : dev.bitspittle.firebase.externals.analytics.AnalyticsCallOptions { 48 | override val global get() = options.global 49 | } 50 | }) 51 | } 52 | 53 | fun setAnalyticsCollectionEnabled(enabled: Boolean) { 54 | dev.bitspittle.firebase.externals.analytics.setAnalyticsCollectionEnabled(wrapped, enabled) 55 | } 56 | } 57 | 58 | data class AnalyticsCallOptions(val global: Boolean) 59 | 60 | // TODO: Support passing in EventParams as well. 61 | class AnalyticsSettings(internal val gtagParams: GtagConfigParams = GtagConfigParams()) 62 | 63 | // https://firebase.google.com/docs/reference/js/analytics.gtagconfigparams 64 | data class GtagConfigParams( 65 | val allowAdPersonalizationSignals: Boolean? = null, 66 | val allowGoogleSignals: Boolean? = null, 67 | val cookieDomain: String? = null, 68 | val cookieExpires: Number? = null, 69 | val cookieFlags: String? = null, 70 | val cookiePrefix: String? = null, 71 | val cookieUpdate: Boolean? = null, 72 | val pageLocation: String? = null, 73 | val pageTitle: String? = null, 74 | val sendPageView: Boolean? = null, 75 | ) { 76 | internal fun toJson() = jsonWithoutNullValues( 77 | "allow_ad_personalization_signals" to allowAdPersonalizationSignals, 78 | "allow_google_signals" to allowGoogleSignals, 79 | "cookie_domain" to cookieDomain, 80 | "cookie_expires" to cookieExpires, 81 | "cookie_flags" to cookieFlags, 82 | "cookie_prefix" to cookiePrefix, 83 | "cookie_update" to cookieUpdate, 84 | "page_location" to pageLocation, 85 | "page_title" to pageTitle, 86 | "send_page_view" to sendPageView, 87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/dev/bitspittle/firebase/app/Bindings.kt: -------------------------------------------------------------------------------- 1 | package dev.bitspittle.firebase.app 2 | 3 | import dev.bitspittle.firebase.analytics.Analytics 4 | import dev.bitspittle.firebase.analytics.AnalyticsSettings 5 | import dev.bitspittle.firebase.auth.Auth 6 | import dev.bitspittle.firebase.database.Database 7 | import kotlin.js.json 8 | 9 | data class FirebaseOptions( 10 | val apiKey: String, 11 | val authDomain: String, 12 | val databaseURL: String, 13 | val projectId: String, 14 | val storageBucket: String, 15 | val messagingSenderId: String, 16 | val appId: String, 17 | val measurementId: String? = null, 18 | ) 19 | 20 | class FirebaseApp internal constructor(private val wrapped: dev.bitspittle.firebase.externals.app.FirebaseApp) { 21 | companion object { 22 | fun initialize(options: FirebaseOptions, name: String? = null) = FirebaseApp( 23 | dev.bitspittle.firebase.externals.app.initializeApp(json( 24 | "apiKey" to options.apiKey, 25 | "authDomain" to options.authDomain, 26 | "databaseURL" to options.databaseURL, 27 | "projectId" to options.projectId, 28 | "storageBucket" to options.storageBucket, 29 | "messagingSenderId" to options.messagingSenderId, 30 | "appId" to options.appId, 31 | "measurementId" to options.measurementId, 32 | ), 33 | name 34 | ) 35 | ) 36 | } 37 | 38 | val name get() = wrapped.name 39 | val options get() = FirebaseOptions( 40 | apiKey = wrapped.options.apiKey, 41 | authDomain = wrapped.options.authDomain, 42 | databaseURL = wrapped.options.databaseURL, 43 | projectId = wrapped.options.projectId, 44 | storageBucket = wrapped.options.storageBucket, 45 | messagingSenderId = wrapped.options.messagingSenderId, 46 | appId = wrapped.options.appId, 47 | measurementId = wrapped.options.measurementId, 48 | ) 49 | 50 | fun getAnalytics() = Analytics(dev.bitspittle.firebase.externals.analytics.getAnalytics(wrapped)) 51 | fun initializeAnalytics(settings: AnalyticsSettings) = 52 | Analytics(dev.bitspittle.firebase.externals.analytics.initializeAnalytics( 53 | wrapped, 54 | object : dev.bitspittle.firebase.externals.analytics.AnalyticsSettings { 55 | override val config = settings.gtagParams.toJson() 56 | } 57 | )) 58 | 59 | fun getDatabase(url: String? = null) = Database(dev.bitspittle.firebase.externals.database.getDatabase(wrapped, url)) 60 | fun getAuth() = Auth(dev.bitspittle.firebase.externals.auth.getAuth(wrapped)) 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/dev/bitspittle/firebase/auth/Bindings.ActionCode.kt: -------------------------------------------------------------------------------- 1 | package dev.bitspittle.firebase.auth 2 | 3 | class ActionCodeSettings( 4 | val url: String, 5 | val handleCodeInApp: Boolean, 6 | val android: AndroidConfig? = null, 7 | val iOs: IosConfig? = null, 8 | val dynamicLinkDomain: String? = null, 9 | ) { 10 | class AndroidConfig( 11 | val packageName: String, 12 | val installApp: Boolean? = null, 13 | val minimumVersion: String? = null, 14 | ) 15 | class IosConfig(val bundleId: String) 16 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/dev/bitspittle/firebase/auth/Bindings.Errors.kt: -------------------------------------------------------------------------------- 1 | package dev.bitspittle.firebase.auth 2 | 3 | import dev.bitspittle.firebase.util.FirebaseError 4 | 5 | class AuthError internal constructor( 6 | private val wrapped: dev.bitspittle.firebase.externals.auth.AuthError 7 | ) : Exception(), FirebaseError { 8 | // From https://firebase.google.com/docs/reference/js/auth 9 | enum class Code(override val text: String) : FirebaseError.Code { 10 | AdminOnlyOperation("auth/admin-restricted-operation"), 11 | AlreadyInitialized("auth/already-initialized"), 12 | ArgumentError("auth/argument-error"), 13 | AppNotAuthorized("auth/app-not-authorized"), 14 | AppNotInstalled("auth/app-not-installed"), 15 | CaptchaCheckFailed("auth/captcha-check-failed"), 16 | CodeExpired("auth/code-expired"), 17 | CordovaNotReady("auth/cordova-not-ready"), 18 | CorsUnsupported("auth/cors-unsupported"), 19 | CredentialAlreadyInUse("auth/credential-already-in-use"), 20 | CredentialMismatch("auth/custom-token-mismatch"), 21 | CredentialTooOldLoginAgain("auth/requires-recent-login"), 22 | DependentSdkInitBeforeAuth("auth/dependent-sdk-initialized-before-auth"), 23 | DynamicLinkNotActivated("auth/dynamic-link-not-activated"), 24 | EmailChangeNeedsVerification("auth/email-change-needs-verification"), 25 | EmailExists("auth/email-already-in-use"), 26 | EmulatorConfigFailed("auth/emulator-config-failed"), 27 | ExpiredOobCode("auth/expired-action-code"), 28 | ExpiredPopupRequest("auth/cancelled-popup-request"), 29 | InternalError("auth/internal-error"), 30 | InvalidApiKey("auth/invalid-api-key"), 31 | InvalidAppCredential("auth/invalid-app-credential"), 32 | InvalidAppId("auth/invalid-app-id"), 33 | InvalidAuth("auth/invalid-user-token"), 34 | InvalidAuthEvent("auth/invalid-auth-event"), 35 | InvalidCertHash("auth/invalid-cert-hash"), 36 | InvalidCode("auth/invalid-verification-code"), 37 | InvalidContinueUri("auth/invalid-continue-uri"), 38 | InvalidCordovaConfiguration("auth/invalid-cordova-configuration"), 39 | InvalidCustomToken("auth/invalid-custom-token"), 40 | InvalidDynamicLinkDomain("auth/invalid-dynamic-link-domain"), 41 | InvalidEmail("auth/invalid-email"), 42 | InvalidEmulatorScheme("auth/invalid-emulator-scheme"), 43 | InvalidIdpResponse("auth/invalid-credential"), 44 | InvalidMessagePayload("auth/invalid-message-payload"), 45 | InvalidMfaSession("auth/invalid-multi-factor-session"), 46 | InvalidOauthClientId("auth/invalid-oauth-client-id"), 47 | InvalidOauthProvider("auth/invalid-oauth-provider"), 48 | InvalidOobCode("auth/invalid-action-code"), 49 | InvalidOrigin("auth/unauthorized-domain"), 50 | InvalidPassword("auth/wrong-password"), 51 | InvalidPersistence("auth/invalid-persistence-type"), 52 | InvalidPhoneNumber("auth/invalid-phone-number"), 53 | InvalidProviderId("auth/invalid-provider-id"), 54 | InvalidRecipientEmail("auth/invalid-recipient-email"), 55 | InvalidSender("auth/invalid-sender"), 56 | InvalidSessionInfo("auth/invalid-verification-id"), 57 | InvalidTenantId("auth/invalid-tenant-id"), 58 | MfaInfoNotFound("auth/multi-factor-info-not-found"), 59 | MfaRequired("auth/multi-factor-auth-required"), 60 | MissingAndroidPackageName("auth/missing-android-pkg-name"), 61 | MissingAppCredential("auth/missing-app-credential"), 62 | MissingAuthDomain("auth/auth-domain-config-required"), 63 | MissingCode("auth/missing-verification-code"), 64 | MissingContinueUri("auth/missing-continue-uri"), 65 | MissingIframeStart("auth/missing-iframe-start"), 66 | MissingIosBundleId("auth/missing-ios-bundle-id"), 67 | MissingOrInvalidNonce("auth/missing-or-invalid-nonce"), 68 | MissingMfaInfo("auth/missing-multi-factor-info"), 69 | MissingMfaSession("auth/missing-multi-factor-session"), 70 | MissingPhoneNumber("auth/missing-phone-number"), 71 | MissingSessionInfo("auth/missing-verification-id"), 72 | ModuleDestroyed("auth/app-deleted"), 73 | NeedConfirmation("auth/account-exists-with-different-credential"), 74 | NetworkRequestFailed("auth/network-request-failed"), 75 | NullUser("auth/null-user"), 76 | NoAuthEvent("auth/no-auth-event"), 77 | NoSuchProvider("auth/no-such-provider"), 78 | OperationNotAllowed("auth/operation-not-allowed"), 79 | OperationNotSupported("auth/operation-not-supported-in-this-environment"), 80 | PopupBlocked("auth/popup-blocked"), 81 | PopupClosedByUser("auth/popup-closed-by-user"), 82 | ProviderAlreadyLinked("auth/provider-already-linked"), 83 | QuotaExceeded("auth/quota-exceeded"), 84 | RedirectCancelledByUser("auth/redirect-cancelled-by-user"), 85 | RedirectOperationPending("auth/redirect-operation-pending"), 86 | RejectedCredential("auth/rejected-credential"), 87 | SecondFactorAlreadyEnrolled("auth/second-factor-already-in-use"), 88 | SecondFactorLimitExceeded("auth/maximum-second-factor-count-exceeded"), 89 | TenantIdMismatch("auth/tenant-id-mismatch"), 90 | Timeout("auth/timeout"), 91 | TokenExpired("auth/user-token-expired"), 92 | TooManyAttemptsTryLater("auth/too-many-requests"), 93 | UnauthorizedDomain("auth/unauthorized-continue-uri"), 94 | UnsupportedFirstFactor("auth/unsupported-first-factor"), 95 | UnsupportedPersistence("auth/unsupported-persistence-type"), 96 | UnsupportedTenantOperation("auth/unsupported-tenant-operation"), 97 | UnverifiedEmail("auth/unverified-email"), 98 | UserCancelled("auth/user-cancelled"), 99 | UserDeleted("auth/user-not-found"), 100 | UserDisabled("auth/user-disabled"), 101 | UserMismatch("auth/user-mismatch"), 102 | UserSignedOut("auth/user-signed-out"), 103 | WeakPassword("auth/weak-password"), 104 | WebStorageUnsupported("auth/web-storage-unsupported"); 105 | 106 | companion object { 107 | fun from(code: String): Code { 108 | return values().first { it.text == code } 109 | } 110 | } 111 | } 112 | 113 | 114 | val customData get() = wrapped.customData 115 | override val code get() = Code.from(wrapped.code) 116 | override val message get() = wrapped.message 117 | } 118 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/dev/bitspittle/firebase/auth/Bindings.Providers.kt: -------------------------------------------------------------------------------- 1 | package dev.bitspittle.firebase.auth 2 | 3 | import dev.bitspittle.firebase.util.jsonWithoutNullValues 4 | 5 | open class AuthProvider internal constructor( 6 | internal open val wrapped: dev.bitspittle.firebase.externals.auth.AuthProvider 7 | ) { 8 | val providerId get() = wrapped.providerId 9 | } 10 | 11 | // See: https://developers.google.com/identity/openid-connect/openid-connect#authenticationuriparameters 12 | class OAuthCustomParameters( 13 | val accessType: AccessType? = null, 14 | val hd: String? = null, 15 | val includeGrantedScopes: Boolean? = null, 16 | val loginHint: String? = null, 17 | val prompt: Prompt? = null, 18 | val state: String? = null, 19 | ) { 20 | enum class AccessType { 21 | Offline, 22 | Online, 23 | } 24 | enum class Prompt { 25 | None, 26 | Consent, 27 | SelectAccount, 28 | } 29 | } 30 | 31 | open class FederatedAuthProvider internal constructor( 32 | override val wrapped: dev.bitspittle.firebase.externals.auth.FederatedAuthProvider 33 | ) : AuthProvider(wrapped) { 34 | 35 | fun setCustomParameters(params: OAuthCustomParameters): AuthProvider { 36 | return AuthProvider( 37 | wrapped.setCustomParameters( 38 | jsonWithoutNullValues( 39 | "accessType" to params.accessType?.name?.lowercase(), 40 | "hd" to params.hd, 41 | "includeGrantedScopes" to params.includeGrantedScopes, 42 | "loginHint" to params.loginHint, 43 | "prompt" to params.prompt?.name?.lowercase(), 44 | "state" to params.state, 45 | ) 46 | ) 47 | ) 48 | } 49 | } 50 | 51 | sealed class Scope(internal val key: String) { 52 | sealed class Google(key: String): Scope("https://www.googleapis.com/auth/$key") { 53 | object Email : Google("userinfo.email") 54 | object Profile : Google("userinfo.profile") 55 | } 56 | } 57 | 58 | open class BaseOAuthProvider internal constructor( 59 | override val wrapped: dev.bitspittle.firebase.externals.auth.BaseOAuthProvider 60 | ) : FederatedAuthProvider(wrapped) { 61 | fun addScope(scope: S): AuthProvider = AuthProvider(wrapped.addScope(scope.key)) 62 | } 63 | 64 | class GoogleAuthProvider internal constructor( 65 | override val wrapped: dev.bitspittle.firebase.externals.auth.GoogleAuthProvider 66 | ) : BaseOAuthProvider(wrapped) { 67 | constructor() : this(dev.bitspittle.firebase.externals.auth.GoogleAuthProvider()) 68 | } 69 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/dev/bitspittle/firebase/auth/Bindings.kt: -------------------------------------------------------------------------------- 1 | package dev.bitspittle.firebase.auth 2 | 3 | import dev.bitspittle.firebase.util.jsonWithoutNullValues 4 | import dev.bitspittle.firebase.util.snakeCaseToTitleCamelCase 5 | import kotlinx.coroutines.await 6 | 7 | @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") 8 | private inline fun runUnsafe(block: () -> T): T { 9 | try { 10 | return block() 11 | } catch (e: Throwable) { 12 | (e as? dev.bitspittle.firebase.externals.auth.AuthError)?.let { throw AuthError(it) } 13 | throw e 14 | } 15 | } 16 | 17 | class Auth internal constructor(private val wrapped: dev.bitspittle.firebase.externals.auth.Auth) { 18 | val config get() = Config(wrapped.config) 19 | val currentUser get() = wrapped.currentUser?.let { User(it) } 20 | val languageCode get() = wrapped.languageCode 21 | val name get() = wrapped.name 22 | val settings get() = AuthSettings(wrapped.settings) 23 | 24 | suspend fun setPersistence(persistence: Persistence) = 25 | wrapped.setPersistence(persistence.wrapped).await() 26 | 27 | suspend fun updateCurrentUser(user: User?) = 28 | wrapped.updateCurrentUser(user?.wrapped).await() 29 | 30 | fun useDeviceLanguage() = wrapped.useDeviceLanguage() 31 | 32 | fun onAuthStateChanged(handleStateChanged: (User?) -> Unit) { 33 | dev.bitspittle.firebase.externals.auth.onAuthStateChanged(wrapped) { user -> 34 | handleStateChanged(user?.let { User(it) }) 35 | } 36 | } 37 | 38 | suspend fun createUserWithEmailAndPassword(email: String, password: String) = runUnsafe { 39 | UserCredential( 40 | dev.bitspittle.firebase.externals.auth.createUserWithEmailAndPassword(wrapped, email, password).await() 41 | ) 42 | } 43 | 44 | fun isSignInLink(emailLink: String) = runUnsafe { 45 | dev.bitspittle.firebase.externals.auth.isSignInWithEmailLink(wrapped, emailLink) 46 | } 47 | 48 | suspend fun sendSignInLinkToEmail(email: String, actionCodeSettings: ActionCodeSettings) = runUnsafe { 49 | dev.bitspittle.firebase.externals.auth.sendSignInLinkToEmail( 50 | auth = wrapped, 51 | email = email, 52 | actionCodeSettings = object : dev.bitspittle.firebase.externals.auth.ActionCodeSettings { 53 | override val android: dynamic = actionCodeSettings.android?.let { android -> 54 | jsonWithoutNullValues( 55 | "installApp" to android.installApp, 56 | "minimumVersion" to android.minimumVersion, 57 | "packageName" to android.packageName, 58 | ) 59 | } ?: undefined 60 | override val dynamicLinkDomain: String? = actionCodeSettings.dynamicLinkDomain ?: undefined 61 | override val handleCodeInApp: Boolean = actionCodeSettings.handleCodeInApp 62 | override val iOS: dynamic = actionCodeSettings.iOs?.let { ios -> 63 | jsonWithoutNullValues("bundleId" to ios.bundleId) 64 | } ?: undefined 65 | override val url: String = actionCodeSettings.url 66 | } 67 | ).await() 68 | } 69 | 70 | suspend fun signInWithEmailAndPassword(email: String, password: String) = runUnsafe { 71 | UserCredential( 72 | dev.bitspittle.firebase.externals.auth.signInWithEmailAndPassword(wrapped, email, password).await() 73 | ) 74 | } 75 | 76 | suspend fun signInWithEmailLink(email: String, emailLink: String?) = runUnsafe { 77 | UserCredential( 78 | dev.bitspittle.firebase.externals.auth.signInWithEmailLink(wrapped, email, emailLink).await() 79 | ) 80 | } 81 | 82 | suspend fun signInWithPopup(provider: AuthProvider) = runUnsafe { 83 | UserCredential( 84 | dev.bitspittle.firebase.externals.auth.signInWithPopup(wrapped, provider.wrapped).await() 85 | ) 86 | } 87 | 88 | suspend fun signOut() = dev.bitspittle.firebase.externals.auth.signOut(wrapped).await() 89 | } 90 | 91 | class AuthSettings internal constructor( 92 | internal val wrapped: dev.bitspittle.firebase.externals.auth.AuthSettings 93 | ) { 94 | val appVerificationDisabledForTesting get() = wrapped.appVerificationDisabledForTesting 95 | } 96 | 97 | // https://firebase.google.com/docs/reference/js/auth.config 98 | class Config internal constructor( 99 | internal val wrapped: dev.bitspittle.firebase.externals.auth.Config 100 | ) { 101 | val apiHost get() = wrapped.apiHost 102 | val apiKey get() = wrapped.apiKey 103 | val apiScheme get() = wrapped.apiScheme 104 | val authDomain get() = wrapped.authDomain 105 | val sdkClientVersion get() = wrapped.sdkClientVersion 106 | val tokenApiHost get() = wrapped.tokenApiHost 107 | } 108 | 109 | enum class OperationType { 110 | Link, 111 | Reauthenticate, 112 | SignIn; 113 | 114 | companion object { 115 | internal fun from(type: dynamic) = run { 116 | val name = type.toString().snakeCaseToTitleCamelCase() 117 | OperationType.values().first { it.name == name } 118 | } 119 | } 120 | } 121 | 122 | class Persistence internal constructor(internal val wrapped: dev.bitspittle.firebase.externals.auth.Persistence) { 123 | val type get() = PersistenceType.from(wrapped.type) 124 | } 125 | 126 | enum class PersistenceType { 127 | None, 128 | Local, 129 | Session; 130 | 131 | companion object { 132 | internal fun from(type: dynamic) = run { 133 | val name = type.toString().snakeCaseToTitleCamelCase() 134 | PersistenceType.values().first { it.name == name } 135 | } 136 | } 137 | } 138 | 139 | class User internal constructor( 140 | override val wrapped: dev.bitspittle.firebase.externals.auth.User 141 | ) : UserInfo(wrapped) { 142 | val emailVerified get() = wrapped.emailVerified 143 | val isAnonymous get() = wrapped.isAnonymous 144 | val metadata get() = UserMetadata(wrapped.metadata) 145 | val providerData get() = wrapped.providerData.map { UserInfo(it) }.toTypedArray() 146 | val refreshToken get() = wrapped.refreshToken 147 | 148 | suspend fun delete() = 149 | dev.bitspittle.firebase.externals.auth.deleteUser(wrapped).await() 150 | 151 | /** 152 | * Returns a JSON Web Token (JWT) used to identify the user to a Firebase service. 153 | * Returns the current token if it has not expired. Otherwise, this will refresh the token and return a new one. 154 | */ 155 | suspend fun getIdToken(forceRefresh: Boolean? = null) = wrapped.getIdToken(forceRefresh).await() 156 | 157 | suspend fun sendEmailVerification() = 158 | dev.bitspittle.firebase.externals.auth.sendEmailVerification(wrapped).await() 159 | } 160 | 161 | class UserCredential internal constructor( 162 | internal val wrapped: dev.bitspittle.firebase.externals.auth.UserCredential 163 | ) { 164 | val operationType get() = wrapped.operationType 165 | val providerId get() = wrapped.providerId 166 | val user get() = User(wrapped.user) 167 | } 168 | 169 | open class UserInfo internal constructor( 170 | internal open val wrapped: dev.bitspittle.firebase.externals.auth.UserInfo 171 | ) { 172 | val displayName get() = wrapped.displayName 173 | val email get() = wrapped.email 174 | val phoneNumber get() = wrapped.phoneNumber 175 | val photoURL get() = wrapped.photoURL 176 | val providerId get() = wrapped.providerId 177 | val uid get() = wrapped.uid 178 | } 179 | 180 | class UserMetadata internal constructor( 181 | internal val wrapped: dev.bitspittle.firebase.externals.auth.UserMetadata 182 | ) { 183 | val creationTime get() = wrapped.creationTime 184 | val lastSignInTime get() = wrapped.lastSignInTime 185 | } 186 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/dev/bitspittle/firebase/database/Bindings.kt: -------------------------------------------------------------------------------- 1 | package dev.bitspittle.firebase.database 2 | 3 | import dev.bitspittle.firebase.util.snakeCaseToTitleCamelCase 4 | import dev.bitspittle.firebase.util.titleCamelCaseToSnakeCase 5 | import kotlinx.coroutines.await 6 | import kotlin.js.Json 7 | import kotlin.js.json 8 | 9 | typealias Unsubscribe = () -> Unit 10 | 11 | // See: https://firebase.google.com/docs/database/web/structure-data#how_data_is_structured_its_a_json_tree 12 | private val INVALID_KEY_CHARS = setOf( 13 | '.', 14 | '$', 15 | '#', 16 | '[', 17 | ']', 18 | '/', 19 | ) 20 | 21 | /** 22 | * A helper method which encodes a key, ensuring a firebase database can accept it. 23 | * 24 | * Note that slashes get encoded! So if you are specifying a full path, e.g. `a/b/c/$key_with_slashes_maybe`, be sure 25 | * to encode just the trailing part! 26 | * 27 | * Later, you can use [decodeKey] to return a key to its original value. 28 | * 29 | * See also the note about valid keys here: 30 | * https://firebase.google.com/docs/database/web/structure-data#how_data_is_structured_its_a_json_tree 31 | */ 32 | fun String.encodeKey(): String { 33 | var encoded = this 34 | INVALID_KEY_CHARS.forEach { c -> 35 | encoded = encoded.replace(c.toString(), "%${c.code.toString(16).uppercase()}") 36 | } 37 | return encoded 38 | } 39 | 40 | private val HEX_STR_REGEX = Regex("""%([a-fA-F0-9]{2})""") 41 | 42 | /** 43 | * A helper method which decodes a value encoded by [encodeKey]. 44 | */ 45 | fun String.decodeKey(): String { 46 | return this.replace(HEX_STR_REGEX) { result -> 47 | Char(result.groupValues[1].toInt(16)).toString() 48 | } 49 | } 50 | 51 | class Database internal constructor(private val wrapped: dev.bitspittle.firebase.externals.database.Database) { 52 | fun ref(path: String? = null) = 53 | DatabaseReference(dev.bitspittle.firebase.externals.database.ref(wrapped, path)) 54 | } 55 | 56 | class ListenOptions(val onlyOnce: Boolean = false) 57 | 58 | open class Query internal constructor( 59 | private val wrapped: dev.bitspittle.firebase.externals.database.Query 60 | ) { 61 | // Note: Must be get() or else you end up with infinite recursion, since initializing a DatabaseReference 62 | // also initialized a Query, which initialized a DatabaseReference.... 63 | val ref get() = DatabaseReference(wrapped.ref) 64 | 65 | suspend fun get() = DataSnapshot( 66 | dev.bitspittle.firebase.externals.database.get(wrapped).await() 67 | ) 68 | 69 | fun onValue(listenOptions: ListenOptions? = null, callback: (snapshot: DataSnapshot) -> Unit): Unsubscribe { 70 | return dev.bitspittle.firebase.externals.database.onValue( 71 | wrapped, 72 | { callback.invoke(DataSnapshot(it)) }, 73 | listenOptions?.let { object : dev.bitspittle.firebase.externals.database.ListenOptions { 74 | override val onlyOnce = it.onlyOnce 75 | }} 76 | ) as Unsubscribe 77 | } 78 | 79 | fun onChildAdded(listenOptions: ListenOptions? = null, callback: (snapshot: DataSnapshot, previousChildName: String?) -> Unit): Unsubscribe { 80 | return dev.bitspittle.firebase.externals.database.onChildAdded( 81 | wrapped, 82 | { _snapshot, _previousChildName -> callback.invoke(DataSnapshot(_snapshot), _previousChildName) }, 83 | listenOptions?.let { object : dev.bitspittle.firebase.externals.database.ListenOptions { 84 | override val onlyOnce = it.onlyOnce 85 | }} 86 | ) as Unsubscribe 87 | } 88 | 89 | fun off(eventType: EventType? = null, listenOptions: ListenOptions? = null, callback: (snapshot: DataSnapshot, previousChildName: String?) -> Unit) { 90 | dev.bitspittle.firebase.externals.database.off( 91 | wrapped, 92 | eventType?.toTypeStr(), 93 | { _snapshot, _previousChildName -> callback.invoke(DataSnapshot(_snapshot), _previousChildName) }, 94 | listenOptions?.let { object : dev.bitspittle.firebase.externals.database.ListenOptions { 95 | override val onlyOnce = it.onlyOnce 96 | }} 97 | ) 98 | } 99 | 100 | fun query(vararg constraints: QueryConstraint): Query { 101 | return Query( 102 | dev.bitspittle.firebase.externals.database.query( 103 | wrapped, 104 | constraints.map { it.wrapped }.toTypedArray() 105 | ) 106 | ) 107 | } 108 | } 109 | 110 | class DatabaseReference internal constructor( 111 | private val wrapped: dev.bitspittle.firebase.externals.database.DatabaseReference 112 | ) : Query(wrapped) { 113 | val key get() = wrapped.key 114 | val parent get() = wrapped.parent?.let { DatabaseReference(it) } 115 | val root get() = DatabaseReference(wrapped.root) 116 | 117 | fun child(path: String) = DatabaseReference(dev.bitspittle.firebase.externals.database.child(wrapped, path)) 118 | fun push() = DatabaseReference(dev.bitspittle.firebase.externals.database.push(wrapped)) 119 | suspend fun remove() = dev.bitspittle.firebase.externals.database.remove(wrapped).await() 120 | suspend fun set(value: Any) = 121 | dev.bitspittle.firebase.externals.database.set(wrapped, value).await() 122 | suspend fun update(values: Json) = 123 | dev.bitspittle.firebase.externals.database.update(wrapped, values).await() 124 | 125 | suspend fun runTransaction( 126 | options: TransactionOptions? = null, 127 | transactionUpdate: (currentData: Any?) -> Any? 128 | ) = TransactionResult( 129 | dev.bitspittle.firebase.externals.database.runTransaction( 130 | wrapped, 131 | transactionUpdate = { transactionUpdate(it) }, 132 | options?.wrapped 133 | ).await() 134 | ) 135 | } 136 | 137 | suspend fun DatabaseReference.update(vararg values: Pair) = update(json(*values)) 138 | suspend fun DatabaseReference.update(values: Collection>) = update(*values.toTypedArray()) 139 | 140 | class DataSnapshot internal constructor( 141 | private val wrapped: dev.bitspittle.firebase.externals.database.DataSnapshot 142 | ) { 143 | val key get() = wrapped.key 144 | val priority get() = Priority.from(wrapped.priority) 145 | val ref get() = DatabaseReference(wrapped.ref) 146 | val size get() = wrapped.size as Int 147 | 148 | fun child(path: String) = DataSnapshot(wrapped.child(path)) 149 | fun exists() = wrapped.exists() 150 | fun forEach(action: (DataSnapshot) -> Unit) { forEachAbortable { action(it); false } } 151 | // return true to abort (meaning, you found your item), false otherwise 152 | fun forEachAbortable(action: (DataSnapshot) -> Boolean): Boolean = wrapped.forEach { action(DataSnapshot(it)) } 153 | fun hasChild(path: String) = wrapped.hasChild(path) 154 | fun hasChildren() = wrapped.hasChildren() 155 | fun `val`() = wrapped.`val`() 156 | } 157 | 158 | /** 159 | * A convenience method that you can call instead of [DataSnapshot.val]. 160 | * 161 | * Since `val` is a reserved keyword in Kotlin, making its syntax a little strange. 162 | */ 163 | fun DataSnapshot.value() = `val`() 164 | 165 | // https://firebase.google.com/docs/reference/js/database.md#eventtype 166 | enum class EventType { 167 | Value, 168 | ChildAdded, 169 | ChildChanged, 170 | ChildMoved, 171 | ChildRemoved; 172 | 173 | companion object { 174 | internal fun from(typeStr: String) = run { 175 | val name = typeStr.snakeCaseToTitleCamelCase() 176 | EventType.values().first { it.name == name } 177 | } 178 | } 179 | 180 | fun toTypeStr() = name.titleCamelCaseToSnakeCase() 181 | } 182 | 183 | sealed interface Priority { 184 | companion object { 185 | internal fun from(priority: dynamic): Priority? { 186 | return when (priority) { 187 | null -> null 188 | is Number -> OfNumber(priority) 189 | is String -> OfString(priority) 190 | else -> error("Unexpected priority value: $priority") 191 | } 192 | } 193 | } 194 | 195 | val value: Any 196 | 197 | class OfNumber(override val value: Number) : Priority 198 | class OfString(override val value: String) : Priority 199 | } 200 | 201 | // Note: Can't be a class because it's used as a vararg parameter 202 | class QueryConstraint internal constructor( 203 | internal val wrapped: dev.bitspittle.firebase.externals.database.QueryConstraint 204 | ) { 205 | companion object { 206 | fun limitToFirst(limit: Number) = QueryConstraint(dev.bitspittle.firebase.externals.database.limitToFirst(limit)) 207 | fun limitToLast(limit: Number) = QueryConstraint(dev.bitspittle.firebase.externals.database.limitToLast(limit)) 208 | fun orderByChild(path: String) = QueryConstraint(dev.bitspittle.firebase.externals.database.orderByChild(path)) 209 | } 210 | 211 | val type get() = QueryConstraintType.from(wrapped.type) 212 | } 213 | 214 | enum class QueryConstraintType { 215 | EndAt, 216 | EndBefore, 217 | StartAt, 218 | StartAfter, 219 | LimitToFirst, 220 | LimitToLast, 221 | OrderByChild, 222 | OrderByKey, 223 | OrderByPriority, 224 | OrderByValue, 225 | EqualTo; 226 | 227 | companion object { 228 | internal fun from(typeStr: String) = run { 229 | val name = typeStr.capitalize() 230 | QueryConstraintType.values().first { it.name == name } 231 | } 232 | } 233 | } 234 | 235 | class TransactionOptions internal constructor( 236 | internal val wrapped: dev.bitspittle.firebase.externals.database.TransactionOptions 237 | ) { 238 | val applyLocally get() = wrapped.applyLocally 239 | } 240 | 241 | class TransactionResult internal constructor( 242 | internal val wrapped: dev.bitspittle.firebase.externals.database.TransactionResult 243 | ) { 244 | val committed get() = wrapped.committed 245 | val snapshot get() = DataSnapshot(wrapped.snapshot) 246 | } 247 | 248 | object ServerValue { 249 | fun increment(delta: Number = 1) = dev.bitspittle.firebase.externals.database.increment(delta) 250 | fun timestamp() = dev.bitspittle.firebase.externals.database.serverTimestamp() 251 | } 252 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/dev/bitspittle/firebase/externals/analytics/Externals.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("firebase/analytics") 2 | @file:JsNonModule 3 | package dev.bitspittle.firebase.externals.analytics 4 | 5 | import dev.bitspittle.firebase.externals.app.FirebaseApp 6 | import kotlin.js.Json 7 | 8 | // https://firebase.google.com/docs/reference/js/analytics.analytics 9 | internal external interface Analytics { 10 | val app: FirebaseApp 11 | } 12 | 13 | // https://firebase.google.com/docs/reference/js/analytics.analyticscalloptions 14 | internal external interface AnalyticsCallOptions { 15 | val global: Boolean 16 | } 17 | 18 | // https://firebase.google.com/docs/reference/js/analytics.analyticssettings 19 | internal external interface AnalyticsSettings { 20 | val config: dynamic // EventParams | GtagConfigParams 21 | } 22 | 23 | // https://firebase.google.com/docs/reference/js/analytics.eventparams 24 | internal external interface EventParams { 25 | val affiliation: String? 26 | val checkoutOption: String? 27 | val checkoutStep: Number? 28 | val contentType: String? 29 | val coupon: String? 30 | val currency: String? 31 | val description: String? 32 | val eventCategory: String? 33 | val eventLabel: String? 34 | val fatal: Boolean? 35 | val firebaseScreenClass: String? 36 | val firebaseScreen: String? 37 | val itemId: String? 38 | val itemListId: String? 39 | val itemListName: String? 40 | val items: Array? 41 | val method: String? 42 | val number: String? 43 | val pageLocation: String? 44 | val pagePath: String? 45 | val pageTitle: String? 46 | val paymentType: String? 47 | val promotionId: String? 48 | val promotionName: String? 49 | val promotions: Array? 50 | val screenName: String? 51 | val searchTerm: String? 52 | val shippingTier: String? 53 | val shipping: dynamic // Currency? 54 | val tax: dynamic // Currency? 55 | val transactionId: String? 56 | val value: Number? 57 | } 58 | 59 | // https://firebase.google.com/docs/reference/js/analytics.gtagconfigparams 60 | internal external interface GtagConfigParams { 61 | val allowAdPersonalizationSignals: Boolean? 62 | val allowGoogleSignals: Boolean? 63 | val cookieDomain: String? 64 | val cookieExpires: Number? 65 | val cookieFlags: String? 66 | val cookiePrefix: String? 67 | val cookieUpdate: Boolean? 68 | val pageLocation: String? 69 | val pageTitle: String? 70 | val sendPageView: Boolean? 71 | } 72 | 73 | // https://firebase.google.com/docs/reference/js/analytics.item 74 | internal external interface Item { 75 | val affiliation: String? 76 | val coupon: String? 77 | val creativeName: String? 78 | val creativeSlot: String? 79 | val discount: dynamic // Currency? 80 | val index: Number? 81 | val itemBrand: String? 82 | val itemCategory: String? 83 | val itemCategory2: String? 84 | val itemCategory3: String? 85 | val itemCategory4: String? 86 | val itemCategory5: String? 87 | val itemId: String? 88 | val itemListId: String? 89 | val itemListName: String? 90 | val itemName: String? 91 | val itemVariant: String? 92 | val locationId: String? 93 | val price: dynamic // Currency? 94 | val promotionId: String? 95 | val promotionName: String? 96 | val quantity: Number? 97 | } 98 | 99 | // https://firebase.google.com/docs/reference/js/analytics.promotion 100 | internal external interface Promotion { 101 | val creativeName: String? 102 | val creativeSlot: String? 103 | val id: String? 104 | val name: String? 105 | } 106 | 107 | // https://firebase.google.com/docs/reference/js/analytics#getanalytics 108 | internal external fun getAnalytics(app: FirebaseApp): Analytics 109 | 110 | // https://firebase.google.com/docs/reference/js/analytics#initializeanalytics 111 | internal external fun initializeAnalytics(app: FirebaseApp, options: AnalyticsSettings?): Analytics 112 | 113 | // https://firebase.google.com/docs/reference/js/analytics#logevent 114 | // Note: There are a bunch of logevent varieties, but we just use a single one here to catch all. 115 | // We'll leave it up to the bindings layer to provide more interesting APIs. 116 | internal external fun logEvent(analytics: Analytics, name: String, params: Json?, options: AnalyticsCallOptions?) 117 | 118 | // https://firebase.google.com/docs/reference/js/analytics#setanalyticscollectionenabled 119 | internal external fun setAnalyticsCollectionEnabled(analytics: Analytics, enabled: Boolean) -------------------------------------------------------------------------------- /src/jsMain/kotlin/dev/bitspittle/firebase/externals/app/Externals.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("firebase/app") 2 | @file:JsNonModule 3 | package dev.bitspittle.firebase.externals.app 4 | 5 | import kotlin.js.Json 6 | 7 | // https://firebase.google.com/docs/reference/js/app.firebaseapp.md 8 | internal external interface FirebaseApp { 9 | val name: String 10 | val options: FirebaseOptions 11 | } 12 | 13 | // https://firebase.google.com/docs/reference/js/app.firebaseoptions 14 | internal external interface FirebaseOptions { 15 | val apiKey: String 16 | val authDomain: String 17 | val databaseURL: String 18 | val projectId: String 19 | val storageBucket: String 20 | val messagingSenderId: String 21 | val appId: String 22 | val measurementId: String? 23 | } 24 | 25 | // https://firebase.google.com/docs/reference/js/app.md#initializeapp_2 26 | internal external fun initializeApp(options: Json, name: String?): FirebaseApp 27 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/dev/bitspittle/firebase/externals/auth/Externals.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("firebase/auth") 2 | @file:JsNonModule 3 | package dev.bitspittle.firebase.externals.auth 4 | 5 | import dev.bitspittle.firebase.externals.app.FirebaseApp 6 | import dev.bitspittle.firebase.externals.util.FirebaseError 7 | import kotlin.js.Json 8 | import kotlin.js.Promise 9 | 10 | // https://firebase.google.com/docs/reference/js/auth.actioncodesettings.md#actioncodesettings_interface 11 | internal external interface ActionCodeSettings { 12 | val android: dynamic // { installApp?: boolean; minimumVersion?: string; packageName: string; } 13 | val dynamicLinkDomain: String? 14 | val handleCodeInApp: Boolean 15 | val iOS: dynamic // { bundleId: string; } 16 | val url: String 17 | } 18 | 19 | // https://firebase.google.com/docs/reference/js/auth.auth 20 | internal external interface Auth { 21 | val app: FirebaseApp 22 | val config: Config 23 | val currentUser: User? 24 | val languageCode: String? 25 | val name: String 26 | val settings: AuthSettings 27 | 28 | fun setPersistence(persistence: Persistence): Promise 29 | fun updateCurrentUser(user: User?): Promise 30 | fun useDeviceLanguage() 31 | } 32 | 33 | // https://firebase.google.com/docs/reference/js/auth.autherror 34 | internal external interface AuthError : FirebaseError { 35 | val customData: Json 36 | } 37 | 38 | // https://firebase.google.com/docs/reference/js/auth.authprovider 39 | internal external interface AuthProvider { 40 | val providerId: String 41 | } 42 | 43 | // https://firebase.google.com/docs/reference/js/auth.authsettings 44 | internal external interface AuthSettings { 45 | val appVerificationDisabledForTesting: Boolean 46 | } 47 | 48 | // https://github.com/firebase/firebase-js-sdk/blob/master/packages/auth/src/core/providers/oauth.ts 49 | internal abstract external class BaseOAuthProvider : FederatedAuthProvider { 50 | fun addScope(scope: String): AuthProvider 51 | fun getScopes(): Array 52 | } 53 | 54 | // https://firebase.google.com/docs/reference/js/auth.config 55 | internal external interface Config { 56 | val apiHost: String 57 | val apiKey: String 58 | val apiScheme: String 59 | val authDomain: String 60 | val sdkClientVersion: String 61 | val tokenApiHost: String 62 | } 63 | 64 | // https://firebase.google.com/docs/reference/js/auth.googleauthprovider 65 | internal external class GoogleAuthProvider : BaseOAuthProvider { 66 | override val providerId: String 67 | } 68 | 69 | // https://github.com/firebase/firebase-js-sdk/blob/master/packages/auth/src/core/providers/federated.ts 70 | internal abstract external class FederatedAuthProvider : AuthProvider { 71 | fun setCustomParameters(params: Json): AuthProvider 72 | } 73 | 74 | // https://firebase.google.com/docs/reference/js/auth.persistence 75 | internal external interface Persistence { 76 | val type: dynamic // 'SESSION' | 'LOCAL' | 'NONE' 77 | } 78 | 79 | // https://firebase.google.com/docs/reference/js/auth.user 80 | internal external interface User : UserInfo { 81 | val emailVerified: Boolean 82 | val isAnonymous: Boolean 83 | val metadata: UserMetadata 84 | val providerData: Array 85 | val refreshToken: String 86 | 87 | // https://firebase.google.com/docs/reference/js/auth.user#usergetidtoken 88 | fun getIdToken(forceRefresh: Boolean?): Promise 89 | } 90 | 91 | // https://firebase.google.com/docs/reference/js/auth.usercredential 92 | internal external interface UserCredential { 93 | val operationType: dynamic // 'LINK' | 'REAUTHENTICATE' | 'SIGN_IN' 94 | val providerId: String? 95 | val user: User 96 | } 97 | 98 | // https://firebase.google.com/docs/reference/js/auth.userinfo 99 | internal external interface UserInfo { 100 | val displayName: String? 101 | val email: String? 102 | val phoneNumber: String? 103 | val photoURL: String? 104 | val providerId: String 105 | val uid: String 106 | } 107 | 108 | // https://firebase.google.com/docs/reference/js/auth.usermetadata 109 | internal external interface UserMetadata { 110 | val creationTime: String 111 | val lastSignInTime: String 112 | } 113 | 114 | // https://firebase.google.com/docs/reference/js/auth#getauth 115 | internal external fun getAuth(app: FirebaseApp): Auth 116 | 117 | 118 | // https://firebase.google.com/docs/reference/js/auth#createuserwithemailandpassword 119 | internal external fun createUserWithEmailAndPassword(auth: Auth, email: String, password: String): Promise 120 | 121 | // https://firebase.google.com/docs/reference/js/auth#deleteuser 122 | internal external fun deleteUser(user: User): Promise 123 | 124 | // https://firebase.google.com/docs/reference/js/auth#issigninwithemaillink 125 | internal external fun isSignInWithEmailLink(auth: Auth, emailLink: String): Boolean 126 | 127 | // https://firebase.google.com/docs/reference/js/auth#onauthstatechanged 128 | internal external fun onAuthStateChanged(auth: Auth, handleStateChanged: (User?) -> Unit) 129 | 130 | // https://firebase.google.com/docs/reference/js/auth#sendemailverification 131 | internal external fun sendEmailVerification(user: User): Promise 132 | 133 | // https://firebase.google.com/docs/reference/js/auth#sendsigninlinktoemail 134 | internal external fun sendSignInLinkToEmail(auth: Auth, email: String, actionCodeSettings: ActionCodeSettings): Promise 135 | 136 | // https://firebase.google.com/docs/reference/js/auth#signinwithemailandpassword 137 | internal external fun signInWithEmailAndPassword(auth: Auth, email: String, password: String): Promise 138 | 139 | // https://firebase.google.com/docs/reference/js/auth#signinwithemaillink 140 | internal external fun signInWithEmailLink(auth: Auth, email: String, emailLink: String?): Promise 141 | 142 | // https://firebase.google.com/docs/reference/js/auth#signinwithpopup 143 | internal external fun signInWithPopup(auth: Auth, provider: AuthProvider): Promise 144 | 145 | // https://firebase.google.com/docs/reference/js/auth#signout 146 | internal external fun signOut(auth: Auth): Promise 147 | 148 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/dev/bitspittle/firebase/externals/database/Externals.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("firebase/database") 2 | @file:JsNonModule 3 | package dev.bitspittle.firebase.externals.database 4 | 5 | import dev.bitspittle.firebase.externals.app.FirebaseApp 6 | import kotlin.js.Json 7 | import kotlin.js.Promise 8 | 9 | // https://firebase.google.com/docs/reference/js/database.database 10 | internal external class Database { 11 | val app: FirebaseApp 12 | } 13 | 14 | // https://firebase.google.com/docs/reference/js/database.databasereference 15 | internal external interface DatabaseReference : Query { 16 | val key: String? 17 | val parent: DatabaseReference? 18 | val root: DatabaseReference 19 | } 20 | 21 | // https://firebase.google.com/docs/reference/js/database.datasnapshot 22 | internal external class DataSnapshot { 23 | val key: String? 24 | val priority: dynamic // String | Number | null 25 | val ref: DatabaseReference 26 | val size: Number 27 | 28 | fun child(path: String): DataSnapshot 29 | fun exists(): Boolean 30 | fun forEach(action: (DataSnapshot) -> Boolean): Boolean 31 | fun hasChild(path: String): Boolean 32 | fun hasChildren(): Boolean 33 | fun `val`(): Any? 34 | } 35 | 36 | // https://firebase.google.com/docs/reference/js/database.listenoptions 37 | internal external interface ListenOptions { 38 | val onlyOnce: Boolean 39 | } 40 | 41 | // https://firebase.google.com/docs/reference/js/database.query 42 | internal external interface Query { 43 | val ref: DatabaseReference 44 | } 45 | 46 | // https://firebase.google.com/docs/reference/js/database.queryconstraint 47 | internal abstract external class QueryConstraint { 48 | val type: String // https://firebase.google.com/docs/reference/js/database#queryconstrainttype 49 | } 50 | 51 | // https://firebase.google.com/docs/reference/js/database.transactionoptions 52 | internal external interface TransactionOptions { 53 | val applyLocally: Boolean 54 | } 55 | 56 | // https://firebase.google.com/docs/reference/js/database.transactionresult 57 | internal external class TransactionResult { 58 | val committed: Boolean 59 | val snapshot: DataSnapshot 60 | } 61 | 62 | // https://firebase.google.com/docs/reference/js/database#getdatabase 63 | internal external fun getDatabase(app: FirebaseApp, url: String?): Database 64 | 65 | 66 | // https://firebase.google.com/docs/reference/js/database#child 67 | internal external fun child(parent: DatabaseReference, path: String): DatabaseReference 68 | 69 | // https://firebase.google.com/docs/reference/js/database#get 70 | internal external fun get(query: Query): Promise 71 | 72 | // https://firebase.google.com/docs/reference/js/database#increment 73 | internal external fun increment(delta: Number): Json 74 | 75 | // https://firebase.google.com/docs/reference/js/database#limittofirst 76 | internal external fun limitToFirst(limit: Number): QueryConstraint 77 | 78 | // https://firebase.google.com/docs/reference/js/database#limittolast 79 | internal external fun limitToLast(limit: Number): QueryConstraint 80 | 81 | // https://firebase.google.com/docs/reference/js/database#onvalue 82 | internal external fun onValue(query: Query, callback: (DataSnapshot) -> Unit, listenOptions: ListenOptions?): dynamic 83 | 84 | // https://firebase.google.com/docs/reference/js/database#onchildadded 85 | internal external fun onChildAdded(query: Query, callback: (DataSnapshot, String?) -> Unit, listenOptions: ListenOptions?): dynamic 86 | 87 | // https://firebase.google.com/docs/reference/js/database#off 88 | internal external fun off(query: Query, eventType: dynamic, callback: (DataSnapshot, String?) -> Unit, listenOptions: ListenOptions?) 89 | 90 | // https://firebase.google.com/docs/reference/js/database#orderbychild 91 | internal external fun orderByChild(path: String): QueryConstraint 92 | 93 | // https://firebase.google.com/docs/reference/js/database#push 94 | internal external fun push(ref: DatabaseReference): DatabaseReference 95 | 96 | // https://firebase.google.com/docs/reference/js/database#query 97 | internal external fun query(query: Query, vararg queryConstraints: dynamic): Query 98 | 99 | // https://firebase.google.com/docs/reference/js/database#ref 100 | internal external fun ref(db: Database, path: String?): DatabaseReference 101 | 102 | // https://firebase.google.com/docs/reference/js/database#remove 103 | internal external fun remove(ref: DatabaseReference): Promise 104 | 105 | // https://firebase.google.com/docs/reference/js/database#runtransaction 106 | internal external fun runTransaction( 107 | ref: DatabaseReference, 108 | transactionUpdate: (currentData: dynamic) -> dynamic, 109 | options: TransactionOptions? 110 | ): Promise 111 | 112 | // https://firebase.google.com/docs/reference/js/database#set 113 | internal external fun set(ref: DatabaseReference, value: dynamic): Promise 114 | 115 | // https://firebase.google.com/docs/reference/js/database#servertimestamp 116 | internal external fun serverTimestamp(): Json 117 | 118 | // https://firebase.google.com/docs/reference/js/database#update 119 | internal external fun update(ref: DatabaseReference, values: Json): Promise 120 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/dev/bitspittle/firebase/externals/util/Externals.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("firebase/util") 2 | @file:JsNonModule 3 | package dev.bitspittle.firebase.externals.util 4 | 5 | internal external interface FirebaseError { 6 | val code: String 7 | val message: String 8 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/dev/bitspittle/firebase/util/FirebaseError.kt: -------------------------------------------------------------------------------- 1 | package dev.bitspittle.firebase.util 2 | 3 | interface FirebaseError { 4 | interface Code { 5 | val text: String 6 | } 7 | val code: C 8 | val message: String 9 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/dev/bitspittle/firebase/util/JsonSupport.kt: -------------------------------------------------------------------------------- 1 | package dev.bitspittle.firebase.util 2 | 3 | import kotlin.js.json 4 | 5 | internal fun Iterable>.filterIfValueIsNull(): List> { 6 | @Suppress("UNCHECKED_CAST") 7 | return this.filter { it.second != null } as List> 8 | } 9 | 10 | internal fun List>.toJson() = run { 11 | json(*this.toTypedArray()) 12 | } 13 | 14 | /** 15 | * Create a json object from the incoming pairs after filtering out entries with a null value. 16 | * 17 | * There's no guarantee that a Firebase API treats "null" as the same thing as simply not set in the first place. So 18 | * when we convert from our Kotlin binding classes, which often use null to indicate "not set", we strip those results 19 | * from the final json passed to Firebase. 20 | */ 21 | internal fun jsonWithoutNullValues(vararg pairs: Pair) = pairs.toList().filterIfValueIsNull().toJson() -------------------------------------------------------------------------------- /src/jsMain/kotlin/dev/bitspittle/firebase/util/StringExtensions.kt: -------------------------------------------------------------------------------- 1 | package dev.bitspittle.firebase.util 2 | 3 | // e.g. "ExampleText" to "example_text" 4 | internal fun String.titleCamelCaseToSnakeCase(): String { 5 | require(this.isNotBlank()) 6 | 7 | val currentWord = StringBuilder() 8 | val words = mutableListOf() 9 | 10 | this.forEach { c -> 11 | if (c.isUpperCase()) { 12 | if (currentWord.isNotEmpty()) { 13 | words.add(currentWord.toString()) 14 | currentWord.clear() 15 | } 16 | } 17 | currentWord.append(c) 18 | } 19 | words.add(currentWord.toString()) 20 | 21 | return words.joinToString("_") { it.decapitalize() } 22 | } 23 | 24 | // e.g. "example_text" to "ExampleText" 25 | internal fun String.snakeCaseToTitleCamelCase(): String { 26 | require(this.isNotBlank()) 27 | 28 | return this.split('_') 29 | .joinToString("") { it.capitalize() } 30 | } --------------------------------------------------------------------------------