Skip to content

Commit

Permalink
DO NOT MERGE: androidApp: Initial support for tickets
Browse files Browse the repository at this point in the history
Signed-off-by: Aayush Gupta <[email protected]>
  • Loading branch information
theimpulson committed Oct 14, 2024
1 parent c516dcb commit e0227fd
Show file tree
Hide file tree
Showing 12 changed files with 475 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2024 OPass
* SPDX-License-Identifier: GPL-3.0-only
*/

package app.opass.ccip.android.ui.extensions

import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import app.opass.ccip.android.ui.navigation.Screen

fun NavHostController.popBackToEventScreen(eventId: String) {
navigate(Screen.Event(eventId)) {
popUpTo(graph.findStartDestination().id) { inclusive = true }
launchSingleTop = true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import app.opass.ccip.android.ui.screens.event.EventScreen
import app.opass.ccip.android.ui.screens.eventpreview.EventPreviewScreen
import app.opass.ccip.android.ui.screens.schedule.ScheduleScreen
import app.opass.ccip.android.ui.screens.session.SessionScreen
import app.opass.ccip.android.ui.screens.ticket.TicketScreen

@Composable
fun SetupNavGraph(navHostController: NavHostController, startDestination: Screen) {
Expand Down Expand Up @@ -51,6 +52,10 @@ fun SetupNavGraph(navHostController: NavHostController, startDestination: Screen
composable<Screen.Session> { backStackEntry ->
backStackEntry.toRoute<Screen.Session>().SessionScreen(navHostController)
}

composable<Screen.Ticket> { backStackEntry ->
backStackEntry.toRoute<Screen.Ticket>().TicketScreen(navHostController)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,10 @@ sealed class Screen(@StringRes val title: Int, @DrawableRes val icon: Int) {
title = R.string.session,
icon = R.drawable.ic_podium
)

@Serializable
data class Ticket(val eventId: String) : Screen(
title = R.string.ticket,
icon = R.drawable.ic_ticket
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,9 @@ fun Screen.Event.EventScreen(
FeatureItem(
label = stringResource(id = R.string.ticket),
iconRes = R.drawable.ic_ticket
)
) {
navHostController.navigate(Screen.Ticket(this@EventScreen.id))
}
}

FeatureType.VENUE -> {
Expand Down Expand Up @@ -260,6 +262,7 @@ private fun HeaderImage(logoUrl: String?) {
.padding(horizontal = 32.dp)
.aspectRatio(2.0f)
.heightIn(max = 180.dp)
.clip(RoundedCornerShape(10.dp))
.shimmer(logoUrl == null),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ import androidx.compose.ui.unit.sp
import androidx.core.content.edit
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import app.opass.ccip.android.R
import app.opass.ccip.android.ui.extensions.popBackToEventScreen
import app.opass.ccip.android.ui.extensions.sharedPreferences
import app.opass.ccip.android.ui.extensions.shimmer
import app.opass.ccip.android.ui.navigation.Screen
Expand Down Expand Up @@ -95,12 +95,7 @@ fun Screen.EventPreview.EventPreviewScreen(
EventPreviewItem(name = event.name, logoUrl = event.logoUrl) {
onEventSelected()
sharedPreferences.edit { putString(CURRENT_EVENT_ID, event.id) }
navHostController.navigate(Screen.Event(event.id)) {
popUpTo(navHostController.graph.findStartDestination().id) {
inclusive = true
}
launchSingleTop = true
}
navHostController.popBackToEventScreen(event.id)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
/*
* SPDX-FileCopyrightText: 2024 OPass
* SPDX-License-Identifier: GPL-3.0-only
*/

package app.opass.ccip.android.ui.screens.ticket

import android.content.pm.PackageManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import app.opass.ccip.android.R
import app.opass.ccip.android.ui.components.TopAppBar
import app.opass.ccip.android.ui.extensions.popBackToEventScreen
import app.opass.ccip.android.ui.extensions.shimmer
import app.opass.ccip.android.ui.navigation.Screen
import coil.compose.SubcomposeAsyncImage
import coil.request.CachePolicy
import coil.request.ImageRequest
import kotlinx.coroutines.android.awaitFrame

@Composable
fun Screen.Ticket.TicketScreen(
navHostController: NavHostController,
viewModel: TicketViewModel = hiltViewModel()
) {
val token by viewModel.token.collectAsStateWithLifecycle()
if (!token.isNullOrBlank()) {
ShowTicket(this, navHostController, viewModel)
} else {
RequestTicket(this, navHostController, viewModel)
}
}

@Composable
private fun ShowTicket(
screen: Screen.Ticket,
navHostController: NavHostController,
viewModel: TicketViewModel
) {
val token by viewModel.token.collectAsStateWithLifecycle()

Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = stringResource(screen.title),
navHostController = navHostController,
navigationIcon = {
IconButton(
onClick = { navHostController.popBackToEventScreen(screen.eventId) }
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null
)
}
},
actions = {
IconButton(onClick = { viewModel.logout(screen.eventId, token!!) }) {
Icon(
painter = painterResource(R.drawable.ic_logout),
contentDescription = stringResource(R.string.enter_token_manually_title)
)
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// TODO: Show current ticket as QR
}
}
}

@Composable
private fun RequestTicket(
screen: Screen.Ticket,
navHostController: NavHostController,
viewModel: TicketViewModel
) {
val context = LocalContext.current

val eventConfig by viewModel.eventConfig.collectAsStateWithLifecycle()
val isVerifying by viewModel.isVerifying.collectAsStateWithLifecycle()

var shouldShowVerificationDialog by rememberSaveable { mutableStateOf(false) }
var shouldShowManualEntryDialog by rememberSaveable { mutableStateOf(false) }

val startActivityForResult = rememberLauncherForActivityResult(
contract = PickVisualMedia(),
onResult = { uri ->
// TODO: Process the image
}
)

LaunchedEffect(key1 = Unit) { viewModel.getEventConfig(screen.eventId) }
LaunchedEffect(key1 = isVerifying) { shouldShowVerificationDialog = isVerifying }

if (isVerifying) {
VerificationDialog(onDismiss = { shouldShowVerificationDialog = false })
}

if (shouldShowManualEntryDialog) {
ManualEntryDialog(
onConfirm = {
shouldShowManualEntryDialog = false
viewModel.getAttendee(eventConfig!!.id, it)
},
onDismiss = { shouldShowManualEntryDialog = false }
)
}

Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = stringResource(screen.title),
navHostController = navHostController,
actions = {
IconButton(onClick = { shouldShowManualEntryDialog = true }) {
Icon(
painter = painterResource(R.drawable.ic_keyboard),
contentDescription = stringResource(R.string.enter_token_manually_title)
)
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
verticalArrangement = Arrangement.spacedBy(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
HeaderImage(logoUrl = eventConfig?.logoUrl)

// Actions
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally)
) {
if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) {
Button(onClick = {}) {
Text(text = stringResource(R.string.scan))
}
}
FilledTonalButton(
onClick = {
startActivityForResult.launch(
PickVisualMediaRequest(PickVisualMedia.ImageOnly)
)
}
) {
Icon(
painter = painterResource(R.drawable.ic_gallery),
contentDescription = null,
modifier = Modifier.padding(end = 5.dp)
)
Text(text = stringResource(R.string.upload_from_gallery))
}
}
}
}
}

@Composable
private fun HeaderImage(logoUrl: String?) {
SubcomposeAsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(logoUrl)
.placeholder(R.drawable.ic_landscape)
.error(R.drawable.ic_broken_image)
.crossfade(true)
.memoryCacheKey(logoUrl)
.diskCacheKey(logoUrl)
.diskCachePolicy(CachePolicy.ENABLED)
.memoryCachePolicy(CachePolicy.ENABLED)
.build(),
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.padding(horizontal = 32.dp)
.aspectRatio(2.0f)
.heightIn(max = 180.dp)
.clip(RoundedCornerShape(10.dp))
.shimmer(logoUrl == null),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary)
)
}

@Composable
private fun VerificationDialog(onDismiss: () -> Unit = {}) {
Dialog(onDismissRequest = { onDismiss() }) {
CircularProgressIndicator(modifier = Modifier.requiredWidth(48.dp))
}
}

@Composable
private fun ManualEntryDialog(onConfirm: (token: String) -> Unit = {}, onDismiss: () -> Unit = {}) {
var token by rememberSaveable { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }

LaunchedEffect(focusRequester) {
awaitFrame()
focusRequester.requestFocus()
}

AlertDialog(
title = { Text(text = stringResource(R.string.enter_token_manually_title)) },
text = {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(text = stringResource(R.string.enter_token_manually_desc))
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
value = token,
onValueChange = { token = it },
shape = RoundedCornerShape(10.dp)
)
}
},
onDismissRequest = { onDismiss() },
confirmButton = {
TextButton (
onClick = { onConfirm(token) },
enabled = token.isNotBlank()
) {
Text(text = stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = { onDismiss() }) {
Text(text = stringResource(android.R.string.cancel))
}
}
)
}
Loading

0 comments on commit e0227fd

Please sign in to comment.