From d109f7fcd05734e2dace3d15ffd5dfbb68499afd Mon Sep 17 00:00:00 2001 From: null Date: Wed, 17 Jun 2026 00:16:42 -0500 Subject: [PATCH] =?UTF-8?q?fix(ui):=20partner=20invite=20flow=20polish=20?= =?UTF-8?q?=E2=80=94=20compact=20layout,=20share=20button,=20back=20afford?= =?UTF-8?q?ances,=20secondary=20route=20styling=20(batch=207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../closer/core/navigation/AppNavigation.kt | 8 +- .../closer/ui/pairing/AcceptInviteScreen.kt | 30 +++-- .../closer/ui/pairing/CreateInviteScreen.kt | 112 ++++++++++++------ .../closer/ui/pairing/InviteConfirmScreen.kt | 36 ++++-- 4 files changed, 126 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/app/closer/core/navigation/AppNavigation.kt b/app/src/main/java/app/closer/core/navigation/AppNavigation.kt index 65a15564..cbc16a0a 100644 --- a/app/src/main/java/app/closer/core/navigation/AppNavigation.kt +++ b/app/src/main/java/app/closer/core/navigation/AppNavigation.kt @@ -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 ) } diff --git a/app/src/main/java/app/closer/ui/pairing/AcceptInviteScreen.kt b/app/src/main/java/app/closer/ui/pairing/AcceptInviteScreen.kt index fe54e237..4e014816 100644 --- a/app/src/main/java/app/closer/ui/pairing/AcceptInviteScreen.kt +++ b/app/src/main/java/app/closer/ui/pairing/AcceptInviteScreen.kt @@ -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( diff --git a/app/src/main/java/app/closer/ui/pairing/CreateInviteScreen.kt b/app/src/main/java/app/closer/ui/pairing/CreateInviteScreen.kt index 80997ae8..444031f4 100644 --- a/app/src/main/java/app/closer/ui/pairing/CreateInviteScreen.kt +++ b/app/src/main/java/app/closer/ui/pairing/CreateInviteScreen.kt @@ -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, diff --git a/app/src/main/java/app/closer/ui/pairing/InviteConfirmScreen.kt b/app/src/main/java/app/closer/ui/pairing/InviteConfirmScreen.kt index 70f39e6c..ac17b970 100644 --- a/app/src/main/java/app/closer/ui/pairing/InviteConfirmScreen.kt +++ b/app/src/main/java/app/closer/ui/pairing/InviteConfirmScreen.kt @@ -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)) } } }