Skip to content

A framework for integrating supabase into compose for desktop, android and web

Notifications You must be signed in to change notification settings

johndpope/SupaCompose

 
 

Repository files navigation

SupaCompose

A framework for building android & desktop apps with Supabase (can be used for other purposes)

Newest version: Maven Central

Installation

dependencies {
    implementation("io.github.jan-tennert.supacompose:Supacompose-[module]:VERSION")
}

Creating a client

To create a client simply call the createClient top level function:

val client = createSupabaseClient {
    supabaseUrl = System.getenv("SUPABASE_URL") //without https:// !
    supabaseKey = System.getenv("SUPABASE_KEY")

    install(Auth) {
        //on desktop, you have to set the session file. On android and web it's managed by the plugin
        sessionFile = File("C:\\Users\\user\\AppData\\Local\\SupaCompose\\usersession.json")
    }
    //install other plugins
    install(Postgrest)
    install(Storage)
}

Features

Core

Creating a custom plugin
class MyPlugin(private val config: MyPlugin.Config): SupacomposePlugin {

    fun doSomethingCool() {
        println("something cool")
    }
    
    data class Config(var someSetting: Boolean = false)

    companion object : SupacomposePluginProvider<Config, MyPlugin> {

        override val key = "myplugin" //this key is used to identify the plugin when retrieving it

        override fun createConfig(init: Config.() -> Unit): Config {
            //used to create the configuration object for the plugin
            return Config().apply(init)
        }

        override fun setup(builder: SupabaseClientBuilder, config: Config) {
            //modify the supabase client builder
        }

        override fun create(supabaseClient: SupabaseClient, config: Config): MyPlugin {
            //modify the supabase client and return the final plugin instance
            return MyPlugin(config)
        }

    }

}

//make an easy extension for accessing the plugin
val SupabaseClient.myplugin get() = pluginManager.getPlugin<MyPlugin>("myplugin")

//then install it:
val client = createSupabaseClient {
    install(MyPlugin) {
        someSetting = true
    }
}

Authentication

Feature table
Login Signup Verifying (Signup, Password Reset, Invite) Logout Otp
Desktop phone, password, oauth2 via callback http server phone, password, oauth2 via callback http server only with token
Android phone, password, oauth2 via deeplinks phone, password, oauth2 via deeplinks token, url via deeplinks
Web phone, password, oauth2 phone, password, oauth2 token, url

❌ = will not be implemented
✅ = implemented

Session saving: ✅

Authentication with Desktop

To add OAuth support, add this link to the redirect urls in supabase

img.png

suspend fun main() {
    val client = createSupabaseClient {
        supabaseUrl = System.getenv("SUPABASE_URL")
        supabaseKey = System.getenv("SUPABASE_KEY")

        install(Auth)
    }
    application {
        Window(::exitApplication) {
            val session by client.auth.currentSession.collectAsState()
            val scope = rememberCoroutineScope()
            if (session != null) {
                Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
                    Text("Logged in as ${session?.user?.email}")
                }
            } else {
                Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
                    var email by remember { mutableStateOf("") }
                    var password by remember { mutableStateOf("") }
                    Column {
                        TextField(email, { email = it }, placeholder = { Text("Email") })
                        TextField(
                            password,
                            { password = it },
                            placeholder = { Text("Password") },
                            visualTransformation = PasswordVisualTransformation()
                        )
                        Button(onClick = {
                            scope.launch {
                                client.auth.signUpWith(Email) {
                                    this.email = email
                                    this.password = password
                                }
                            }
                        }, modifier = Modifier.align(Alignment.CenterHorizontally)) {
                            Text("Login")
                        }
                        Button(
                            {
                                scope.launch {
                                    client.auth.loginWith(Discord) {
                                        onFail = {
                                            when (it) {
                                                is OAuthFail.Timeout -> {
                                                    println("Timeout")
                                                }
                                                is OAuthFail.Error -> {
                                                    //log error
                                                }
                                            }
                                        }
                                        timeout = 50.seconds
                                        htmlTitle = "SupaCompose"
                                        htmlText = "Logged in. You may continue in the app."
                                    }
                                }
                            },
                            modifier = Modifier.align(Alignment.CenterHorizontally)
                        ) {
                            Icon(painterResource("discord_icon.svg"), "", modifier = Modifier.size(25.dp))
                            Text("Log in with Discord")
                        }
                    }
                }

            }
        }
    }

}

Authentication with Android

When you set the deep link scheme and host in the supabase deeplink plugin and in the android manifest you have to remember to set the additional redirect url in the subabase auth settings. E.g. if you have supacompose as your scheme and login as your host set this to the additional redirect url:

img.png

MainActivity

Note: you should probably use a viewmodel for suspending functions from the SupaCompose library

class MainActivity : AppCompatActivity() {

