Skip to content

[paymentsheet-example] Search box to filter settings in Playground #10900

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,45 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.stripe.android.customersheet.CustomerSheet
import com.stripe.android.customersheet.CustomerSheetResult
Expand Down Expand Up @@ -197,7 +222,17 @@ internal class PaymentSheetPlaygroundActivity :
)
}

var settingsSearchQuery by rememberSaveable { mutableStateOf("") }
PlaygroundTheme(
topBarContent = {
SearchSettingsField(
modifier = Modifier
.statusBarsPadding()
.padding(horizontal = 16.dp),
query = settingsSearchQuery,
onQueryChanged = { settingsSearchQuery = it }
)
},
content = {
playgroundState?.asPaymentState()?.endpoint?.let { customEndpoint ->
Text(
Expand All @@ -215,7 +250,10 @@ internal class PaymentSheetPlaygroundActivity :
)
}

SettingsUi(playgroundSettings = localPlaygroundSettings)
SettingsUi(
searchQuery = settingsSearchQuery,
playgroundSettings = localPlaygroundSettings,
)

AppearanceButton()

Expand All @@ -240,6 +278,14 @@ internal class PaymentSheetPlaygroundActivity :
)
}
}

Spacer(
Modifier.height(
WindowInsets.ime.exclude(WindowInsets.systemBars)
.asPaddingValues()
.calculateBottomPadding()
)
)
},
)

Expand Down Expand Up @@ -320,8 +366,10 @@ internal class PaymentSheetPlaygroundActivity :

@Composable
private fun ReloadButton(playgroundSettings: PlaygroundSettings) {
val keyboardController = LocalSoftwareKeyboardController.current
Button(
onClick = {
keyboardController?.hide()
viewModel.prepare(
playgroundSettings = playgroundSettings,
)
Expand Down Expand Up @@ -666,5 +714,73 @@ internal class PaymentSheetPlaygroundActivity :
}
}

@Composable
private fun SearchSettingsField(
query: String,
onQueryChanged: (String) -> Unit,
modifier: Modifier = Modifier,
) {
var hasFocus by remember { mutableStateOf(false) }
val keyboardController = LocalSoftwareKeyboardController.current
TextField(
modifier = modifier
.onFocusChanged { hasFocus = it.isFocused }
.onKeyEvent {
if (it.key == Key.Enter) {
keyboardController?.hide()
true
} else {
false
}
}
.fillMaxWidth(),
value = query,
placeholder = if (hasFocus) {
null
} else {
@Composable {
Text(text = "Search settings")
}
},
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = { keyboardController?.show() }
),
leadingIcon = {
Icon(
imageVector = Icons.Default.Search,
contentDescription = null,
)
},
trailingIcon = if (query.isEmpty()) {
null
} else {
@Composable {
IconButton(onClick = { onQueryChanged("") }) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
)
}
}
},
onValueChange = onQueryChanged,
)
}

@Preview(showBackground = true)
@Composable
private fun SearchSettingsFieldPreview() {
var query by remember { mutableStateOf("") }
SearchSettingsField(
query = query,
onQueryChanged = { query = it },
)
}

const val RELOAD_TEST_TAG = "RELOAD"
private const val PLAYGROUND_BOTTOM_BAR_LABEL = "PlaygroundBottomBar"
Original file line number Diff line number Diff line change
Expand Up @@ -22,41 +22,81 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.stripe.android.paymentsheet.example.playground.PlaygroundTheme
import kotlinx.coroutines.flow.StateFlow

@Composable
internal fun SettingsUi(playgroundSettings: PlaygroundSettings) {
internal fun SettingsUi(
playgroundSettings: PlaygroundSettings,
searchQuery: String,
) {
val configurationData by playgroundSettings.configurationData.collectAsState()
val displayableDefinitions by playgroundSettings.displayableDefinitions.collectAsState()
val filteredDefinitions = remember(displayableDefinitions, searchQuery) {
displayableDefinitions.filter { it.displayName.matchesQuery(searchQuery) }
}

Column(
modifier = Modifier.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row {
IntegrationTypeConfigurableSetting(
configurationData,
playgroundSettings::updateConfigurationData
)
}

for (settingDefinition in displayableDefinitions) {
if (IntegrationTypeSettingName.matchesQuery(searchQuery)) {
Row {
Setting(settingDefinition, playgroundSettings)
IntegrationTypeConfigurableSetting(
configurationData,
playgroundSettings::updateConfigurationData
)
}
}

for (settingDefinition in filteredDefinitions) {
Setting(settingDefinition, playgroundSettings)
}
}
}

private val WordBoundaryRegex by lazy(LazyThreadSafetyMode.NONE) { "\\s+".toRegex() }

/**
* Returns true if the string matches the query.
*/
private fun String.matchesQuery(query: String): Boolean {
if (query.isBlank()) {
return true
}

val words = this.trim().split(WordBoundaryRegex)
val queryWords = query.trim().split(WordBoundaryRegex)
return queryWords.all { queryWord ->
words.any { word -> word.startsWith(queryWord, ignoreCase = true) }
}
}

@Preview
@Composable
private fun SettingsUiPreview() {
PlaygroundTheme(
content = {
SettingsUi(
playgroundSettings = PlaygroundSettings.createFromDefaults(),
searchQuery = "",
)
},
bottomBarContent = {},
topBarContent = {}
)
}

@Composable
private fun <T> Setting(
settingDefinition: PlaygroundSettingDefinition.Displayable<T>,
playgroundSettings: PlaygroundSettings,
) {
val configurationData by playgroundSettings.configurationData.collectAsState()

val options = remember(configurationData) {
val options = remember(settingDefinition, configurationData) {
settingDefinition.createOptions(configurationData)
}

Expand Down Expand Up @@ -101,13 +141,15 @@ private fun <T> Setting(
}
}

private const val IntegrationTypeSettingName = "Integration Type"

@Composable
private fun IntegrationTypeConfigurableSetting(
configurationData: PlaygroundConfigurationData,
updateConfigurationData: (updater: (PlaygroundConfigurationData) -> PlaygroundConfigurationData) -> Unit
) {
DropdownSetting(
name = "Integration Type",
name = IntegrationTypeSettingName,
options = listOf(
PlaygroundSettingDefinition.Displayable.Option(
name = "Payment Sheet",
Expand Down Expand Up @@ -167,7 +209,9 @@ private fun <T> RadioButtonSetting(
)
}

val selectedOption = remember(value) { options.firstOrNull { it.value == value } }
val selectedOption = remember(options, value) {
options.firstOrNull { it.value == value }
}

Row {
options.forEach { option ->
Expand Down Expand Up @@ -212,7 +256,9 @@ internal fun <T> DropdownSetting(
onOptionChanged: (T) -> Unit,
) {
var expanded by remember { mutableStateOf(false) }
val selectedOption = remember(value) { options.firstOrNull { it.value == value } }
val selectedOption = remember(options, value) {
options.firstOrNull { it.value == value }
}

ExposedDropdownMenuBox(
expanded = expanded,
Expand Down
Loading