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 <aayushgupta219@gmail.com>
  • Loading branch information
theimpulson committed Oct 14, 2024
1 parent 00d094a commit 7189e28
Show file tree
Hide file tree
Showing 22 changed files with 753 additions and 35 deletions.
5 changes: 5 additions & 0 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,9 @@ dependencies {
ksp(libs.google.hilt.android.compiler)
implementation(libs.androidx.hilt.navigation)
implementation(libs.google.hilt.android.core)

// ZXing/Camera (QR)
implementation(libs.androidx.camera)
implementation(libs.zxing.core)
implementation(libs.zxing.cpp)
}
11 changes: 11 additions & 0 deletions androidApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,18 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<!-- For scanning QR codes -->
<uses-feature
android:name="android.hardware.camera.any"
android:required="false" />

<!-- For suggesting WiFi connections -->
<uses-feature
android:name="android.hardware.wifi"
android:required="false" />

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />

<application
android:name=".OPassApp"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.navigation.compose.rememberNavController
import app.opass.ccip.android.ui.extensions.currentEventId
import app.opass.ccip.android.ui.extensions.sharedPreferences
import app.opass.ccip.android.ui.navigation.Screen
import app.opass.ccip.android.ui.navigation.SetupNavGraph
import app.opass.ccip.android.ui.theme.OPassTheme
import app.opass.ccip.android.utils.Preferences.CURRENT_EVENT_ID
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
Expand All @@ -24,7 +24,7 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
super.onCreate(savedInstanceState)

val currentEventId = sharedPreferences.getString(CURRENT_EVENT_ID, null)
val currentEventId = sharedPreferences.currentEventId