    val supabaseClient = createSupabaseClient {

        supabaseUrl = "your supabase url"
        supabaseKey = "your supabase key"

        install(Auth) {
            scheme = "supacompose"
            host = "login"
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        initializeAndroid(supabaseClient) //if you don't call this function the library will throw an error when trying to authenticate with oauth
        setContent {
            MaterialTheme {
                val session by supabaseClient.auth.currentSession.collectAsState()
                println(session)
                val scope = rememberCoroutineScope()
                if (session != null) {
                    Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
                        Text("Logged in as ${session?.user?.email}")
                    }
                } else {
                    Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
                        var email by remember { mutableStateOf("") }
                        var password by remember { mutableStateOf("") }
                        Column {
                            TextField(email, { email = it }, placeholder = { Text("Email") })
                            TextField(
                                password,
                                { password = it },
                                placeholder = { Text("Password") },
                                visualTransformation = PasswordVisualTransformation()
                            )
                            Button(onClick = {
                                scope.launch {
                                    supabaseClient.auth.loginWith(Email) {
                                        this.email = email
                                        this.password = password
                                    }
                                }
                            }, modifier = Modifier.align(Alignment.CenterHorizontally)) {
                                Text("Login")
                            }
                            Button(
                                {
                                    scope.launch {
                                        client.auth.loginWith(Discord) {
                                            onFail = {
                                                when (it) {
                                                    is OAuthFail.Timeout -> {
                                                        println("Timeout")
                                                    }
                                                    is OAuthFail.Error -> {
                                                        //log error
                                                    }
                                                }
                                            }
                                            timeout = 50.seconds
                                            htmlTitle = "SupaCompose"
                                            htmlText = "Logged in. You may continue in the app."
                                        }
                                    }
                                },
                                modifier = Modifier.align(Alignment.CenterHorizontally)
                            ) {
                                Icon(painterResource("discord_icon.svg"), "", modifier = Modifier.size(25.dp))
                                Text("Log in with Discord")
                            }
                        }
                    }
                }
            }
        }
    }

}

AndroidManifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http:https://schemas.android.com/apk/res/android" package="io.github.jan.supacompose.android">

    <uses-permission android:name="android.permission.INTERNET"/>

    <application
            android:allowBackup="false"
            android:supportsRtl="true"
            android:theme="@style/Theme.AppCompat.Light.NoActionBar">
        <activity android:name=".MainActivity" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <!-- This is important for deeplinks. -->
                <data android:scheme="supacompose"
                      android:host="login"/>
            </intent-filter>
        </activity>
    </application>
</manifest>

Authentication with Web

val client = createSupabaseClient {
    supabaseUrl = ""
    supabaseKey = ""

    install(Auth)
}
client.auth.initializeWeb()

renderComposable(rootElementId = "root") {
    val session by client.auth.currentSession.collectAsState()
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    val scope = rememberCoroutineScope()
    if(session != null) {
        Span({ style { padding(15.px) } }) {
            Text("Logged in as ${session!!.user?.email}")
        }
    } else {
        EmailInput(email) {
            onInput {
                email = it.value
            }
        }
        PasswordInput(password) {
            onInput {
                password = it.value
            }
        }
        Button({
            onClick {
                scope.launch {
                    client.auth.loginWith(Email) {
                        this.email = email
                        this.password = password
                    }
                }
            }
        }) {
            Text("Login")
        }
        Button({
            onClick {
                scope.launch {
                    client.auth.loginWith(Discord)
                }
            }
        }) {
            Text("Login with Discord")
        }
    }
}

Database/Postgres

Make database calls
//a data class for a message

data class Message(val text: String, @SerialName("author_id") val authorId: String, val id: Int)

If you use the syntax with property references the client will automatically look for @SerialName annotiations on your class property and if it has one it will use the value as the column name. (Only JVM)

Select
client.postgrest["messages"]
    .select {
        //you can use that syntax
        Message::authorId eq "someid"
        Message::text neq "This is a text!"
        Message::authorId isIn listOf("test", "test2")

        //or this. But they are the same
        eq("author_id", "someid")
        neq("text", "This is a text!")
        isIn("author_id", listOf("test", "test2"))
    }
Insert
client.postgrest["messages"]
    .insert(Message("This is a text!", "someid", 1))
Update
client.postgrest["messages"]
    .update(
        {
            Message::text setTo "This is the edited text!"
        }
    ) {
        Message::id eq 2
    }
Delete
client.postgrest["messages"]
    .delete {
        Message::id eq 2
    }

Storage

Managing buckets
//create a bucket
client.storage.createBucket(name = "images", id = "images", public = false)

//empty bucket
client.storage.emptyBucket(id = "images")

//and so on
Uploading files
val bucket = client.storage["images"]

//upload a file (jvm)
bucket.upload("landscape.png", File("landscape.png"))

//download a file (jvm)
bucket.downloadTo("landscape.png", File("landscape.png"))

//copy a file

bucket.copy("landscape.png", "landscape2.png")

//and so on

Realtime

Listening for database changes
//in some suspending function
client.realtime.connect()
client.realtime.createAndJoinChannel {
    table = "test"
    schema = "public"

    on<ChannelAction.Insert> {
        println(record)
    }

    onAll {
        println(oldRecord)
    }
}
Broadcast API (soon)
Presence API (soon)

Credits

About

A framework for integrating supabase into compose for desktop, android and web

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Kotlin 99.9%
  • HTML 0.1%