Skip to content

Commit 483083c

Browse files
committed
androidApp: Initial support for tickets [1/3]
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
1 parent 00d094a commit 483083c

File tree

22 files changed

+767
-35
lines changed

22 files changed

+767
-35
lines changed

androidApp/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,9 @@ dependencies {
8989
ksp(libs.google.hilt.android.compiler)
9090
implementation(libs.androidx.hilt.navigation)
9191
implementation(libs.google.hilt.android.core)
92+
93+
// ZXing/Camera (QR)
94+
implementation(libs.androidx.camera)
95+
implementation(libs.zxing.core)
96+
implementation(libs.zxing.cpp)
9297
}

androidApp/src/main/AndroidManifest.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,18 @@
66
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
77
xmlns:tools="http://schemas.android.com/tools">
88

9+
<!-- For scanning QR codes -->
10+
<uses-feature
11+
android:name="android.hardware.camera.any"
12+
android:required="false" />
13+
14+
<!-- For suggesting WiFi connections -->
15+
<uses-feature
16+
android:name="android.hardware.wifi"
17+
android:required="false" />
18+
919
<uses-permission android:name="android.permission.INTERNET" />
20+
<uses-permission android:name="android.permission.CAMERA" />
1021

1122
<application
1223
android:name=".OPassApp"

androidApp/src/main/java/app/opass/ccip/android/MainActivity.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import androidx.activity.ComponentActivity
1010
import androidx.activity.compose.setContent
1111
import androidx.activity.enableEdgeToEdge
1212
import androidx.navigation.compose.rememberNavController
13+
import app.opass.ccip.android.ui.extensions.currentEventId
1314
import app.opass.ccip.android.ui.extensions.sharedPreferences
1415
import app.opass.ccip.android.ui.navigation.Screen
1516
import app.opass.ccip.android.ui.navigation.SetupNavGraph
1617
import app.opass.ccip.android.ui.theme.OPassTheme
17-
import app.opass.ccip.android.utils.Preferences.CURRENT_EVENT_ID
1818
import dagger.hilt.android.AndroidEntryPoint
1919

