Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 18eee5d5 authored by Mohammed Althaf T's avatar Mohammed Althaf T 😊
Browse files

AM: Add legacy login for testing

parent d74c82eb
Loading
Loading
Loading
Loading
+268 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2025 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 */
package foundation.e.accountmanager.ui.setup

import android.net.Uri
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.Password
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.composable.Assistant
import at.bitfire.davdroid.ui.composable.PasswordTextField
import at.bitfire.davdroid.ui.setup.LoginInfo
import at.bitfire.davdroid.ui.setup.LoginType
import foundation.e.accountmanager.AccountTypes
import foundation.e.accountmanager.utils.AccountHelper

object MurenaLegacyLogin : LoginType {

    override val title = R.string.legacy_murena_login

    override val helpUrl: Uri
        get() = "https://doc.e.foundation/support-topics".toUri()

    override val accountType: String
        get() = AccountTypes.Murena.accountType

    @Composable
    override fun LoginScreen(
        snackbarHostState: SnackbarHostState,
        initialLoginInfo: LoginInfo,
        onLogin: (LoginInfo) -> Unit
    ) {
        val context = LocalContext.current
        var showAccountDialog by remember { mutableStateOf(false) }

        if (AccountHelper.alreadyHasAccount(context)) {
            showAccountDialog = true
        }

        if (showAccountDialog) {
            MultipleECloudAccountNotAcceptedDialog {
                showAccountDialog = false
            }
            return
        }

        val model: MurenaLegacyLoginModel = hiltViewModel(
            creationCallback = { factory: MurenaLegacyLoginModel.Factory ->
                factory.create(loginInfo = initialLoginInfo)
            }
        )

        val uiState = model.uiState
        MurenaLegacyLoginScreen(
            url = uiState.url,
            onSetUrl = model::setUrl,
            username = uiState.username,
            onSetUsername = model::setUsername,
            password = uiState.password,
            canContinue = uiState.canContinue,
            onLogin = {
                if (uiState.canContinue)
                    onLogin(uiState.asLoginInfo())
            }
        )
    }
}

@Composable
fun MurenaLegacyLoginScreen(
    url: String,
    onSetUrl: (String) -> Unit = {},
    username: String,
    onSetUsername: (String) -> Unit = {},
    password: TextFieldState,
    canContinue: Boolean,
    onLogin: () -> Unit = {}
) {
    val focusRequester = remember { FocusRequester() }

    Assistant(
        nextLabel = stringResource(R.string.login_login),
        nextEnabled = canContinue,
        onNext = onLogin
    ) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(horizontal = 24.dp),
            horizontalAlignment = Alignment.CenterHorizontally) {
            Spacer(modifier = Modifier.height(40.dp))
            // Murena + e logo (replace with actual logos if available)
            Icon(
                painter = painterResource(id = R.drawable.ic_murena_logo),
                contentDescription = stringResource(R.string.eelo_account_name),
                tint = Color.Unspecified // To display original logo colors
            )

            Spacer(modifier = Modifier.height(24.dp))

            Text(
                text = stringResource(R.string.login_eelo_title),
                textAlign = TextAlign.Center,
                style = MaterialTheme.typography.bodyMedium
            )

            Spacer(modifier = Modifier.height(24.dp))

            OutlinedTextField(
                value = url,
                onValueChange = onSetUrl,
                label = { Text(stringResource(R.string.login_base_url)) },
                placeholder = { Text("murena.io") },
                singleLine = true,
                leadingIcon = {
                    Icon(Icons.Default.Folder, null)
                },
                keyboardOptions = KeyboardOptions(
                    keyboardType = KeyboardType.Uri,
                    imeAction = ImeAction.Next
                ),
                modifier = Modifier
                    .fillMaxWidth()
                    .focusRequester(focusRequester)
            )

            OutlinedTextField(
                value = username,
                onValueChange = onSetUsername,
                label = { Text(stringResource(R.string.login_user_id)) },
                singleLine = true,
                leadingIcon = {
                    Icon(Icons.Default.AccountCircle, null)
                },
                keyboardOptions = KeyboardOptions(
                    keyboardType = KeyboardType.Email,
                    imeAction = ImeAction.Next
                ),
                modifier = Modifier.fillMaxWidth()
            )

            // Suggestion buttons to add domain suffixes
            if (!username.contains("@")) {
                Row(
                    horizontalArrangement = Arrangement.spacedBy(8.dp),
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(top = 8.dp)
                ) {
                    OutlinedButton(
                        onClick = { onSetUsername("$username@murena.io") },
                        contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
                        shape = RoundedCornerShape(12.dp),
                        modifier = Modifier
                            .defaultMinSize(minHeight = 32.dp)
                    ) {
                        Text(
                            text = "@murena.io",
                            style = MaterialTheme.typography.bodySmall
                        )
                    }

                    OutlinedButton(
                        onClick = { onSetUsername("$username@e.email") },
                        contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
                        shape = RoundedCornerShape(12.dp),
                        modifier = Modifier
                            .defaultMinSize(minHeight = 32.dp)
                    ) {
                        Text(
                            text = "@e.email",
                            style = MaterialTheme.typography.bodySmall
                        )
                    }
                }
            }

            PasswordTextField(
                password = password,
                labelText = stringResource(R.string.login_password),
                leadingIcon = {
                    Icon(Icons.Default.Password, null)
                },
                keyboardOptions = KeyboardOptions(
                    keyboardType = KeyboardType.Password,
                    imeAction = ImeAction.Done
                ),
                onKeyboardAction = {
                    if (canContinue)
                        onLogin()
                },
                modifier = Modifier.fillMaxWidth()
            )
        }
    }

    LaunchedEffect(Unit) {
        focusRequester.requestFocus()
    }
}

