diff --git a/app/src/main/java/dev/lonami/talaria/TalariaApp.kt b/app/src/main/java/dev/lonami/talaria/TalariaApp.kt
index 4952b7f..d113930 100644
--- a/app/src/main/java/dev/lonami/talaria/TalariaApp.kt
+++ b/app/src/main/java/dev/lonami/talaria/TalariaApp.kt
@@ -72,7 +72,7 @@ fun TalariaApp() {
ChatScreen(selectedDialog)
}
composable(route = TalariaScreen.Login.name) {
- LoginScreen(onConfirmOtp = {
+ LoginScreen(onLoginComplete = {
navController.navigate(TalariaScreen.Dialog.name)
})
}
diff --git a/app/src/main/java/dev/lonami/talaria/bindings/Native.kt b/app/src/main/java/dev/lonami/talaria/bindings/Native.kt
index 607fdaf..2dd4d02 100644
--- a/app/src/main/java/dev/lonami/talaria/bindings/Native.kt
+++ b/app/src/main/java/dev/lonami/talaria/bindings/Native.kt
@@ -8,7 +8,10 @@ object Native {
external fun initClient()
external fun needLogin(): Boolean
external fun requestLoginCode(phone: String): Long
- external fun signIn(tokenPtr: Long, code: String)
+ // Returns true if sign in complete, false if 2fa password needed
+ // TODO: more rich return type including password hint / type of error
+ external fun signIn(tokenPtr: Long, code: String): Boolean
+ external fun checkPassword(password: String)
external fun getDialogs(): Long
external fun dialogCount(dialogsPtr: Long): Int
external fun dialogPacked(dialogsPtr: Long, index: Int): String
diff --git a/app/src/main/java/dev/lonami/talaria/ui/LoginScreen.kt b/app/src/main/java/dev/lonami/talaria/ui/LoginScreen.kt
index 26a1497..09edce0 100644
--- a/app/src/main/java/dev/lonami/talaria/ui/LoginScreen.kt
+++ b/app/src/main/java/dev/lonami/talaria/ui/LoginScreen.kt
@@ -24,10 +24,12 @@ import dev.lonami.talaria.ui.theme.TalariaTheme
enum class LoginStage {
ASK_PHONE,
ASK_CODE,
+ ASK_PASSWORD,
}
fun isPhoneValid(phone: String): Boolean = phone.trim('+', ' ').isNotEmpty()
fun isLoginCodeValid(code: String): Boolean = code.trim().count { it.isDigit() } == 5
+fun isPasswordValid(password: String): Boolean = password.isNotEmpty()
@Composable
fun PhoneInput(phone: String, onPhoneChanged: (String) -> Unit, onSendCode: () -> Unit) {
@@ -80,16 +82,50 @@ fun OtpInput(otp: String, onOtpChanged: (String) -> Unit, onConfirmOtp: () -> Un
enabled = isLoginCodeValid(otp),
modifier = Modifier.fillMaxWidth(),
onClick = onConfirmOtp
+ ) {
+ Text(stringResource(R.string.do_continue))
+ }
+}
+
+@Composable
+fun PasswordInput(
+ password: String,
+ onPasswordChanged: (String) -> Unit,
+ onConfirmPassword: () -> Unit
+) {
+ val focusManager = LocalFocusManager.current
+
+ Text(stringResource(R.string.enter_2fa_password))
+ TextField(
+ password,
+ label = { Text(stringResource(R.string.password)) },
+ placeholder = { Text(stringResource(R.string.password_example)) },
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Password,
+ imeAction = ImeAction.Done
+ ),
+ keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
+ modifier = Modifier.fillMaxWidth(),
+ onValueChange = onPasswordChanged
+ )
+ Spacer(Modifier.height(16.dp))
+ Button(
+ enabled = isPasswordValid(password),
+ modifier = Modifier.fillMaxWidth(),
+ onClick = onConfirmPassword
) {
Text(stringResource(R.string.do_login))
}
}
@Composable
-fun LoginScreen(onConfirmOtp: () -> Unit) {
+fun LoginScreen(onLoginComplete: () -> Unit) {
var stage by remember { mutableStateOf(LoginStage.ASK_PHONE) }
var phone by remember { mutableStateOf("") }
var otp by remember { mutableStateOf("") }
+ var password by remember { mutableStateOf("") }
+
var tokenPtr by remember { mutableStateOf(0L) }
@@ -121,10 +157,21 @@ fun LoginScreen(onConfirmOtp: () -> Unit) {
otp,
onOtpChanged = { otp = it },
onConfirmOtp = {
- Native.signIn(tokenPtr, otp)
- onConfirmOtp()
+ val signInComplete = Native.signIn(tokenPtr, otp)
+ if (signInComplete) {
+ onLoginComplete()
+ } else {
+ stage = LoginStage.ASK_PASSWORD;
+ }
}
)
+ LoginStage.ASK_PASSWORD -> PasswordInput(
+ password,
+ onPasswordChanged = { password = it },
+ onConfirmPassword = {
+ Native.checkPassword(password)
+ onLoginComplete();
+ })
}
}
}
@@ -133,6 +180,6 @@ fun LoginScreen(onConfirmOtp: () -> Unit) {
@Composable
fun LoginPreview() {
TalariaTheme {
- LoginScreen(onConfirmOtp = { })
+ LoginScreen(onLoginComplete = { })
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 01c9a0e..7276ef2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -8,6 +8,10 @@
Enter the code you received:
Code
12345
+ Enter your 2-factor authentication password:
+ Password
+ hunter2
+ Continue
Login
Profile Picture
Write a messageā¦
diff --git a/native/src/lib.rs b/native/src/lib.rs
index 249f743..757506c 100644
--- a/native/src/lib.rs
+++ b/native/src/lib.rs
@@ -4,12 +4,12 @@
use std::ffi::{CStr, CString};
use std::future::Future;
-use grammers_client::{Client, Config};
-use grammers_client::types::{Dialog, LoginToken};
+use grammers_client::types::{Dialog, LoginToken, PasswordToken};
+use grammers_client::{Client, Config, SignInError};
use grammers_session::{PackedChat, Session};
-use jni::JNIEnv;
use jni::objects::{JObject, JString};
use jni::sys::{jboolean, jint, jlong, jstring};
+use jni::JNIEnv;
use log;
use log::{error, info, Level};
use once_cell::sync::OnceCell;
@@ -61,7 +61,7 @@ async fn init_client() -> Result<()> {
api_hash: API_HASH.to_string(),
params: Default::default(),
})
- .await?;
+ .await?;
info!("Connected!");
@@ -83,10 +83,31 @@ async fn request_login_code(phone: &str) -> Result {
Ok(token)
}
-async fn sign_in(token: LoginToken, code: &str) -> Result<()> {
+async fn sign_in(token: LoginToken, code: &str) -> Result {
let client = CLIENT.get().ok_or("Client not initialized")?;
- client.sign_in(&token, &code).await?;
- Ok(())
+ return match client.sign_in(&token, &code).await {
+ Err(SignInError::PasswordRequired(token)) => {
+ info!("Sign in 2fa password required. Hint: {:?}", token.hint());
+ Ok(false)
+ }
+ Err(e) => {
+ client.sign_out().await?;
+ Err(Box::new(e))
+ }
+ Ok(_) => Ok(true),
+ };
+}
+
+async fn check_password(password: &str) -> Result<()> {
+ let client = CLIENT.get().ok_or("Client not initialized")?;
+ let token = PasswordToken::new("hint");
+ match client.check_password(token, &password).await {
+ Err(e) => {
+ client.sign_out().await?;
+ Err(Box::new(e))
+ }
+ Ok(_) => Ok(())
+ }
}
async fn get_dialogs() -> Result> {
@@ -151,14 +172,46 @@ pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_signIn(
_: JObject,
token_ptr: jlong,
code: JString,
-) {
+) -> jboolean {
let token = *Box::from_raw(token_ptr as *mut LoginToken);
let code = CString::from(CStr::from_ptr(env.get_string(code).unwrap().as_ptr()));
- match block_on(sign_in(token, code.to_str().unwrap())) {
- Ok(_) => info!("Sign in success"),
- Err(e) => error!("Failed to sign in: {}", e),
- }
+ return match block_on(sign_in(token, code.to_str().unwrap())) {
+ Ok(sign_in_complete) => {
+ if sign_in_complete {
+ info!("Sign in success");
+ }
+ sign_in_complete.into()
+ }
+ Err(e) => {
+ error!("Failed to sign in: {}", e);
+ true.into()
+ }
+ };
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_checkPassword(
+ env: JNIEnv,
+ _: JObject,
+ token_ptr: jlong,
+ code: JString,
+) -> jboolean {
+ let token = *Box::from_raw(token_ptr as *mut LoginToken);
+ let code = CString::from(CStr::from_ptr(env.get_string(code).unwrap().as_ptr()));
+
+ return match block_on(sign_in(token, code.to_str().unwrap())) {
+ Ok(sign_in_complete) => {
+ if sign_in_complete {
+ info!("Sign in success");
+ }
+ sign_in_complete.into()
+ }
+ Err(e) => {
+ error!("Failed to sign in: {}", e);
+ true.into()
+ }
+ };
}
#[no_mangle]