Add sign in screen
This commit is contained in:
@@ -15,6 +15,7 @@ import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
class DatabaseNostrRepository(
|
||||
@@ -40,8 +41,8 @@ class DatabaseNostrRepository(
|
||||
|
||||
override suspend fun createNewProfile(
|
||||
publicKey: HexKey,
|
||||
name: String,
|
||||
biography: String,
|
||||
name: String?,
|
||||
biography: String?,
|
||||
onError: () -> Unit,
|
||||
onProfileReady: (Profile) -> Unit
|
||||
) {
|
||||
@@ -50,13 +51,21 @@ class DatabaseNostrRepository(
|
||||
about = biography,
|
||||
)
|
||||
|
||||
val signedAt = if (name == null && biography == null) {
|
||||
// We are most likely try to sign in to an already existing block...
|
||||
Instant.fromEpochMilliseconds(1231006505000L)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val unsignedNostrEvent = saveUnsignedNostrEvent(
|
||||
UnsignedNostrEvent(
|
||||
pubKey = publicKey,
|
||||
kind = profileEventTemplate.kind,
|
||||
createdAt = Instant.fromEpochMilliseconds(profileEventTemplate.createdAt),
|
||||
tags = profileEventTemplate.tags,
|
||||
content = profileEventTemplate.content
|
||||
content = profileEventTemplate.content,
|
||||
signedAt = signedAt // This will keep us from trying to sign this event... but we should automatically sync the npub profile
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ interface NostrRepository {
|
||||
|
||||
suspend fun createNewProfile(
|
||||
publicKey: HexKey,
|
||||
name: String,
|
||||
biography: String,
|
||||
name: String?,
|
||||
biography: String?,
|
||||
onError: () -> Unit,
|
||||
// onUnsignProfile: (UnsignedNostrEvent) -> Unit,
|
||||
// onUnbroadcastedProfile: (NostrEvent) -> Unit,
|
||||
@@ -64,8 +64,8 @@ interface NostrRepository {
|
||||
|
||||
override suspend fun createNewProfile(
|
||||
publicKey: HexKey,
|
||||
name: String,
|
||||
biography: String,
|
||||
name: String?,
|
||||
biography: String?,
|
||||
onError: () -> Unit,
|
||||
onProfileReady: (Profile) -> Unit
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
package ac.aux.compose.ui.composable
|
||||
|
||||
import ac.aux.compose.repository.NostrRepository
|
||||
import ac.aux.compose.ui.theme.AuxTheme
|
||||
import ac.aux.compose.ui.view.model.SignInToProfileViewModel
|
||||
import ac.aux.compose.ui.view.state.SignInToProfileUIState
|
||||
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.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Create
|
||||
import androidx.compose.material.icons.filled.NavigateNext
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LoadingIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun SignInToProfileScreen(
|
||||
initialSignInToProfileUIState: SignInToProfileUIState = SignInToProfileUIState.InputPrompt,
|
||||
nostrRepository: NostrRepository
|
||||
) {
|
||||
val signInToProfileViewModel: SignInToProfileViewModel = viewModel (
|
||||
factory = SignInToProfileViewModel.factory(
|
||||
initialSignInToProfileUIState,
|
||||
nostrRepository
|
||||
)
|
||||
)
|
||||
Scaffold { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
) {
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(
|
||||
10.dp
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
) {
|
||||
|
||||
when (val signInToProfileUIState = signInToProfileViewModel.signInToProfileUIState.value) {
|
||||
is SignInToProfileUIState.InputPrompt -> {
|
||||
Text(
|
||||
"Sign In",
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
|
||||
TextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = signInToProfileViewModel.signInToProfileFormState.textField.text.value,
|
||||
onValueChange = {
|
||||
signInToProfileViewModel.signInToProfileFormState.textField.text.value = it
|
||||
},
|
||||
isError = signInToProfileViewModel.signInToProfileFormState.textField.errorMessage.value != null,
|
||||
supportingText = signInToProfileViewModel.signInToProfileFormState.textField.errorMessage.value?.let {
|
||||
{
|
||||
Text(
|
||||
text = it,
|
||||
modifier = Modifier.padding(
|
||||
bottom = 8.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
label = {
|
||||
Text(
|
||||
text = "nsec, npub, nip-05 (static address)",
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "Enter the nsec or npub (read only) that you want to sign in as",
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Default.Create,
|
||||
contentDescription = "Write note"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
signInToProfileViewModel.signInToProfileUIState.value = signInToProfileViewModel.validateCredential().let { credentialEntity ->
|
||||
when (credentialEntity) {
|
||||
is NSec -> {
|
||||
SignInToProfileUIState.ConfirmNsecSignIn(
|
||||
nsec = signInToProfileViewModel.signInToProfileFormState.textField.text.value,
|
||||
hexKey = credentialEntity.hex
|
||||
)
|
||||
}
|
||||
is NPub -> {
|
||||
SignInToProfileUIState.ConfirmNpubSignIn(
|
||||
npub = signInToProfileViewModel.signInToProfileFormState.textField.text.value,
|
||||
hexKey = credentialEntity.hex
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
SignInToProfileUIState.Error
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
"Next"
|
||||
)
|
||||
Icon(
|
||||
Icons.Default.NavigateNext,
|
||||
contentDescription = "Next"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
is SignInToProfileUIState.ConfirmNpubSignIn-> {
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Text(
|
||||
text = "Sign in to Npub",
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
|
||||
Text(
|
||||
text = signInToProfileUIState.npub,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Text(
|
||||
text = signInToProfileUIState.hexKey,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier.weight(4f)
|
||||
)
|
||||
|
||||
|
||||
|
||||
Text(
|
||||
text = "This will give you read only access to the profile.",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
if (signInToProfileViewModel.isActionPending.value) {
|
||||
LoadingIndicator(
|
||||
modifier = Modifier
|
||||
)
|
||||
} else {
|
||||
Button(
|
||||
onClick = {
|
||||
signInToProfileViewModel.signInToAccount(
|
||||
onError = {
|
||||
signInToProfileViewModel.signInToProfileUIState.value =
|
||||
SignInToProfileUIState.Error
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = "Sign In"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
is SignInToProfileUIState.ConfirmNsecSignIn-> {
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Text(
|
||||
text = "Sign in to nsec",
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Be sure to keep this nsec safe.",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier.weight(4f)
|
||||
)
|
||||
|
||||
|
||||
|
||||
Text(
|
||||
text = "This will give you write access to the profile.",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
if (signInToProfileViewModel.isActionPending.value) {
|
||||
LoadingIndicator(
|
||||
modifier = Modifier
|
||||
)
|
||||
} else {
|
||||
Button(
|
||||
onClick = {
|
||||
signInToProfileViewModel.signInToAccount(
|
||||
onError = {
|
||||
signInToProfileViewModel.signInToProfileUIState.value =
|
||||
SignInToProfileUIState.Error
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = "Sign In"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
is SignInToProfileUIState.Error -> {
|
||||
Spacer(
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Text(
|
||||
text = "Something went wrong and we were unable to sign in to your provided profile. Please try again later.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SignInToProfileScreenPreview() {
|
||||
AuxTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
SignInToProfileScreen(
|
||||
initialSignInToProfileUIState =
|
||||
SignInToProfileUIState.InputPrompt,
|
||||
// SignInToProfileUIState.Error,
|
||||
// SignInToProfileUIState.ConfirmNpubSignIn(
|
||||
// npub = "npub1uh366qnj68a7atvwgwdy4569xd6v5jtferftapqztwjwlzh9553q47pqsx",
|
||||
// hexKey = "e5e3ad0272d1fbeead8e439a4ad3453374ca4969c8d2be84025ba4ef8ae5a522"
|
||||
// ),
|
||||
// SignInToProfileUIState.ConfirmNsecSignIn(
|
||||
// nsec = "nsecthingsaretobekeptsecret",
|
||||
// hexKey = "e5e3ad0272d1fbeead8e439a4ad3453374ca4969c8d2be84025ba4ef8ae5a522"
|
||||
// ),
|
||||
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import ac.aux.compose.ui.composable.FeedScreen
|
||||
import ac.aux.compose.ui.composable.ImplementationPendingScreen
|
||||
import ac.aux.compose.ui.composable.LandingScreen
|
||||
import ac.aux.compose.ui.composable.LoadingScreen
|
||||
import ac.aux.compose.ui.composable.SignInToProfileScreen
|
||||
import ac.aux.compose.ui.composable.SocialPreconditionScreen
|
||||
import ac.aux.compose.ui.composable.UnannouncedProfileScreen
|
||||
import ac.aux.compose.ui.composable.UnindexedProfileScreen
|
||||
@@ -20,6 +21,7 @@ import ac.aux.compose.ui.composable.navigation.routes.FeedRoute
|
||||
import ac.aux.compose.ui.composable.navigation.routes.ImplementationPendingRoute
|
||||
import ac.aux.compose.ui.composable.navigation.routes.LandingRoute
|
||||
import ac.aux.compose.ui.composable.navigation.routes.LoadingRoute
|
||||
import ac.aux.compose.ui.composable.navigation.routes.SignInRoute
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
@@ -175,7 +177,7 @@ fun AuxNavHost(
|
||||
},
|
||||
onNavigateToSignIn = {
|
||||
navController.navigate(
|
||||
route = ImplementationPendingRoute("Sign in")
|
||||
route = SignInRoute
|
||||
)
|
||||
},
|
||||
onNavigateToCreateProfile = {
|
||||
@@ -212,6 +214,11 @@ fun AuxNavHost(
|
||||
nostrRepository = nostrRepository
|
||||
)
|
||||
}
|
||||
composable<SignInRoute> {
|
||||
SignInToProfileScreen(
|
||||
nostrRepository = nostrRepository
|
||||
)
|
||||
}
|
||||
composable<UnsignedProfileRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<UnsignedProfileRoute>()
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package ac.aux.compose.ui.composable.navigation.routes
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
object SignInRoute: Route()
|
||||
@@ -0,0 +1,103 @@
|
||||
package ac.aux.compose.ui.view.model
|
||||
|
||||
import ac.aux.compose.managers.SeedManager
|
||||
import ac.aux.compose.repository.NostrRepository
|
||||
import ac.aux.compose.ui.view.state.SignInToProfileUIState
|
||||
import ac.aux.compose.ui.view.state.form.SignInToProfileFormState
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.Entity
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SignInToProfileViewModel(
|
||||
initialWriteNewNoteUIState: SignInToProfileUIState,
|
||||
val signInToProfileFormState: SignInToProfileFormState = SignInToProfileFormState(),
|
||||
val nostrRepository: NostrRepository
|
||||
): ViewModel() {
|
||||
companion object {
|
||||
private const val TAG = "SignInToProfileViewModel"
|
||||
|
||||
fun factory(
|
||||
initialWriteNewNoteUIState: SignInToProfileUIState,
|
||||
nostrRepository: NostrRepository
|
||||
): ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
SignInToProfileViewModel(
|
||||
initialWriteNewNoteUIState,
|
||||
nostrRepository = nostrRepository
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val logger = Logger.withTag(TAG)
|
||||
|
||||
val isActionPending: MutableState<Boolean> = mutableStateOf(false)
|
||||
|
||||
var signInToProfileUIState: MutableState<SignInToProfileUIState> = mutableStateOf(
|
||||
initialWriteNewNoteUIState
|
||||
)
|
||||
|
||||
fun validateCredential(): Entity? {
|
||||
return signInToProfileFormState.textField.text.value.let { credentialText ->
|
||||
if (credentialText.isBlank()) {
|
||||
signInToProfileFormState.textField.errorMessage.value = "Please input a credential"
|
||||
return null
|
||||
} else {
|
||||
signInToProfileFormState.textField.errorMessage.value = null
|
||||
}
|
||||
|
||||
Nip19Parser.uriToRoute(signInToProfileFormState.textField.text.value)?.entity
|
||||
}
|
||||
}
|
||||
|
||||
fun signInToAccount(
|
||||
onError: () -> Unit,
|
||||
) {
|
||||
logger.d { "signInToAccount" }
|
||||
validateCredential().let { credentialEntity ->
|
||||
isActionPending.value = true
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
when (credentialEntity) {
|
||||
is NPub -> {
|
||||
nostrRepository.createNewProfile(
|
||||
publicKey = credentialEntity.hex,
|
||||
name = null,
|
||||
biography = null,
|
||||
onError = onError,
|
||||
onProfileReady = {}
|
||||
)
|
||||
}
|
||||
is NSec -> {
|
||||
nostrRepository.createNewProfile(
|
||||
publicKey = credentialEntity.toPubKeyHex(),
|
||||
name = null,
|
||||
biography = null,
|
||||
onError = onError,
|
||||
onProfileReady = {}
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
// TODO: Might want to give reasons for the error.
|
||||
signInToProfileUIState.value = SignInToProfileUIState.Error
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package ac.aux.compose.ui.view.state
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
abstract class SignInToProfileUIState {
|
||||
data object Error: SignInToProfileUIState()
|
||||
|
||||
data object InputPrompt: SignInToProfileUIState()
|
||||
|
||||
data class ConfirmNpubSignIn(
|
||||
val npub: String,
|
||||
val hexKey: HexKey
|
||||
) : SignInToProfileUIState()
|
||||
|
||||
data class ConfirmNsecSignIn(
|
||||
val nsec: String,
|
||||
val hexKey: HexKey
|
||||
) : SignInToProfileUIState()
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package ac.aux.compose.ui.view.state.form
|
||||
|
||||
data class SignInToProfileFormState(
|
||||
val textField: TextField = TextField(),
|
||||
)
|
||||
Reference in New Issue
Block a user