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)
}
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
)
}

View File

@ -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(

View File

@ -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,14 +164,18 @@ fun CreateInviteScreen(
}
}
Spacer(Modifier.height(20.dp))
Spacer(Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
onClick = {
clipboard.setText(AnnotatedString(state.inviteCode!!))
scope.launch { snackbar.showSnackbar("Code copied!") }
},
modifier = Modifier.fillMaxWidth().height(52.dp),
modifier = Modifier.weight(1f).height(52.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = SettingsPrimary,
@ -173,7 +188,36 @@ fun CreateInviteScreen(
) {
Icon(Icons.Filled.ContentCopy, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text("Copy code", style = MaterialTheme.typography.labelLarge)
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",
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,

View File

@ -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))
}
}
}