Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add password recovery to chat sample & simplify sample #723

Merged
merged 1 commit into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion sample/chat-demo-mpp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ This is a demo of a chat app using Compose Multiplatform, Koin and supabase-kt.

**Available platforms:** Android, iOS, Desktop, JS Canvas

**Modules used:** Realtime, GoTrue, Postgrest, Compose Auth UI
**Modules used:** Realtime, Auth*, Postgrest, Compose Auth UI

* Integrated flows: Password, Google login & password recovery

https://user-images.githubusercontent.com/26686035/216710629-d809ff58-cd3b-449f-877f-4c6c773daec4.mp4

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package io.github.jan.supabase.common.di

import io.github.jan.supabase.gotrue.AuthConfig
import io.github.jan.supabase.gotrue.ExternalAuthAction

actual fun AuthConfig.platformGoTrueConfig() {
scheme = "io.jan.supabase"
host = "login"
defaultExternalAuthAction = ExternalAuthAction.CustomTabs()
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,18 @@ package io.github.jan.supabase.common

import co.touchlab.kermit.Logger
import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.common.net.AuthApi
import io.github.jan.supabase.common.net.Message
import io.github.jan.supabase.common.net.MessageApi
import io.github.jan.supabase.gotrue.auth
import io.github.jan.supabase.gotrue.providers.Google
import io.github.jan.supabase.gotrue.providers.builtin.Email
import io.github.jan.supabase.realtime.PostgresAction
import io.github.jan.supabase.realtime.RealtimeChannel
import io.github.jan.supabase.realtime.decodeRecord
import io.github.jan.supabase.realtime.postgresChangeFlow
import io.github.jan.supabase.gotrue.SessionStatus
import io.github.jan.supabase.realtime.realtime
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonPrimitive

expect open class MPViewModel() {

Expand All @@ -28,89 +23,100 @@ expect open class MPViewModel() {
}
class ChatViewModel(
val supabaseClient: SupabaseClient,
private val realtimeChannel: RealtimeChannel,
private val messageApi: MessageApi
private val messageApi: MessageApi,
private val authApi: AuthApi
) : MPViewModel() {

val sessionStatus = supabaseClient.auth.sessionStatus
val loginAlert = MutableStateFlow<String?>(null)
val sessionStatus = authApi.sessionStatus().stateIn(coroutineScope, SharingStarted.Eagerly, SessionStatus.NotAuthenticated(false))
val alert = MutableStateFlow<String?>(null)
val messages = MutableStateFlow<List<Message>>(emptyList())
val passwordReset = MutableStateFlow<Boolean>(false)

//Auth

fun signUp(email: String, password: String) {
coroutineScope.launch {
kotlin.runCatching {
supabaseClient.auth.signUpWith(Email) {
this.email = email
this.password = password
}
authApi.signUp(email, password)
}.onSuccess {
loginAlert.value = "Successfully registered! Check your E-Mail to verify your account."
alert.value = "Successfully registered! Check your E-Mail to verify your account."
}.onFailure {
loginAlert.value = "There was an error while registering: ${it.message}"
alert.value = "There was an error while registering: ${it.message}"
}
}
}

fun login(email: String, password: String) {
coroutineScope.launch {
kotlin.runCatching {
supabaseClient.auth.signInWith(Email) {
this.email = email
this.password = password
}
authApi.signIn(email, password)
}.onFailure {
it.printStackTrace()
loginAlert.value = "There was an error while logging in. Check your credentials and try again."
alert.value = "There was an error while logging in. Check your credentials and try again."
}
}
}

fun loginWithGoogle() {
coroutineScope.launch {
kotlin.runCatching {
supabaseClient.auth.signInWith(Google)
authApi.signInWithGoogle()
}
}
}

fun logout() {
fun loginWithOTP(email: String, code: String, reset: Boolean) {
coroutineScope.launch {
kotlin.runCatching {
supabaseClient.auth.signOut()
messages.value = emptyList()
authApi.verifyOtp(email, code)
}.onSuccess {
passwordReset.value = reset
}.onFailure {
alert.value = "There was an error while verifying the OTP: ${it.message}"
}
}
}

//Realtime
fun connectToRealtime() {
fun resetPassword(email: String) {
coroutineScope.launch {
kotlin.runCatching {
realtimeChannel.postgresChangeFlow<PostgresAction>("public") {
table = "messages"
}.onEach {
when(it) {
is PostgresAction.Delete -> messages.value = messages.value.filter { message -> message.id != it.oldRecord["id"]!!.jsonPrimitive.int }
is PostgresAction.Insert -> messages.value = messages.value + it.decodeRecord<Message>()
is PostgresAction.Select -> error("Select should not be possible")
is PostgresAction.Update -> error("Update should not be possible")
}
}.launchIn(coroutineScope)

realtimeChannel.subscribe()
authApi.resetPassword(email)
}
}
}

fun changePassword(password: String) {
coroutineScope.launch {
kotlin.runCatching {
authApi.changePassword(password)
}.onSuccess {
alert.value = "Password changed successfully!"
}.onFailure {
it.printStackTrace()
alert.value = "There was an error while changing the password: ${it.message}"
}
}
}

fun disconnectFromRealtime() {
fun logout() {
coroutineScope.launch {
kotlin.runCatching {
supabaseClient.realtime.disconnect()
authApi.signOut()
messages.value = emptyList()
}
}
}

//Realtime
fun retrieveMessages() {
coroutineScope.launch {
kotlin.runCatching {
messageApi.retrieveMessages()
.onEach {
messages.value = it
}
.launchIn(coroutineScope)
}.onFailure {
Logger.e(it) { "Error while retrieving messages" }
}
}
}
Expand All @@ -136,14 +142,10 @@ class ChatViewModel(
}
}

fun retrieveMessages() {
fun disconnectFromRealtime() {
coroutineScope.launch {
kotlin.runCatching {
messageApi.retrieveMessages()
}.onSuccess {
messages.value = it
}.onFailure {
Logger.e(it) { "Error while retrieving messages" }
supabaseClient.realtime.removeAllChannels()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package io.github.jan.supabase.common.di

import io.github.jan.supabase.common.net.AuthApi
import io.github.jan.supabase.common.net.AuthApiImpl
import io.github.jan.supabase.common.net.MessageApi
import io.github.jan.supabase.common.net.MessageApiImpl
import org.koin.dsl.module

val netModule = module {
single<MessageApi> { MessageApiImpl(get()) }
single<AuthApi> { AuthApiImpl(get()) }
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package io.github.jan.supabase.common.di

import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.createSupabaseClient
import io.github.jan.supabase.gotrue.Auth
import io.github.jan.supabase.gotrue.AuthConfig
import io.github.jan.supabase.gotrue.FlowType
import io.github.jan.supabase.logging.LogLevel
import io.github.jan.supabase.postgrest.Postgrest
import io.github.jan.supabase.realtime.Realtime
import io.github.jan.supabase.realtime.channel
import org.koin.dsl.module

expect fun AuthConfig.platformGoTrueConfig()
Expand All @@ -17,14 +17,13 @@ val supabaseModule = module {
supabaseUrl = "YOUR_URL",
supabaseKey = "YOUR_KEY"
) {
defaultLogLevel = LogLevel.DEBUG
install(Postgrest)
install(Auth) {
platformGoTrueConfig()
flowType = FlowType.PKCE
}
install(Realtime)
}
}
single {
get<SupabaseClient>().channel("messages")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.github.jan.supabase.common.net

import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.gotrue.OtpType
import io.github.jan.supabase.gotrue.SessionStatus
import io.github.jan.supabase.gotrue.auth
import io.github.jan.supabase.gotrue.providers.Google
import io.github.jan.supabase.gotrue.providers.builtin.Email
import kotlinx.coroutines.flow.Flow

sealed interface AuthApi {

suspend fun signIn(email: String, password: String)

suspend fun signUp(email: String, password: String)

suspend fun signInWithGoogle()

suspend fun verifyOtp(email: String, otp: String)

suspend fun signOut()

suspend fun resetPassword(email: String)

suspend fun changePassword(newPassword: String)

fun sessionStatus(): Flow<SessionStatus>

}

internal class AuthApiImpl(
private val client: SupabaseClient
) : AuthApi {

private val auth = client.auth

override fun sessionStatus(): Flow<SessionStatus> {
return auth.sessionStatus
}

override suspend fun verifyOtp(email: String, otp: String) {
auth.verifyEmailOtp(OtpType.Email.EMAIL, email, otp)
}

override suspend fun signInWithGoogle() {
auth.signInWith(Google)
}

override suspend fun signIn(email: String, password: String) {
auth.signInWith(Email) {
this.email = email
this.password = password
}
}

override suspend fun signUp(email: String, password: String) {
auth.signUpWith(Email) {
this.email = email
this.password = password
}
}

override suspend fun changePassword(newPassword: String) {
auth.updateUser {
this.password = newPassword
}
}

override suspend fun signOut() {
auth.signOut()
}

override suspend fun resetPassword(email: String) {
auth.resetPasswordForEmail(email)
}


}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package io.github.jan.supabase.common.net

import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.annotations.SupabaseExperimental
import io.github.jan.supabase.gotrue.auth
import io.github.jan.supabase.postgrest.postgrest
import io.github.jan.supabase.realtime.selectAsFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
Expand All @@ -21,7 +24,7 @@ data class Message(

sealed interface MessageApi {

suspend fun retrieveMessages(): List<Message>
suspend fun retrieveMessages(): Flow<List<Message>>

suspend fun createMessage(content: String): Message

Expand All @@ -35,7 +38,8 @@ internal class MessageApiImpl(

private val table = client.postgrest["messages"]

override suspend fun retrieveMessages(): List<Message> = table.select().decodeList()
@OptIn(SupabaseExperimental::class)
override suspend fun retrieveMessages(): Flow<List<Message>> = table.selectAsFlow(Message::id)

override suspend fun createMessage(content: String): Message {
val user = (client.auth.currentSessionOrNull() ?: error("No session available")).user ?: error("No user available")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ fun MessageCard(message: Message, own: Boolean, modifier: Modifier, onDelete: ()
ElevatedCard(modifier = Modifier.widthIn(max = 200.dp), colors = CardDefaults.elevatedCardColors(containerColor = backgroundColor)) {
Column(modifier = Modifier.padding(12.dp)) {
Text(message.content)
Text(message.creatorId, fontSize = 8.sp, modifier = Modifier.padding(top = 4.dp))
Text("UID: " + message.creatorId, fontSize = 8.sp, modifier = Modifier.padding(top = 4.dp))
}
}
if(own) {
Expand Down
Loading