fix(ui): partner invite flow polish — compact layout, share button, back affordances, secondary route styling (batch 7)
This commit is contained in:
parent
cc974661d3
commit
d109f7fcd0
|
|
@ -244,7 +244,10 @@ fun AppNavigation(
|
|||
EmailInviteScreen(onNavigate = navigateRoute)
|
||||
}
|
||||
composable(route = AppRoute.ACCEPT_INVITE) {
|
||||
AcceptInviteScreen(onNavigate = navigateRoute)
|
||||
AcceptInviteScreen(
|
||||
onNavigate = navigateRoute,
|
||||
onBack = navigateBackOrHome
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = AppRoute.INVITE_CONFIRM,
|
||||
|
|
@ -252,7 +255,8 @@ fun AppNavigation(
|
|||
) {
|
||||
InviteConfirmScreen(
|
||||
inviteCode = it.arguments?.getString("inviteCode") ?: "",
|
||||
onNavigate = navigateRoute
|
||||
onNavigate = navigateRoute,
|
||||
onBack = navigateBackOrHome
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package app.closer.ui.pairing
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
|
@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
|
|
@ -46,10 +47,11 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.input.OffsetMapping
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.OffsetMapping
|
||||
import androidx.compose.ui.text.input.TransformedText
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
|
@ -69,6 +71,7 @@ import app.closer.ui.settings.SettingsSoft
|
|||
@Composable
|
||||
fun AcceptInviteScreen(
|
||||
onNavigate: (String) -> Unit = {},
|
||||
onBack: () -> Unit = {},
|
||||
viewModel: AcceptInviteViewModel = hiltViewModel()
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
|
@ -87,7 +90,7 @@ fun AcceptInviteScreen(
|
|||
TopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onNavigate(AppRoute.CREATE_INVITE) }) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
|
|
@ -114,11 +117,12 @@ fun AcceptInviteScreen(
|
|||
|
||||
Text(
|
||||
"Enter the code",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = SettingsInk,
|
||||
textAlign = TextAlign.Center
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(
|
||||
"Ask your partner to share their 6-character invite code.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
|
|
@ -126,7 +130,7 @@ fun AcceptInviteScreen(
|
|||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(36.dp))
|
||||
Spacer(Modifier.height(28.dp))
|
||||
|
||||
InviteCodeEntryCard(
|
||||
value = state.code,
|
||||
|
|
@ -143,12 +147,13 @@ fun AcceptInviteScreen(
|
|||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus(); viewModel.lookupCode() })
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Spacer(Modifier.height(20.dp))
|
||||
|
||||
Button(
|
||||
onClick = { focusManager.clearFocus(); viewModel.lookupCode() },
|
||||
enabled = !state.isLoading && state.code.length == 6,
|
||||
modifier = Modifier.fillMaxWidth().height(52.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = SettingsPrimary,
|
||||
contentColor = SettingsOnPrimary
|
||||
|
|
@ -162,9 +167,12 @@ fun AcceptInviteScreen(
|
|||
else Text("Continue", style = MaterialTheme.typography.labelLarge)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Spacer(Modifier.height(28.dp))
|
||||
|
||||
TextButton(onClick = { onNavigate(AppRoute.CREATE_INVITE) }) {
|
||||
TextButton(
|
||||
onClick = { onNavigate(AppRoute.CREATE_INVITE) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
"Need to create an invite instead?",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
|
|
@ -199,7 +207,7 @@ private fun InviteCodeEntryCard(
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
.padding(vertical = 28.dp, horizontal = 24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
BasicTextField(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package app.closer.ui.pairing
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -19,6 +20,7 @@ import androidx.compose.foundation.verticalScroll
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
|
|
@ -45,6 +47,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
|
@ -71,6 +74,7 @@ fun CreateInviteScreen(
|
|||
val snackbar = remember { SnackbarHostState() }
|
||||
val scope = rememberCoroutineScope()
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(state.navigateTo) {
|
||||
state.navigateTo?.let { onNavigate(it); viewModel.onNavigated() }
|
||||
|
|
@ -79,6 +83,11 @@ fun CreateInviteScreen(
|
|||
state.error?.let { snackbar.showSnackbar(it); viewModel.dismissError() }
|
||||
}
|
||||
|
||||
val formattedCode = state.inviteCode?.chunked(3)?.joinToString(" – ")
|
||||
val shareMessage = state.inviteCode?.let { code ->
|
||||
"Join me on Closer! Here's my invite code: ${code.chunked(3).joinToString(" - ")}"
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = { SnackbarHost(snackbar) },
|
||||
containerColor = Color.Transparent,
|
||||
|
|
@ -107,28 +116,30 @@ fun CreateInviteScreen(
|
|||
.padding(padding)
|
||||
.padding(horizontal = 28.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
if (state.isLoading) {
|
||||
Spacer(Modifier.height(160.dp))
|
||||
CircularProgressIndicator(modifier = Modifier.size(40.dp))
|
||||
} else if (state.inviteCode != null) {
|
||||
Spacer(Modifier.height(48.dp))
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
"Invite your person",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = SettingsInk,
|
||||
textAlign = TextAlign.Center
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(
|
||||
"Share this code with your partner. They'll enter it to connect.",
|
||||
"Share this code with your partner so they can connect with you.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = SettingsMuted,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(40.dp))
|
||||
Spacer(Modifier.height(28.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
|
@ -140,11 +151,11 @@ fun CreateInviteScreen(
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
.padding(vertical = 28.dp, horizontal = 24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = state.inviteCode!!.chunked(3).joinToString(" – "),
|
||||
text = formattedCode!!,
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
color = SettingsPrimaryDeep,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
@ -153,27 +164,60 @@ fun CreateInviteScreen(
|
|||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(20.dp))
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
clipboard.setText(AnnotatedString(state.inviteCode!!))
|
||||
scope.launch { snackbar.showSnackbar("Code copied!") }
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().height(52.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = SettingsPrimary,
|
||||
contentColor = SettingsOnPrimary
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
Button(
|
||||
onClick = {
|
||||
clipboard.setText(AnnotatedString(state.inviteCode!!))
|
||||
scope.launch { snackbar.showSnackbar("Code copied!") }
|
||||
},
|
||||
modifier = Modifier.weight(1f).height(52.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = SettingsPrimary,
|
||||
contentColor = SettingsOnPrimary
|
||||
)
|
||||
) {
|
||||
Icon(Icons.Filled.ContentCopy, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Copy code", style = MaterialTheme.typography.labelLarge)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Filled.ContentCopy, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Copy", style = MaterialTheme.typography.labelLarge)
|
||||
}
|
||||
}
|
||||
|
||||
shareMessage?.let { message ->
|
||||
Button(
|
||||
onClick = {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, message)
|
||||
}
|
||||
val chooser = Intent.createChooser(intent, "Share invite code")
|
||||
context.startActivity(chooser)
|
||||
},
|
||||
modifier = Modifier.weight(1f).height(52.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = SettingsPrimary.copy(alpha = 0.16f),
|
||||
contentColor = SettingsPrimaryDeep
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Filled.Share, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Share", style = MaterialTheme.typography.labelLarge)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -186,20 +230,20 @@ fun CreateInviteScreen(
|
|||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(32.dp))
|
||||
Spacer(Modifier.height(28.dp))
|
||||
|
||||
TextButton(onClick = { onNavigate(AppRoute.ACCEPT_INVITE) }) {
|
||||
TextButton(
|
||||
onClick = { onNavigate(AppRoute.ACCEPT_INVITE) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
"Partner already has a code? Accept instead",
|
||||
"Partner already has a code? Accept instead",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = SettingsPrimaryDeep
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(32.dp))
|
||||
} else {
|
||||
// Empty / error state when no code is available
|
||||
Spacer(Modifier.height(48.dp))
|
||||
Spacer(Modifier.height(160.dp))
|
||||
Text(
|
||||
"No invite code yet",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
|
|
@ -36,6 +37,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
|
|
@ -46,12 +48,14 @@ import app.closer.ui.settings.SettingsMuted
|
|||
import app.closer.ui.settings.SettingsOnPrimary
|
||||
import app.closer.ui.settings.SettingsPrimary
|
||||
import app.closer.ui.settings.SettingsPrimaryDeep
|
||||
import app.closer.ui.settings.SettingsSoft
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun InviteConfirmScreen(
|
||||
inviteCode: String,
|
||||
onNavigate: (String) -> Unit = {},
|
||||
onBack: () -> Unit = {},
|
||||
viewModel: InviteConfirmViewModel = hiltViewModel()
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
|
@ -64,6 +68,8 @@ fun InviteConfirmScreen(
|
|||
state.error?.let { snackbar.showSnackbar(it); viewModel.dismissError() }
|
||||
}
|
||||
|
||||
val formattedCode = inviteCode.chunked(3).joinToString(" – ")
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = { SnackbarHost(snackbar) },
|
||||
containerColor = Color.Transparent,
|
||||
|
|
@ -72,7 +78,7 @@ fun InviteConfirmScreen(
|
|||
TopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onNavigate(AppRoute.ACCEPT_INVITE) }) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back",
|
||||
|
|
@ -92,12 +98,13 @@ fun InviteConfirmScreen(
|
|||
.padding(padding)
|
||||
.padding(horizontal = 28.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
if (state.isLoading) {
|
||||
Spacer(Modifier.height(160.dp))
|
||||
CircularProgressIndicator(modifier = Modifier.size(40.dp))
|
||||
} else {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
"♡",
|
||||
|
|
@ -105,15 +112,16 @@ fun InviteConfirmScreen(
|
|||
color = SettingsPrimaryDeep
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(20.dp))
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
"Pair with ${state.inviterName}?",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = SettingsInk,
|
||||
textAlign = TextAlign.Center
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(
|
||||
"Once you confirm, you'll be connected and can start exploring questions together.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
|
|
@ -121,10 +129,10 @@ fun InviteConfirmScreen(
|
|||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Spacer(Modifier.height(28.dp))
|
||||
|
||||
Text(
|
||||
"Code: $inviteCode",
|
||||
"Invite code $formattedCode",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = SettingsMuted
|
||||
)
|
||||
|
|
@ -135,6 +143,7 @@ fun InviteConfirmScreen(
|
|||
onClick = viewModel::confirmPairing,
|
||||
enabled = !state.isConfirming,
|
||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = SettingsPrimary,
|
||||
contentColor = SettingsOnPrimary
|
||||
|
|
@ -150,15 +159,16 @@ fun InviteConfirmScreen(
|
|||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
TextButton(onClick = { onNavigate(AppRoute.ACCEPT_INVITE) }) {
|
||||
TextButton(
|
||||
onClick = { onNavigate(AppRoute.ACCEPT_INVITE) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
"That's not right — go back",
|
||||
"That's not right — enter a different code",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = SettingsPrimaryDeep
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue