diff --git a/app/app/src/ethos_dapp/java/com/bringyour/network/LoginActivity.kt b/app/app/src/ethos_dapp/java/com/bringyour/network/LoginActivity.kt index d57cb6f0b..764dedcdf 100644 --- a/app/app/src/ethos_dapp/java/com/bringyour/network/LoginActivity.kt +++ b/app/app/src/ethos_dapp/java/com/bringyour/network/LoginActivity.kt @@ -236,7 +236,7 @@ class LoginActivity : AppCompatActivity() { if (!success || jwt == null) { Log.i(TAG, "guest login: local byJwt parse failed") createGuestNetworkAndFinish(app) - } else if (jwt.guestMode) { + } else if (jwt.networkName != null) { setLinksAndStartMain(targetUrl, defaultLocation) } else { currentNetworkName = jwt.networkName @@ -332,7 +332,6 @@ class LoginActivity : AppCompatActivity() { private fun createGuestNetworkAndFinish(app: MainApplication) { val args = NetworkCreateArgs() args.terms = true - args.guestMode = true app.api?.networkCreate(args) { result, err -> lifecycleScope.launch { diff --git a/app/app/src/ethos_dapp/java/com/bringyour/network/ui/login/LoginInitial.kt b/app/app/src/ethos_dapp/java/com/bringyour/network/ui/login/LoginInitial.kt index 8460b98c3..649c8d608 100644 --- a/app/app/src/ethos_dapp/java/com/bringyour/network/ui/login/LoginInitial.kt +++ b/app/app/src/ethos_dapp/java/com/bringyour/network/ui/login/LoginInitial.kt @@ -10,6 +10,10 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bolt +import androidx.compose.material.icons.filled.Key +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -38,12 +42,10 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.EnterTransition import androidx.compose.animation.fadeOut -import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.clickable import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.LaunchedEffect @@ -52,20 +54,15 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.focus.onFocusChanged import androidx.lifecycle.compose.LifecycleResumeEffect import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.common.api.ApiException -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString 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.text.withStyle import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp import androidx.navigation.NavController @@ -73,18 +70,15 @@ import androidx.navigation.compose.rememberNavController import com.bringyour.network.BuildConfig import com.bringyour.sdk.AuthLoginResult import com.bringyour.sdk.Api -import com.bringyour.sdk.NetworkCreateArgs import com.bringyour.network.LoginActivity import com.bringyour.network.MainApplication import com.bringyour.network.R import com.bringyour.network.TAG import com.bringyour.network.ui.components.URInlineErrorText import com.bringyour.network.ui.components.overlays.WelcomeAnimatedOverlayLogin -import com.bringyour.network.ui.theme.BlueMedium import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.solana.mobilewalletadapter.clientlib.ActivityResultSender import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.ethereumphone.walletsdk.WalletSDK @@ -297,6 +291,14 @@ fun LoginInitial( } } + val onSeedphraseLogin: () -> Unit = { + navController.navigate("login_seedphrase") + } + + val onInstantAccountCreate: () -> Unit = { + navController.navigate("create-network-instant") + } + LoginInitial( navController, userAuth = loginViewModel.userAuth, @@ -309,8 +311,6 @@ fun LoginInitial( setGoogleAuthInProgress = loginViewModel.setGoogleAuthInProgress, setLoginError = loginViewModel.setLoginError, googleAuthInProgress = loginViewModel.googleAuthInProgress, - createGuestModeInProgress = loginViewModel.createGuestModeInProgress, - setCreateGuestModeInProgress = loginViewModel.setCreateGuestModeInProgress, allowGoogleSso = loginViewModel.allowGoogleSso, solanaLogin = { connectSolanaWallet() @@ -331,7 +331,9 @@ fun LoginInitial( welcomeOverlayVisible = welcomeOverlayVisible, setWelcomeOverlayVisible = { welcomeOverlayVisible = it - } + }, + onSeedphraseLogin = onSeedphraseLogin, + onInstantAccountCreate = onInstantAccountCreate ) if (noSolanaWalletsFound) { @@ -369,9 +371,7 @@ fun LoginInitial( loginError: String?, setLoginError: (String?) -> Unit, googleAuthInProgress: Boolean, - createGuestModeInProgress: Boolean, setGoogleAuthInProgress: (Boolean) -> Unit, - setCreateGuestModeInProgress: (Boolean) -> Unit, allowGoogleSso: () -> Boolean, solanaLogin: () -> Unit, ethOsLogin: () -> Unit, @@ -385,6 +385,8 @@ fun LoginInitial( setContentVisible: (Boolean) -> Unit, welcomeOverlayVisible: Boolean, setWelcomeOverlayVisible: (Boolean) -> Unit, + onSeedphraseLogin: () -> Unit, + onInstantAccountCreate: () -> Unit, ) { val context = LocalContext.current @@ -392,15 +394,6 @@ fun LoginInitial( val scope = rememberCoroutineScope() - var guestModeOverlayVisible by remember { mutableStateOf(false) } - - val setGuestModeOverlayVisible: (Boolean) -> Unit = { isVisible -> - if (isVisible) { - setLoginError(null) - } - guestModeOverlayVisible = isVisible - } - var authCodeLoginSheetVisible by remember { mutableStateOf(false) } val setAuthCodeLoginSheetVisible: (Boolean) -> Unit = { isVisible -> @@ -426,68 +419,6 @@ fun LoginInitial( .build() val googleSignInClient = GoogleSignIn.getClient(context, googleSignInOpts) - - val createGuestNetwork = createGuestNetwork@{ - if (createGuestModeInProgress) { - return@createGuestNetwork - } - - setLoginError(null) - setCreateGuestModeInProgress(true) - - val args = NetworkCreateArgs() - args.terms = true - args.guestMode = true - - application?.api?.networkCreate(args) { result, err -> - scope.launch { - - if (err != null) { - Log.i("OnboardingGuestModeOverlay", "error ${err.message}") - setLoginError(err.message) - setCreateGuestModeInProgress(false) - } else if (result.error != null) { - Log.i("OnboardingGuestModeOverlay", "error ${result.error.message}") - setLoginError(result.error.message) - setCreateGuestModeInProgress(false) - } else if (result.network != null && result.network.byJwt.isNotEmpty()) { - setLoginError(null) - setGuestModeOverlayVisible(false) - - application.login(result.network.byJwt) - - setContentVisible(false) - - delay(500) - - setWelcomeOverlayVisible(true) - - delay(2250) - - loginActivity?.authClientAndFinish( - { error -> - setCreateGuestModeInProgress(false) - - setLoginError(error) - - if (error != null) { - setContentVisible(true) - } - } - ) - - } else { - setLoginError(createNetworkLoginError) - setCreateGuestModeInProgress(false) - } - } - } ?: run { - setLoginError(createNetworkLoginError) - setCreateGuestModeInProgress(false) - } - - } - LaunchedEffect(Unit) { googleSignInClient.signOut() setGoogleAuthInProgress(false) @@ -565,9 +496,7 @@ fun LoginInitial( setUserAuth = setUserAuth, userAuthInProgress = userAuthInProgress, isValidUserAuth = isValidUserAuth, - setGuestModeOverlayVisible = setGuestModeOverlayVisible, googleAuthInProgress = googleAuthInProgress, - createGuestModeInProgress = createGuestModeInProgress, loginError = loginError, onLogin = { login( @@ -590,7 +519,9 @@ fun LoginInitial( bittensorAuthInProgress = bittensorAuthInProgress, launchAuthCodeLoginSheet = { setAuthCodeLoginSheetVisible(true) - } + }, + onSeedphraseLogin = onSeedphraseLogin, + onInstantAccountCreate = onInstantAccountCreate ) } @@ -608,18 +539,6 @@ fun LoginInitial( } ) - OnboardingGuestModeSheet( - isPresenting = guestModeOverlayVisible, - setIsPresenting = { - setGuestModeOverlayVisible(it) - }, - onCreateGuestNetwork = { - createGuestNetwork() - }, - createGuestModeInProgress = createGuestModeInProgress, - errorMessage = if (guestModeOverlayVisible) loginError else null - ) - if (welcomeOverlayVisible) { WelcomeAnimatedOverlayLogin() @@ -633,9 +552,7 @@ fun LoginInitialActions( setUserAuth: (TextFieldValue) -> Unit, userAuthInProgress: Boolean, isValidUserAuth: Boolean, - setGuestModeOverlayVisible: (Boolean) -> Unit, googleAuthInProgress: Boolean, - createGuestModeInProgress: Boolean, loginError: String?, onLogin: () -> Unit, onGoogleLogin: () -> Unit, @@ -647,10 +564,11 @@ fun LoginInitialActions( solanaAuthInProgress: Boolean, onBittensorLogin: () -> Unit, bittensorAuthInProgress: Boolean, - launchAuthCodeLoginSheet: () -> Unit + launchAuthCodeLoginSheet: () -> Unit, + onSeedphraseLogin: () -> Unit, + onInstantAccountCreate: () -> Unit, ) { - - val isLoginInProgress = userAuthInProgress || googleAuthInProgress || solanaAuthInProgress || ethOsAuthInProgress || bittensorAuthInProgress || createGuestModeInProgress + val isLoginInProgress = userAuthInProgress || googleAuthInProgress || solanaAuthInProgress || ethOsAuthInProgress || bittensorAuthInProgress Row( modifier = Modifier.fillMaxWidth(), @@ -863,68 +781,75 @@ fun LoginInitialActions( // } - if (!loginError.isNullOrEmpty()) { - Spacer(modifier = Modifier.height(16.dp)) - URInlineErrorText(loginError) - } - Spacer(modifier = Modifier.height(16.dp)) - TryGuestMode( - setGuestModeOverlayVisible = setGuestModeOverlayVisible, + /** + * Seedphrase Sign in + */ + URButton( + style = ButtonStyle.SECONDARY, + onClick = { + onSeedphraseLogin() + }, enabled = !isLoginInProgress - ) + ) { buttonTextStyle -> + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Filled.Key, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Sign in with Seedphrase", + style = buttonTextStyle + ) + } + } Spacer(modifier = Modifier.height(16.dp)) - NetworkServerSelector(enabled = !isLoginInProgress) - } - } - -} + /** + * Instant account creation + */ + URButton( + style = ButtonStyle.SECONDARY, + onClick = { + onInstantAccountCreate() + }, + enabled = !isLoginInProgress + ) { buttonTextStyle -> + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Filled.Bolt, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Create Instant Account", + style = buttonTextStyle + ) + } + } -@Composable -private fun TryGuestMode( - setGuestModeOverlayVisible: (Boolean) -> Unit, - enabled: Boolean -) { + Spacer(modifier = Modifier.height(16.dp)) - var isFocused by remember { mutableStateOf(false) } + if (!loginError.isNullOrEmpty()) { + Spacer(modifier = Modifier.height(16.dp)) + URInlineErrorText(loginError) + } - val guestModeStr = buildAnnotatedString { - append(stringResource(id = R.string.commitment_issues)) + Spacer(modifier = Modifier.height(16.dp)) - pushStringAnnotation( - tag = "GUEST_MODE", - annotation = "Guest Mode" - ) - withStyle( - style = SpanStyle( - color = if (!enabled) TextMuted else if (isFocused) BlueMedium else Color.White - ) - ) { - append(" ${stringResource(id = R.string.try_guest_mode)}") + NetworkServerSelector(enabled = !isLoginInProgress) } - pop() - } - Row { - Text( - text = guestModeStr, - modifier = Modifier - .clickable { - if (enabled) { - setGuestModeOverlayVisible(true) - } - } - .onFocusChanged { - isFocused = it.isFocused - } - .focusable(), - style = MaterialTheme.typography.bodyLarge.copy(color = TextMuted), - ) - } } fun hasEthOsWalletService(context: Context): Boolean { @@ -977,9 +902,7 @@ private fun LoginInitialPreview() { loginError = null, setLoginError = {}, googleAuthInProgress = false, - createGuestModeInProgress = false, setGoogleAuthInProgress = {}, - setCreateGuestModeInProgress = {}, allowGoogleSso = { true }, solanaAuthInProgress = false, ethOsAuthInProgress = false, @@ -992,7 +915,9 @@ private fun LoginInitialPreview() { contentVisible = true, setContentVisible = {}, welcomeOverlayVisible = false, - setWelcomeOverlayVisible = {} + setWelcomeOverlayVisible = {}, + onSeedphraseLogin = {}, + onInstantAccountCreate = {}, ) } } @@ -1046,9 +971,7 @@ private fun LoginInitialLandscapePreview() { loginError = null, setLoginError = {}, googleAuthInProgress = false, - createGuestModeInProgress = false, setGoogleAuthInProgress = {}, - setCreateGuestModeInProgress = {}, allowGoogleSso = { true }, solanaAuthInProgress = false, ethOsAuthInProgress = false, @@ -1061,7 +984,9 @@ private fun LoginInitialLandscapePreview() { contentVisible = true, setContentVisible = {}, welcomeOverlayVisible = false, - setWelcomeOverlayVisible = {} + setWelcomeOverlayVisible = {}, + onSeedphraseLogin = {}, + onInstantAccountCreate = {}, ) } } diff --git a/app/app/src/ethos_dapp/java/com/bringyour/network/ui/login/LoginViewModel.kt b/app/app/src/ethos_dapp/java/com/bringyour/network/ui/login/LoginViewModel.kt index 6807d7c6b..d3a09d261 100644 --- a/app/app/src/ethos_dapp/java/com/bringyour/network/ui/login/LoginViewModel.kt +++ b/app/app/src/ethos_dapp/java/com/bringyour/network/ui/login/LoginViewModel.kt @@ -55,11 +55,11 @@ class LoginViewModel @Inject constructor( var loginError by mutableStateOf(null) private set - var createGuestModeInProgress by mutableStateOf(false) + var seedphraseAuthInProgress by mutableStateOf(false) private set - val setCreateGuestModeInProgress: (Boolean) -> Unit = { inProgress -> - createGuestModeInProgress = inProgress + val setSeedphraseAuthInProgress: (Boolean) -> Unit = { inProgress -> + seedphraseAuthInProgress = inProgress } val login: ( @@ -119,6 +119,48 @@ class LoginViewModel @Inject constructor( } } + fun loginWithSeedphrase( + ctx: Context, + api: Api?, + seedphrase: String, + onSuccess: (String) -> Unit, + onError: (String) -> Unit, + ) { + if (seedphraseAuthInProgress) return + + val authApi = api ?: run { + setLoginError(ctx.getString(R.string.login_error)) + return + } + + setLoginError(null) + seedphraseAuthInProgress = true + + val normalized = seedphrase.lowercase().trim().replace(Regex("\\s+"), " ") + val args = AuthLoginArgs() + args.seedphrase = normalized + + authApi.authLogin(args) { result, err -> + viewModelScope.launch { + if (err != null) { + setLoginError(err.message) + onError(err.message ?: "Seedphrase login failed") + } else if (result.error != null) { + setLoginError(result.error.message) + onError(result.error.message ?: "Invalid seedphrase") + } else if (result.network != null && result.network.byJwt.isNotEmpty()) { + setLoginError(null) + onSuccess(result.network.byJwt) + } else { + val msg = "Invalid seedphrase" + setLoginError(msg) + onError(msg) + } + seedphraseAuthInProgress = false + } + } + } + val allowGoogleSso = { networkSpaceManagerProvider.getNetworkSpace()?.ssoGoogle ?: false } diff --git a/app/app/src/ethos_dapp/java/com/bringyour/network/ui/settings/GoogleAddAuthButton.kt b/app/app/src/ethos_dapp/java/com/bringyour/network/ui/settings/GoogleAddAuthButton.kt new file mode 100644 index 000000000..152ca9258 --- /dev/null +++ b/app/app/src/ethos_dapp/java/com/bringyour/network/ui/settings/GoogleAddAuthButton.kt @@ -0,0 +1,78 @@ +package com.bringyour.network.ui.settings + +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.bringyour.network.R +import com.bringyour.network.ui.components.URButton +import com.bringyour.network.ui.theme.TextMuted +import com.bringyour.sdk.AddAuthArgs +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.api.ApiException + +@Composable +fun GoogleAddAuthButton( + addAuth: (AddAuthArgs, onSuccess: () -> Unit, onError: (String) -> Unit) -> Unit, + isAddingAuth: Boolean, + onAdded: () -> Unit, + onError: (String) -> Unit, +) { + val context = LocalContext.current + + val googleClientId = stringResource(id = R.string.google_client_id) + val googleSignInOpts = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestIdToken(googleClientId) + .requestEmail() + .build() + val googleSignInClient = GoogleSignIn.getClient(context, googleSignInOpts) + + val googleSignInLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + val task = GoogleSignIn.getSignedInAccountFromIntent(result.data) + try { + val account = task.getResult(ApiException::class.java) + val idToken = account.idToken + if (idToken == null) { + onError("Could not get Google ID token") + return@rememberLauncherForActivityResult + } + val args = AddAuthArgs() + args.authJwt = idToken + args.authJwtType = "google" + addAuth( + args, + { + Toast.makeText(context, "Google sign-in method added", Toast.LENGTH_SHORT).show() + onAdded() + }, + { msg -> onError(msg) } + ) + } catch (e: ApiException) { + onError("Error signing in with Google") + } + } + + Text( + "Sign in with Google to add it as a sign-in method.", + style = MaterialTheme.typography.bodyMedium, + color = TextMuted + ) + Spacer(modifier = Modifier.height(12.dp)) + URButton( + onClick = { googleSignInLauncher.launch(googleSignInClient.signInIntent) }, + enabled = !isAddingAuth + ) { buttonTextStyle -> + Text("Sign in with Google", style = buttonTextStyle) + } +} diff --git a/app/app/src/google/java/com/bringyour/network/LoginActivity.kt b/app/app/src/google/java/com/bringyour/network/LoginActivity.kt index cc7dc20d8..0e11ced12 100644 --- a/app/app/src/google/java/com/bringyour/network/LoginActivity.kt +++ b/app/app/src/google/java/com/bringyour/network/LoginActivity.kt @@ -277,7 +277,7 @@ class LoginActivity : AppCompatActivity() { if (!success || jwt == null) { Log.i(TAG, "guest login: local byJwt parse failed") createGuestNetworkAndFinish(app) - } else if (jwt.guestMode) { + } else if (jwt.networkName != null) { setLinksAndStartMain(targetUrl, defaultLocation) } else { currentNetworkName = jwt.networkName @@ -373,7 +373,6 @@ class LoginActivity : AppCompatActivity() { private fun createGuestNetworkAndFinish(app: MainApplication) { val args = NetworkCreateArgs() args.terms = true - args.guestMode = true app.api?.networkCreate(args) { result, err -> lifecycleScope.launch { diff --git a/app/app/src/google/java/com/bringyour/network/ui/login/LoginInitial.kt b/app/app/src/google/java/com/bringyour/network/ui/login/LoginInitial.kt index e85f419c7..b053c7529 100644 --- a/app/app/src/google/java/com/bringyour/network/ui/login/LoginInitial.kt +++ b/app/app/src/google/java/com/bringyour/network/ui/login/LoginInitial.kt @@ -9,7 +9,10 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size -import androidx.compose.material3.MaterialTheme +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bolt +import androidx.compose.material.icons.filled.Key +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -37,12 +40,10 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.EnterTransition import androidx.compose.animation.fadeOut -import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.clickable import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.LaunchedEffect @@ -51,33 +52,25 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.focus.onFocusChanged import androidx.lifecycle.compose.LifecycleResumeEffect import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.common.api.ApiException -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.withStyle import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.bringyour.sdk.AuthLoginResult import com.bringyour.sdk.Api -import com.bringyour.sdk.NetworkCreateArgs import com.bringyour.network.LoginActivity import com.bringyour.network.MainApplication import com.bringyour.network.R import com.bringyour.network.TAG import com.bringyour.network.ui.components.URInlineErrorText import com.bringyour.network.ui.components.overlays.WelcomeAnimatedOverlayLogin -import com.bringyour.network.ui.theme.BlueMedium import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.solana.mobilewalletadapter.clientlib.ActivityResultSender -import kotlinx.coroutines.delay import kotlinx.coroutines.launch @Composable() @@ -223,6 +216,14 @@ fun LoginInitial( } } + val onSeedphraseLogin: () -> Unit = { + navController.navigate("login_seedphrase") + } + + val onInstantAccountCreate: () -> Unit = { + navController.navigate("create-network-instant") + } + LoginInitial( navController, userAuth = loginViewModel.userAuth, @@ -235,8 +236,6 @@ fun LoginInitial( setGoogleAuthInProgress = loginViewModel.setGoogleAuthInProgress, setLoginError = loginViewModel.setLoginError, googleAuthInProgress = loginViewModel.googleAuthInProgress, - createGuestModeInProgress = loginViewModel.createGuestModeInProgress, - setCreateGuestModeInProgress = loginViewModel.setCreateGuestModeInProgress, allowGoogleSso = loginViewModel.allowGoogleSso, solanaLogin = { connectSolanaWallet() @@ -254,7 +253,9 @@ fun LoginInitial( welcomeOverlayVisible = welcomeOverlayVisible, setWelcomeOverlayVisible = { welcomeOverlayVisible = it - } + }, + onSeedphraseLogin = onSeedphraseLogin, + onInstantAccountCreate = onInstantAccountCreate ) if (noSolanaWalletsFound) { @@ -292,9 +293,7 @@ fun LoginInitial( loginError: String?, setLoginError: (String?) -> Unit, googleAuthInProgress: Boolean, - createGuestModeInProgress: Boolean, setGoogleAuthInProgress: (Boolean) -> Unit, - setCreateGuestModeInProgress: (Boolean) -> Unit, allowGoogleSso: () -> Boolean, solanaLogin: () -> Unit, solanaAuthInProgress: Boolean, @@ -305,21 +304,14 @@ fun LoginInitial( setContentVisible: (Boolean) -> Unit, welcomeOverlayVisible: Boolean, setWelcomeOverlayVisible: (Boolean) -> Unit, + onSeedphraseLogin: () -> Unit, + onInstantAccountCreate: () -> Unit, ) { val context = LocalContext.current val application = context.applicationContext as? MainApplication val scope = rememberCoroutineScope() - var guestModeOverlayVisible by remember { mutableStateOf(false) } - - val setGuestModeOverlayVisible: (Boolean) -> Unit = { isVisible -> - if (isVisible) { - setLoginError(null) - } - guestModeOverlayVisible = isVisible - } - var authCodeLoginSheetVisible by remember { mutableStateOf(false) } val setAuthCodeLoginSheetVisible: (Boolean) -> Unit = { isVisible -> @@ -345,67 +337,6 @@ fun LoginInitial( val googleSignInClient = GoogleSignIn.getClient(context, googleSignInOpts) val loginErrorMsg = stringResource(id = R.string.create_network_error) - val createGuestNetwork = createGuestNetwork@{ - if (createGuestModeInProgress) { - return@createGuestNetwork - } - - setLoginError(null) - setCreateGuestModeInProgress(true) - - val args = NetworkCreateArgs() - args.terms = true - args.guestMode = true - - application?.api?.networkCreate(args) { result, err -> - scope.launch { - - if (err != null) { - Log.i("OnboardingGuestModeOverlay", "error ${err.message}") - setLoginError(err.message) - setCreateGuestModeInProgress(false) - } else if (result.error != null) { - Log.i("OnboardingGuestModeOverlay", "error ${result.error.message}") - setLoginError(result.error.message) - setCreateGuestModeInProgress(false) - } else if (result.network != null && result.network.byJwt.isNotEmpty()) { - setLoginError(null) - setGuestModeOverlayVisible(false) - - application.login(result.network.byJwt) - - setContentVisible(false) - - delay(500) - - setWelcomeOverlayVisible(true) - - delay(2250) - - loginActivity?.authClientAndFinish( - { error -> - setCreateGuestModeInProgress(false) - - setLoginError(error) - - if (error != null) { - setContentVisible(true) - } - } - ) - - } else { - setLoginError(loginErrorMsg) - setCreateGuestModeInProgress(false) - } - } - } ?: run { - setLoginError(loginErrorMsg) - setCreateGuestModeInProgress(false) - } - - } - LaunchedEffect(Unit) { googleSignInClient.signOut() setGoogleAuthInProgress(false) @@ -471,9 +402,7 @@ fun LoginInitial( setUserAuth = setUserAuth, userAuthInProgress = userAuthInProgress, isValidUserAuth = isValidUserAuth, - setGuestModeOverlayVisible = setGuestModeOverlayVisible, googleAuthInProgress = googleAuthInProgress, - createGuestModeInProgress = createGuestModeInProgress, loginError = loginError, onLogin = { login( @@ -493,7 +422,9 @@ fun LoginInitial( bittensorAuthInProgress = bittensorAuthInProgress, launchAuthCodeLoginSheet = { setAuthCodeLoginSheetVisible(true) - } + }, + onSeedphraseLogin = onSeedphraseLogin, + onInstantAccountCreate = onInstantAccountCreate ) } @@ -509,18 +440,6 @@ fun LoginInitial( onLogin = onLogin ) - OnboardingGuestModeSheet( - isPresenting = guestModeOverlayVisible, - setIsPresenting = { - setGuestModeOverlayVisible(it) - }, - onCreateGuestNetwork = { - createGuestNetwork() - }, - createGuestModeInProgress = createGuestModeInProgress, - errorMessage = if (guestModeOverlayVisible) loginError else null - ) - if (welcomeOverlayVisible) { WelcomeAnimatedOverlayLogin() @@ -534,9 +453,7 @@ fun LoginInitialActions( setUserAuth: (TextFieldValue) -> Unit, userAuthInProgress: Boolean, isValidUserAuth: Boolean, - setGuestModeOverlayVisible: (Boolean) -> Unit, googleAuthInProgress: Boolean, - createGuestModeInProgress: Boolean, loginError: String?, onLogin: () -> Unit, onGoogleLogin: () -> Unit, @@ -545,10 +462,11 @@ fun LoginInitialActions( solanaAuthInProgress: Boolean, onBittensorLogin: () -> Unit, bittensorAuthInProgress: Boolean, - launchAuthCodeLoginSheet: () -> Unit + launchAuthCodeLoginSheet: () -> Unit, + onSeedphraseLogin: () -> Unit, + onInstantAccountCreate: () -> Unit, ) { - - val isLoginInProgress = userAuthInProgress || googleAuthInProgress || solanaAuthInProgress || bittensorAuthInProgress || createGuestModeInProgress + val isLoginInProgress = userAuthInProgress || googleAuthInProgress || solanaAuthInProgress || bittensorAuthInProgress Row( modifier = Modifier.fillMaxWidth(), @@ -735,61 +653,66 @@ fun LoginInitialActions( Spacer(modifier = Modifier.height(16.dp)) - TryGuestMode( - setGuestModeOverlayVisible = setGuestModeOverlayVisible, + /** + * Seedphrase Sign in + */ + URButton( + style = ButtonStyle.SECONDARY, + onClick = { + onSeedphraseLogin() + }, enabled = !isLoginInProgress - ) + ) { buttonTextStyle -> + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Filled.Key, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Sign in with Seedphrase", + style = buttonTextStyle + ) + } + } Spacer(modifier = Modifier.height(16.dp)) - NetworkServerSelector(enabled = !isLoginInProgress) - } - } - -} - -@Composable -private fun TryGuestMode( - setGuestModeOverlayVisible: (Boolean) -> Unit, - enabled: Boolean -) { - - var isFocused by remember { mutableStateOf(false) } + /** + * Instant account creation + */ + URButton( + style = ButtonStyle.SECONDARY, + onClick = { + onInstantAccountCreate() + }, + enabled = !isLoginInProgress + ) { buttonTextStyle -> + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Filled.Bolt, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Create Instant Account", + style = buttonTextStyle + ) + } + } - val guestModeStr = buildAnnotatedString { - append(stringResource(id = R.string.commitment_issues)) + Spacer(modifier = Modifier.height(16.dp)) - pushStringAnnotation( - tag = "GUEST_MODE", - annotation = "Guest Mode" - ) - withStyle( - style = SpanStyle( - color = if (!enabled) TextMuted else if (isFocused) BlueMedium else Color.White - ) - ) { - append(" ${stringResource(id = R.string.try_guest_mode)}") + NetworkServerSelector(enabled = !isLoginInProgress) } - pop() - } - Row { - Text( - text = guestModeStr, - modifier = Modifier - .clickable { - if (enabled) { - setGuestModeOverlayVisible(true) - } - } - .onFocusChanged { - isFocused = it.isFocused - } - .focusable(), - style = MaterialTheme.typography.bodyLarge.copy(color = TextMuted), - ) - } } @Preview() @@ -837,9 +760,7 @@ private fun LoginInitialPreview() { loginError = null, setLoginError = {}, googleAuthInProgress = false, - createGuestModeInProgress = false, setGoogleAuthInProgress = {}, - setCreateGuestModeInProgress = {}, allowGoogleSso = { true }, solanaAuthInProgress = false, solanaLogin = {}, @@ -849,7 +770,9 @@ private fun LoginInitialPreview() { contentVisible = true, setContentVisible = {}, welcomeOverlayVisible = false, - setWelcomeOverlayVisible = {} + setWelcomeOverlayVisible = {}, + onSeedphraseLogin = {}, + onInstantAccountCreate = {}, ) } } @@ -903,9 +826,7 @@ private fun LoginInitialLandscapePreview() { loginError = null, setLoginError = {}, googleAuthInProgress = false, - createGuestModeInProgress = false, setGoogleAuthInProgress = {}, - setCreateGuestModeInProgress = {}, allowGoogleSso = { true }, solanaAuthInProgress = false, solanaLogin = {}, @@ -915,7 +836,9 @@ private fun LoginInitialLandscapePreview() { contentVisible = true, setContentVisible = {}, welcomeOverlayVisible = false, - setWelcomeOverlayVisible = {} + setWelcomeOverlayVisible = {}, + onSeedphraseLogin = {}, + onInstantAccountCreate = {}, ) } } diff --git a/app/app/src/google/java/com/bringyour/network/ui/login/LoginViewModel.kt b/app/app/src/google/java/com/bringyour/network/ui/login/LoginViewModel.kt index 2440277cc..b73649815 100644 --- a/app/app/src/google/java/com/bringyour/network/ui/login/LoginViewModel.kt +++ b/app/app/src/google/java/com/bringyour/network/ui/login/LoginViewModel.kt @@ -52,11 +52,11 @@ class LoginViewModel @Inject constructor( var loginError by mutableStateOf(null) private set - var createGuestModeInProgress by mutableStateOf(false) + var seedphraseAuthInProgress by mutableStateOf(false) private set - val setCreateGuestModeInProgress: (Boolean) -> Unit = { inProgress -> - createGuestModeInProgress = inProgress + val setSeedphraseAuthInProgress: (Boolean) -> Unit = { inProgress -> + seedphraseAuthInProgress = inProgress } val login: ( @@ -116,6 +116,48 @@ class LoginViewModel @Inject constructor( } } + fun loginWithSeedphrase( + ctx: Context, + api: Api?, + seedphrase: String, + onSuccess: (String) -> Unit, + onError: (String) -> Unit, + ) { + if (seedphraseAuthInProgress) return + + val authApi = api ?: run { + setLoginError(ctx.getString(R.string.login_error)) + return + } + + setLoginError(null) + seedphraseAuthInProgress = true + + val normalized = seedphrase.lowercase().trim().replace(Regex("\\s+"), " ") + val args = AuthLoginArgs() + args.seedphrase = normalized + + authApi.authLogin(args) { result, err -> + viewModelScope.launch { + if (err != null) { + setLoginError(err.message) + onError(err.message ?: "Seedphrase login failed") + } else if (result.error != null) { + setLoginError(result.error.message) + onError(result.error.message ?: "Invalid seedphrase") + } else if (result.network != null && result.network.byJwt.isNotEmpty()) { + setLoginError(null) + onSuccess(result.network.byJwt) + } else { + val msg = "Invalid seedphrase" + setLoginError(msg) + onError(msg) + } + seedphraseAuthInProgress = false + } + } + } + val allowGoogleSso = { networkSpaceManagerProvider.getNetworkSpace()?.ssoGoogle ?: false } diff --git a/app/app/src/google/java/com/bringyour/network/ui/settings/GoogleAddAuthButton.kt b/app/app/src/google/java/com/bringyour/network/ui/settings/GoogleAddAuthButton.kt new file mode 100644 index 000000000..152ca9258 --- /dev/null +++ b/app/app/src/google/java/com/bringyour/network/ui/settings/GoogleAddAuthButton.kt @@ -0,0 +1,78 @@ +package com.bringyour.network.ui.settings + +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.bringyour.network.R +import com.bringyour.network.ui.components.URButton +import com.bringyour.network.ui.theme.TextMuted +import com.bringyour.sdk.AddAuthArgs +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.api.ApiException + +@Composable +fun GoogleAddAuthButton( + addAuth: (AddAuthArgs, onSuccess: () -> Unit, onError: (String) -> Unit) -> Unit, + isAddingAuth: Boolean, + onAdded: () -> Unit, + onError: (String) -> Unit, +) { + val context = LocalContext.current + + val googleClientId = stringResource(id = R.string.google_client_id) + val googleSignInOpts = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestIdToken(googleClientId) + .requestEmail() + .build() + val googleSignInClient = GoogleSignIn.getClient(context, googleSignInOpts) + + val googleSignInLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + val task = GoogleSignIn.getSignedInAccountFromIntent(result.data) + try { + val account = task.getResult(ApiException::class.java) + val idToken = account.idToken + if (idToken == null) { + onError("Could not get Google ID token") + return@rememberLauncherForActivityResult + } + val args = AddAuthArgs() + args.authJwt = idToken + args.authJwtType = "google" + addAuth( + args, + { + Toast.makeText(context, "Google sign-in method added", Toast.LENGTH_SHORT).show() + onAdded() + }, + { msg -> onError(msg) } + ) + } catch (e: ApiException) { + onError("Error signing in with Google") + } + } + + Text( + "Sign in with Google to add it as a sign-in method.", + style = MaterialTheme.typography.bodyMedium, + color = TextMuted + ) + Spacer(modifier = Modifier.height(12.dp)) + URButton( + onClick = { googleSignInLauncher.launch(googleSignInClient.signInIntent) }, + enabled = !isAddingAuth + ) { buttonTextStyle -> + Text("Sign in with Google", style = buttonTextStyle) + } +} diff --git a/app/app/src/main/java/com/bringyour/network/ui/LoginNavHost.kt b/app/app/src/main/java/com/bringyour/network/ui/LoginNavHost.kt index 9fd900e43..8b80c328f 100644 --- a/app/app/src/main/java/com/bringyour/network/ui/LoginNavHost.kt +++ b/app/app/src/main/java/com/bringyour/network/ui/LoginNavHost.kt @@ -14,26 +14,33 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.bringyour.network.ui.components.overlays.FullScreenOverlay import com.bringyour.network.ui.login.AuthCodeLoadingScreen +import com.bringyour.network.ui.login.CreateNetworkInstant import com.bringyour.network.ui.login.LoginCreateNetwork import com.bringyour.network.ui.login.LoginCreateNetworkParams import com.bringyour.network.ui.login.LoginInitial import com.bringyour.network.ui.login.LoginPassword import com.bringyour.network.ui.login.LoginPasswordReset import com.bringyour.network.ui.login.LoginPasswordResetAfterSend +import com.bringyour.network.ui.login.LoginSeedphrase import com.bringyour.network.ui.login.LoginVerify import com.bringyour.network.ui.login.LoginViewModel +import com.bringyour.network.ui.login.SeedphraseDisplayScreen import com.bringyour.network.ui.login.SwitchAccountScreen +import com.bringyour.network.ui.login.handleLoginFlow import com.bringyour.network.ui.login.toWalletCreateBundle import com.bringyour.network.ui.shared.viewmodels.OverlayViewModel import com.solana.mobilewalletadapter.clientlib.ActivityResultSender +import kotlinx.coroutines.launch @Composable fun LoginNavHost( @@ -227,6 +234,74 @@ fun LoginNavHost( navController ) } + + composable("login_seedphrase") { + val context = LocalContext.current + val application = context.applicationContext as? com.bringyour.network.MainApplication + val loginActivity = context as? com.bringyour.network.LoginActivity + val coroutineScope = rememberCoroutineScope() + + LoginSeedphrase( + onLoginSuccess = { jwt -> + coroutineScope.launch { + handleLoginFlow( + networkJwt = jwt, + scope = coroutineScope, + appLogin = { application?.login(jwt) }, + onContentVisibilityChange = {}, + onErr = { + android.widget.Toast.makeText(context, "Error logging in, please try again.", android.widget.Toast.LENGTH_LONG).show() + }, + onWelcomeOverlayVisibilityChange = {}, + authClientAndFinish = { cb -> + loginActivity?.authClientAndFinish(cb) + } + ) + } + }, + onBack = { + navController.popBackStack() + } + ) + } + + composable("create-network-instant") { + val context = LocalContext.current + val application = context.applicationContext as? com.bringyour.network.MainApplication + val loginActivity = context as? com.bringyour.network.LoginActivity + val coroutineScope = rememberCoroutineScope() + + var seedphraseResult by remember { mutableStateOf?>(null) } + + CreateNetworkInstant( + onSeedphraseCreated = { seedphrase, jwt -> + seedphraseResult = Pair(seedphrase, jwt) + }, + onBack = { + navController.popBackStack() + } + ) + + seedphraseResult?.let { (sp, jwt) -> + SeedphraseDisplayScreen( + seedphrase = sp, + onConfirmed = { + seedphraseResult = null + application?.login(jwt) + coroutineScope.launch { + loginActivity?.authClientAndFinish { error -> + if (error != null) { + android.util.Log.e("LoginNavHost", "auth client finish err: $error") + } + } + } + }, + onBack = { + seedphraseResult = null + } + ) + } + } } // an unlinked wallet auth was received by the activity (eg the bittensor diff --git a/app/app/src/main/java/com/bringyour/network/ui/MainNavHost.kt b/app/app/src/main/java/com/bringyour/network/ui/MainNavHost.kt index 2d6162b87..b7f864ba4 100644 --- a/app/app/src/main/java/com/bringyour/network/ui/MainNavHost.kt +++ b/app/app/src/main/java/com/bringyour/network/ui/MainNavHost.kt @@ -374,6 +374,21 @@ fun MainNavHost( onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } + DisposableEffect(lifecycleOwner) { + + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + mainNavViewModel.refreshTokenOnForeground() + } + } + + lifecycleOwner.lifecycle.addObserver(observer) + + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + AnimatedContent( targetState = displayIntroFunnel, label = "intro-main-switch", diff --git a/app/app/src/main/java/com/bringyour/network/ui/MainNavViewModel.kt b/app/app/src/main/java/com/bringyour/network/ui/MainNavViewModel.kt index fb8e73b3b..3b06d32c5 100644 --- a/app/app/src/main/java/com/bringyour/network/ui/MainNavViewModel.kt +++ b/app/app/src/main/java/com/bringyour/network/ui/MainNavViewModel.kt @@ -43,6 +43,10 @@ class MainNavViewModel @Inject constructor( deviceManager.canPromptIntroFunnel = false } + val refreshTokenOnForeground: () -> Unit = { + deviceManager.device?.refreshToken(0) + } + init { _allowDisplayIntroFunnel.value = deviceManager.canPromptIntroFunnel } diff --git a/app/app/src/main/java/com/bringyour/network/ui/account/AccountScreen.kt b/app/app/src/main/java/com/bringyour/network/ui/account/AccountScreen.kt index 7e1487bd1..5f3a4de37 100644 --- a/app/app/src/main/java/com/bringyour/network/ui/account/AccountScreen.kt +++ b/app/app/src/main/java/com/bringyour/network/ui/account/AccountScreen.kt @@ -359,7 +359,7 @@ fun AccountScreenContent( .offset(y = (-8).dp) .clickable { if (loginMode == LoginMode.Guest) { - launchOverlay(OverlayMode.GuestMode) + context.startActivity(Intent(context, LoginActivity::class.java)) } else { navController.navigate(Route.Wallets) } @@ -401,7 +401,7 @@ fun AccountScreenContent( if (loginMode == LoginMode.Authenticated) { navController.navigate(Route.Profile) } else { - launchOverlay(OverlayMode.GuestMode) + context.startActivity(Intent(context, LoginActivity::class.java)) } } ) @@ -413,7 +413,7 @@ fun AccountScreenContent( if (loginMode == LoginMode.Authenticated) { navController.navigate(Route.Settings) } else { - launchOverlay(OverlayMode.GuestMode) + context.startActivity(Intent(context, LoginActivity::class.java)) } } ) @@ -425,7 +425,7 @@ fun AccountScreenContent( if (loginMode == LoginMode.Authenticated) { navController.navigate(Route.Wallets) } else { - launchOverlay(OverlayMode.GuestMode) + context.startActivity(Intent(context, LoginActivity::class.java)) } } ) @@ -437,7 +437,7 @@ fun AccountScreenContent( if (loginMode == LoginMode.Authenticated) { launchOverlay(OverlayMode.Refer) } else { - launchOverlay(OverlayMode.GuestMode) + context.startActivity(Intent(context, LoginActivity::class.java)) } } ) diff --git a/app/app/src/main/java/com/bringyour/network/ui/account/AccountViewModel.kt b/app/app/src/main/java/com/bringyour/network/ui/account/AccountViewModel.kt index 8538606cb..0df533a54 100644 --- a/app/app/src/main/java/com/bringyour/network/ui/account/AccountViewModel.kt +++ b/app/app/src/main/java/com/bringyour/network/ui/account/AccountViewModel.kt @@ -73,7 +73,7 @@ class AccountViewModel @Inject constructor( localState?.parseByJwt { jwt, success -> viewModelScope.launch { - setLoginMode(if (success && jwt?.guestMode != true) LoginMode.Authenticated else LoginMode.Guest) + setLoginMode(if (success && jwt?.networkName != null) LoginMode.Authenticated else LoginMode.Guest) // setNetworkName(jwt?.networkName ?: "guest") } } diff --git a/app/app/src/main/java/com/bringyour/network/ui/components/overlays/FullScreenOverlay.kt b/app/app/src/main/java/com/bringyour/network/ui/components/overlays/FullScreenOverlay.kt index f9c319456..ac27db2ec 100644 --- a/app/app/src/main/java/com/bringyour/network/ui/components/overlays/FullScreenOverlay.kt +++ b/app/app/src/main/java/com/bringyour/network/ui/components/overlays/FullScreenOverlay.kt @@ -11,12 +11,10 @@ import com.bringyour.network.ui.shared.viewmodels.OverlayViewModel import com.bringyour.network.ui.shared.viewmodels.ReferralCodeViewModel enum class OverlayMode { - GuestMode, Upgrade, Refer, FeedbackSubmitted, Onboarding, - OnboardingGuestMode, // a purchase Play accepted but has not completed -- awaiting approval or an // out-of-band payment. Distinct from Upgrade, which means it actually went through. PurchasePending, @@ -33,20 +31,6 @@ fun FullScreenOverlay( val overlayMode = overlayViewModel.overlayModeState.collectAsState().value - // You're in Guest mode overlay - AnimatedVisibility( - visible = overlayMode == OverlayMode.GuestMode, - enter = enterTransition, - exit = exitTransition, - ) { - - GuestModeOverlay( - onDismiss = { - overlayViewModel.launch(null) - } - ) - } - // Refer overlay AnimatedVisibility( visible = overlayMode == OverlayMode.Refer, diff --git a/app/app/src/main/java/com/bringyour/network/ui/components/overlays/GuestModeOverlay.kt b/app/app/src/main/java/com/bringyour/network/ui/components/overlays/GuestModeOverlay.kt deleted file mode 100644 index b8ce98956..000000000 --- a/app/app/src/main/java/com/bringyour/network/ui/components/overlays/GuestModeOverlay.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.bringyour.network.ui.components.overlays - -import android.app.Activity -import android.content.Intent -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.bringyour.network.LoginActivity -import com.bringyour.network.MainApplication -import com.bringyour.network.R -import com.bringyour.network.ui.components.ButtonStyle -import com.bringyour.network.ui.components.URButton -import com.bringyour.network.ui.theme.Black -import com.bringyour.network.ui.theme.Blue200 -import com.bringyour.network.ui.theme.URNetworkTheme - -@Composable -fun GuestModeOverlay( - onDismiss: () -> Unit -) { - - val context = LocalContext.current - val application = context.applicationContext as? MainApplication - - OverlayBackground( - onDismiss = onDismiss, - bgImageResourceId = R.drawable.overlay_guest_mode_bg - ) { - - OverlayContent( - backgroundColor = Blue200, - ) { - Text( - stringResource(id = R.string.in_guest_mode), - style = MaterialTheme.typography.headlineMedium, - color = Black - ) - - Text( - stringResource(id = R.string.start_earning_join), - style = MaterialTheme.typography.headlineLarge, - color = Black - ) - - Spacer(modifier = Modifier.height(128.dp)) - - URButton( - onClick = { - - application?.logout() - - onDismiss() - - val intent = Intent(context, LoginActivity::class.java) - context.startActivity(intent) - - (context as? Activity)?.finish() - - }, - style = ButtonStyle.OUTLINE - ) { buttonTextStyle -> - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - Text(stringResource(id = R.string.create_account), style = buttonTextStyle, color = Black) - } - } - } - } -} - -@Preview -@Composable -private fun GuestModeOverlayPreview() { - URNetworkTheme { - GuestModeOverlay( - onDismiss = {} - ) - } -} - -@Preview( - name = "Landscape Preview", - device = "spec:width=1920dp,height=1080dp,dpi=480" -) -@Composable -private fun GuestModeOverlayLandscapePreview() { - URNetworkTheme { - GuestModeOverlay( - onDismiss = {} - ) - } -} \ No newline at end of file diff --git a/app/app/src/main/java/com/bringyour/network/ui/components/overlays/OnboardingOverlay.kt b/app/app/src/main/java/com/bringyour/network/ui/components/overlays/OnboardingOverlay.kt index 3627c588f..17c65575d 100644 --- a/app/app/src/main/java/com/bringyour/network/ui/components/overlays/OnboardingOverlay.kt +++ b/app/app/src/main/java/com/bringyour/network/ui/components/overlays/OnboardingOverlay.kt @@ -32,7 +32,7 @@ import com.bringyour.network.ui.theme.Yellow fun OnboardingOverlay() { val context = LocalContext.current - val backgroundBitmap: ImageBitmap = remember { ImageBitmap.imageResource(context.resources, R.drawable.overlay_guest_onboarding_bg) } + val backgroundBitmap: ImageBitmap = remember { ImageBitmap.imageResource(context.resources, R.drawable.overlay_plan_upgraded_bg) } Box( modifier = Modifier diff --git a/app/app/src/main/java/com/bringyour/network/ui/components/overlays/OverlayBackground.kt b/app/app/src/main/java/com/bringyour/network/ui/components/overlays/OverlayBackground.kt index 98f483caf..6a7e33434 100644 --- a/app/app/src/main/java/com/bringyour/network/ui/components/overlays/OverlayBackground.kt +++ b/app/app/src/main/java/com/bringyour/network/ui/components/overlays/OverlayBackground.kt @@ -132,7 +132,7 @@ private fun FullScreenOverlayPreview() { URNetworkTheme { OverlayBackground( onDismiss = {}, - bgImageResourceId = R.drawable.overlay_guest_mode_bg + bgImageResourceId = R.drawable.overlay_refer_bg ) { Text("Hello world") } diff --git a/app/app/src/main/java/com/bringyour/network/ui/components/overlays/WelcomeAnimatedLoginOverlay.kt b/app/app/src/main/java/com/bringyour/network/ui/components/overlays/WelcomeAnimatedLoginOverlay.kt index f34619fe9..475128067 100644 --- a/app/app/src/main/java/com/bringyour/network/ui/components/overlays/WelcomeAnimatedLoginOverlay.kt +++ b/app/app/src/main/java/com/bringyour/network/ui/components/overlays/WelcomeAnimatedLoginOverlay.kt @@ -45,7 +45,7 @@ fun WelcomeAnimatedOverlayLogin() { val enterTransition = fadeIn() val context = LocalContext.current - val backgroundBitmap: ImageBitmap = remember { ImageBitmap.imageResource(context.resources, R.drawable.overlay_guest_onboarding_bg) } + val backgroundBitmap: ImageBitmap = remember { ImageBitmap.imageResource(context.resources, R.drawable.overlay_plan_upgraded_bg) } val maskVector = ImageVector.vectorResource(id = R.drawable.connect_mask) val maskPainter = rememberVectorPainter(image = maskVector) diff --git a/app/app/src/main/java/com/bringyour/network/ui/components/overlays/WelcomeAnimatedMainOverlay.kt b/app/app/src/main/java/com/bringyour/network/ui/components/overlays/WelcomeAnimatedMainOverlay.kt index 5a0db6411..bb0ab6394 100644 --- a/app/app/src/main/java/com/bringyour/network/ui/components/overlays/WelcomeAnimatedMainOverlay.kt +++ b/app/app/src/main/java/com/bringyour/network/ui/components/overlays/WelcomeAnimatedMainOverlay.kt @@ -83,7 +83,7 @@ fun WelcomeAnimatedMainOverlay( close: () -> Unit, ) { val context = LocalContext.current - val backgroundBitmap: ImageBitmap = remember { ImageBitmap.imageResource(context.resources, R.drawable.overlay_guest_onboarding_bg) } + val backgroundBitmap: ImageBitmap = remember { ImageBitmap.imageResource(context.resources, R.drawable.overlay_plan_upgraded_bg) } var isPresentedWelcomeCard by remember { mutableStateOf(false) } diff --git a/app/app/src/main/java/com/bringyour/network/ui/login/AuthCodeLoginSheet.kt b/app/app/src/main/java/com/bringyour/network/ui/login/AuthCodeLoginSheet.kt index 412650d57..0abeff27f 100644 --- a/app/app/src/main/java/com/bringyour/network/ui/login/AuthCodeLoginSheet.kt +++ b/app/app/src/main/java/com/bringyour/network/ui/login/AuthCodeLoginSheet.kt @@ -182,7 +182,7 @@ fun AuthCodeLoginSheet( @Preview @Composable -private fun OnboardingGuestModeSheetPreview() { +private fun AuthCodeLoginSheetPreview() { URNetworkTheme { Scaffold { innerPadding -> diff --git a/app/app/src/main/java/com/bringyour/network/ui/login/CreateNetworkInstant.kt b/app/app/src/main/java/com/bringyour/network/ui/login/CreateNetworkInstant.kt new file mode 100644 index 000000000..421ae1756 --- /dev/null +++ b/app/app/src/main/java/com/bringyour/network/ui/login/CreateNetworkInstant.kt @@ -0,0 +1,166 @@ +package com.bringyour.network.ui.login + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.bringyour.network.MainApplication +import com.bringyour.network.ui.components.TermsCheckbox +import com.bringyour.network.ui.components.URButton +import com.bringyour.network.ui.components.URInlineErrorText +import com.bringyour.network.ui.theme.Black +import com.bringyour.sdk.NetworkCreateArgs +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreateNetworkInstant( + onSeedphraseCreated: (seedphrase: String, jwt: String) -> Unit, + onBack: () -> Unit, +) { + val context = LocalContext.current + val application = context.applicationContext as? MainApplication + val scope = rememberCoroutineScope() + + var termsAgreed by remember { mutableStateOf(false) } + var inProgress by remember { mutableStateOf(false) } + var error by remember { mutableStateOf(null) } + + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = {}, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowLeft, + contentDescription = "Back" + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Black + ), + actions = {} + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(innerPadding) + .padding(16.dp) + .imePadding(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Column( + modifier = Modifier.widthIn(max = 512.dp) + ) { + Text( + "Create Instant Account", + style = MaterialTheme.typography.headlineLarge, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + "No email or password needed. Your account is secured by a seedphrase.", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(32.dp)) + + TermsCheckbox( + checked = termsAgreed, + onCheckChanged = { termsAgreed = it }, + enabled = !inProgress + ) + + Spacer(modifier = Modifier.height(16.dp)) + + URButton( + onClick = { + if (inProgress) return@URButton + inProgress = true + error = null + + val api = application?.api ?: run { + inProgress = false + error = "Unable to connect. Please try again." + return@URButton + } + + val args = NetworkCreateArgs() + args.terms = termsAgreed + // No userAuth, userName, password, walletAuth — triggers seedphrase path + + api.networkCreate(args) { result, err -> + scope.launch { + if (err != null) { + error = err.message ?: "Unable to connect. Please try again." + inProgress = false + } else if (result.error != null) { + error = result.error.message ?: "Failed to create account" + inProgress = false + } else if (result.seedphrase != null && result.network?.byJwt != null) { + error = null + inProgress = false + onSeedphraseCreated( + result.seedphrase, + result.network.byJwt + ) + } else { + error = "Failed to create account" + inProgress = false + } + } + } + }, + enabled = termsAgreed && !inProgress, + isProcessing = inProgress + ) { buttonTextStyle -> + Text("Create Account", style = buttonTextStyle) + } + + if (error != null) { + Spacer(modifier = Modifier.height(8.dp)) + URInlineErrorText(error) + } + } + } + } +} diff --git a/app/app/src/main/java/com/bringyour/network/ui/login/LoginSeedphrase.kt b/app/app/src/main/java/com/bringyour/network/ui/login/LoginSeedphrase.kt new file mode 100644 index 000000000..17e1f68a9 --- /dev/null +++ b/app/app/src/main/java/com/bringyour/network/ui/login/LoginSeedphrase.kt @@ -0,0 +1,158 @@ +package com.bringyour.network.ui.login + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +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.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.bringyour.network.MainApplication +import com.bringyour.network.ui.components.URButton +import com.bringyour.network.ui.components.URInlineErrorText +import com.bringyour.network.ui.theme.Black +import com.bringyour.network.ui.theme.TextMuted + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoginSeedphrase( + onLoginSuccess: (String) -> Unit, + onBack: () -> Unit, + loginViewModel: LoginViewModel = hiltViewModel() +) { + val context = LocalContext.current + val application = context.applicationContext as? MainApplication + var seedphrase by remember { mutableStateOf("") } + var error by remember { mutableStateOf(null) } + + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = {}, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowLeft, + contentDescription = "Back" + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Black + ), + actions = {} + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(innerPadding) + .padding(16.dp) + .imePadding(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Text( + "Sign in with Seedphrase", + style = MaterialTheme.typography.headlineLarge, + textAlign = TextAlign.Center + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + Column( + modifier = Modifier.widthIn(max = 512.dp) + ) { + OutlinedTextField( + value = seedphrase, + onValueChange = { + seedphrase = it + error = null + }, + modifier = Modifier.fillMaxWidth(), + maxLines = 6, + placeholder = { + Text( + "Paste your 12 or 24-word seedphrase here", + color = TextMuted + ) + }, + label = { Text("Seedphrase") } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + URButton( + onClick = { + val trimmed = seedphrase.trim() + if (trimmed.isEmpty()) { + error = "Please enter a seedphrase" + return@URButton + } + val normalized = trimmed.lowercase() + .replace(Regex("\\s+"), " ") + val words = normalized.split(" ") + if (words.size != 12 && words.size != 24) { + error = "Seedphrase must be 12 or 24 words" + return@URButton + } + loginViewModel.loginWithSeedphrase( + ctx = context, + api = application?.api, + seedphrase = normalized, + onSuccess = { jwt -> + onLoginSuccess(jwt) + }, + onError = { msg -> + error = msg + } + ) + }, + enabled = seedphrase.isNotBlank(), + isProcessing = loginViewModel.seedphraseAuthInProgress + ) { buttonTextStyle -> + Text("Sign In", style = buttonTextStyle) + } + + if (error != null) { + Spacer(modifier = Modifier.height(8.dp)) + URInlineErrorText(error) + } + } + } + } +} diff --git a/app/app/src/main/java/com/bringyour/network/ui/login/OnboardingGuestModeSheet.kt b/app/app/src/main/java/com/bringyour/network/ui/login/OnboardingGuestModeSheet.kt deleted file mode 100644 index e137847a8..000000000 --- a/app/app/src/main/java/com/bringyour/network/ui/login/OnboardingGuestModeSheet.kt +++ /dev/null @@ -1,144 +0,0 @@ -package com.bringyour.network.ui.login - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -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.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.bringyour.network.ui.components.TermsCheckbox -import com.bringyour.network.ui.components.URButton -import com.bringyour.network.ui.components.URInlineErrorText -import com.bringyour.network.ui.theme.Black -import com.bringyour.network.ui.theme.TextMuted -import com.bringyour.network.ui.theme.URNetworkTheme - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun OnboardingGuestModeSheet( - isPresenting: Boolean, - setIsPresenting: (Boolean) -> Unit, - onCreateGuestNetwork: () -> Unit, - createGuestModeInProgress: Boolean = false, - errorMessage: String? = null -) { - - var termsAgreed by remember { mutableStateOf(false) } - - val sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true - ) - - if (isPresenting) { - - ModalBottomSheet( - onDismissRequest = { - if (!createGuestModeInProgress) { - setIsPresenting(false) - } - }, - sheetState = sheetState, - ) { - - Column( - modifier = Modifier - .padding(16.dp) - ) { - - Text( - "Try guest mode.", - style = MaterialTheme.typography.headlineLarge, - ) - - Text( - "Step into the internet \nas it should be.", - style = MaterialTheme.typography.headlineMedium, - color = TextMuted - ) - - Spacer(modifier = Modifier.height(32.dp)) - - TermsCheckbox( - checked = termsAgreed, - onCheckChanged = { - termsAgreed = it - }, - enabled = !createGuestModeInProgress, - ) - - Spacer(modifier = Modifier.height(48.dp)) - - URButton( - onClick = { - onCreateGuestNetwork() - - }, - borderColor = if (termsAgreed) Black else TextMuted, - enabled = termsAgreed && !createGuestModeInProgress, - isProcessing = createGuestModeInProgress - ) { buttonTextStyle -> - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - Text( - "Enter", - style = buttonTextStyle, - color = if (termsAgreed) Black else TextMuted - ) - } - } - - Spacer(modifier = Modifier.height(8.dp)) - URInlineErrorText(errorMessage) - - Spacer(modifier = Modifier.height(16.dp)) - - } - - } - - } -} - -@Preview -@Composable -private fun OnboardingGuestModeSheetPreview() { - URNetworkTheme { - - Scaffold { innerPadding -> - - Column( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) { - - OnboardingGuestModeSheet( - isPresenting = true, - setIsPresenting = {}, - onCreateGuestNetwork = {} - ) - - } - - } - - } -} diff --git a/app/app/src/main/java/com/bringyour/network/ui/login/SeedphraseDisplayScreen.kt b/app/app/src/main/java/com/bringyour/network/ui/login/SeedphraseDisplayScreen.kt new file mode 100644 index 000000000..7f694206c --- /dev/null +++ b/app/app/src/main/java/com/bringyour/network/ui/login/SeedphraseDisplayScreen.kt @@ -0,0 +1,249 @@ +package com.bringyour.network.ui.login + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.widget.Toast +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +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.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.bringyour.network.ui.components.URButton +import com.bringyour.network.ui.theme.Black +import com.bringyour.network.ui.theme.MainTintedBackgroundBase +import com.bringyour.network.ui.theme.TextMuted +import com.bringyour.network.ui.theme.Yellow + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SeedphraseDisplayScreen( + seedphrase: String, + onConfirmed: () -> Unit, + onBack: () -> Unit, +) { + val context = LocalContext.current + var showBackConfirmDialog by remember { mutableStateOf(false) } + + BackHandler { + showBackConfirmDialog = true + } + + if (showBackConfirmDialog) { + AlertDialog( + onDismissRequest = { showBackConfirmDialog = false }, + title = { Text("Save Your Seedphrase") }, + text = { Text("Are you sure? This is the ONLY time you'll see your seedphrase. Make sure you've saved it before going back.") }, + confirmButton = { + TextButton(onClick = { + showBackConfirmDialog = false + onBack() + }) { + Text("Go Back") + } + }, + dismissButton = { + TextButton(onClick = { showBackConfirmDialog = false }) { + Text("Stay") + } + } + ) + } + + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = { + Text( + "Secure Your Account", + style = MaterialTheme.typography.titleMedium + ) + }, + navigationIcon = { + IconButton(onClick = { + showBackConfirmDialog = true + }) { + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowLeft, + contentDescription = "Back" + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Black + ), + actions = {} + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(innerPadding) + .padding(16.dp) + .imePadding(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Column( + modifier = Modifier.widthIn(max = 512.dp) + ) { + Text( + "Your Seedphrase", + style = MaterialTheme.typography.headlineLarge, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + "⚠️ This is the ONLY time you'll see this.", + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold), + textAlign = TextAlign.Center, + color = Yellow, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + "Write it down and store it somewhere safe. If you lose it, you'll lose access to your account.", + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = TextMuted, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(32.dp)) + + SelectionContainer { + val words = seedphrase.split(" ").filter { it.isNotEmpty() } + + Column( + modifier = Modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = TextMuted.copy(alpha = 0.3f), + shape = RoundedCornerShape(8.dp) + ) + .padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + words.withIndex().toList().chunked(2).forEach { rowWords -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + rowWords.forEach { (wordIndex, word) -> + SeedWordChip( + number = wordIndex + 1, + word = word, + modifier = Modifier.weight(1f) + ) + } + // odd word count on the last row: keep the grid's 2 columns aligned + if (rowWords.size < 2) { + Spacer(modifier = Modifier.weight(1f)) + } + } + } + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + URButton( + onClick = { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("Seedphrase", seedphrase) + clipboard.setPrimaryClip(clip) + Toast.makeText(context, "Seedphrase copied to clipboard", Toast.LENGTH_SHORT).show() + } + ) { buttonTextStyle -> + Text("Copy to Clipboard", style = buttonTextStyle) + } + + Spacer(modifier = Modifier.height(24.dp)) + + URButton( + onClick = onConfirmed + ) { buttonTextStyle -> + Text("I've Saved My Seedphrase", style = buttonTextStyle) + } + } + } + } +} + +@Composable +private fun SeedWordChip( + number: Int, + word: String, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .background( + color = MainTintedBackgroundBase, + shape = RoundedCornerShape(6.dp) + ) + .padding(vertical = 6.dp, horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "$number.", + style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), + color = TextMuted, + textAlign = TextAlign.End, + modifier = Modifier.width(24.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + word, + style = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace), + color = Color.White, + maxLines = 1 + ) + } +} diff --git a/app/app/src/main/java/com/bringyour/network/ui/login/SwitchAccountScreen.kt b/app/app/src/main/java/com/bringyour/network/ui/login/SwitchAccountScreen.kt index ca96bed1c..95892d945 100644 --- a/app/app/src/main/java/com/bringyour/network/ui/login/SwitchAccountScreen.kt +++ b/app/app/src/main/java/com/bringyour/network/ui/login/SwitchAccountScreen.kt @@ -58,7 +58,6 @@ fun SwitchAccountScreen( setCreateGuestNetworkInProgress(true) val args = NetworkCreateArgs() args.terms = true - args.guestMode = true application.api?.networkCreate(args) { result, err -> scope.launch { diff --git a/app/app/src/main/java/com/bringyour/network/ui/profile/ProfileScreen.kt b/app/app/src/main/java/com/bringyour/network/ui/profile/ProfileScreen.kt index d2d8dbc0a..825d52f6d 100644 --- a/app/app/src/main/java/com/bringyour/network/ui/profile/ProfileScreen.kt +++ b/app/app/src/main/java/com/bringyour/network/ui/profile/ProfileScreen.kt @@ -3,6 +3,7 @@ package com.bringyour.network.ui.profile import android.widget.Toast import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -10,9 +11,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -20,9 +23,9 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf @@ -48,6 +51,8 @@ import com.bringyour.network.R import com.bringyour.network.ui.account.AccountViewModel import com.bringyour.network.ui.components.AccountSwitcher import com.bringyour.network.ui.components.LoginMode +import com.bringyour.network.ui.components.URButton +import com.bringyour.network.ui.components.URInlineErrorText import com.bringyour.network.ui.components.URTextInput import com.bringyour.network.ui.components.overlays.OverlayMode import com.bringyour.network.ui.shared.viewmodels.OverlayViewModel @@ -73,17 +78,9 @@ fun ProfileScreen( ) { val networkUser by accountViewModel.networkUser.collectAsState() - - DisposableEffect(Unit) { - val updateSuccessSub = profileViewModel.updateSuccessSub { - accountViewModel.refreshNetworkUser() - profileViewModel.setIsEditingProfile(false) - } - - onDispose { - updateSuccessSub?.close() - } - } + val isSavingNetworkName by profileViewModel.isSavingNetworkName.collectAsState() + val networkNameError by profileViewModel.networkNameError.collectAsState() + val needsNameClaim by profileViewModel.needsNameClaim.collectAsState() LaunchedEffect(networkUser) { profileViewModel.setNetworkUser(networkUser) @@ -101,13 +98,18 @@ fun ProfileScreen( isEditingProfile = profileViewModel.isEditingProfile, setIsEditingProfile = profileViewModel.setIsEditingProfile, cancelEdits = profileViewModel.cancelEdits, - updateProfile = profileViewModel.updateProfile, - isUpdating = profileViewModel.isUpdatingProfile, + needsNameClaim = needsNameClaim, + saveNetworkName = profileViewModel.saveNetworkName, + claimNetworkName = profileViewModel.claimNetworkName, + isSavingNetworkName = isSavingNetworkName, + networkNameError = networkNameError, networkNameIsValid = profileViewModel.networkNameIsValid, networkNameIsValidating = profileViewModel.isValidatingNetworkName, validateNetworkName = profileViewModel.validateNetworkName, - errorUpdatingProfile = profileViewModel.errorUpdatingProfile, - setErrorUpdatingProfile = profileViewModel.setErrorUpdatingProfile, + onSaved = { + accountViewModel.refreshNetworkUser() + profileViewModel.setIsEditingProfile(false) + }, launchOverlay = overlayViewModel.launch ) @@ -127,13 +129,15 @@ fun ProfileScreen( isEditingProfile: Boolean, setIsEditingProfile: (Boolean) -> Unit, cancelEdits: () -> Unit, - updateProfile: () -> Unit, - isUpdating: Boolean, + needsNameClaim: Boolean, + saveNetworkName: (onSuccess: () -> Unit, onError: (String) -> Unit) -> Unit, + claimNetworkName: (onSuccess: () -> Unit, onError: (String) -> Unit) -> Unit, + onSaved: () -> Unit, + isSavingNetworkName: Boolean, + networkNameError: String?, validateNetworkName: (String) -> Unit, networkNameIsValid: Boolean, networkNameIsValidating: Boolean, - errorUpdatingProfile: Boolean, - setErrorUpdatingProfile: (Boolean) -> Unit, launchOverlay: (OverlayMode) -> Unit ) { @@ -209,69 +213,92 @@ fun ProfileScreen( } Spacer(modifier = Modifier.height(64.dp)) - URTextInput( - value = networkNameTextFieldValue, - onValueChange = { - setNetworkName(it) + if (isEditingProfile) { + URTextInput( + value = networkNameTextFieldValue, + onValueChange = { + setNetworkName(it) - debounceJob?.cancel() - debounceJob = coroutineScope.launch { - delay(500L) - validateNetworkName(it.text) - } + debounceJob?.cancel() + debounceJob = coroutineScope.launch { + delay(500L) + validateNetworkName(it.text) + } - }, - enabled = isEditingProfile && !isUpdating, - label = stringResource(id = R.string.network_name_label), - isValidating = networkNameIsValidating, - isValid = networkNameIsValid, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Done - ), - ) + }, + enabled = !isSavingNetworkName, + label = stringResource(id = R.string.network_name_label), + isValidating = networkNameIsValidating, + isValid = networkNameIsValid, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done + ), + ) - // todo - temporarily remove edits until new API changes are made - // -// if (isEditingProfile) { -// -// Row { -// Text( -// stringResource(id = R.string.cancel), -// modifier = Modifier.clickable { -// cancelEdits() -// }, -// style = TextStyle( -// color = BlueMedium -// ) -// ) -// -// Spacer(modifier = Modifier.width(24.dp)) -// -// Text( -// stringResource(id = R.string.save), -// modifier = Modifier.clickable { -// updateProfile() -// }, -// style = TextStyle( -// color = BlueMedium -// ) -// ) -// } -// -// } else { -// Text( -// stringResource(id = R.string.edit_profile), -// modifier = Modifier.clickable { -// setIsEditingProfile(true) -// }, -// style = TextStyle( -// color = BlueMedium -// ) -// ) -// } + if (networkNameError != null) { + Spacer(modifier = Modifier.height(8.dp)) + URInlineErrorText(networkNameError) + } - // Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(12.dp)) + + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box(modifier = Modifier.weight(1f)) { + URButton( + onClick = { + if (needsNameClaim) { + claimNetworkName(onSaved) { } + } else { + saveNetworkName(onSaved) { } + } + }, + enabled = !isSavingNetworkName && networkNameTextFieldValue.text.isNotBlank(), + isProcessing = isSavingNetworkName + ) { buttonTextStyle -> + Text(stringResource(id = R.string.save), style = buttonTextStyle) + } + } + TextButton(onClick = cancelEdits) { + Text(stringResource(id = R.string.cancel)) + } + } + } else { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { setIsEditingProfile(true) }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + networkName, + style = MaterialTheme.typography.bodyLarge, + color = androidx.compose.ui.graphics.Color.White + ) + Icon( + Icons.Filled.Edit, + contentDescription = stringResource(id = R.string.edit_network_name), + tint = TextMuted, + modifier = Modifier.size(16.dp) + ) + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + if (needsNameClaim) { + stringResource(id = R.string.claim_network_name_hint) + } else { + stringResource(id = R.string.change_network_name_hint) + }, + style = MaterialTheme.typography.bodySmall, + color = TextMuted + ) + } + + Spacer(modifier = Modifier.height(32.dp)) URTextInput( value = TextFieldValue(""), @@ -315,21 +342,6 @@ fun ProfileScreen( } } } -// URSnackBar( -// type = SnackBarType.ERROR, -// isVisible = errorUpdatingProfile, -// onDismiss = { -// if (errorUpdatingProfile) { -// setErrorUpdatingProfile(false) -// } -// } -// ) { -// -// Column() { -// Text(stringResource(id = R.string.something_went_wrong)) -// Text(stringResource(id = R.string.please_wait)) -// } -// } } @Preview @@ -349,13 +361,15 @@ fun ProfileScreenPreview() { isEditingProfile = false, setIsEditingProfile = {}, cancelEdits = {}, - updateProfile = {}, - isUpdating = false, + needsNameClaim = false, + saveNetworkName = { _, _ -> }, + claimNetworkName = { _, _ -> }, + onSaved = {}, + isSavingNetworkName = false, + networkNameError = null, networkNameIsValid = true, networkNameIsValidating = false, validateNetworkName = {}, - errorUpdatingProfile = false, - setErrorUpdatingProfile = {}, launchOverlay = {} ) } @@ -378,13 +392,15 @@ fun ProfileScreenEditingPreview() { isEditingProfile = true, setIsEditingProfile = {}, cancelEdits = {}, - updateProfile = {}, - isUpdating = false, + needsNameClaim = false, + saveNetworkName = { _, _ -> }, + claimNetworkName = { _, _ -> }, + onSaved = {}, + isSavingNetworkName = false, + networkNameError = null, networkNameIsValid = false, networkNameIsValidating = false, validateNetworkName = {}, - errorUpdatingProfile = false, - setErrorUpdatingProfile = {}, launchOverlay = {} ) } @@ -392,7 +408,38 @@ fun ProfileScreenEditingPreview() { @Preview @Composable -fun ProfileScreenErrorUpdatingPreview() { +fun ProfileScreenClaimingPreview() { + val navController = rememberNavController() + URNetworkTheme { + ProfileScreen( + navController, + loginMode = LoginMode.Authenticated, + isSendingResetPassLink = false, + sendResetLink = {_, _, _ ->}, + userAuth = null, + networkName = "auto_generated_name_123", + networkNameTextFieldValue = TextFieldValue("auto_generated_name_123"), + setNetworkName = {}, + isEditingProfile = false, + setIsEditingProfile = {}, + cancelEdits = {}, + needsNameClaim = true, + saveNetworkName = { _, _ -> }, + claimNetworkName = { _, _ -> }, + onSaved = {}, + isSavingNetworkName = false, + networkNameError = null, + networkNameIsValid = true, + networkNameIsValidating = false, + validateNetworkName = {}, + launchOverlay = {} + ) + } +} + +@Preview +@Composable +fun ProfileScreenErrorPreview() { val navController = rememberNavController() URNetworkTheme { ProfileScreen( @@ -404,17 +451,19 @@ fun ProfileScreenErrorUpdatingPreview() { networkName = "my_network", networkNameTextFieldValue = TextFieldValue("my_network"), setNetworkName = {}, - isEditingProfile = false, + isEditingProfile = true, setIsEditingProfile = {}, cancelEdits = {}, - updateProfile = {}, - isUpdating = false, + needsNameClaim = false, + saveNetworkName = { _, _ -> }, + claimNetworkName = { _, _ -> }, + onSaved = {}, + isSavingNetworkName = false, + networkNameError = "That name is already taken", networkNameIsValid = true, networkNameIsValidating = false, validateNetworkName = {}, - errorUpdatingProfile = true, - setErrorUpdatingProfile = {}, launchOverlay = {} ) } -} \ No newline at end of file +} diff --git a/app/app/src/main/java/com/bringyour/network/ui/profile/ProfileViewModel.kt b/app/app/src/main/java/com/bringyour/network/ui/profile/ProfileViewModel.kt index 1e6c8e2b3..36238b7c1 100644 --- a/app/app/src/main/java/com/bringyour/network/ui/profile/ProfileViewModel.kt +++ b/app/app/src/main/java/com/bringyour/network/ui/profile/ProfileViewModel.kt @@ -6,14 +6,17 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.bringyour.sdk.Sdk -import com.bringyour.sdk.NetworkNameValidationViewController -import com.bringyour.sdk.NetworkUser -import com.bringyour.sdk.NetworkUserViewController -import com.bringyour.sdk.Sub import com.bringyour.network.DeviceManager import com.bringyour.network.NetworkSpaceManagerProvider +import com.bringyour.network.ui.settings.authTypesContains +import com.bringyour.sdk.ChangeNetworkNameArgs +import com.bringyour.sdk.ClaimNetworkNameArgs +import com.bringyour.sdk.NetworkNameValidationViewController +import com.bringyour.sdk.NetworkUser +import com.bringyour.sdk.Sdk import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -24,9 +27,7 @@ class ProfileViewModel @Inject constructor( networkSpaceManagerProvider: NetworkSpaceManagerProvider ): ViewModel() { - private var networkUserVc: NetworkUserViewController? = null private var networkNameValidationVc: NetworkNameValidationViewController? = null - private val subs = mutableListOf() var isEditingProfile by mutableStateOf(false) private set @@ -38,17 +39,6 @@ class ProfileViewModel @Inject constructor( isValidatingNetworkName = iv } - var isUpdatingProfile by mutableStateOf(false) - private set - - // todo - make this more robust with error messages - var errorUpdatingProfile by mutableStateOf(false) - private set - - val setErrorUpdatingProfile: (Boolean) -> Unit = { errExists -> - errorUpdatingProfile = errExists - } - private var networkUser: NetworkUser? = null var networkNameTextFieldValue by mutableStateOf(TextFieldValue()) @@ -61,16 +51,14 @@ class ProfileViewModel @Inject constructor( networkNameIsValid = isValid } - var usernameIsValid by mutableStateOf(false) - private set + private val _isSavingNetworkName = MutableStateFlow(false) + val isSavingNetworkName: StateFlow = _isSavingNetworkName - private val addIsUpdatingListener = { - networkUserVc?.addIsUpdatingListener { isUpdating -> - viewModelScope.launch { - isUpdatingProfile = isUpdating - } - }?.let { subs.add(it) } - } + private val _networkNameError = MutableStateFlow(null) + val networkNameError: StateFlow = _networkNameError + + private val _needsNameClaim = MutableStateFlow(false) + val needsNameClaim: StateFlow = _needsNameClaim val setNetworkNameTextFieldValue: (TextFieldValue) -> Unit = { networkNameTextFieldValue = it @@ -78,6 +66,9 @@ class ProfileViewModel @Inject constructor( val setIsEditingProfile: (Boolean) -> Unit = { isEditingProfile = it + if (it) { + _networkNameError.value = null + } } val validateNetworkName: (String) -> Unit = { nn -> @@ -108,80 +99,111 @@ class ProfileViewModel @Inject constructor( } - val updateProfile: () -> Unit = { - if (networkNameIsValid && usernameIsValid) { - setIsEditingProfile(false) - networkUserVc?.updateNetworkUser(networkNameTextFieldValue.text) - } else { - setErrorUpdatingProfile(true) + val saveNetworkName: ( + onSuccess: () -> Unit, + onError: (String) -> Unit + ) -> Unit = { onSuccess, onError -> + + val name = networkNameTextFieldValue.text + _isSavingNetworkName.value = true + _networkNameError.value = null + + val args = ChangeNetworkNameArgs() + args.newName = name + + deviceManager.device?.api?.changeNetworkName(args) { result, err -> + viewModelScope.launch { + _isSavingNetworkName.value = false + + if (err != null) { + val msg = err.message ?: "Failed to change network name" + _networkNameError.value = msg + onError(msg) + } else if (result?.error != null) { + val msg = result.error.message ?: "Failed to change network name" + _networkNameError.value = msg + onError(msg) + } else { + deviceManager.device?.refreshToken(0) + onSuccess() + } + } + } ?: run { + _isSavingNetworkName.value = false + val msg = "Unable to connect. Please try again." + _networkNameError.value = msg + onError(msg) + } + } + + val claimNetworkName: ( + onSuccess: () -> Unit, + onError: (String) -> Unit + ) -> Unit = { onSuccess, onError -> + + val name = networkNameTextFieldValue.text + _isSavingNetworkName.value = true + _networkNameError.value = null + + val args = ClaimNetworkNameArgs() + args.newName = name + + deviceManager.device?.api?.claimNetworkName(args) { result, err -> + viewModelScope.launch { + _isSavingNetworkName.value = false + + if (err != null) { + val msg = err.message ?: "Failed to claim network name" + _networkNameError.value = msg + onError(msg) + } else if (result?.error != null) { + val msg = result.error.message ?: "Failed to claim network name" + _networkNameError.value = msg + onError(msg) + } else { + deviceManager.device?.refreshToken(0) + onSuccess() + } + } + } ?: run { + _isSavingNetworkName.value = false + val msg = "Unable to connect. Please try again." + _networkNameError.value = msg + onError(msg) } } val cancelEdits = { setNetworkNameTextFieldValue(TextFieldValue(networkUser?.networkName ?: "")) + _networkNameError.value = null setIsEditingProfile(false) } val setNetworkUser: (NetworkUser?) -> Unit = { nu -> networkUser = nu setNetworkNameTextFieldValue(TextFieldValue(nu?.networkName ?: "")) - } - - private var updateSuccessListener: (() -> Unit)? = null - private var updateSuccessSubInstance: Sub? = null - - val updateSuccessSub: (() -> Unit) -> Sub? = { callback -> - updateSuccessSubInstance?.let { oldSub -> - subs.remove(oldSub) - oldSub.close() - } - updateSuccessListener = callback - val sub = networkUserVc?.addNetworkUserUpdateSuccessListener { - updateSuccessListener?.invoke() - } - updateSuccessSubInstance = sub - sub?.let { subs.add(it) } - sub - } - - val addUpdateErrorListener = { - networkUserVc?.addNetworkUserUpdateErrorListener { - viewModelScope.launch { - cancelEdits() - setErrorUpdatingProfile(true) - } - }?.let { subs.add(it) } + _needsNameClaim.value = nu?.let { + val hasIdentityMethod = authTypesContains(it.authTypes, "email") || + authTypesContains(it.authTypes, "phone") || + authTypesContains(it.authTypes, "google") || + authTypesContains(it.authTypes, "apple") || + authTypesContains(it.authTypes, "solana") + !hasIdentityMethod + } ?: false } init { - networkUserVc = deviceManager.device?.openNetworkUserViewController() - networkNameValidationVc = Sdk.newNetworkNameValidationViewController( networkSpaceManagerProvider.getNetworkSpace()?.api ) - addIsUpdatingListener() - - addUpdateErrorListener() - - networkUserVc?.start() - } override fun onCleared() { super.onCleared() - subs.forEach { it.close() } - subs.clear() - - networkUserVc?.let { - deviceManager.device?.closeViewController(it) - } - networkNameValidationVc?.close() - - updateSuccessListener = null } -} \ No newline at end of file +} diff --git a/app/app/src/main/java/com/bringyour/network/ui/settings/AddAuthMethodSheet.kt b/app/app/src/main/java/com/bringyour/network/ui/settings/AddAuthMethodSheet.kt new file mode 100644 index 000000000..ea9ae4184 --- /dev/null +++ b/app/app/src/main/java/com/bringyour/network/ui/settings/AddAuthMethodSheet.kt @@ -0,0 +1,277 @@ +package com.bringyour.network.ui.settings + +import android.util.Log +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import com.bringyour.network.MainApplication +import com.bringyour.network.ui.components.ButtonStyle +import com.bringyour.network.ui.components.URButton +import com.bringyour.network.ui.components.URInlineErrorText +import com.bringyour.network.ui.components.URTextInput +import com.bringyour.network.ui.login.SolanaChallengeSignResult +import com.bringyour.network.ui.login.requestAndSignSolanaChallenge +import com.bringyour.network.ui.theme.TextMuted +import com.bringyour.sdk.AddAuthArgs +import com.bringyour.sdk.WalletAuthArgs +import com.solana.mobilewalletadapter.clientlib.ActivityResultSender +import kotlinx.coroutines.launch + +private enum class AddAuthMethod { GOOGLE, WALLET, EMAIL } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddAuthMethodSheet( + visible: Boolean, + onDismiss: () -> Unit, + showGoogleOption: Boolean, + activityResultSender: ActivityResultSender?, + isAddingAuth: Boolean, + addAuth: (AddAuthArgs, onSuccess: () -> Unit, onError: (String) -> Unit) -> Unit, + onAdded: () -> Unit, +) { + if (!visible) { + return + } + + val context = LocalContext.current + val scope = rememberCoroutineScope() + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + val methods = remember(showGoogleOption) { + if (showGoogleOption) { + listOf(AddAuthMethod.GOOGLE, AddAuthMethod.WALLET, AddAuthMethod.EMAIL) + } else { + listOf(AddAuthMethod.WALLET, AddAuthMethod.EMAIL) + } + } + var selectedMethod by remember(methods) { mutableStateOf(methods.first()) } + + var email by remember { mutableStateOf(TextFieldValue("")) } + var password by remember { mutableStateOf(TextFieldValue("")) } + var addError by remember { mutableStateOf(null) } + + var walletConnectJob by remember { mutableStateOf(null) } + var isConnectingWallet by remember { mutableStateOf(false) } + + DisposableEffect(Unit) { + onDispose { + walletConnectJob?.cancel() + } + } + + val formValid = when (selectedMethod) { + AddAuthMethod.EMAIL -> email.text.isNotBlank() && password.text.length >= 12 + else -> true + } + + val onAddClick: () -> Unit = { + addError = null + when (selectedMethod) { + AddAuthMethod.EMAIL -> { + val args = AddAuthArgs() + args.userAuth = email.text + args.password = password.text + addAuth( + args, + { + Toast.makeText(context, "Sign-in method added successfully", Toast.LENGTH_SHORT).show() + onAdded() + }, + { msg -> addError = msg } + ) + } + else -> { /* Google/Wallet complete on their own callback, no explicit Add click */ } + } + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + "Add a sign-in method", + style = MaterialTheme.typography.headlineSmall + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + "Link another way to sign in to your account.", + style = MaterialTheme.typography.bodyMedium, + color = TextMuted + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + methods.forEach { method -> + // URButton has no `modifier` parameter (checked against its + // real signature in URButton.kt) — wrap it in a weighted Box + // instead of trying to pass modifier through to URButton itself. + androidx.compose.foundation.layout.Box(modifier = Modifier.weight(1f)) { + URButton( + style = if (method == selectedMethod) ButtonStyle.PRIMARY else ButtonStyle.SECONDARY, + onClick = { + selectedMethod = method + addError = null + } + ) { buttonTextStyle -> + Text( + when (method) { + AddAuthMethod.GOOGLE -> "Google" + AddAuthMethod.WALLET -> "Wallet" + AddAuthMethod.EMAIL -> "Email" + }, + style = buttonTextStyle + ) + } + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + when (selectedMethod) { + AddAuthMethod.GOOGLE -> { + // Google Sign-In is per-flavor code (com.google.android.gms.* + // is not on github's classpath) -- every flavor's source set + // provides its own GoogleAddAuthButton with this exact + // signature (real impl on google/solana_dapp/ethos_dapp, + // no-op stub on ungoogle/github). + GoogleAddAuthButton( + addAuth = addAuth, + isAddingAuth = isAddingAuth, + onAdded = onAdded, + onError = { msg -> addError = msg } + ) + } + AddAuthMethod.WALLET -> { + Text( + "Connect a Solana wallet to add it as a sign-in method.", + style = MaterialTheme.typography.bodyMedium, + color = TextMuted + ) + Spacer(modifier = Modifier.height(12.dp)) + URButton( + onClick = { + walletConnectJob = scope.launch { + activityResultSender?.let { sender -> + val api = (context.applicationContext as? MainApplication)?.api + if (api == null) { + addError = "Error connecting to wallet" + return@launch + } + isConnectingWallet = true + when (val result = requestAndSignSolanaChallenge(sender, api)) { + is SolanaChallengeSignResult.Success -> { + val walletAuth = WalletAuthArgs() + walletAuth.publicKey = result.signed.publicKey + walletAuth.signature = result.signed.signature + walletAuth.message = result.signed.message + walletAuth.blockchain = "solana" + val args = AddAuthArgs() + args.walletAuth = walletAuth + addAuth( + args, + { + Toast.makeText(context, "Wallet sign-in method added", Toast.LENGTH_SHORT).show() + onAdded() + }, + { msg -> addError = msg } + ) + } + is SolanaChallengeSignResult.NoWalletFound -> { + addError = "No compatible wallet app found on this device." + } + is SolanaChallengeSignResult.Failure -> { + Log.i("AddAuthMethodSheet", "Error connecting to wallet: ${result.error}") + addError = "Error connecting to wallet" + } + } + isConnectingWallet = false + } + } + }, + enabled = !isAddingAuth && !isConnectingWallet, + isProcessing = isConnectingWallet + ) { buttonTextStyle -> + Text("Connect Wallet", style = buttonTextStyle) + } + } + AddAuthMethod.EMAIL -> { + URTextInput( + value = email, + onValueChange = { email = it }, + label = "Email", + placeholder = "your@email.com", + keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(keyboardType = KeyboardType.Email) + ) + Spacer(modifier = Modifier.height(12.dp)) + URTextInput( + value = password, + onValueChange = { password = it }, + label = "Password", + placeholder = "Enter a password", + isPassword = true + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + "Password must be at least 12 characters", + style = MaterialTheme.typography.bodySmall, + color = TextMuted + ) + } + } + + if (addError != null) { + Spacer(modifier = Modifier.height(12.dp)) + URInlineErrorText(addError) + } + + if (selectedMethod == AddAuthMethod.EMAIL) { + Spacer(modifier = Modifier.height(16.dp)) + URButton( + onClick = onAddClick, + enabled = !isAddingAuth && formValid, + isProcessing = isAddingAuth + ) { buttonTextStyle -> + Text("Add Sign-In Method", style = buttonTextStyle) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + } + } +} diff --git a/app/app/src/main/java/com/bringyour/network/ui/settings/AuthMethods.kt b/app/app/src/main/java/com/bringyour/network/ui/settings/AuthMethods.kt new file mode 100644 index 000000000..c4db50ff7 --- /dev/null +++ b/app/app/src/main/java/com/bringyour/network/ui/settings/AuthMethods.kt @@ -0,0 +1,42 @@ +package com.bringyour.network.ui.settings + +import com.bringyour.network.utils.sdkStringListToList +import com.bringyour.sdk.NetworkUser +import com.bringyour.sdk.StringList + +fun authTypesContains(authTypes: StringList?, method: String): Boolean { + return sdkStringListToList(authTypes).contains(method) +} + +fun parseAuthMethods(networkUser: NetworkUser): List { + val fromAuthTypes = sdkStringListToList(networkUser.authTypes).filter { it.isNotEmpty() } + if (fromAuthTypes.isNotEmpty()) { + return fromAuthTypes + } + + // Fallback for old server: read single authType + userAuth + val methods = mutableListOf() + if (networkUser.authType.isNotEmpty()) { + methods.add(networkUser.authType) + } + val userAuth = networkUser.userAuth + if (userAuth.isNotEmpty()) { + val methodLabel = if (userAuth.contains("@")) "email" else userAuth + if (!methods.contains(methodLabel)) { + methods.add(methodLabel) + } + } + + return methods +} + +fun methodDisplayName(method: String): String { + return when (method) { + "email" -> "Email" + "google" -> "Google" + "apple" -> "Apple" + "solana" -> "Solana Wallet" + "seedphrase" -> "Seedphrase" + else -> method.replaceFirstChar { it.uppercase() } + } +} diff --git a/app/app/src/main/java/com/bringyour/network/ui/settings/SettingsScreen.kt b/app/app/src/main/java/com/bringyour/network/ui/settings/SettingsScreen.kt index 42b6ae274..0734003ee 100644 --- a/app/app/src/main/java/com/bringyour/network/ui/settings/SettingsScreen.kt +++ b/app/app/src/main/java/com/bringyour/network/ui/settings/SettingsScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -89,8 +90,10 @@ import androidx.compose.ui.text.input.TextFieldValue import com.bringyour.network.ui.components.URTextInputLabel import com.bringyour.network.ui.components.URTextInput import com.bringyour.network.ui.components.URDialog +import com.bringyour.network.ui.components.URInlineErrorText import com.bringyour.network.ui.theme.Black import com.bringyour.network.ui.theme.BlueLight +import com.bringyour.network.ui.theme.TextDanger import com.bringyour.network.ui.theme.TextMuted import com.bringyour.network.ui.theme.TopBarTitleTextStyle import com.bringyour.network.ui.theme.URNetworkTheme @@ -125,6 +128,7 @@ import com.bringyour.network.TAG import com.bringyour.network.ui.components.CopyReferralCode import com.bringyour.network.ui.components.ProvideCellPicker import com.bringyour.network.ui.components.ProvideControlModePicker +import com.bringyour.network.ui.login.SeedphraseDisplayScreen @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -147,6 +151,32 @@ fun SettingsScreen( val isPresentingAuthCodeDialog = settingsViewModel.isPresentingAuthCodeDialog.collectAsState().value val authCode = settingsViewModel.authCode.collectAsState().value + val networkUser = accountViewModel.networkUser.collectAsState().value + val authMethods = networkUser?.let { parseAuthMethods(it) } ?: emptyList() + val isAddingAuth = settingsViewModel.isAddingAuth.collectAsState().value + val isRemovingAuth = settingsViewModel.isRemovingAuth.collectAsState().value + + var presentAddAuthSheet by remember { mutableStateOf(false) } + var pendingRemoveMethod by remember { mutableStateOf(null) } + var removeAuthError by remember { mutableStateOf(null) } + + val generatedSeedphrase = settingsViewModel.generatedSeedphrase.collectAsState().value + val isGeneratingSeedphrase = settingsViewModel.isGeneratingSeedphrase.collectAsState().value + val isRegeneratingSeedphrase = settingsViewModel.isRegeneratingSeedphrase.collectAsState().value + + var pendingSeedphraseAction by remember { mutableStateOf(null) } + var seedphraseActionError by remember { mutableStateOf(null) } + + // Close the confirm dialog only once the request actually succeeds (mirrors + // the remove-sign-in-method dialog, which closes on its onSuccess callback, + // not eagerly on tap) -- otherwise an async error has nowhere to render, + // since seedphraseActionError lives inside this dialog's own content. + LaunchedEffect(generatedSeedphrase) { + if (generatedSeedphrase != null) { + pendingSeedphraseAction = null + } + } + val scope = rememberCoroutineScope() val updateReferralNetworkSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -274,7 +304,14 @@ fun SettingsScreen( provideIndicatorColor = settingsViewModel.provideIndicatorColor, provideIndicatorRingColor = settingsViewModel.provideIndicatorRingColor, stripePortalUrl = settingsViewModel.stripePortalUrl.collectAsState().value, - totalReferrals = totalReferrals + totalReferrals = totalReferrals, + authMethods = authMethods, + onRemoveAuthMethod = { method -> pendingRemoveMethod = method }, + onAddAuthMethodClick = { presentAddAuthSheet = true }, + hasSeedphrase = authTypesContains(networkUser?.authTypes, "seedphrase"), + isGeneratingSeedphrase = isGeneratingSeedphrase, + isRegeneratingSeedphrase = isRegeneratingSeedphrase, + onSeedphraseActionClick = { action -> pendingSeedphraseAction = action }, ) if (isPresentingRenameDevice) { @@ -359,11 +396,129 @@ fun SettingsScreen( ) } + AddAuthMethodSheet( + visible = presentAddAuthSheet, + onDismiss = { presentAddAuthSheet = false }, + showGoogleOption = BuildConfig.BRINGYOUR_BUNDLE_SSO_GOOGLE, + activityResultSender = activityResultSender, + isAddingAuth = isAddingAuth, + addAuth = settingsViewModel.addAuth, + onAdded = { + presentAddAuthSheet = false + accountViewModel.refreshNetworkUser() + } + ) + + URDialog( + visible = pendingRemoveMethod != null, + onDismiss = { + pendingRemoveMethod = null + removeAuthError = null + } + ) { + Column(modifier = Modifier.fillMaxWidth()) { + Text( + "Remove ${pendingRemoveMethod?.let { methodDisplayName(it) } ?: ""}?", + style = MaterialTheme.typography.headlineSmall, + color = Color.White + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + "You won't be able to sign in with this method anymore.", + style = MaterialTheme.typography.bodyMedium, + color = Color.White + ) + if (removeAuthError != null) { + Spacer(modifier = Modifier.height(8.dp)) + URInlineErrorText(removeAuthError) + } + Spacer(modifier = Modifier.height(16.dp)) + URButton( + onClick = { + val method = pendingRemoveMethod ?: return@URButton + removeAuthError = null + settingsViewModel.removeAuth( + method, + { + pendingRemoveMethod = null + accountViewModel.refreshNetworkUser() + }, + { msg -> removeAuthError = msg } + ) + }, + enabled = !isRemovingAuth, + isProcessing = isRemovingAuth + ) { buttonTextStyle -> + Text("Remove", style = buttonTextStyle) + } + } + } + + URDialog( + visible = pendingSeedphraseAction != null, + onDismiss = { + pendingSeedphraseAction = null + seedphraseActionError = null + } + ) { + Column(modifier = Modifier.fillMaxWidth()) { + val isRegenerate = pendingSeedphraseAction == SeedphraseAction.REGENERATE + Text( + if (isRegenerate) "Regenerate your seedphrase?" else "Generate a seedphrase?", + style = MaterialTheme.typography.headlineSmall, + color = Color.White + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + if (isRegenerate) { + "Your current seedphrase will stop working. You'll be shown a new one to save." + } else { + "A seedphrase lets you recover your account if you lose access. You'll be shown it once." + }, + style = MaterialTheme.typography.bodyMedium, + color = Color.White + ) + if (seedphraseActionError != null) { + Spacer(modifier = Modifier.height(8.dp)) + URInlineErrorText(seedphraseActionError) + } + Spacer(modifier = Modifier.height(16.dp)) + URButton( + onClick = { + seedphraseActionError = null + val onError: (String) -> Unit = { msg -> seedphraseActionError = msg } + if (isRegenerate) { + settingsViewModel.regenerateSeedphrase(onError) + } else { + settingsViewModel.generateSeedphrase(onError) + } + }, + enabled = !isGeneratingSeedphrase && !isRegeneratingSeedphrase, + isProcessing = isGeneratingSeedphrase || isRegeneratingSeedphrase + ) { buttonTextStyle -> + Text(if (isRegenerate) "Regenerate" else "Generate", style = buttonTextStyle) + } + } + } + + if (generatedSeedphrase != null) { + SeedphraseDisplayScreen( + seedphrase = generatedSeedphrase, + onConfirmed = { + settingsViewModel.dismissSeedphraseDisplay() + accountViewModel.refreshNetworkUser() + }, + onBack = { + settingsViewModel.dismissSeedphraseDisplay() + } + ) + } + } @OptIn(ExperimentalMaterial3Api::class) @Composable -fun SettingsScreen( +private fun SettingsScreen( navController: NavController, clientId: String, currentPlan: Plan, @@ -401,7 +556,14 @@ fun SettingsScreen( provideIndicatorColor: Color, provideIndicatorRingColor: Color? = null, stripePortalUrl: String?, - totalReferrals: Long = 0L + totalReferrals: Long = 0L, + authMethods: List, + onRemoveAuthMethod: (String) -> Unit, + onAddAuthMethodClick: () -> Unit, + hasSeedphrase: Boolean, + isGeneratingSeedphrase: Boolean, + isRegeneratingSeedphrase: Boolean, + onSeedphraseActionClick: (SeedphraseAction) -> Unit, ) { val context = LocalContext.current @@ -614,6 +776,86 @@ fun SettingsScreen( } + Spacer(modifier = Modifier.height(32.dp)) + + /** + * Seedphrase + */ + URTextInputLabel(text = "Seedphrase") + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 6.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + if (hasSeedphrase) "Regenerate Seedphrase" else "Generate Seedphrase", + style = MaterialTheme.typography.bodyMedium, + color = Color.White + ) + + Row(verticalAlignment = Alignment.CenterVertically) { + if ((hasSeedphrase && isRegeneratingSeedphrase) || (!hasSeedphrase && isGeneratingSeedphrase)) { + CircularProgressIndicator(modifier = Modifier.width(16.dp)) + } else { + TextButton(onClick = { + onSeedphraseActionClick( + if (hasSeedphrase) SeedphraseAction.REGENERATE else SeedphraseAction.GENERATE + ) + }) { + Text( + if (hasSeedphrase) "Regenerate" else "Generate", + color = BlueMedium + ) + } + } + } + } + Text( + "A seedphrase lets you recover your account if you lose access.", + style = MaterialTheme.typography.bodySmall, + color = TextMuted + ) + + Spacer(modifier = Modifier.height(32.dp)) + + /** + * Sign-In Methods + */ + URTextInputLabel(text = "Sign-In Methods") + + authMethods.forEach { method -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 6.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + methodDisplayName(method), + style = MaterialTheme.typography.bodyMedium, + color = Color.White + ) + + TextButton(onClick = { onRemoveAuthMethod(method) }) { + Text( + "Remove", + color = TextDanger + ) + } + } + } + + TextButton(onClick = onAddAuthMethodClick) { + Text( + "Add sign-in method", + color = BlueMedium + ) + } + Spacer(modifier = Modifier.height(32.dp)) /** @@ -1300,7 +1542,14 @@ private fun SettingsScreenPreview() { isCreatingAuthCode = false, setDisplayAuthCodeDialog = {}, provideIndicatorColor = Green, - stripePortalUrl = null + stripePortalUrl = null, + authMethods = listOf("email"), + onRemoveAuthMethod = {}, + onAddAuthMethodClick = {}, + hasSeedphrase = false, + isGeneratingSeedphrase = false, + isRegeneratingSeedphrase = false, + onSeedphraseActionClick = {}, ) } } @@ -1375,7 +1624,14 @@ private fun SettingsScreenSupporterPreview() { isCreatingAuthCode = false, setDisplayAuthCodeDialog = {}, provideIndicatorColor = Green, - stripePortalUrl = null + stripePortalUrl = null, + authMethods = listOf("email"), + onRemoveAuthMethod = {}, + onAddAuthMethodClick = {}, + hasSeedphrase = false, + isGeneratingSeedphrase = false, + isRegeneratingSeedphrase = false, + onSeedphraseActionClick = {}, ) } } @@ -1418,7 +1674,14 @@ private fun SettingsScreenNotificationsDisabledPreview() { isCreatingAuthCode = false, setDisplayAuthCodeDialog = {}, provideIndicatorColor = Green, - stripePortalUrl = null + stripePortalUrl = null, + authMethods = listOf("email"), + onRemoveAuthMethod = {}, + onAddAuthMethodClick = {}, + hasSeedphrase = false, + isGeneratingSeedphrase = false, + isRegeneratingSeedphrase = false, + onSeedphraseActionClick = {}, ) } } @@ -1461,7 +1724,14 @@ private fun SettingsScreenNotificationsAllowedPreview() { isCreatingAuthCode = false, setDisplayAuthCodeDialog = {}, provideIndicatorColor = Green, - stripePortalUrl = null + stripePortalUrl = null, + authMethods = listOf("email"), + onRemoveAuthMethod = {}, + onAddAuthMethodClick = {}, + hasSeedphrase = false, + isGeneratingSeedphrase = false, + isRegeneratingSeedphrase = false, + onSeedphraseActionClick = {}, ) } } @@ -1504,7 +1774,16 @@ private fun SettingsScreenDeleteAccountDialogPreview() { isCreatingAuthCode = false, setDisplayAuthCodeDialog = {}, provideIndicatorColor = Green, - stripePortalUrl = null + stripePortalUrl = null, + authMethods = listOf("email"), + onRemoveAuthMethod = {}, + onAddAuthMethodClick = {}, + hasSeedphrase = false, + isGeneratingSeedphrase = false, + isRegeneratingSeedphrase = false, + onSeedphraseActionClick = {}, ) } } + +private enum class SeedphraseAction { GENERATE, REGENERATE } diff --git a/app/app/src/main/java/com/bringyour/network/ui/settings/SettingsViewModel.kt b/app/app/src/main/java/com/bringyour/network/ui/settings/SettingsViewModel.kt index 637a6ef62..8b4fdce90 100644 --- a/app/app/src/main/java/com/bringyour/network/ui/settings/SettingsViewModel.kt +++ b/app/app/src/main/java/com/bringyour/network/ui/settings/SettingsViewModel.kt @@ -22,8 +22,12 @@ import com.bringyour.network.ui.shared.models.ProvideNetworkMode import com.bringyour.network.ui.theme.Green import com.bringyour.network.ui.theme.Red import com.bringyour.network.ui.theme.Yellow +import com.bringyour.sdk.AddAuthArgs import com.bringyour.sdk.AuthCodeCreateArgs +import com.bringyour.sdk.GenerateSeedphraseArgs import com.bringyour.sdk.ReferralNetwork +import com.bringyour.sdk.RegenerateSeedphraseArgs +import com.bringyour.sdk.RemoveAuthArgs import com.bringyour.sdk.Sdk import com.bringyour.sdk.StripeCreateCustomerPortalArgs import com.bringyour.sdk.Sub @@ -291,6 +295,124 @@ class SettingsViewModel @Inject constructor( } } + private val _isAddingAuth = MutableStateFlow(false) + val isAddingAuth: StateFlow = _isAddingAuth + + val addAuth: ( + args: AddAuthArgs, + onSuccess: () -> Unit, + onError: (String) -> Unit + ) -> Unit = { args, onSuccess, onError -> + + _isAddingAuth.value = true + + deviceManager.device?.api?.addAuth(args) { result, err -> + viewModelScope.launch { + _isAddingAuth.value = false + + if (err != null) { + onError(err.message ?: "Failed to add sign-in method") + } else if (result?.error != null) { + onError(result.error.message ?: "Failed to add sign-in method") + } else { + onSuccess() + } + } + } ?: run { + _isAddingAuth.value = false + onError("Unable to connect. Please try again.") + } + } + + private val _isRemovingAuth = MutableStateFlow(false) + val isRemovingAuth: StateFlow = _isRemovingAuth + + val removeAuth: ( + authType: String, + onSuccess: () -> Unit, + onError: (String) -> Unit + ) -> Unit = { authType, onSuccess, onError -> + + _isRemovingAuth.value = true + + val args = RemoveAuthArgs() + args.authType = authType + + deviceManager.device?.api?.removeAuth(args) { result, err -> + viewModelScope.launch { + _isRemovingAuth.value = false + + if (err != null) { + onError(err.message ?: "Failed to remove sign-in method") + } else if (result?.error != null) { + onError(result.error.message ?: "Failed to remove sign-in method") + } else { + onSuccess() + } + } + } ?: run { + _isRemovingAuth.value = false + onError("Unable to connect. Please try again.") + } + } + + private val _generatedSeedphrase = MutableStateFlow(null) + val generatedSeedphrase: StateFlow = _generatedSeedphrase + + private val _isGeneratingSeedphrase = MutableStateFlow(false) + val isGeneratingSeedphrase: StateFlow = _isGeneratingSeedphrase + + private val _isRegeneratingSeedphrase = MutableStateFlow(false) + val isRegeneratingSeedphrase: StateFlow = _isRegeneratingSeedphrase + + val generateSeedphrase: (onError: (String) -> Unit) -> Unit = { onError -> + + _isGeneratingSeedphrase.value = true + + deviceManager.device?.api?.generateSeedphrase(GenerateSeedphraseArgs()) { result, err -> + viewModelScope.launch { + _isGeneratingSeedphrase.value = false + + if (err != null) { + onError(err.message ?: "Failed to generate seedphrase") + } else if (result?.error != null) { + onError(result.error.message ?: "Failed to generate seedphrase") + } else if (result != null) { + _generatedSeedphrase.value = result.seedphrase + } + } + } ?: run { + _isGeneratingSeedphrase.value = false + onError("Unable to connect. Please try again.") + } + } + + val regenerateSeedphrase: (onError: (String) -> Unit) -> Unit = { onError -> + + _isRegeneratingSeedphrase.value = true + + deviceManager.device?.api?.regenerateSeedphrase(RegenerateSeedphraseArgs()) { result, err -> + viewModelScope.launch { + _isRegeneratingSeedphrase.value = false + + if (err != null) { + onError(err.message ?: "Failed to regenerate seedphrase") + } else if (result?.error != null) { + onError(result.error.message ?: "Failed to regenerate seedphrase") + } else if (result != null) { + _generatedSeedphrase.value = result.seedphrase + } + } + } ?: run { + _isRegeneratingSeedphrase.value = false + onError("Unable to connect. Please try again.") + } + } + + val dismissSeedphraseDisplay: () -> Unit = { + _generatedSeedphrase.value = null + } + val toggleRouteLocal: () -> Unit = { val currentRouteLocal = routeLocal.value deviceManager.device?.routeLocal = !currentRouteLocal diff --git a/app/app/src/main/res/drawable-land-xhdpi/overlay_guest_mode_bg.webp b/app/app/src/main/res/drawable-land-xhdpi/overlay_guest_mode_bg.webp deleted file mode 100644 index 4aa1604e6..000000000 Binary files a/app/app/src/main/res/drawable-land-xhdpi/overlay_guest_mode_bg.webp and /dev/null differ diff --git a/app/app/src/main/res/drawable-land-xhdpi/overlay_guest_onboarding_bg.webp b/app/app/src/main/res/drawable-land-xhdpi/overlay_guest_onboarding_bg.webp deleted file mode 100644 index 07484f7dc..000000000 Binary files a/app/app/src/main/res/drawable-land-xhdpi/overlay_guest_onboarding_bg.webp and /dev/null differ diff --git a/app/app/src/main/res/drawable-land/overlay_guest_mode_bg.webp b/app/app/src/main/res/drawable-land/overlay_guest_mode_bg.webp deleted file mode 100644 index 56c398207..000000000 Binary files a/app/app/src/main/res/drawable-land/overlay_guest_mode_bg.webp and /dev/null differ diff --git a/app/app/src/main/res/drawable-land/overlay_guest_onboarding_bg.webp b/app/app/src/main/res/drawable-land/overlay_guest_onboarding_bg.webp deleted file mode 100644 index bb18109e2..000000000 Binary files a/app/app/src/main/res/drawable-land/overlay_guest_onboarding_bg.webp and /dev/null differ diff --git a/app/app/src/main/res/drawable-xhdpi/overlay_guest_mode_bg.webp b/app/app/src/main/res/drawable-xhdpi/overlay_guest_mode_bg.webp deleted file mode 100644 index d3ddf9a4a..000000000 Binary files a/app/app/src/main/res/drawable-xhdpi/overlay_guest_mode_bg.webp and /dev/null differ diff --git a/app/app/src/main/res/drawable-xhdpi/overlay_guest_onboarding_bg.webp b/app/app/src/main/res/drawable-xhdpi/overlay_guest_onboarding_bg.webp deleted file mode 100644 index 99fdbae92..000000000 Binary files a/app/app/src/main/res/drawable-xhdpi/overlay_guest_onboarding_bg.webp and /dev/null differ diff --git a/app/app/src/main/res/drawable/overlay_guest_mode_bg.webp b/app/app/src/main/res/drawable/overlay_guest_mode_bg.webp deleted file mode 100644 index c7709c42a..000000000 Binary files a/app/app/src/main/res/drawable/overlay_guest_mode_bg.webp and /dev/null differ diff --git a/app/app/src/main/res/drawable/overlay_guest_onboarding_bg.webp b/app/app/src/main/res/drawable/overlay_guest_onboarding_bg.webp deleted file mode 100644 index cc1196ee6..000000000 Binary files a/app/app/src/main/res/drawable/overlay_guest_onboarding_bg.webp and /dev/null differ diff --git a/app/app/src/main/res/values/strings.xml b/app/app/src/main/res/values/strings.xml index a012d58e8..9799482e7 100644 --- a/app/app/src/main/res/values/strings.xml +++ b/app/app/src/main/res/values/strings.xml @@ -149,6 +149,9 @@ Earnings You\'re earning x2 points Edit device name + Edit network name + Claim a custom network name to replace your auto-generated one + Tap the edit icon to change your network name Edit Profile Edit referral code Edit split rule diff --git a/app/app/src/solana_dapp/java/com/bringyour/network/LoginActivity.kt b/app/app/src/solana_dapp/java/com/bringyour/network/LoginActivity.kt index 1e2bae31e..3c80f1acc 100644 --- a/app/app/src/solana_dapp/java/com/bringyour/network/LoginActivity.kt +++ b/app/app/src/solana_dapp/java/com/bringyour/network/LoginActivity.kt @@ -239,7 +239,7 @@ class LoginActivity : AppCompatActivity() { if (!success || jwt == null) { Log.i(TAG, "guest login: local byJwt parse failed") createGuestNetworkAndFinish(app) - } else if (jwt.guestMode) { + } else if (jwt.networkName != null) { setLinksAndStartMain(targetUrl, defaultLocation) } else { currentNetworkName = jwt.networkName @@ -335,7 +335,6 @@ class LoginActivity : AppCompatActivity() { private fun createGuestNetworkAndFinish(app: MainApplication) { val args = NetworkCreateArgs() args.terms = true - args.guestMode = true app.api?.networkCreate(args) { result, err -> lifecycleScope.launch { diff --git a/app/app/src/solana_dapp/java/com/bringyour/network/ui/login/LoginInitial.kt b/app/app/src/solana_dapp/java/com/bringyour/network/ui/login/LoginInitial.kt index 51bc7b100..e7cf2ead2 100644 --- a/app/app/src/solana_dapp/java/com/bringyour/network/ui/login/LoginInitial.kt +++ b/app/app/src/solana_dapp/java/com/bringyour/network/ui/login/LoginInitial.kt @@ -9,7 +9,10 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size -import androidx.compose.material3.MaterialTheme +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bolt +import androidx.compose.material.icons.filled.Key +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -37,12 +40,10 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.EnterTransition import androidx.compose.animation.fadeOut -import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.clickable import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.LaunchedEffect @@ -51,33 +52,25 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.focus.onFocusChanged import androidx.lifecycle.compose.LifecycleResumeEffect import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.common.api.ApiException -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.withStyle import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.bringyour.sdk.AuthLoginResult import com.bringyour.sdk.Api -import com.bringyour.sdk.NetworkCreateArgs import com.bringyour.network.LoginActivity import com.bringyour.network.MainApplication import com.bringyour.network.R import com.bringyour.network.TAG import com.bringyour.network.ui.components.URInlineErrorText import com.bringyour.network.ui.components.overlays.WelcomeAnimatedOverlayLogin -import com.bringyour.network.ui.theme.BlueMedium import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.solana.mobilewalletadapter.clientlib.ActivityResultSender -import kotlinx.coroutines.delay import kotlinx.coroutines.launch @Composable() @@ -223,6 +216,14 @@ fun LoginInitial( } } + val onSeedphraseLogin: () -> Unit = { + navController.navigate("login_seedphrase") + } + + val onInstantAccountCreate: () -> Unit = { + navController.navigate("create-network-instant") + } + LoginInitial( navController, userAuth = loginViewModel.userAuth, @@ -235,8 +236,6 @@ fun LoginInitial( setGoogleAuthInProgress = loginViewModel.setGoogleAuthInProgress, setLoginError = loginViewModel.setLoginError, googleAuthInProgress = loginViewModel.googleAuthInProgress, - createGuestModeInProgress = loginViewModel.createGuestModeInProgress, - setCreateGuestModeInProgress = loginViewModel.setCreateGuestModeInProgress, allowGoogleSso = loginViewModel.allowGoogleSso, solanaLogin = { connectSolanaWallet() @@ -254,7 +253,9 @@ fun LoginInitial( welcomeOverlayVisible = welcomeOverlayVisible, setWelcomeOverlayVisible = { welcomeOverlayVisible = it - } + }, + onSeedphraseLogin = onSeedphraseLogin, + onInstantAccountCreate = onInstantAccountCreate ) if (noSolanaWalletsFound) { @@ -292,9 +293,7 @@ fun LoginInitial( loginError: String?, setLoginError: (String?) -> Unit, googleAuthInProgress: Boolean, - createGuestModeInProgress: Boolean, setGoogleAuthInProgress: (Boolean) -> Unit, - setCreateGuestModeInProgress: (Boolean) -> Unit, allowGoogleSso: () -> Boolean, solanaLogin: () -> Unit, solanaAuthInProgress: Boolean, @@ -305,21 +304,14 @@ fun LoginInitial( setContentVisible: (Boolean) -> Unit, welcomeOverlayVisible: Boolean, setWelcomeOverlayVisible: (Boolean) -> Unit, + onSeedphraseLogin: () -> Unit, + onInstantAccountCreate: () -> Unit, ) { val context = LocalContext.current val application = context.applicationContext as? MainApplication val scope = rememberCoroutineScope() - var guestModeOverlayVisible by remember { mutableStateOf(false) } - - val setGuestModeOverlayVisible: (Boolean) -> Unit = { isVisible -> - if (isVisible) { - setLoginError(null) - } - guestModeOverlayVisible = isVisible - } - var authCodeLoginSheetVisible by remember { mutableStateOf(false) } val setAuthCodeLoginSheetVisible: (Boolean) -> Unit = { isVisible -> @@ -344,67 +336,6 @@ fun LoginInitial( val googleSignInClient = GoogleSignIn.getClient(context, googleSignInOpts) val createNetworkError = stringResource(id = R.string.create_network_error) - val createGuestNetwork = createGuestNetwork@{ - if (createGuestModeInProgress) { - return@createGuestNetwork - } - - setLoginError(null) - setCreateGuestModeInProgress(true) - - val args = NetworkCreateArgs() - args.terms = true - args.guestMode = true - - application?.api?.networkCreate(args) { result, err -> - scope.launch { - - if (err != null) { - Log.i("OnboardingGuestModeOverlay", "error ${err.message}") - setLoginError(err.message) - setCreateGuestModeInProgress(false) - } else if (result.error != null) { - Log.i("OnboardingGuestModeOverlay", "error ${result.error.message}") - setLoginError(result.error.message) - setCreateGuestModeInProgress(false) - } else if (result.network != null && result.network.byJwt.isNotEmpty()) { - setLoginError(null) - setGuestModeOverlayVisible(false) - - application.login(result.network.byJwt) - - setContentVisible(false) - - delay(500) - - setWelcomeOverlayVisible(true) - - delay(2250) - - loginActivity?.authClientAndFinish( - { error -> - setCreateGuestModeInProgress(false) - - setLoginError(error) - - if (error != null) { - setContentVisible(true) - } - } - ) - - } else { - setLoginError(createNetworkError) - setCreateGuestModeInProgress(false) - } - } - } ?: run { - setLoginError(createNetworkError) - setCreateGuestModeInProgress(false) - } - - } - LaunchedEffect(Unit) { googleSignInClient.signOut() setGoogleAuthInProgress(false) @@ -470,9 +401,7 @@ fun LoginInitial( setUserAuth = setUserAuth, userAuthInProgress = userAuthInProgress, isValidUserAuth = isValidUserAuth, - setGuestModeOverlayVisible = setGuestModeOverlayVisible, googleAuthInProgress = googleAuthInProgress, - createGuestModeInProgress = createGuestModeInProgress, loginError = loginError, onLogin = { login( @@ -492,7 +421,9 @@ fun LoginInitial( bittensorAuthInProgress = bittensorAuthInProgress, launchAuthCodeLoginSheet = { setAuthCodeLoginSheetVisible(true) - } + }, + onSeedphraseLogin = onSeedphraseLogin, + onInstantAccountCreate = onInstantAccountCreate ) } @@ -500,18 +431,6 @@ fun LoginInitial( } } - OnboardingGuestModeSheet( - isPresenting = guestModeOverlayVisible, - setIsPresenting = { - setGuestModeOverlayVisible(it) - }, - onCreateGuestNetwork = { - createGuestNetwork() - }, - createGuestModeInProgress = createGuestModeInProgress, - errorMessage = if (guestModeOverlayVisible) loginError else null - ) - AuthCodeLoginSheet( isPresenting = authCodeLoginSheetVisible, setIsPresenting = { @@ -535,9 +454,7 @@ fun LoginInitialActions( setUserAuth: (TextFieldValue) -> Unit, userAuthInProgress: Boolean, isValidUserAuth: Boolean, - setGuestModeOverlayVisible: (Boolean) -> Unit, googleAuthInProgress: Boolean, - createGuestModeInProgress: Boolean, loginError: String?, onLogin: () -> Unit, onGoogleLogin: () -> Unit, @@ -546,10 +463,12 @@ fun LoginInitialActions( solanaAuthInProgress: Boolean, onBittensorLogin: () -> Unit, bittensorAuthInProgress: Boolean, - launchAuthCodeLoginSheet: () -> Unit + launchAuthCodeLoginSheet: () -> Unit, + onSeedphraseLogin: () -> Unit, + onInstantAccountCreate: () -> Unit, ) { - val isLoginInProgress = userAuthInProgress || googleAuthInProgress || solanaAuthInProgress || bittensorAuthInProgress || createGuestModeInProgress + val isLoginInProgress = userAuthInProgress || googleAuthInProgress || solanaAuthInProgress || bittensorAuthInProgress Row( modifier = Modifier.fillMaxWidth(), @@ -731,68 +650,75 @@ fun LoginInitialActions( // } - if (!loginError.isNullOrEmpty()) { - Spacer(modifier = Modifier.height(16.dp)) - URInlineErrorText(loginError) - } - Spacer(modifier = Modifier.height(16.dp)) - TryGuestMode( - setGuestModeOverlayVisible = setGuestModeOverlayVisible, + /** + * Seedphrase Sign in + */ + URButton( + style = ButtonStyle.SECONDARY, + onClick = { + onSeedphraseLogin() + }, enabled = !isLoginInProgress - ) + ) { buttonTextStyle -> + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Filled.Key, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Sign in with Seedphrase", + style = buttonTextStyle + ) + } + } Spacer(modifier = Modifier.height(16.dp)) - NetworkServerSelector(enabled = !isLoginInProgress) - } - } - -} + /** + * Instant account creation + */ + URButton( + style = ButtonStyle.SECONDARY, + onClick = { + onInstantAccountCreate() + }, + enabled = !isLoginInProgress + ) { buttonTextStyle -> + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Filled.Bolt, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Create Instant Account", + style = buttonTextStyle + ) + } + } -@Composable -private fun TryGuestMode( - setGuestModeOverlayVisible: (Boolean) -> Unit, - enabled: Boolean -) { + Spacer(modifier = Modifier.height(16.dp)) - var isFocused by remember { mutableStateOf(false) } + if (!loginError.isNullOrEmpty()) { + Spacer(modifier = Modifier.height(16.dp)) + URInlineErrorText(loginError) + } - val guestModeStr = buildAnnotatedString { - append(stringResource(id = R.string.commitment_issues)) + Spacer(modifier = Modifier.height(16.dp)) - pushStringAnnotation( - tag = "GUEST_MODE", - annotation = "Guest Mode" - ) - withStyle( - style = SpanStyle( - color = if (!enabled) TextMuted else if (isFocused) BlueMedium else Color.White - ) - ) { - append(" ${stringResource(id = R.string.try_guest_mode)}") + NetworkServerSelector(enabled = !isLoginInProgress) } - pop() - } - Row { - Text( - text = guestModeStr, - modifier = Modifier - .clickable { - if (enabled) { - setGuestModeOverlayVisible(true) - } - } - .onFocusChanged { - isFocused = it.isFocused - } - .focusable(), - style = MaterialTheme.typography.bodyLarge.copy(color = TextMuted), - ) - } } @Preview() @@ -840,9 +766,7 @@ private fun LoginInitialPreview() { loginError = null, setLoginError = {}, googleAuthInProgress = false, - createGuestModeInProgress = false, setGoogleAuthInProgress = {}, - setCreateGuestModeInProgress = {}, allowGoogleSso = { true }, solanaAuthInProgress = false, solanaLogin = {}, @@ -852,7 +776,9 @@ private fun LoginInitialPreview() { contentVisible = true, setContentVisible = {}, welcomeOverlayVisible = false, - setWelcomeOverlayVisible = {} + setWelcomeOverlayVisible = {}, + onSeedphraseLogin = {}, + onInstantAccountCreate = {}, ) } } @@ -906,9 +832,7 @@ private fun LoginInitialLandscapePreview() { loginError = null, setLoginError = {}, googleAuthInProgress = false, - createGuestModeInProgress = false, setGoogleAuthInProgress = {}, - setCreateGuestModeInProgress = {}, allowGoogleSso = { true }, solanaAuthInProgress = false, solanaLogin = {}, @@ -918,7 +842,9 @@ private fun LoginInitialLandscapePreview() { contentVisible = true, setContentVisible = {}, welcomeOverlayVisible = false, - setWelcomeOverlayVisible = {} + setWelcomeOverlayVisible = {}, + onSeedphraseLogin = {}, + onInstantAccountCreate = {}, ) } } diff --git a/app/app/src/solana_dapp/java/com/bringyour/network/ui/login/LoginViewModel.kt b/app/app/src/solana_dapp/java/com/bringyour/network/ui/login/LoginViewModel.kt index 2440277cc..2935598a8 100644 --- a/app/app/src/solana_dapp/java/com/bringyour/network/ui/login/LoginViewModel.kt +++ b/app/app/src/solana_dapp/java/com/bringyour/network/ui/login/LoginViewModel.kt @@ -52,11 +52,11 @@ class LoginViewModel @Inject constructor( var loginError by mutableStateOf(null) private set - var createGuestModeInProgress by mutableStateOf(false) + var seedphraseAuthInProgress by mutableStateOf(false) private set - val setCreateGuestModeInProgress: (Boolean) -> Unit = { inProgress -> - createGuestModeInProgress = inProgress + val setSeedphraseAuthInProgress: (Boolean) -> Unit = { inProgress -> + seedphraseAuthInProgress = inProgress } val login: ( @@ -249,6 +249,48 @@ class LoginViewModel @Inject constructor( } } + fun loginWithSeedphrase( + ctx: Context, + api: Api?, + seedphrase: String, + onSuccess: (String) -> Unit, + onError: (String) -> Unit, + ) { + if (seedphraseAuthInProgress) return + + val authApi = api ?: run { + setLoginError(ctx.getString(R.string.login_error)) + return + } + + setLoginError(null) + seedphraseAuthInProgress = true + + val normalized = seedphrase.lowercase().trim().replace(Regex("\\s+"), " ") + val args = AuthLoginArgs() + args.seedphrase = normalized + + authApi.authLogin(args) { result, err -> + viewModelScope.launch { + if (err != null) { + setLoginError(err.message) + onError(err.message ?: "Seedphrase login failed") + } else if (result.error != null) { + setLoginError(result.error.message) + onError(result.error.message ?: "Invalid seedphrase") + } else if (result.network != null && result.network.byJwt.isNotEmpty()) { + setLoginError(null) + onSuccess(result.network.byJwt) + } else { + val msg = "Invalid seedphrase" + setLoginError(msg) + onError(msg) + } + seedphraseAuthInProgress = false + } + } + } + val setLoginError: (String?) -> Unit = { msg -> loginError = msg } diff --git a/app/app/src/solana_dapp/java/com/bringyour/network/ui/settings/GoogleAddAuthButton.kt b/app/app/src/solana_dapp/java/com/bringyour/network/ui/settings/GoogleAddAuthButton.kt new file mode 100644 index 000000000..152ca9258 --- /dev/null +++ b/app/app/src/solana_dapp/java/com/bringyour/network/ui/settings/GoogleAddAuthButton.kt @@ -0,0 +1,78 @@ +package com.bringyour.network.ui.settings + +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.bringyour.network.R +import com.bringyour.network.ui.components.URButton +import com.bringyour.network.ui.theme.TextMuted +import com.bringyour.sdk.AddAuthArgs +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.api.ApiException + +@Composable +fun GoogleAddAuthButton( + addAuth: (AddAuthArgs, onSuccess: () -> Unit, onError: (String) -> Unit) -> Unit, + isAddingAuth: Boolean, + onAdded: () -> Unit, + onError: (String) -> Unit, +) { + val context = LocalContext.current + + val googleClientId = stringResource(id = R.string.google_client_id) + val googleSignInOpts = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestIdToken(googleClientId) + .requestEmail() + .build() + val googleSignInClient = GoogleSignIn.getClient(context, googleSignInOpts) + + val googleSignInLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + val task = GoogleSignIn.getSignedInAccountFromIntent(result.data) + try { + val account = task.getResult(ApiException::class.java) + val idToken = account.idToken + if (idToken == null) { + onError("Could not get Google ID token") + return@rememberLauncherForActivityResult + } + val args = AddAuthArgs() + args.authJwt = idToken + args.authJwtType = "google" + addAuth( + args, + { + Toast.makeText(context, "Google sign-in method added", Toast.LENGTH_SHORT).show() + onAdded() + }, + { msg -> onError(msg) } + ) + } catch (e: ApiException) { + onError("Error signing in with Google") + } + } + + Text( + "Sign in with Google to add it as a sign-in method.", + style = MaterialTheme.typography.bodyMedium, + color = TextMuted + ) + Spacer(modifier = Modifier.height(12.dp)) + URButton( + onClick = { googleSignInLauncher.launch(googleSignInClient.signInIntent) }, + enabled = !isAddingAuth + ) { buttonTextStyle -> + Text("Sign in with Google", style = buttonTextStyle) + } +} diff --git a/app/app/src/ungoogle/java/com/bringyour/network/LoginActivity.kt b/app/app/src/ungoogle/java/com/bringyour/network/LoginActivity.kt index 72e26f17a..a45f602ba 100644 --- a/app/app/src/ungoogle/java/com/bringyour/network/LoginActivity.kt +++ b/app/app/src/ungoogle/java/com/bringyour/network/LoginActivity.kt @@ -241,7 +241,7 @@ class LoginActivity : AppCompatActivity() { if (!success || jwt == null) { Log.i(TAG, "guest login: local byJwt parse failed") createGuestNetworkAndFinish(app) - } else if (jwt.guestMode) { + } else if (jwt.networkName != null) { setLinksAndStartMain(targetUrl, defaultLocation) } else { currentNetworkName = jwt.networkName @@ -337,7 +337,6 @@ class LoginActivity : AppCompatActivity() { private fun createGuestNetworkAndFinish(app: MainApplication) { val args = NetworkCreateArgs() args.terms = true - args.guestMode = true app.api?.networkCreate(args) { result, err -> lifecycleScope.launch { diff --git a/app/app/src/ungoogle/java/com/bringyour/network/ui/login/LoginInitial.kt b/app/app/src/ungoogle/java/com/bringyour/network/ui/login/LoginInitial.kt index 7c83b8fbe..e6cf5d1d6 100644 --- a/app/app/src/ungoogle/java/com/bringyour/network/ui/login/LoginInitial.kt +++ b/app/app/src/ungoogle/java/com/bringyour/network/ui/login/LoginInitial.kt @@ -9,7 +9,10 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size -import androidx.compose.material3.MaterialTheme +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bolt +import androidx.compose.material.icons.filled.Key +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -35,12 +38,10 @@ import com.bringyour.network.ui.theme.TextMuted import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.EnterTransition import androidx.compose.animation.fadeOut -import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.clickable import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.getValue @@ -48,29 +49,21 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.focus.onFocusChanged import androidx.lifecycle.compose.LifecycleResumeEffect -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.withStyle import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import com.bringyour.sdk.AuthLoginResult import com.bringyour.sdk.Api -import com.bringyour.sdk.NetworkCreateArgs import com.bringyour.network.LoginActivity import com.bringyour.network.MainApplication import com.bringyour.network.R import com.bringyour.network.TAG import com.bringyour.network.ui.components.URInlineErrorText import com.bringyour.network.ui.components.overlays.WelcomeAnimatedOverlayLogin -import com.bringyour.network.ui.theme.BlueMedium import com.solana.mobilewalletadapter.clientlib.ActivityResultSender -import kotlinx.coroutines.delay import kotlinx.coroutines.launch @Composable() @@ -209,6 +202,14 @@ fun LoginInitial( } } + val onSeedphraseLogin: () -> Unit = { + navController.navigate("login_seedphrase") + } + + val onInstantAccountCreate: () -> Unit = { + navController.navigate("create-network-instant") + } + LoginInitial( navController, userAuth = loginViewModel.userAuth, @@ -218,8 +219,6 @@ fun LoginInitial( login = loginViewModel.login, loginError = loginViewModel.loginError, setLoginError = loginViewModel.setLoginError, - createGuestModeInProgress = loginViewModel.createGuestModeInProgress, - setCreateGuestModeInProgress = loginViewModel.setCreateGuestModeInProgress, solanaLogin = { connectSolanaWallet() }, @@ -236,7 +235,9 @@ fun LoginInitial( welcomeOverlayVisible = welcomeOverlayVisible, setWelcomeOverlayVisible = { welcomeOverlayVisible = it - } + }, + onSeedphraseLogin = onSeedphraseLogin, + onInstantAccountCreate = onInstantAccountCreate ) if (noSolanaWalletsFound) { @@ -266,8 +267,6 @@ fun LoginInitial( ) -> Unit, loginError: String?, setLoginError: (String?) -> Unit, - createGuestModeInProgress: Boolean, - setCreateGuestModeInProgress: (Boolean) -> Unit, solanaLogin: () -> Unit, solanaAuthInProgress: Boolean, bittensorLogin: () -> Unit, @@ -277,21 +276,14 @@ fun LoginInitial( setContentVisible: (Boolean) -> Unit, welcomeOverlayVisible: Boolean, setWelcomeOverlayVisible: (Boolean) -> Unit, + onSeedphraseLogin: () -> Unit, + onInstantAccountCreate: () -> Unit, ) { val context = LocalContext.current val application = context.applicationContext as? MainApplication val scope = rememberCoroutineScope() - var guestModeOverlayVisible by remember { mutableStateOf(false) } - - val setGuestModeOverlayVisible: (Boolean) -> Unit = { isVisible -> - if (isVisible) { - setLoginError(null) - } - guestModeOverlayVisible = isVisible - } - var authCodeLoginSheetVisible by remember { mutableStateOf(false) } val setAuthCodeLoginSheetVisible: (Boolean) -> Unit = { isVisible -> @@ -310,67 +302,6 @@ fun LoginInitial( val createNetworkErrorMsg = stringResource(id = R.string.create_network_error) - val createGuestNetwork = createGuestNetwork@{ - if (createGuestModeInProgress) { - return@createGuestNetwork - } - - setLoginError(null) - setCreateGuestModeInProgress(true) - - val args = NetworkCreateArgs() - args.terms = true - args.guestMode = true - - application?.api?.networkCreate(args) { result, err -> - scope.launch { - - if (err != null) { - Log.i("OnboardingGuestModeOverlay", "error ${err.message}") - setLoginError(err.message) - setCreateGuestModeInProgress(false) - } else if (result.error != null) { - Log.i("OnboardingGuestModeOverlay", "error ${result.error.message}") - setLoginError(result.error.message) - setCreateGuestModeInProgress(false) - } else if (result.network != null && result.network.byJwt.isNotEmpty()) { - setLoginError(null) - setGuestModeOverlayVisible(false) - - application.login(result.network.byJwt) - - setContentVisible(false) - - delay(500) - - setWelcomeOverlayVisible(true) - - delay(2250) - - loginActivity?.authClientAndFinish( - { error -> - setCreateGuestModeInProgress(false) - - setLoginError(error) - - if (error != null) { - setContentVisible(true) - } - } - ) - - } else { - setLoginError(createNetworkErrorMsg) - setCreateGuestModeInProgress(false) - } - } - } ?: run { - setLoginError(createNetworkErrorMsg) - setCreateGuestModeInProgress(false) - } - - } - AnimatedVisibility( visible = contentVisible, enter = EnterTransition.None, @@ -402,8 +333,6 @@ fun LoginInitial( setUserAuth = setUserAuth, userAuthInProgress = userAuthInProgress, isValidUserAuth = isValidUserAuth, - setGuestModeOverlayVisible = setGuestModeOverlayVisible, - createGuestModeInProgress = createGuestModeInProgress, loginError = loginError, onLogin = { login( @@ -419,7 +348,9 @@ fun LoginInitial( bittensorAuthInProgress = bittensorAuthInProgress, launchAuthCodeLoginSheet = { setAuthCodeLoginSheetVisible(true) - } + }, + onSeedphraseLogin = onSeedphraseLogin, + onInstantAccountCreate = onInstantAccountCreate ) } @@ -438,18 +369,6 @@ fun LoginInitial( ) - OnboardingGuestModeSheet( - isPresenting = guestModeOverlayVisible, - setIsPresenting = { - setGuestModeOverlayVisible(it) - }, - onCreateGuestNetwork = { - createGuestNetwork() - }, - createGuestModeInProgress = createGuestModeInProgress, - errorMessage = if (guestModeOverlayVisible) loginError else null - ) - if (welcomeOverlayVisible) { WelcomeAnimatedOverlayLogin() @@ -463,8 +382,6 @@ fun LoginInitialActions( setUserAuth: (TextFieldValue) -> Unit, userAuthInProgress: Boolean, isValidUserAuth: Boolean, - setGuestModeOverlayVisible: (Boolean) -> Unit, - createGuestModeInProgress: Boolean, loginError: String?, onLogin: () -> Unit, onSolanaLogin: () -> Unit, @@ -472,9 +389,11 @@ fun LoginInitialActions( onBittensorLogin: () -> Unit, bittensorAuthInProgress: Boolean, launchAuthCodeLoginSheet: () -> Unit, + onSeedphraseLogin: () -> Unit, + onInstantAccountCreate: () -> Unit, ) { - val isLoginInProgress = userAuthInProgress || solanaAuthInProgress || bittensorAuthInProgress || createGuestModeInProgress + val isLoginInProgress = userAuthInProgress || solanaAuthInProgress || bittensorAuthInProgress Row( modifier = Modifier.fillMaxWidth(), @@ -627,61 +546,66 @@ fun LoginInitialActions( Spacer(modifier = Modifier.height(16.dp)) - TryGuestMode( - setGuestModeOverlayVisible = setGuestModeOverlayVisible, + /** + * Seedphrase Sign in + */ + URButton( + style = ButtonStyle.SECONDARY, + onClick = { + onSeedphraseLogin() + }, enabled = !isLoginInProgress - ) + ) { buttonTextStyle -> + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Filled.Key, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Sign in with Seedphrase", + style = buttonTextStyle + ) + } + } Spacer(modifier = Modifier.height(16.dp)) - NetworkServerSelector(enabled = !isLoginInProgress) - } - } - -} - -@Composable -private fun TryGuestMode( - setGuestModeOverlayVisible: (Boolean) -> Unit, - enabled: Boolean -) { - - var isFocused by remember { mutableStateOf(false) } + /** + * Instant account creation + */ + URButton( + style = ButtonStyle.SECONDARY, + onClick = { + onInstantAccountCreate() + }, + enabled = !isLoginInProgress + ) { buttonTextStyle -> + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Filled.Bolt, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Create Instant Account", + style = buttonTextStyle + ) + } + } - val guestModeStr = buildAnnotatedString { - append(stringResource(id = R.string.commitment_issues)) + Spacer(modifier = Modifier.height(16.dp)) - pushStringAnnotation( - tag = "GUEST_MODE", - annotation = "Guest Mode" - ) - withStyle( - style = SpanStyle( - color = if (!enabled) TextMuted else if (isFocused) BlueMedium else Color.White - ) - ) { - append(" ${stringResource(id = R.string.try_guest_mode)}") + NetworkServerSelector(enabled = !isLoginInProgress) } - pop() - } - Row { - Text( - text = guestModeStr, - modifier = Modifier - .clickable { - if (enabled) { - setGuestModeOverlayVisible(true) - } - } - .onFocusChanged { - isFocused = it.isFocused - } - .focusable(), - style = MaterialTheme.typography.bodyLarge.copy(color = TextMuted), - ) - } } @Preview() @@ -717,8 +641,6 @@ private fun LoginInitialPreview() { login = login, loginError = null, setLoginError = {}, - createGuestModeInProgress = false, - setCreateGuestModeInProgress = {}, solanaAuthInProgress = false, solanaLogin = {}, bittensorAuthInProgress = false, @@ -727,7 +649,9 @@ private fun LoginInitialPreview() { contentVisible = true, setContentVisible = {}, welcomeOverlayVisible = false, - setWelcomeOverlayVisible = {} + setWelcomeOverlayVisible = {}, + onSeedphraseLogin = {}, + onInstantAccountCreate = {}, ) } } @@ -769,8 +693,6 @@ private fun LoginInitialLandscapePreview() { login = login, loginError = null, setLoginError = {}, - createGuestModeInProgress = false, - setCreateGuestModeInProgress = {}, solanaAuthInProgress = false, solanaLogin = {}, bittensorAuthInProgress = false, @@ -780,6 +702,8 @@ private fun LoginInitialLandscapePreview() { welcomeOverlayVisible = false, setWelcomeOverlayVisible = {}, onLogin = {}, + onSeedphraseLogin = {}, + onInstantAccountCreate = {}, ) } } diff --git a/app/app/src/ungoogle/java/com/bringyour/network/ui/login/LoginViewModel.kt b/app/app/src/ungoogle/java/com/bringyour/network/ui/login/LoginViewModel.kt index f3446d18d..5a33cdc77 100644 --- a/app/app/src/ungoogle/java/com/bringyour/network/ui/login/LoginViewModel.kt +++ b/app/app/src/ungoogle/java/com/bringyour/network/ui/login/LoginViewModel.kt @@ -45,11 +45,11 @@ class LoginViewModel @Inject constructor( var loginError by mutableStateOf(null) private set - var createGuestModeInProgress by mutableStateOf(false) + var seedphraseAuthInProgress by mutableStateOf(false) private set - val setCreateGuestModeInProgress: (Boolean) -> Unit = { inProgress -> - createGuestModeInProgress = inProgress + val setSeedphraseAuthInProgress: (Boolean) -> Unit = { inProgress -> + seedphraseAuthInProgress = inProgress } var solanaAuthInProgress by mutableStateOf(false) @@ -123,6 +123,48 @@ class LoginViewModel @Inject constructor( } } + fun loginWithSeedphrase( + ctx: Context, + api: Api?, + seedphrase: String, + onSuccess: (String) -> Unit, + onError: (String) -> Unit, + ) { + if (seedphraseAuthInProgress) return + + val authApi = api ?: run { + setLoginError(ctx.getString(R.string.login_error)) + return + } + + setLoginError(null) + seedphraseAuthInProgress = true + + val normalized = seedphrase.lowercase().trim().replace(Regex("\\s+"), " ") + val args = AuthLoginArgs() + args.seedphrase = normalized + + authApi.authLogin(args) { result, err -> + viewModelScope.launch { + if (err != null) { + setLoginError(err.message) + onError(err.message ?: "Seedphrase login failed") + } else if (result.error != null) { + setLoginError(result.error.message) + onError(result.error.message ?: "Invalid seedphrase") + } else if (result.network != null && result.network.byJwt.isNotEmpty()) { + setLoginError(null) + onSuccess(result.network.byJwt) + } else { + val msg = "Invalid seedphrase" + setLoginError(msg) + onError(msg) + } + seedphraseAuthInProgress = false + } + } + } + fun walletLogin( ctx: Context, api: Api?, diff --git a/app/app/src/ungoogle/java/com/bringyour/network/ui/settings/GoogleAddAuthButton.kt b/app/app/src/ungoogle/java/com/bringyour/network/ui/settings/GoogleAddAuthButton.kt new file mode 100644 index 000000000..a6076f0e4 --- /dev/null +++ b/app/app/src/ungoogle/java/com/bringyour/network/ui/settings/GoogleAddAuthButton.kt @@ -0,0 +1,18 @@ +package com.bringyour.network.ui.settings + +import androidx.compose.runtime.Composable +import com.bringyour.sdk.AddAuthArgs + +// github is deliberately de-Googled (no play-services-auth dependency), so +// this flavor never shows the Google option (showGoogleOption is false via +// BuildConfig.BRINGYOUR_BUNDLE_SSO_GOOGLE) and this body is unreachable in +// practice. It must still exist so AddAuthMethodSheet.kt's call to +// GoogleAddAuthButton(...) resolves for this flavor's compilation. +@Composable +fun GoogleAddAuthButton( + addAuth: (AddAuthArgs, onSuccess: () -> Unit, onError: (String) -> Unit) -> Unit, + isAddingAuth: Boolean, + onAdded: () -> Unit, + onError: (String) -> Unit, +) { +}