Skip to content

Commit 2325171

Browse files
authored
Improve permissions checking/handling (#61)
* Initial work * Support parsing user permissions from the API * Ignore Sentry events in debug builds Fixes #59 * A lot of work on permissions enforcement * Work on insufficient permissions composable * Add required permissions dialog * Finish up permissions improvements * Fully implement permissions error alert * Callout UI work * Warning callout if you can't see hidden teams * Detekt style fixes, remove TODOs * Style * Add Detekt to concourse PR pipeline steps * Fix detekt-check inputs * Fix run-detekt task input/output mapping * Fix path * Minor copy edit * Add release management info to readme * Flesh out release management instructions * Address review comments * hopefully maybe fix callout padding * Increase overall padding on missing teams callout * Style * Add detekt to build-main job Co-authored-by: Evan Strat <[email protected]>
1 parent fe8c64f commit 2325171

35 files changed

+985
-160
lines changed

README.md

+58-2
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ Detekt is used for linting Kotlin code. The recommended command to run it is
7575

7676
_(Windows)_
7777
```bash
78-
gradlew detektAll -PdetektAutoFix=true
78+
./gradlew detektAll -PdetektAutoFix=true
7979
```
8080

8181
_(*nix)_
@@ -99,4 +99,60 @@ You can install the Fastlane dependencies by running `bundle install` from the r
9999
`which sentry-cli` is run. `which` does not exist on Windows, but the similarly named `where`
100100
command does, so you can get around any errors stemming from this by aliasing the command `where`
101101
to `which`.
102-
- You need to install [GitVersion](https://github.com/GitTools/GitVersion) yourself.
102+
- You need to install [GitVersion](https://github.com/GitTools/GitVersion) yourself.
103+
104+
## Release management
105+
106+
Below are some instructions on the MyRoboJackets Android release process. Note that you will need
107+
additional permissions on this repo and the MyRoboJackets Android Google Play application to
108+
fully carry out this step:
109+
- Permission on this repo to create tags and releases (write access)
110+
- Permissions on the MyRoboJackets Android Google Play app. At a minimum:
111+
- `Release apps to testing tracks`
112+
- `Release to production, exclude devices, and use Play App Signing`
113+
114+
App releases don't have to perfectly coincide with PRs being merged, especially if two PRs are
115+
merged in close proximity. Our Concourse pipeline has jobs to automatically handle building,
116+
signing, and uploading production releases of the app.
117+
118+
1. After you've merged all PRs to be included in the release, ensure the `.update-priority`
119+
file is set correctly according to the table below. Use priority 2 as the default. If you want to
120+
use priority 4 or 5, post in #apiary-mobile first.
121+
1. Update priority affects if and how often users receive in-app update prompts to update the
122+
app to the latest version.
123+
124+
| Update priority | Description | Examples | Update timeline for users |
125+
| --- | --- |--- | --- |
126+
| 0/1/2 | Very low or low priority | UI touchups that don't impact functionality, releases with options to opt-in to beta features | No prompt initially. Optional prompt starting 14 days after release. Immediate update 21 days after release. |
127+
| 3 | Medium priority | Medium-priority bug fixes, performance improvements, non-time-sensitive feature launches | No prompts for the first 3 days. Optional starting 4 days after release. Immediate update 21 days after release. |
128+
| 4 | High priority or time-sensitive | High priority bug fixes, time-sensitive feature launches | Optional for the first 24 hours, then immediate. |
129+
| 5 | Critical bug fixes | Crashes/bugs impacting major features, urgent vulnerabilities | Immediate update required. |
130+
131+
2. Create a new release on `main` using a tag with a name like `v1.0.0`. Use semantic versioning
132+
to determine how to increment the version number.
133+
1. Go to https://github.com/RoboJackets/apiary-mobile/releases
134+
2. Press the **Draft a new release** button.
135+
3. Decide on the new version tag; it **must** start with `v`. In general, you should increment
136+
the previous release's version using [semantic versioning](https://semver.org/) guidelines (most
137+
releases will be a 0.1.0 (minor) or 0.0.1 (patch) increment). Press **Choose a tag**, then enter the
138+
new tag name to create it on publish.
139+
4. Leave **Release title** blank. Instead, press **Generate release notes**. The release title
140+
and description should automatically fill in with the changes since the last release.
141+
1. In general, you shouldn't need to manually set the value of the `Previous tag` field,
142+
unless the release notes seem incorrect.
143+
3. When you publish the release (which creates a new tag), a Concourse job to create a draft Google
144+
Play internal test release will begin shortly.
145+
1. If it doesn't start, a common reason is that it wasn't alphabetically the latest tag, so the
146+
[`tagged-release`](https://concourse.sandbox.aws.robojackets.net/teams/information-technology/pipelines/apiary-mobile/resources/tagged-release)
147+
resource didn't trigger a new build. We can disable old versions of the resource to trigger a new
148+
build.
149+
4. If the Concourse [build-release job](https://concourse.sandbox.aws.robojackets.net/teams/information-technology/pipelines/apiary-mobile/jobs/build-release)
150+
finishes successfully, you'll find a new draft release on the Internal Test track in Google Play.
151+
At this point, you should do some small QA efforts to verify the new build. Post in #apiary-mobile
152+
to have some people help you test. Note that access to the internal test track must be granted via
153+
the Google Play Console.
154+
1. Internal testers may need to uninstall the app to see the update if it was recently published.
155+
6. If no issues are found, it's time to release! Promote the build to the Production track in
156+
Google Play, add release notes, and save the release.
157+
7. Google Play typically spends a day or two reviewing the release, then makes it available. In
158+
general, expect it to take at least ~24 hours for a production release to be available to users.

app/src/main/java/org/robojackets/apiary/ApiaryMobileApplication.kt

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import timber.log.Timber
1414
class ApiaryMobileApplication : Application() {
1515
override fun onCreate() {
1616
super.onCreate()
17+
1718
if (BuildConfig.DEBUG) {
1819
Timber.plant(Timber.DebugTree())
1920
} else {

app/src/main/java/org/robojackets/apiary/MainActivity.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ import org.robojackets.apiary.attendance.AttendanceScreen
3737
import org.robojackets.apiary.attendance.ui.AttendableSelectionScreen
3838
import org.robojackets.apiary.attendance.ui.AttendableTypeSelectionScreen
3939
import org.robojackets.apiary.auth.AuthStateManager
40-
import org.robojackets.apiary.auth.AuthenticationScreen
4140
import org.robojackets.apiary.auth.oauth2.AuthManager
41+
import org.robojackets.apiary.auth.ui.AuthenticationScreen
4242
import org.robojackets.apiary.base.GlobalSettings
4343
import org.robojackets.apiary.base.model.AttendableType
4444
import org.robojackets.apiary.base.ui.nfc.NfcRequired

app/src/main/java/org/robojackets/apiary/di/MainActivityModule.kt

+24-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package org.robojackets.apiary.di
33
import android.content.Context
44
import com.nxp.nfclib.NxpNfcLib
55
import com.skydoves.sandwich.coroutines.CoroutinesResponseCallAdapterFactory
6+
import com.squareup.moshi.Moshi
7+
import com.squareup.moshi.Types
68
import dagger.Module
79
import dagger.Provides
810
import dagger.hilt.InstallIn
@@ -13,10 +15,12 @@ import okhttp3.OkHttpClient
1315
import okhttp3.logging.HttpLoggingInterceptor
1416
import org.robojackets.apiary.BuildConfig
1517
import org.robojackets.apiary.auth.AuthStateManager
18+
import org.robojackets.apiary.auth.model.Permission
1619
import org.robojackets.apiary.auth.network.AuthHeaderInterceptor
1720
import org.robojackets.apiary.auth.network.UserApiService
1821
import org.robojackets.apiary.auth.oauth2.AuthManager
1922
import org.robojackets.apiary.base.GlobalSettings
23+
import org.robojackets.apiary.base.adapter.SkipNotFoundEnumInEnumListAdapter
2024
import org.robojackets.apiary.base.service.ServerInfoApiService
2125
import org.robojackets.apiary.network.UserAgentInterceptor
2226
import retrofit2.Retrofit
@@ -30,7 +34,7 @@ object MainActivityModule {
3034

3135
@Provides
3236
fun providesAuthService(
33-
@ApplicationContext context: Context
37+
@ApplicationContext context: Context,
3438
) = AuthorizationService(context)
3539

3640
@Provides
@@ -40,8 +44,10 @@ object MainActivityModule {
4044
authManager: AuthManager,
4145
): OkHttpClient {
4246
val loggingInterceptor = HttpLoggingInterceptor()
43-
loggingInterceptor.setLevel(if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
44-
else HttpLoggingInterceptor.Level.BASIC) // Only log detailed
47+
loggingInterceptor.setLevel(
48+
if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
49+
else HttpLoggingInterceptor.Level.BASIC
50+
) // Only log detailed
4551
// network requests in debug builds
4652
loggingInterceptor.redactHeader("Authorization") // Redact access tokens in headers
4753

@@ -52,24 +58,35 @@ object MainActivityModule {
5258
.build()
5359
}
5460

61+
@Provides
62+
fun providesMoshi(): Moshi {
63+
return Moshi.Builder()
64+
.add(
65+
Types.newParameterizedType(List::class.java, Permission::class.java),
66+
SkipNotFoundEnumInEnumListAdapter(Permission::class.java)
67+
)
68+
.build()
69+
}
70+
5571
@Provides
5672
fun providesRetrofit(
5773
globalSettings: GlobalSettings,
58-
okHttpClient: OkHttpClient
74+
moshi: Moshi,
75+
okHttpClient: OkHttpClient,
5976
): Retrofit = Retrofit.Builder()
6077
.client(okHttpClient)
6178
.baseUrl(globalSettings.appEnv.apiBaseUrl.toString())
6279
.addCallAdapterFactory(CoroutinesResponseCallAdapterFactory.create())
63-
.addConverterFactory(MoshiConverterFactory.create())
80+
.addConverterFactory(MoshiConverterFactory.create(moshi))
6481
.build()
6582

6683
@Provides
6784
fun providesServerInfoApiService(
68-
retrofit: Retrofit
85+
retrofit: Retrofit,
6986
) = retrofit.create(ServerInfoApiService::class.java)
7087

7188
@Provides
7289
fun providesUserApiService(
73-
retrofit: Retrofit
90+
retrofit: Retrofit,
7491
) = retrofit.create(UserApiService::class.java)
7592
}

app/src/main/java/org/robojackets/apiary/ui/settings/Settings.kt

+16-3
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,15 @@ import org.robojackets.apiary.base.ui.util.ContentPadding
3232
import org.robojackets.apiary.base.ui.util.MadeWithLove
3333
import org.robojackets.apiary.ui.update.UpdateStatus
3434

35-
@Suppress("LongMethod")
35+
@Suppress("LongMethod", "LongParameterList")
3636
@Composable
3737
private fun Settings(
3838
appEnv: AppEnvironment,
3939
user: UserInfo?,
4040
onLogout: () -> Unit,
4141
onOpenPrivacyPolicy: () -> Unit,
4242
onOpenMakeAWish: () -> Unit,
43+
onRefreshUser: () -> Unit,
4344
) {
4445
val context = LocalContext.current
4546

@@ -56,8 +57,16 @@ import org.robojackets.apiary.ui.update.UpdateStatus
5657
icon = { Icon(Icons.Outlined.Person, contentDescription = "person") },
5758
title = { Text(text = user?.name ?: "Refreshing data...") },
5859
subtitle = { Text(text = user?.uid ?: "") },
59-
onClick = {}
60+
onClick = { onRefreshUser() }
6061
)
62+
if (BuildConfig.DEBUG) {
63+
SettingsMenuLink(
64+
icon = { Icon(Icons.Outlined.VerifiedUser, contentDescription = "verified user") },
65+
title = { Text(text = "DEBUG: Recognized permissions") },
66+
subtitle = { Text(text = user?.allPermissions?.joinToString(separator = ", ") ?: "None") },
67+
onClick = { onRefreshUser() }
68+
)
69+
}
6170
SettingsMenuLink(
6271
icon = { Icon(Icons.Outlined.Logout, contentDescription = "logout") },
6372
title = { Text(text = "Logout") },
@@ -149,6 +158,9 @@ fun SettingsScreen(
149158
onOpenMakeAWish = {
150159
val customTabsIntent = viewModel.getCustomTabsIntent(secondaryThemeColor.toArgb())
151160
customTabsIntent.launchUrl(context, viewModel.makeAWishUrl)
161+
},
162+
onRefreshUser = {
163+
viewModel.getUser(forceRefresh = true)
152164
}
153165
)
154166
}
@@ -163,6 +175,7 @@ private fun SettingsPreview() {
163175
user = null,
164176
onLogout = {},
165177
onOpenPrivacyPolicy = {},
166-
onOpenMakeAWish = {}
178+
onOpenMakeAWish = {},
179+
onRefreshUser = {},
167180
)
168181
}

app/src/main/java/org/robojackets/apiary/ui/settings/SettingsViewModel.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,9 @@ class SettingsViewModel @Inject constructor(
115115
}
116116

117117
@Suppress("TooGenericExceptionCaught",)
118-
fun getUser() {
118+
fun getUser(forceRefresh: Boolean = false) {
119119
viewModelScope.launch {
120-
if (user.value == null) {
120+
if (user.value == null || forceRefresh) {
121121
try {
122122
user.value = userRepository.getLoggedInUserInfo().getOrThrow().user
123123
val sentryUser = SentryUser()

app/src/main/java/org/robojackets/apiary/ui/update/UpdateGate.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ fun isImmediateUpdateRequired(priority: Int, staleness: Int): Boolean {
6161
}
6262
}
6363

64-
@Suppress("ComplexMethod")
64+
@Suppress("ComplexMethod", "LongMethod")
6565
@Composable
6666
fun UpdateGate(
6767
navReady: Boolean,

attendance/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ dependencies {
1111
// Other modules
1212
implementation(project(mapOf("path" to ":base")))
1313
implementation(project(mapOf("path" to ":navigation")))
14+
implementation(project(mapOf("path" to ":auth")))
1415
implementation("androidx.navigation:navigation-common-ktx:2.3.5")
1516

1617
// Dependencies

attendance/src/main/java/org/robojackets/apiary/attendance/Attendance.kt

+4-12
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package org.robojackets.apiary.attendance
22

33
import androidx.compose.foundation.layout.*
44
import androidx.compose.material.Button
5-
import androidx.compose.material.CircularProgressIndicator
65
import androidx.compose.material.Text
76
import androidx.compose.runtime.Composable
87
import androidx.compose.runtime.LaunchedEffect
@@ -17,8 +16,7 @@ import androidx.compose.ui.text.font.FontWeight
1716
import androidx.compose.ui.text.style.TextAlign
1817
import androidx.compose.ui.unit.dp
1918
import com.nxp.nfclib.NxpNfcLib
20-
import org.robojackets.apiary.attendance.model.AttendanceScreenState.Loading
21-
import org.robojackets.apiary.attendance.model.AttendanceScreenState.ReadyForTap
19+
import org.robojackets.apiary.attendance.model.AttendanceScreenState.*
2220
import org.robojackets.apiary.attendance.model.AttendanceState
2321
import org.robojackets.apiary.attendance.model.AttendanceViewModel
2422
import org.robojackets.apiary.base.model.AttendableType
@@ -31,6 +29,7 @@ import org.robojackets.apiary.base.ui.nfc.BuzzCardPromptExternalError
3129
import org.robojackets.apiary.base.ui.nfc.BuzzCardTap
3230
import org.robojackets.apiary.base.ui.theme.danger
3331
import org.robojackets.apiary.base.ui.util.ContentPadding
32+
import org.robojackets.apiary.base.ui.util.LoadingSpinner
3433

3534
private fun getExternalError(error: String?): BuzzCardPromptExternalError? {
3635
error?.let {
@@ -48,16 +47,9 @@ private fun Attendance(
4847
onBuzzcardTap: (buzzcardTap: BuzzCardTap) -> Unit,
4948
onNavigateToAttendableSelection: () -> Unit,
5049
) {
51-
if (viewState.selectedAttendable == null) {
52-
Column(
53-
verticalArrangement = Arrangement.Center,
54-
horizontalAlignment = CenterHorizontally,
55-
modifier = Modifier.fillMaxWidth()
56-
.fillMaxHeight()
57-
) {
58-
CircularProgressIndicator()
59-
}
6050

51+
if (viewState.selectedAttendable == null) {
52+
LoadingSpinner()
6153
return
6254
}
6355

0 commit comments

Comments
 (0)