2020
@AndroidEntryPoint
@@ -24,7 +24,7 @@ class MainActivity : ComponentActivity() {
2424
enableEdgeToEdge()
2525
super.onCreate(savedInstanceState)
2626

27-
val currentEventId = sharedPreferences.getString(CURRENT_EVENT_ID, null)
27+
val currentEventId = sharedPreferences.currentEventId
2828

2929
setContent {
3030
OPassTheme {

androidApp/src/main/java/app/opass/ccip/android/ui/components/TopAppBar.kt

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

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

8+
import androidx.compose.foundation.layout.Arrangement
9+
import androidx.compose.foundation.layout.Column
810
import androidx.compose.foundation.layout.RowScope
911
import androidx.compose.material.icons.Icons
1012
import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -15,25 +17,41 @@ import androidx.compose.material3.IconButton
1517
import androidx.compose.material3.MaterialTheme
1618
import androidx.compose.material3.Text
1719
import androidx.compose.runtime.Composable
20+
import androidx.compose.ui.Alignment
1821
import androidx.compose.ui.text.style.TextOverflow
22+
import androidx.compose.ui.unit.dp
1923
import androidx.navigation.NavHostController
2024

2125
@Composable
2226
@OptIn(ExperimentalMaterial3Api::class)
2327
fun TopAppBar(
2428
title: String = String(),
29+
subtitle: String = String(),
2530
navHostController: NavHostController? = null,
2631
navigationIcon: @Composable () -> Unit = { DefaultNavigationIcon(navHostController) },
27-
actions: @Composable() (RowScope.() -> Unit) = {}
32+
actions: @Composable (RowScope.() -> Unit) = {}
2833
) {
2934
CenterAlignedTopAppBar(
3035
title = {
31-
Text(
32-
text = title,
33-
maxLines = 1,
34-
overflow = TextOverflow.Ellipsis,
35-
style = MaterialTheme.typography.titleLarge
36-
)
36+
Column(
37+
verticalArrangement = Arrangement.spacedBy(5.dp),
38+
horizontalAlignment = Alignment.CenterHorizontally
39+
) {
40+
Text(
41+
text = title,
42+
maxLines = 1,
43+
overflow = TextOverflow.Ellipsis,
44+
style = MaterialTheme.typography.titleLarge
45+
)
46+
if (subtitle.isNotBlank()) {
47+
Text(
48+
text = subtitle,
49+
maxLines = 1,
50+
overflow = TextOverflow.Ellipsis,
51+
style = MaterialTheme.typography.bodySmall
52+
)
53+
}
54+
}
3755
},
3856
navigationIcon = navigationIcon,
3957
actions = actions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2024 OPass
3+
* SPDX-License-Identifier: GPL-3.0-only
4+
*/
5+
6+
package app.opass.ccip.android.ui.extensions
7+
8+
import androidx.navigation.NavGraph.Companion.findStartDestination
9+
import androidx.navigation.NavHostController
10+
import app.opass.ccip.android.ui.navigation.Screen
11+
12+
fun NavHostController.popBackToEventScreen(eventId: String) {
13+
navigate(Screen.Event(eventId)) {
14+
popUpTo(graph.findStartDestination().id) { inclusive = true }
15+
launchSingleTop = true
16+
}
17+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2024 OPass
3+
* SPDX-License-Identifier: GPL-3.0-only
4+
*/
5+
6+
package app.opass.ccip.android.ui.extensions
7+
8+
import android.content.SharedPreferences
9+
import androidx.core.content.edit
10+
11+
private const val CURRENT_EVENT_ID = "CURRENT_EVENT_ID"
12+
private const val TOKEN = "TOKEN"
13+
private const val AUTO_BRIGHTEN = "AUTO_BRIGHTEN"
14+
15+
val SharedPreferences.autoBrighten: Boolean
16+
get() = this.getBoolean(AUTO_BRIGHTEN, true)
17+
18+
val SharedPreferences.currentEventId: String?
19+
get() = this.getString(CURRENT_EVENT_ID, null)
20+
21+
fun SharedPreferences.saveCurrentEventId(eventId: String) {
22+
return this.edit { putString(CURRENT_EVENT_ID, eventId) }
23+
}
24+
25+
fun SharedPreferences.getToken(eventId: String): String? {
26+
return this.getString("${eventId}_$TOKEN", null)
27+
}
28+
29+
fun SharedPreferences.saveToken(eventId: String, token: String) {
30+
return this.edit { putString("${eventId}_$TOKEN", token) }
31+
}
32+
33+
fun SharedPreferences.removeToken(eventId: String) {
34+
return this.edit { remove("${eventId}_$TOKEN") }
35+
}
36+
37+
fun SharedPreferences.autoBrighten(enabled: Boolean) {
38+
return this.edit { putBoolean(AUTO_BRIGHTEN, enabled) }
39+
}

androidApp/src/main/java/app/opass/ccip/android/ui/navigation/NavGraph.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import app.opass.ccip.android.ui.screens.event.EventScreen
2121
import app.opass.ccip.android.ui.screens.eventpreview.EventPreviewScreen
2222
import app.opass.ccip.android.ui.screens.schedule.ScheduleScreen
2323
import app.opass.ccip.android.ui.screens.session.SessionScreen
24+
import app.opass.ccip.android.ui.screens.ticket.TicketScreen
2425

2526
@Composable
2627
fun SetupNavGraph(navHostController: NavHostController, startDestination: Screen) {
@@ -51,6 +52,10 @@ fun SetupNavGraph(navHostController: NavHostController, startDestination: Screen
5152
composable<Screen.Session> { backStackEntry ->
5253
backStackEntry.toRoute<Screen.Session>().SessionScreen(navHostController)
5354
}
55+
56+
composable<Screen.Ticket> { backStackEntry ->
57+
backStackEntry.toRoute<Screen.Ticket>().TicketScreen(navHostController)
58+
}
5459
}
5560
}
5661

androidApp/src/main/java/app/opass/ccip/android/ui/navigation/Screen.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,10 @@ sealed class Screen(@StringRes val title: Int, @DrawableRes val icon: Int) {
3636
title = R.string.session,
3737
icon = R.drawable.ic_podium
3838
)
39+
40+
@Serializable
41+
data class Ticket(val eventId: String) : Screen(
42+
title = R.string.ticket,
43+
icon = R.drawable.ic_ticket
44+
)
3945
}

androidApp/src/main/java/app/opass/ccip/android/ui/screens/event/EventScreen.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ fun Screen.Event.EventScreen(
7777
val windowWidth = currentWindowAdaptiveInfo().windowSizeClass.windowWidthSizeClass
7878
val context = LocalContext.current
7979
val eventConfig by viewModel.eventConfig.collectAsStateWithLifecycle()
80+
val attendee by viewModel.attendee.collectAsStateWithLifecycle()
8081

8182
var shouldShowBottomSheet by rememberSaveable { mutableStateOf(false) }
8283

@@ -87,6 +88,7 @@ fun Screen.Event.EventScreen(
8788
topBar = {
8889
TopAppBar(
8990
title = eventConfig?.name ?: String(),
91+
subtitle = attendee?.userId ?: String(),
9092
navigationIcon = {
9193
IconButton(onClick = { shouldShowBottomSheet = true }) {
9294
Icon(
@@ -127,8 +129,14 @@ fun Screen.Event.EventScreen(
127129
maxItemsInEachRow = if (windowWidth == WindowWidthSizeClass.COMPACT) 4 else 6,
128130
verticalArrangement = Arrangement.spacedBy(8.dp),
129131
) {
130-
// TODO: Show and hide features based on roles
131132
eventConfig!!.features.fastForEach { feature ->
133+
134+
// Return early if feature is limited to certain attendee roles
135+
// Roles requires attendee to be logged in by verifying their ticket
136+
if (!feature.roles.isNullOrEmpty() && !feature.roles!!.contains(attendee?.role)) {
137+
return@fastForEach
138+
}
139+
132140
when (feature.type) {
133141
FeatureType.ANNOUNCEMENT -> {
134142
FeatureItem(
@@ -202,7 +210,9 @@ fun Screen.Event.EventScreen(
202210
FeatureItem(
203211
label = stringResource(id = R.string.ticket),
204212
iconRes = R.drawable.ic_ticket
205-
)
213+
) {
214+
navHostController.navigate(Screen.Ticket(this@EventScreen.id))
215+
}
206216
}
207217

208218
FeatureType.VENUE -> {
@@ -260,6 +270,7 @@ private fun HeaderImage(logoUrl: String?) {
260270
.padding(horizontal = 32.dp)
261271
.aspectRatio(2.0f)
262272
.heightIn(max = 180.dp)
273+
.clip(RoundedCornerShape(10.dp))
263274
.shimmer(logoUrl == null),
264275
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary)
265276
)

androidApp/src/main/java/app/opass/ccip/android/ui/screens/event/EventViewModel.kt

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@
55

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

8+
import android.content.Context
89
import android.util.Log
910
import androidx.lifecycle.ViewModel
1011
import androidx.lifecycle.viewModelScope
12+
import app.opass.ccip.android.ui.extensions.getToken
13+
import app.opass.ccip.android.ui.extensions.sharedPreferences
1114
import app.opass.ccip.helpers.PortalHelper
1215
import app.opass.ccip.network.models.eventconfig.EventConfig
16+
import app.opass.ccip.network.models.fastpass.Attendee
1317
import app.opass.ccip.network.models.schedule.Schedule
1418
import dagger.hilt.android.lifecycle.HiltViewModel
19+
import dagger.hilt.android.qualifiers.ApplicationContext
1520
import javax.inject.Inject
1621
import kotlinx.coroutines.flow.MutableStateFlow
1722
import kotlinx.coroutines.flow.asStateFlow
@@ -21,7 +26,8 @@ import java.text.SimpleDateFormat
2126
@HiltViewModel
2227
class EventViewModel @Inject constructor(
2328
val sdf: SimpleDateFormat,
24-
private val portalHelper: PortalHelper
29+
private val portalHelper: PortalHelper,
30+
@ApplicationContext private val context: Context
2531
): ViewModel() {
2632

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

41+
private val _attendee: MutableStateFlow<Attendee?> = MutableStateFlow(null)
42+
val attendee = _attendee.asStateFlow()
43+
3544
private val _isRefreshing = MutableStateFlow(false)
3645
val isRefreshing = _isRefreshing.asStateFlow()
3746

@@ -40,6 +49,9 @@ class EventViewModel @Inject constructor(
4049
try {
4150
_isRefreshing.value = true
4251
_eventConfig.value = portalHelper.getEventConfig(eventId, forceReload)
52+
53+
// Fetch attendee as well
54+
getAttendee(eventId, forceReload)
4355
} catch (exception: Exception) {
4456
Log.e(TAG, "Failed to fetch event config", exception)
4557
_eventConfig.value = null
@@ -59,4 +71,20 @@ class EventViewModel @Inject constructor(
5971
}
6072
}
6173
}
74+
75+
private fun getAttendee(eventId: String, forceReload: Boolean = false) {
76+
viewModelScope.launch {
77+
try {
78+
val token = context.sharedPreferences.getToken(eventId)
79+
if (token != null) {
80+
_attendee.value = portalHelper.getAttendee(eventId, token, forceReload)
81+
} else {
82+
_attendee.value = null
83+
}
84+
} catch (exception: Exception) {
85+
Log.e(TAG, "Failed to fetch attendee", exception)
86+
_attendee.value = null
87+
}
88+
}
89+
}
6290
}

androidApp/src/main/java/app/opass/ccip/android/ui/screens/eventpreview/EventPreviewScreen.kt

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,15 @@ import androidx.compose.ui.res.painterResource
4848
import androidx.compose.ui.res.stringResource
4949
import androidx.compose.ui.unit.dp
5050
import androidx.compose.ui.unit.sp
51-
import androidx.core.content.edit
5251
import androidx.hilt.navigation.compose.hiltViewModel
5352
import androidx.lifecycle.compose.collectAsStateWithLifecycle
54-
import androidx.navigation.NavGraph.Companion.findStartDestination
5553
import androidx.navigation.NavHostController
5654
import app.opass.ccip.android.R
55+
import app.opass.ccip.android.ui.extensions.popBackToEventScreen
56+
import app.opass.ccip.android.ui.extensions.saveCurrentEventId
5757
import app.opass.ccip.android.ui.extensions.sharedPreferences
5858
import app.opass.ccip.android.ui.extensions.shimmer
5959
import app.opass.ccip.android.ui.navigation.Screen
60-
import app.opass.ccip.android.utils.Preferences.CURRENT_EVENT_ID
6160
import app.opass.ccip.network.models.event.Event
6261
import coil.compose.SubcomposeAsyncImage
6362
import coil.request.CachePolicy
@@ -94,13 +93,8 @@ fun Screen.EventPreview.EventPreviewScreen(
9493
items(items = list!!, key = { e -> e.id }) { event: Event ->
9594
EventPreviewItem(name = event.name, logoUrl = event.logoUrl) {
9695
onEventSelected()
97-
sharedPreferences.edit { putString(CURRENT_EVENT_ID, event.id) }
98-
navHostController.navigate(Screen.Event(event.id)) {
99-
popUpTo(navHostController.graph.findStartDestination().id) {
100-
inclusive = true
101-
}
102-
launchSingleTop = true
103-
}
96+
sharedPreferences.saveCurrentEventId(event.id)
97+
navHostController.popBackToEventScreen(event.id)
10498
}
10599
}
106100
}
@@ -160,7 +154,7 @@ fun Screen.EventPreview.EventPreviewScreen(
160154
key = { e -> e.id }
161155
) { event: Event ->
162156
EventPreviewItem(name = event.name, logoUrl = event.logoUrl) {
163-
sharedPreferences.edit { putString(CURRENT_EVENT_ID, event.id) }
157+
sharedPreferences.saveCurrentEventId(event.id)
164158
navHostController.navigate(Screen.Event(event.id))
165159
}
166160
}

0 commit comments

Comments
 (0)