setContent {
OPassTheme {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

package app.opass.ccip.android.ui.components

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
Expand All @@ -15,25 +17,41 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController

@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun TopAppBar(
title: String = String(),
subtitle: String = String(),
navHostController: NavHostController? = null,
navigationIcon: @Composable () -> Unit = { DefaultNavigationIcon(navHostController) },
actions: @Composable() (RowScope.() -> Unit) = {}
actions: @Composable (RowScope.() -> Unit) = {}
) {
CenterAlignedTopAppBar(
title = {
Text(
text = title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleLarge
)
Column(
verticalArrangement = Arrangement.spacedBy(5.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleLarge
)
if (subtitle.isNotBlank()) {
Text(
text = subtitle,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall
)
}
}
},
navigationIcon = navigationIcon,
actions = actions
Expand Down
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
@@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2024 OPass
* SPDX-License-Identifier: GPL-3.0-only
*/

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

import android.content.SharedPreferences
import androidx.core.content.edit

private const val CURRENT_EVENT_ID = "CURRENT_EVENT_ID"
private const val TOKEN = "TOKEN"

val SharedPreferences.currentEventId: String?
get() = this.getString(CURRENT_EVENT_ID, null)

fun SharedPreferences.saveCurrentEventId(eventId: String) {
return this.edit { putString(CURRENT_EVENT_ID, eventId) }
}

fun SharedPreferences.getToken(eventId: String): String? {
return this.getString("${eventId}_$TOKEN", null)
}

fun SharedPreferences.saveToken(eventId: String, token: String) {
return this.edit { putString("${eventId}_$TOKEN", token) }
}

fun SharedPreferences.removeToken(eventId: String) {
return this.edit { remove("${eventId}_$TOKEN") }
}
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 @@ -77,6 +77,7 @@ fun Screen.Event.EventScreen(
val windowWidth = currentWindowAdaptiveInfo().windowSizeClass.windowWidthSizeClass
val context = LocalContext.current
val eventConfig by viewModel.eventConfig.collectAsStateWithLifecycle()
val attendee by viewModel.attendee.collectAsStateWithLifecycle()

var shouldShowBottomSheet by rememberSaveable { mutableStateOf(false) }

Expand All @@ -87,6 +88,7 @@ fun Screen.Event.EventScreen(
topBar = {
TopAppBar(
title = eventConfig?.name ?: String(),
subtitle = attendee?.userId ?: String(),
navigationIcon = {
IconButton(onClick = { shouldShowBottomSheet = true }) {
Icon(
Expand Down Expand Up @@ -127,8 +129,14 @@ fun Screen.Event.EventScreen(
maxItemsInEachRow = if (windowWidth == WindowWidthSizeClass.COMPACT) 4 else 6,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
// TODO: Show and hide features based on roles
eventConfig!!.features.fastForEach { feature ->

// Return early if feature is limited to certain attendee roles
// Roles requires attendee to be logged in by verifying their ticket
if (!feature.roles.isNullOrEmpty() && !feature.roles!!.contains(attendee?.role)) {
return@fastForEach
}

when (feature.type) {
FeatureType.ANNOUNCEMENT -> {
FeatureItem(
Expand Down Expand Up @@ -202,7 +210,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 +270,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 @@ -5,13 +5,18 @@

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

import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.opass.ccip.android.ui.extensions.getToken
import app.opass.ccip.android.ui.extensions.sharedPreferences
import app.opass.ccip.helpers.PortalHelper
import app.opass.ccip.network.models.eventconfig.EventConfig
import app.opass.ccip.network.models.fastpass.Attendee
import app.opass.ccip.network.models.schedule.Schedule
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand All @@ -21,7 +26,8 @@ import java.text.SimpleDateFormat
@HiltViewModel
class EventViewModel @Inject constructor(
val sdf: SimpleDateFormat,
private val portalHelper: PortalHelper
private val portalHelper: PortalHelper,
@ApplicationContext private val context: Context
): ViewModel() {

private val TAG = EventViewModel::class.java.simpleName
Expand All @@ -32,6 +38,9 @@ class EventViewModel @Inject constructor(
private val _schedule: MutableStateFlow<Schedule?> = MutableStateFlow(null)
val schedule = _schedule.asStateFlow()

private val _attendee: MutableStateFlow<Attendee?> = MutableStateFlow(null)
val attendee = _attendee.asStateFlow()

private val _isRefreshing = MutableStateFlow(false)
val isRefreshing = _isRefreshing.asStateFlow()

Expand All @@ -40,6 +49,9 @@ class EventViewModel @Inject constructor(
try {
_isRefreshing.value = true
_eventConfig.value = portalHelper.getEventConfig(eventId, forceReload)

// Fetch attendee as well
getAttendee(eventId, forceReload)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch event config", exception)
_eventConfig.value = null
Expand All @@ -59,4 +71,20 @@ class EventViewModel @Inject constructor(
}
}
}

private fun getAttendee(eventId: String, forceReload: Boolean = false) {
viewModelScope.launch {
try {
val token = context.sharedPreferences.getToken(eventId)
if (token != null) {
_attendee.value = portalHelper.getAttendee(eventId, token, forceReload)
} else {
_attendee.value = null
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch attendee", exception)
_attendee.value = null
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,15 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
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.saveCurrentEventId
import app.opass.ccip.android.ui.extensions.sharedPreferences
import app.opass.ccip.android.ui.extensions.shimmer
import app.opass.ccip.android.ui.navigation.Screen
import app.opass.ccip.android.utils.Preferences.CURRENT_EVENT_ID
import app.opass.ccip.network.models.event.Event
import coil.compose.SubcomposeAsyncImage
import coil.request.CachePolicy
Expand Down Expand Up @@ -94,13 +93,8 @@ fun Screen.EventPreview.EventPreviewScreen(
items(items = list!!, key = { e -> e.id }) { event: Event ->
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
}
sharedPreferences.saveCurrentEventId(event.id)
navHostController.popBackToEventScreen(event.id)
}
}
}
Expand Down Expand Up @@ -160,7 +154,7 @@ fun Screen.EventPreview.EventPreviewScreen(
key = { e -> e.id }
) { event: Event ->
EventPreviewItem(name = event.name, logoUrl = event.logoUrl) {
sharedPreferences.edit { putString(CURRENT_EVENT_ID, event.id) }
sharedPreferences.saveCurrentEventId(event.id)
navHostController.navigate(Screen.Event(event.id))
}
}
Expand Down
Loading

0 comments on commit 7189e28

Please sign in to comment.