fix(ui): partner invite flow polish — compact layout, share button, back affordances, secondary route styling (batch 7)

This commit is contained in:
null 2026-06-17 00:16:42 -05:00
parent cc974661d3
commit d109f7fcd0
4 changed files with 126 additions and 60 deletions

View File

@ -244,7 +244,10 @@ fun AppNavigation(
EmailInviteScreen(onNavigate = navigateRoute) EmailInviteScreen(onNavigate = navigateRoute)
} }
composable(route = AppRoute.ACCEPT_INVITE) { composable(route = AppRoute.ACCEPT_INVITE) {
AcceptInviteScreen(onNavigate = navigateRoute) AcceptInviteScreen(
onNavigate = navigateRoute,
onBack = navigateBackOrHome
)
} }
composable( composable(
route = AppRoute.INVITE_CONFIRM, route = AppRoute.INVITE_CONFIRM,
@ -252,7 +255,8 @@ fun AppNavigation(
) { ) {
InviteConfirmScreen( InviteConfirmScreen(
inviteCode = it.arguments?.getString("inviteCode") ?: "", inviteCode = it.arguments?.getString("inviteCode") ?: "",
onNavigate = navigateRoute onNavigate = navigateRoute,
onBack = navigateBackOrHome
) )
} }

View File

@ -1,8 +1,8 @@
package app.closer.ui.pairing package app.closer.ui.pairing
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize 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.safeDrawingPadding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions 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.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.AnnotatedString 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.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType 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.TransformedText
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -69,6 +71,7 @@ import app.closer.ui.settings.SettingsSoft
@Composable @Composable
fun AcceptInviteScreen( fun AcceptInviteScreen(
onNavigate: (String) -> Unit = {}, onNavigate: (String) -> Unit = {},
onBack: () -> Unit = {},
viewModel: AcceptInviteViewModel = hiltViewModel() viewModel: AcceptInviteViewModel = hiltViewModel()
) { ) {
val state by viewModel.uiState.collectAsState() val state by viewModel.uiState.collectAsState()
@ -87,7 +90,7 @@ fun AcceptInviteScreen(
TopAppBar( TopAppBar(
title = {}, title = {},
navigationIcon = { navigationIcon = {
IconButton(onClick = { onNavigate(AppRoute.CREATE_INVITE) }) { IconButton(onClick = onBack) {
Icon( Icon(
Icons.AutoMirrored.Filled.ArrowBack, Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back", contentDescription = "Back",
@ -114,11 +117,12 @@ fun AcceptInviteScreen(
Text( Text(
"Enter the code", "Enter the code",
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineSmall,
color = SettingsInk, color = SettingsInk,
textAlign = TextAlign.Center textAlign = TextAlign.Center,
fontWeight = FontWeight.SemiBold
) )
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(6.dp))
Text( Text(
"Ask your partner to share their 6-character invite code.", "Ask your partner to share their 6-character invite code.",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
@ -126,7 +130,7 @@ fun AcceptInviteScreen(
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(Modifier.height(36.dp)) Spacer(Modifier.height(28.dp))
InviteCodeEntryCard( InviteCodeEntryCard(
value = state.code, value = state.code,
@ -143,12 +147,13 @@ fun AcceptInviteScreen(
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus(); viewModel.lookupCode() }) keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus(); viewModel.lookupCode() })
) )
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(20.dp))
Button( Button(
onClick = { focusManager.clearFocus(); viewModel.lookupCode() }, onClick = { focusManager.clearFocus(); viewModel.lookupCode() },
enabled = !state.isLoading && state.code.length == 6, enabled = !state.isLoading && state.code.length == 6,
modifier = Modifier.fillMaxWidth().height(52.dp), modifier = Modifier.fillMaxWidth().height(52.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = SettingsPrimary, containerColor = SettingsPrimary,
contentColor = SettingsOnPrimary contentColor = SettingsOnPrimary
@ -162,9 +167,12 @@ fun AcceptInviteScreen(
else Text("Continue", style = MaterialTheme.typography.labelLarge) 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( Text(
"Need to create an invite instead?", "Need to create an invite instead?",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
@ -199,7 +207,7 @@ private fun InviteCodeEntryCard(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(32.dp), .padding(vertical = 28.dp, horizontal = 24.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
BasicTextField( BasicTextField(

View File

@ -1,5 +1,6 @@
package app.closer.ui.pairing package app.closer.ui.pairing
import android.content.Intent
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
@ -45,6 +47,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -71,6 +74,7 @@ fun CreateInviteScreen(
val snackbar = remember { SnackbarHostState() } val snackbar = remember { SnackbarHostState() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
val context = LocalContext.current
LaunchedEffect(state.navigateTo) { LaunchedEffect(state.navigateTo) {
state.navigateTo?.let { onNavigate(it); viewModel.onNavigated() } state.navigateTo?.let { onNavigate(it); viewModel.onNavigated() }
@ -79,6 +83,11 @@ fun CreateInviteScreen(
state.error?.let { snackbar.showSnackbar(it); viewModel.dismissError() } 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( Scaffold(
snackbarHost = { SnackbarHost(snackbar) }, snackbarHost = { SnackbarHost(snackbar) },
containerColor = Color.Transparent, containerColor = Color.Transparent,
@ -107,28 +116,30 @@ fun CreateInviteScreen(
.padding(padding) .padding(padding)
.padding(horizontal = 28.dp), .padding(horizontal = 28.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Top
) { ) {
if (state.isLoading) { if (state.isLoading) {
Spacer(Modifier.height(160.dp))
CircularProgressIndicator(modifier = Modifier.size(40.dp)) CircularProgressIndicator(modifier = Modifier.size(40.dp))
} else if (state.inviteCode != null) { } else if (state.inviteCode != null) {
Spacer(Modifier.height(48.dp)) Spacer(Modifier.height(24.dp))
Text( Text(
"Invite your person", "Invite your person",
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineSmall,
color = SettingsInk, color = SettingsInk,
textAlign = TextAlign.Center textAlign = TextAlign.Center,
fontWeight = FontWeight.SemiBold
) )
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(6.dp))
Text( 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, style = MaterialTheme.typography.bodyMedium,
color = SettingsMuted, color = SettingsMuted,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(Modifier.height(40.dp)) Spacer(Modifier.height(28.dp))
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -140,11 +151,11 @@ fun CreateInviteScreen(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(32.dp), .padding(vertical = 28.dp, horizontal = 24.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = state.inviteCode!!.chunked(3).joinToString(" "), text = formattedCode!!,
style = MaterialTheme.typography.displaySmall, style = MaterialTheme.typography.displaySmall,
color = SettingsPrimaryDeep, color = SettingsPrimaryDeep,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@ -153,27 +164,60 @@ fun CreateInviteScreen(
} }
} }
Spacer(Modifier.height(20.dp)) Spacer(Modifier.height(16.dp))
Button( Row(
onClick = { modifier = Modifier.fillMaxWidth(),
clipboard.setText(AnnotatedString(state.inviteCode!!)) horizontalArrangement = Arrangement.spacedBy(12.dp)
scope.launch { snackbar.showSnackbar("Code copied!") }
},
modifier = Modifier.fillMaxWidth().height(52.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = SettingsPrimary,
contentColor = SettingsOnPrimary
)
) { ) {
Row( Button(
horizontalArrangement = Arrangement.Center, onClick = {
verticalAlignment = Alignment.CenterVertically 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)) Row(
Spacer(Modifier.width(8.dp)) horizontalArrangement = Arrangement.Center,
Text("Copy code", style = MaterialTheme.typography.labelLarge) 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 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( Text(
"Partner already has a code? Accept instead", "Partner already has a code? Accept instead",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = SettingsPrimaryDeep color = SettingsPrimaryDeep
) )
} }
Spacer(Modifier.height(32.dp))
} else { } else {
// Empty / error state when no code is available Spacer(Modifier.height(160.dp))
Spacer(Modifier.height(48.dp))
Text( Text(
"No invite code yet", "No invite code yet",
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,

View File

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel 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.SettingsOnPrimary
import app.closer.ui.settings.SettingsPrimary import app.closer.ui.settings.SettingsPrimary
import app.closer.ui.settings.SettingsPrimaryDeep import app.closer.ui.settings.SettingsPrimaryDeep
import app.closer.ui.settings.SettingsSoft
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun InviteConfirmScreen( fun InviteConfirmScreen(
inviteCode: String, inviteCode: String,
onNavigate: (String) -> Unit = {}, onNavigate: (String) -> Unit = {},
onBack: () -> Unit = {},
viewModel: InviteConfirmViewModel = hiltViewModel() viewModel: InviteConfirmViewModel = hiltViewModel()
) { ) {
val state by viewModel.uiState.collectAsState() val state by viewModel.uiState.collectAsState()
@ -64,6 +68,8 @@ fun InviteConfirmScreen(
state.error?.let { snackbar.showSnackbar(it); viewModel.dismissError() } state.error?.let { snackbar.showSnackbar(it); viewModel.dismissError() }
} }
val formattedCode = inviteCode.chunked(3).joinToString(" ")
Scaffold( Scaffold(
snackbarHost = { SnackbarHost(snackbar) }, snackbarHost = { SnackbarHost(snackbar) },
containerColor = Color.Transparent, containerColor = Color.Transparent,
@ -72,7 +78,7 @@ fun InviteConfirmScreen(
TopAppBar( TopAppBar(
title = {}, title = {},
navigationIcon = { navigationIcon = {
IconButton(onClick = { onNavigate(AppRoute.ACCEPT_INVITE) }) { IconButton(onClick = onBack) {
Icon( Icon(
Icons.AutoMirrored.Filled.ArrowBack, Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back", contentDescription = "Back",
@ -92,12 +98,13 @@ fun InviteConfirmScreen(
.padding(padding) .padding(padding)
.padding(horizontal = 28.dp), .padding(horizontal = 28.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Top
) { ) {
if (state.isLoading) { if (state.isLoading) {
Spacer(Modifier.height(160.dp))
CircularProgressIndicator(modifier = Modifier.size(40.dp)) CircularProgressIndicator(modifier = Modifier.size(40.dp))
} else { } else {
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(24.dp))
Text( Text(
"", "",
@ -105,15 +112,16 @@ fun InviteConfirmScreen(
color = SettingsPrimaryDeep color = SettingsPrimaryDeep
) )
Spacer(Modifier.height(20.dp)) Spacer(Modifier.height(16.dp))
Text( Text(
"Pair with ${state.inviterName}?", "Pair with ${state.inviterName}?",
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineSmall,
color = SettingsInk, color = SettingsInk,
textAlign = TextAlign.Center textAlign = TextAlign.Center,
fontWeight = FontWeight.SemiBold
) )
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(6.dp))
Text( Text(
"Once you confirm, you'll be connected and can start exploring questions together.", "Once you confirm, you'll be connected and can start exploring questions together.",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
@ -121,10 +129,10 @@ fun InviteConfirmScreen(
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(28.dp))
Text( Text(
"Code: $inviteCode", "Invite code $formattedCode",
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = SettingsMuted color = SettingsMuted
) )
@ -135,6 +143,7 @@ fun InviteConfirmScreen(
onClick = viewModel::confirmPairing, onClick = viewModel::confirmPairing,
enabled = !state.isConfirming, enabled = !state.isConfirming,
modifier = Modifier.fillMaxWidth().height(56.dp), modifier = Modifier.fillMaxWidth().height(56.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = SettingsPrimary, containerColor = SettingsPrimary,
contentColor = SettingsOnPrimary contentColor = SettingsOnPrimary
@ -150,15 +159,16 @@ fun InviteConfirmScreen(
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
TextButton(onClick = { onNavigate(AppRoute.ACCEPT_INVITE) }) { TextButton(
onClick = { onNavigate(AppRoute.ACCEPT_INVITE) },
modifier = Modifier.fillMaxWidth()
) {
Text( Text(
"That's not right — go back", "That's not right — enter a different code",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = SettingsPrimaryDeep color = SettingsPrimaryDeep
) )
} }
Spacer(Modifier.height(32.dp))
} }
} }
} }