@Composable
@Preview
fun MurenaLegacyLoginScreen_Preview() {
    MurenaLegacyLoginScreen(
        url = "",
        username = "user",
        password = rememberTextFieldState(""),
        canContinue = false
    )
}
+95 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2025 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 */
package foundation.e.accountmanager.ui.setup

import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.settings.Credentials
import at.bitfire.davdroid.ui.setup.LoginInfo
import at.bitfire.davdroid.util.DavUtils.toURIorNull
import at.bitfire.davdroid.util.SensitiveString.Companion.toSensitiveString
import at.bitfire.davdroid.util.trimToNull
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import java.net.URI

@HiltViewModel(assistedFactory = MurenaLegacyLoginModel.Factory::class)
class MurenaLegacyLoginModel @AssistedInject constructor(
    @Assisted val initialLoginInfo: LoginInfo
): ViewModel() {

    @AssistedFactory
    interface Factory {
        fun create(loginInfo: LoginInfo): MurenaLegacyLoginModel
    }

    val defaultUrl = "${URI(BuildConfig.MURENA_BASE_URL).host}"

    data class UiState(
        val url: String = "",
        val username: String = "",
        val password: TextFieldState = TextFieldState()
    ) {

        val urlWithPrefix =
            if (url.startsWith("http://") || url.startsWith("https://"))
                url
            else
                "https://$url"
        val uri = urlWithPrefix.trim().toURIorNull()

        val canContinue
            get() = uri != null && username.isNotEmpty() && password.text.toString().isNotEmpty()

        fun asLoginInfo(): LoginInfo =
            LoginInfo(
                baseUri = uri,
                credentials = Credentials(
                    username = username.trimToNull(),
                    password = password.text.toString().trimToNull()?.toSensitiveString()
                )
            )

    }

    var uiState by mutableStateOf(UiState())
        private set

    init {
        uiState = UiState(
            url = initialLoginInfo.baseUri?.toString()?.removePrefix("https://") ?: defaultUrl,
            username = initialLoginInfo.credentials?.username ?: "",
            password = TextFieldState(initialLoginInfo.credentials?.password?.asString() ?: ""),
        )
    }

    fun setUrl(url: String) {
        uiState = uiState.copy(url = url)
    }

    fun setUsername(username: String) {
        uiState = uiState.copy(username = username)
    }

}
+23 −28
Original line number Original line Diff line number Diff line
@@ -79,6 +79,7 @@ import at.bitfire.davdroid.ui.setup.LoginInfo
import at.bitfire.davdroid.ui.setup.LoginType
import at.bitfire.davdroid.ui.setup.LoginType
import foundation.e.accountmanager.AccountTypes
import foundation.e.accountmanager.AccountTypes
import foundation.e.accountmanager.network.OAuthMurena
import foundation.e.accountmanager.network.OAuthMurena
import foundation.e.accountmanager.utils.AccountHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.launch
@@ -95,33 +96,6 @@ object MurenaLogin : LoginType {
    override val accountType: String
    override val accountType: String
        get() = AccountTypes.Murena.accountType
        get() = AccountTypes.Murena.accountType


    @Composable
    fun MultipleECloudAccountNotAcceptedDialog(onDismiss: () -> Unit) {
        val activity = LocalActivity.current

        AlertDialog(
            onDismissRequest = {},
            confirmButton = {
                TextButton(onClick = {
                    activity?.finish()
                    onDismiss()
                }) {
                    Text(stringResource(id = android.R.string.ok))
                }
            },
            text = {
                Text(text = stringResource(R.string.multiple_ecloud_account_not_permitted_message))
            },
            tonalElevation = 8.dp
        )
    }

    fun alreadyHasAccount(context: Context): Boolean {
        val accountManager = AccountManager.get(context)
        val accounts = accountManager.getAccountsByType(accountType)
        return accounts.isNotEmpty()
    }

    @Composable
    @Composable
    override fun LoginScreen(
    override fun LoginScreen(
        snackbarHostState: SnackbarHostState,
        snackbarHostState: SnackbarHostState,
@@ -131,7 +105,7 @@ object MurenaLogin : LoginType {
        val context = LocalContext.current
        val context = LocalContext.current
        var showAccountDialog by remember { mutableStateOf(false) }
        var showAccountDialog by remember { mutableStateOf(false) }


        if (alreadyHasAccount(context)) {
        if (AccountHelper.alreadyHasAccount(context)) {
            showAccountDialog = true
            showAccountDialog = true
        }
        }


@@ -204,6 +178,27 @@ object MurenaLogin : LoginType {
    }
    }
}
}


@Composable
fun MultipleECloudAccountNotAcceptedDialog(onDismiss: () -> Unit) {
    val activity = LocalActivity.current

    AlertDialog(
        onDismissRequest = {},
        confirmButton = {
            TextButton(onClick = {
                activity?.finish()
                onDismiss()
            }) {
                Text(stringResource(id = android.R.string.ok))
            }
        },
        text = {
            Text(text = stringResource(R.string.multiple_ecloud_account_not_permitted_message))
        },
        tonalElevation = 8.dp
    )
}

@Composable
@Composable
fun MurenaLoginScreen(
fun MurenaLoginScreen(
    email: String,
    email: String,
+6 −0
Original line number Original line Diff line number Diff line
@@ -54,6 +54,12 @@ object AccountHelper {
        return allAccounts.toTypedArray()
        return allAccounts.toTypedArray()
    }
    }


    fun alreadyHasAccount(context: Context): Boolean {
        val accountManager = AccountManager.get(context)
        val accounts = accountManager.getAccountsByType(AccountTypes.Murena.accountType)
        return accounts.isNotEmpty()
    }

    fun isOidcAccount(context: Context, account: Account): Boolean {
    fun isOidcAccount(context: Context, account: Account): Boolean {
        val accountManager = AccountManager.get(context)
        val accountManager = AccountManager.get(context)
        val authState = accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE)
        val authState = accountManager.getUserData(account, AccountSettings.KEY_AUTH_STATE)
+1 −0
Original line number Original line Diff line number Diff line
@@ -33,4 +33,5 @@
    <string name="privacy_policy_title">"Account Manager's Privacy Policy"</string>
    <string name="privacy_policy_title">"Account Manager's Privacy Policy"</string>
    <string name="privacy_policy_title_nav">"Privacy Policy"</string>
    <string name="privacy_policy_title_nav">"Privacy Policy"</string>
    <string name="navigation_drawer_open_webcalmanager">Web Calendar Manager</string>
    <string name="navigation_drawer_open_webcalmanager">Web Calendar Manager</string>
    <string name="legacy_murena_login">Legacy Murena.io</string>
</resources>
</resources>
Loading