Skip to content

Commit d58b7ab

Browse files
authored
Merge pull request #192 from fmasa/password-reset
Add password reset dialog to desktop version
2 parents 3a1f6d0 + faf7fd5 commit d58b7ab

File tree

14 files changed

+323
-49
lines changed

14 files changed

+323
-49
lines changed

app/src/main/kotlin/cz/muni/fi/rpg/ui/WfrpMasterApp.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import cz.frantisekmasa.wfrp_master.common.invitation.InvitationLinkScreen
1919
import cz.frantisekmasa.wfrp_master.common.partyList.PartyListScreen
2020
import cz.frantisekmasa.wfrp_master.common.shell.DrawerShell
2121
import cz.frantisekmasa.wfrp_master.common.shell.NetworkStatusBanner
22+
import cz.frantisekmasa.wfrp_master.common.shell.SnackbarScaffold
2223
import cz.muni.fi.rpg.ui.shell.ProvideDIContainer
2324
import cz.muni.fi.rpg.ui.shell.Startup
2425
import kotlinx.coroutines.launch
@@ -47,10 +48,12 @@ fun WfrpMasterApp() {
4748
true
4849
}
4950
) { navigator ->
50-
DrawerShell(drawerState) {
51-
SlideTransition(navigator) {
52-
ProvideNavigationTransaction(it) {
53-
it.Content()
51+
SnackbarScaffold {
52+
DrawerShell(drawerState) {
53+
SlideTransition(navigator) {
54+
ProvideNavigationTransaction(it) {
55+
it.Content()
56+
}
5457
}
5558
}
5659
}

common/src/androidMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/shared/SettingsStorage.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,16 @@ private val Context.settingsDataStore by preferencesDataStore("settings")
1515
actual class SettingsStorage(context: Context,) {
1616
private val storage = context.settingsDataStore
1717

18-
actual suspend fun <T> edit(key: SettingsKey<T>, update: (T?) -> T) {
19-
storage.edit { it[key] = update(it[key]) }
18+
actual suspend fun <T> edit(key: SettingsKey<T>, update: (T?) -> T?) {
19+
storage.edit {
20+
val newValue = update(it[key])
21+
22+
if (newValue == null) {
23+
it.remove(key)
24+
} else {
25+
it[key] = newValue
26+
}
27+
}
2028
}
2129

2230
actual fun <T> watch(key: SettingsKey<T>): Flow<T?> {

common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/shared/SettingsStorage.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import kotlinx.coroutines.flow.Flow
66
import kotlin.jvm.JvmName
77

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

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

14-
suspend fun <T> SettingsStorage.edit(key: SettingsKey<T>, value: T) {
14+
suspend fun <T> SettingsStorage.edit(key: SettingsKey<T>, value: T?) {
1515
edit(key) { value }
1616
}
1717

common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/shell/DrawerShell.kt

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
package cz.frantisekmasa.wfrp_master.common.shell
22

33
import androidx.compose.foundation.layout.Column
4-
import androidx.compose.foundation.layout.PaddingValues
54
import androidx.compose.material.DrawerState
65
import androidx.compose.material.ExperimentalMaterialApi
76
import androidx.compose.material.ModalDrawer
8-
import androidx.compose.material.Scaffold
9-
import androidx.compose.material.SnackbarHostState
10-
import androidx.compose.material.rememberScaffoldState
117
import androidx.compose.runtime.Composable
128
import androidx.compose.runtime.CompositionLocalProvider
139
import androidx.compose.runtime.SideEffect
@@ -18,8 +14,6 @@ import cz.frantisekmasa.wfrp_master.common.core.shared.SettingsStorage
1814
import cz.frantisekmasa.wfrp_master.common.core.ui.buttons.LocalHamburgerButtonHandler
1915
import cz.frantisekmasa.wfrp_master.common.core.ui.flow.collectWithLifecycle
2016
import cz.frantisekmasa.wfrp_master.common.core.ui.primitives.FullScreenProgress
21-
import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.LocalPersistentSnackbarHolder
22-
import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.PersistentSnackbarHolder
2317
import cz.frantisekmasa.wfrp_master.common.settings.AppSettings
2418
import cz.frantisekmasa.wfrp_master.common.settings.Language
2519
import dev.icerock.moko.resources.desc.StringDesc
@@ -30,7 +24,7 @@ import org.kodein.di.instance
3024

3125
@Composable
3226
@ExperimentalMaterialApi
33-
fun DrawerShell(drawerState: DrawerState, bodyContent: @Composable (PaddingValues) -> Unit) {
27+
fun DrawerShell(drawerState: DrawerState, bodyContent: @Composable () -> Unit) {
3428
val settings: SettingsStorage by localDI().instance()
3529
val language = remember {
3630
settings.watch(AppSettings.LANGUAGE)
@@ -56,21 +50,10 @@ fun DrawerShell(drawerState: DrawerState, bodyContent: @Composable (PaddingValue
5650
},
5751
content = {
5852
val coroutineScope = rememberCoroutineScope()
59-
val snackbarHostState = remember { SnackbarHostState() }
60-
val persistentSnackbarHolder =
61-
remember(coroutineScope, snackbarHostState) {
62-
PersistentSnackbarHolder(coroutineScope, snackbarHostState)
63-
}
6453

6554
CompositionLocalProvider(
6655
LocalHamburgerButtonHandler provides { coroutineScope.launch { drawerState.open() } },
67-
LocalPersistentSnackbarHolder provides persistentSnackbarHolder,
68-
content = {
69-
Scaffold(
70-
scaffoldState = rememberScaffoldState(snackbarHostState = snackbarHostState),
71-
content = bodyContent,
72-
)
73-
},
56+
content = bodyContent,
7457
)
7558
},
7659
)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package cz.frantisekmasa.wfrp_master.common.shell
2+
3+
import androidx.compose.material.Scaffold
4+
import androidx.compose.material.SnackbarHostState
5+
import androidx.compose.material.rememberScaffoldState
6+
import androidx.compose.runtime.Composable
7+
import androidx.compose.runtime.CompositionLocalProvider
8+
import androidx.compose.runtime.remember
9+
import androidx.compose.runtime.rememberCoroutineScope
10+
import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.LocalPersistentSnackbarHolder
11+
import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.PersistentSnackbarHolder
12+
13+
@Composable
14+
fun SnackbarScaffold(content: @Composable () -> Unit) {
15+
val coroutineScope = rememberCoroutineScope()
16+
val snackbarHostState = remember { SnackbarHostState() }
17+
val persistentSnackbarHolder =
18+
remember(coroutineScope, snackbarHostState) {
19+
PersistentSnackbarHolder(coroutineScope, snackbarHostState)
20+
}
21+
22+
Scaffold(
23+
scaffoldState = rememberScaffoldState(snackbarHostState = snackbarHostState),
24+
content = {
25+
CompositionLocalProvider(
26+
LocalPersistentSnackbarHolder provides persistentSnackbarHolder,
27+
content = content,
28+
)
29+
},
30+
)
31+
}

common/src/commonMain/resources/MR/base/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
<string name="armour_types_plate">Plate</string>
3030
<string name="armour_types_soft_leather">Soft Leather</string>
3131
<string name="authentication_button_sign_in">Sign in</string>
32+
<string name="authentication_button_log_out">Log out</string>
33+
<string name="authentication_button_reset_password">Reset password</string>
34+
<string name="authentication_button_send">Send</string>
3235
<string name="authentication_label_email">Email</string>
3336
<string name="authentication_label_password">Password</string>
3437
<string name="authentication_messages_duplicate_account">Existing account found</string>
@@ -38,6 +41,7 @@
3841
<string name="authentication_messages_invalid_password">Invalid password</string>
3942
<string name="authentication_messages_lose_access_to_parties">You will lose access to these parties:</string>
4043
<string name="authentication_messages_not_signed_in_description">You are not signed-in.\nSigning-in lets you keep access to parties between devices.</string>
44+
<string name="authentication_messages_reset_password_email_sent">Email with instructions for password reset was sent</string>
4145
<string name="authentication_messages_signed_in_as">You are signed-in as</string>
4246
<string name="authentication_messages_unknown_error">Unknown error occurred</string>
4347
<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>

common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/FirebaseTokenHolder.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package cz.frantisekmasa.wfrp_master.common
33
class FirebaseTokenHolder {
44
private var token: String? = null
55

6-
fun setToken(token: String) {
6+
fun setToken(token: String?) {
77
this.token = token
88
}
99

common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/auth/AuthenticationManager.kt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@ class AuthenticationManager(
127127
return body
128128
}
129129

130+
suspend fun logout() {
131+
tokenHolder.setToken(null)
132+
settings.edit(REFRESH_TOKEN, null)
133+
statusFlow.emit(AuthenticationStatus.NotAuthenticated)
134+
}
135+
130136
@Serializable
131137
sealed class SignInResponse {
132138
@Serializable
@@ -155,6 +161,50 @@ class AuthenticationManager(
155161
val returnSecureToken: Boolean = true,
156162
)
157163

164+
@Serializable
165+
data class Failure(val error: Error)
166+
167+
/**
168+
* https://firebase.google.com/docs/reference/rest/auth#section-send-password-reset-email
169+
*/
170+
suspend fun resetPassword(email: String): PasswordResetResult {
171+
val response = http.post(
172+
"https://identitytoolkit.googleapis.com/v1/accounts:sendOobCode?key=$API_KEY"
173+
) {
174+
contentType(ContentType.Application.Json)
175+
setBody(
176+
PasswordResetRequest(
177+
requestType = "PASSWORD_RESET",
178+
email = email,
179+
)
180+
)
181+
}
182+
183+
if (response.status != HttpStatusCode.OK) {
184+
val body = response.body<Failure>()
185+
186+
if (body.error.message == "EMAIL_NOT_FOUND") {
187+
return PasswordResetResult.EmailNotFound
188+
}
189+
190+
return PasswordResetResult.UnknownError
191+
}
192+
193+
return PasswordResetResult.Success
194+
}
195+
196+
@Serializable
197+
private data class PasswordResetRequest(
198+
val requestType: String,
199+
val email: String,
200+
)
201+
202+
sealed interface PasswordResetResult {
203+
object Success : PasswordResetResult
204+
object EmailNotFound : PasswordResetResult
205+
object UnknownError : PasswordResetResult
206+
}
207+
158208
companion object {
159209
private val REFRESH_TOKEN = stringKey("firebase_refresh_token")
160210
private const val API_KEY = "AIzaSyDO4Y4wWcY4HdYcsp8zcLMpMjwUJ_9q3Fw"

common/src/jvmMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/shared/SettingsStorage.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,15 @@ actual class SettingsStorage() {
2424
awaitClose { preferences.removePreferenceChangeListener(listener) }
2525
}
2626

27-
actual suspend fun <T> edit(key: SettingsKey<T>, update: (T?) -> T) {
28-
key.set(preferences, update(key.get(preferences)))
27+
actual suspend fun <T> edit(key: SettingsKey<T>, update: (T?) -> T?) {
28+
val newValue = update(key.get(preferences))
29+
30+
if (newValue == null) {
31+
preferences.remove(key.name)
32+
} else {
33+
key.set(preferences, newValue)
34+
}
35+
2936
preferences.sync()
3037
}
3138

@@ -41,16 +48,19 @@ actual class SettingsStorage() {
4148
}
4249

4350
actual class SettingsKey<T>(
51+
val name: String,
4452
val set: (Preferences, T) -> Unit,
4553
val get: (Preferences) -> T?
4654
)
4755

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

5362
actual fun stringSetKey(name: String): SettingsKey<Set<String>> = SettingsKey(
63+
name = name,
5464
get = { preferences ->
5565
if (name in preferences.keys())
5666
preferences.get(name, "")
@@ -68,6 +78,7 @@ actual fun stringSetKey(name: String): SettingsKey<Set<String>> = SettingsKey(
6878
)
6979

7080
actual fun stringKey(name: String): SettingsKey<String> = SettingsKey(
81+
name = name,
7182
get = { if (name in it.keys()) it.get(name, "") else null },
7283
set = { preferences, value -> preferences.put(name, value) },
7384
)
Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,58 @@
11
package cz.frantisekmasa.wfrp_master.common.settings
22

3+
import androidx.compose.foundation.layout.Column
4+
import androidx.compose.foundation.layout.fillMaxWidth
5+
import androidx.compose.foundation.layout.padding
36
import androidx.compose.runtime.Composable
7+
import androidx.compose.runtime.rememberCoroutineScope
8+
import androidx.compose.ui.Alignment
9+
import androidx.compose.ui.Modifier
10+
import androidx.compose.ui.unit.dp
11+
import cz.frantisekmasa.wfrp_master.common.Str
12+
import cz.frantisekmasa.wfrp_master.common.auth.AuthenticationManager
13+
import cz.frantisekmasa.wfrp_master.common.core.auth.LocalUser
14+
import cz.frantisekmasa.wfrp_master.common.core.ui.buttons.CardButton
15+
import cz.frantisekmasa.wfrp_master.common.core.ui.cards.CardContainer
16+
import cz.frantisekmasa.wfrp_master.common.core.ui.cards.CardTitle
17+
import cz.frantisekmasa.wfrp_master.common.core.ui.text.SingleLineTextValue
18+
import cz.frantisekmasa.wfrp_master.common.core.utils.launchLogged
19+
import dev.icerock.moko.resources.compose.stringResource
20+
import kotlinx.coroutines.Dispatchers
21+
import org.kodein.di.compose.localDI
22+
import org.kodein.di.instance
423

524
@Composable
625
actual fun SignInCard(settingsScreenModel: SettingsScreenModel) {
7-
// Add support for Google auth
26+
CardContainer(
27+
Modifier
28+
.fillMaxWidth()
29+
.padding(horizontal = 8.dp)
30+
) {
31+
Column(
32+
modifier = Modifier
33+
.fillMaxWidth()
34+
.padding(horizontal = 8.dp),
35+
horizontalAlignment = Alignment.CenterHorizontally,
36+
) {
37+
CardTitle(stringResource(Str.settings_title_account))
38+
39+
val email = LocalUser.current.email
40+
41+
if (email != null) {
42+
SingleLineTextValue(stringResource(Str.authentication_label_email), email)
43+
}
44+
45+
val auth: AuthenticationManager by localDI().instance()
46+
val coroutineScope = rememberCoroutineScope()
47+
48+
CardButton(
49+
text = stringResource(Str.authentication_button_log_out),
50+
onClick = {
51+
coroutineScope.launchLogged(Dispatchers.IO) {
52+
auth.logout()
53+
}
54+
}
55+
)
56+
}
57+
}
858
}

0 commit comments

Comments
 (0)