Skip to content

Commit

Permalink
Merge pull request #192 from fmasa/password-reset
Browse files Browse the repository at this point in the history
Add password reset dialog to desktop version
  • Loading branch information
fmasa authored Sep 8, 2023
2 parents 3a1f6d0 + faf7fd5 commit d58b7ab
Show file tree
Hide file tree
Showing 14 changed files with 323 additions and 49 deletions.
11 changes: 7 additions & 4 deletions app/src/main/kotlin/cz/muni/fi/rpg/ui/WfrpMasterApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import cz.frantisekmasa.wfrp_master.common.invitation.InvitationLinkScreen
import cz.frantisekmasa.wfrp_master.common.partyList.PartyListScreen
import cz.frantisekmasa.wfrp_master.common.shell.DrawerShell
import cz.frantisekmasa.wfrp_master.common.shell.NetworkStatusBanner
import cz.frantisekmasa.wfrp_master.common.shell.SnackbarScaffold
import cz.muni.fi.rpg.ui.shell.ProvideDIContainer
import cz.muni.fi.rpg.ui.shell.Startup
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -47,10 +48,12 @@ fun WfrpMasterApp() {
true
}
) { navigator ->
DrawerShell(drawerState) {
SlideTransition(navigator) {
ProvideNavigationTransaction(it) {
it.Content()
SnackbarScaffold {
DrawerShell(drawerState) {
SlideTransition(navigator) {
ProvideNavigationTransaction(it) {
it.Content()
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,16 @@ private val Context.settingsDataStore by preferencesDataStore("settings")
actual class SettingsStorage(context: Context,) {
private val storage = context.settingsDataStore

actual suspend fun <T> edit(key: SettingsKey<T>, update: (T?) -> T) {
storage.edit { it[key] = update(it[key]) }
actual suspend fun <T> edit(key: SettingsKey<T>, update: (T?) -> T?) {
storage.edit {
val newValue = update(it[key])

if (newValue == null) {
it.remove(key)
} else {
it[key] = newValue
}
}
}

actual fun <T> watch(key: SettingsKey<T>): Flow<T?> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import kotlinx.coroutines.flow.Flow
import kotlin.jvm.JvmName

expect class SettingsStorage {
suspend fun <T> edit(key: SettingsKey<T>, update: (T?) -> T)
suspend fun <T> edit(key: SettingsKey<T>, update: (T?) -> T?)

fun <T> watch(key: SettingsKey<T>): Flow<T?>
}

suspend fun <T> SettingsStorage.edit(key: SettingsKey<T>, value: T) {
suspend fun <T> SettingsStorage.edit(key: SettingsKey<T>, value: T?) {
edit(key) { value }
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package cz.frantisekmasa.wfrp_master.common.shell

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.DrawerState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalDrawer
import androidx.compose.material.Scaffold
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
Expand All @@ -18,8 +14,6 @@ import cz.frantisekmasa.wfrp_master.common.core.shared.SettingsStorage
import cz.frantisekmasa.wfrp_master.common.core.ui.buttons.LocalHamburgerButtonHandler
import cz.frantisekmasa.wfrp_master.common.core.ui.flow.collectWithLifecycle
import cz.frantisekmasa.wfrp_master.common.core.ui.primitives.FullScreenProgress
import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.LocalPersistentSnackbarHolder
import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.PersistentSnackbarHolder
import cz.frantisekmasa.wfrp_master.common.settings.AppSettings
import cz.frantisekmasa.wfrp_master.common.settings.Language
import dev.icerock.moko.resources.desc.StringDesc
Expand All @@ -30,7 +24,7 @@ import org.kodein.di.instance

@Composable
@ExperimentalMaterialApi
fun DrawerShell(drawerState: DrawerState, bodyContent: @Composable (PaddingValues) -> Unit) {
fun DrawerShell(drawerState: DrawerState, bodyContent: @Composable () -> Unit) {
val settings: SettingsStorage by localDI().instance()
val language = remember {
settings.watch(AppSettings.LANGUAGE)
Expand All @@ -56,21 +50,10 @@ fun DrawerShell(drawerState: DrawerState, bodyContent: @Composable (PaddingValue
},
content = {
val coroutineScope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val persistentSnackbarHolder =
remember(coroutineScope, snackbarHostState) {
PersistentSnackbarHolder(coroutineScope, snackbarHostState)
}

CompositionLocalProvider(
LocalHamburgerButtonHandler provides { coroutineScope.launch { drawerState.open() } },
LocalPersistentSnackbarHolder provides persistentSnackbarHolder,
content = {
Scaffold(
scaffoldState = rememberScaffoldState(snackbarHostState = snackbarHostState),
content = bodyContent,
)
},
content = bodyContent,
)
},
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package cz.frantisekmasa.wfrp_master.common.shell

import androidx.compose.material.Scaffold
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.LocalPersistentSnackbarHolder
import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.PersistentSnackbarHolder

@Composable
fun SnackbarScaffold(content: @Composable () -> Unit) {
val coroutineScope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val persistentSnackbarHolder =
remember(coroutineScope, snackbarHostState) {
PersistentSnackbarHolder(coroutineScope, snackbarHostState)
}

Scaffold(
scaffoldState = rememberScaffoldState(snackbarHostState = snackbarHostState),
content = {
CompositionLocalProvider(
LocalPersistentSnackbarHolder provides persistentSnackbarHolder,
content = content,
)
},
)
}
4 changes: 4 additions & 0 deletions common/src/commonMain/resources/MR/base/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
<string name="armour_types_plate">Plate</string>
<string name="armour_types_soft_leather">Soft Leather</string>
<string name="authentication_button_sign_in">Sign in</string>
<string name="authentication_button_log_out">Log out</string>
<string name="authentication_button_reset_password">Reset password</string>
<string name="authentication_button_send">Send</string>
<string name="authentication_label_email">Email</string>
<string name="authentication_label_password">Password</string>
<string name="authentication_messages_duplicate_account">Existing account found</string>
Expand All @@ -38,6 +41,7 @@
<string name="authentication_messages_invalid_password">Invalid password</string>
<string name="authentication_messages_lose_access_to_parties">You will lose access to these parties:</string>
<string name="authentication_messages_not_signed_in_description">You are not signed-in.\nSigning-in lets you keep access to parties between devices.</string>
<string name="authentication_messages_reset_password_email_sent">Email with instructions for password reset was sent</string>
<string name="authentication_messages_signed_in_as">You are signed-in as</string>
<string name="authentication_messages_unknown_error">Unknown error occurred</string>
<string name="authentication_startup_google_sign_in_failed">We could not sign you in via Google.\nYour data will be tied to this device, but you can always sign in later in Settings.</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package cz.frantisekmasa.wfrp_master.common
class FirebaseTokenHolder {
private var token: String? = null

fun setToken(token: String) {
fun setToken(token: String?) {
this.token = token
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ class AuthenticationManager(
return body
}

suspend fun logout() {
tokenHolder.setToken(null)
settings.edit(REFRESH_TOKEN, null)
statusFlow.emit(AuthenticationStatus.NotAuthenticated)
}

@Serializable
sealed class SignInResponse {
@Serializable
Expand Down Expand Up @@ -155,6 +161,50 @@ class AuthenticationManager(
val returnSecureToken: Boolean = true,
)

@Serializable
data class Failure(val error: Error)

/**
* https://firebase.google.com/docs/reference/rest/auth#section-send-password-reset-email
*/
suspend fun resetPassword(email: String): PasswordResetResult {
val response = http.post(
"https://identitytoolkit.googleapis.com/v1/accounts:sendOobCode?key=$API_KEY"
) {
contentType(ContentType.Application.Json)
setBody(
PasswordResetRequest(
requestType = "PASSWORD_RESET",
email = email,
)
)
}

if (response.status != HttpStatusCode.OK) {
val body = response.body<Failure>()

if (body.error.message == "EMAIL_NOT_FOUND") {
return PasswordResetResult.EmailNotFound
}

return PasswordResetResult.UnknownError
}

return PasswordResetResult.Success
}

@Serializable
private data class PasswordResetRequest(
val requestType: String,
val email: String,
)

sealed interface PasswordResetResult {
object Success : PasswordResetResult
object EmailNotFound : PasswordResetResult
object UnknownError : PasswordResetResult
}

companion object {
private val REFRESH_TOKEN = stringKey("firebase_refresh_token")
private const val API_KEY = "AIzaSyDO4Y4wWcY4HdYcsp8zcLMpMjwUJ_9q3Fw"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,15 @@ actual class SettingsStorage() {
awaitClose { preferences.removePreferenceChangeListener(listener) }
}

actual suspend fun <T> edit(key: SettingsKey<T>, update: (T?) -> T) {
key.set(preferences, update(key.get(preferences)))
actual suspend fun <T> edit(key: SettingsKey<T>, update: (T?) -> T?) {
val newValue = update(key.get(preferences))

if (newValue == null) {
preferences.remove(key.name)
} else {
key.set(preferences, newValue)
}

preferences.sync()
}

Expand All @@ -41,16 +48,19 @@ actual class SettingsStorage() {
}

actual class SettingsKey<T>(
val name: String,
val set: (Preferences, T) -> Unit,
val get: (Preferences) -> T?
)

actual fun booleanSettingsKey(name: String): SettingsKey<Boolean> = SettingsKey(
name = name,
get = { if (name in it.keys()) it.getBoolean(name, false) else null },
set = { preferences, value -> preferences.putBoolean(name, value) },
)

actual fun stringSetKey(name: String): SettingsKey<Set<String>> = SettingsKey(
name = name,
get = { preferences ->
if (name in preferences.keys())
preferences.get(name, "")
Expand All @@ -68,6 +78,7 @@ actual fun stringSetKey(name: String): SettingsKey<Set<String>> = SettingsKey(
)

actual fun stringKey(name: String): SettingsKey<String> = SettingsKey(
name = name,
get = { if (name in it.keys()) it.get(name, "") else null },
set = { preferences, value -> preferences.put(name, value) },
)
Original file line number Diff line number Diff line change
@@ -1,8 +1,58 @@
package cz.frantisekmasa.wfrp_master.common.settings

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import cz.frantisekmasa.wfrp_master.common.Str
import cz.frantisekmasa.wfrp_master.common.auth.AuthenticationManager
import cz.frantisekmasa.wfrp_master.common.core.auth.LocalUser
import cz.frantisekmasa.wfrp_master.common.core.ui.buttons.CardButton
import cz.frantisekmasa.wfrp_master.common.core.ui.cards.CardContainer
import cz.frantisekmasa.wfrp_master.common.core.ui.cards.CardTitle
import cz.frantisekmasa.wfrp_master.common.core.ui.text.SingleLineTextValue
import cz.frantisekmasa.wfrp_master.common.core.utils.launchLogged
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.Dispatchers
import org.kodein.di.compose.localDI
import org.kodein.di.instance

@Composable
actual fun SignInCard(settingsScreenModel: SettingsScreenModel) {
// Add support for Google auth
CardContainer(
Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
CardTitle(stringResource(Str.settings_title_account))

val email = LocalUser.current.email

if (email != null) {
SingleLineTextValue(stringResource(Str.authentication_label_email), email)
}

val auth: AuthenticationManager by localDI().instance()
val coroutineScope = rememberCoroutineScope()

CardButton(
text = stringResource(Str.authentication_button_log_out),
onClick = {
coroutineScope.launchLogged(Dispatchers.IO) {
auth.logout()
}
}
)
}
}
}
3 changes: 3 additions & 0 deletions desktop/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ kotlin {

sourceSets {
named("jvmMain") {
languageSettings.apply {
optIn("androidx.compose.material.ExperimentalMaterialApi")
}
dependencies {
implementation(compose.desktop.currentOs)
implementation(project(":common:firebase"))
Expand Down
Loading

0 comments on commit d58b7ab

Please sign in to comment.