Compare commits

..
3 Commits
Author SHA1 Message Date
admin 72db88b25c Add Firebase Crashlytics diagnostics 2026-07-05 00:06:47 +02:00
admin 97f0eb56d5 Polish driver leave request status labels 2026-07-04 23:41:59 +02:00
admin c6d6392656 Refine driver leave request mobile flow 2026-07-04 23:26:26 +02:00
16 changed files with 408 additions and 35 deletions
+4 -2
View File
@@ -10,6 +10,7 @@ plugins {
if (file("google-services.json").exists()) {
apply(plugin = "com.google.gms.google-services")
apply(plugin = "com.google.firebase.crashlytics")
}
val keystorePropertiesFile = rootProject.file("keystore.properties")
@@ -33,8 +34,8 @@ android {
applicationId = "pl.firmatpp.kierowca"
minSdk = 26
targetSdk = 35
versionCode = 39
versionName = "1.0.38"
versionCode = 42
versionName = "1.0.41"
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -123,6 +124,7 @@ dependencies {
implementation(libs.compose.ui.tooling.preview)
implementation(libs.coroutines.android)
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.crashlytics)
implementation(libs.firebase.messaging)
implementation(libs.okhttp)
implementation(libs.okhttp.logging)
+1
View File
@@ -10,6 +10,7 @@
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:name=".DriverApplication"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TppKierowca">
@@ -0,0 +1,12 @@
package pl.firmatpp.kierowca
import android.app.Application
import pl.firmatpp.kierowca.diagnostics.AppDiagnostics
class DriverApplication : Application() {
override fun onCreate() {
super.onCreate()
AppDiagnostics.installFirebaseCrashlytics()
AppDiagnostics.log("app_started")
}
}
@@ -8,6 +8,7 @@ import java.io.File
import pl.firmatpp.kierowca.data.ApiErrorKind
import pl.firmatpp.kierowca.data.ApiErrorMapper
import pl.firmatpp.kierowca.data.DriverRepository
import pl.firmatpp.kierowca.diagnostics.AppDiagnostics
class DispatchSheetUploadWorker(
appContext: Context,
@@ -64,6 +65,17 @@ class DispatchSheetUploadWorker(
Result.success()
}.getOrElse { throwable ->
val error = ApiErrorMapper.map(throwable)
AppDiagnostics.reportNonFatal(
throwable = throwable,
operation = "dispatch_sheet_upload_worker",
keys = mapOf(
"client_request_id" to clientRequestId,
"api_error_kind" to error.kind.name,
"api_error_code" to error.code,
"api_status_code" to error.statusCode,
"retryable" to error.retryable,
),
)
val status = if (error.retryable) PhotoUploadStatus.FailedRetryable else PhotoUploadStatus.FailedPermanent
dao.updateStatus(
clientRequestId = clientRequestId,
@@ -8,6 +8,7 @@ import java.io.File
import pl.firmatpp.kierowca.data.ApiErrorKind
import pl.firmatpp.kierowca.data.ApiErrorMapper
import pl.firmatpp.kierowca.data.DriverRepository
import pl.firmatpp.kierowca.diagnostics.AppDiagnostics
class PhotoUploadWorker(
appContext: Context,
@@ -64,6 +65,18 @@ class PhotoUploadWorker(
Result.success()
}.getOrElse { throwable ->
val error = ApiErrorMapper.map(throwable)
AppDiagnostics.reportNonFatal(
throwable = throwable,
operation = "photo_upload_worker",
keys = mapOf(
"client_request_id" to clientRequestId,
"route_id" to upload.routeId,
"api_error_kind" to error.kind.name,
"api_error_code" to error.code,
"api_status_code" to error.statusCode,
"retryable" to error.retryable,
),
)
val status = if (error.retryable) PhotoUploadStatus.FailedRetryable else PhotoUploadStatus.FailedPermanent
dao.updateStatus(
clientRequestId = clientRequestId,
@@ -0,0 +1,75 @@
package pl.firmatpp.kierowca.diagnostics
import com.google.firebase.crashlytics.FirebaseCrashlytics
interface DiagnosticsSink {
fun log(message: String)
fun setUserId(userId: String)
fun setCustomKey(key: String, value: String)
fun recordException(throwable: Throwable)
}
object NoOpDiagnosticsSink : DiagnosticsSink {
override fun log(message: String) = Unit
override fun setUserId(userId: String) = Unit
override fun setCustomKey(key: String, value: String) = Unit
override fun recordException(throwable: Throwable) = Unit
}
class FirebaseCrashlyticsDiagnosticsSink(
private val crashlytics: FirebaseCrashlytics = FirebaseCrashlytics.getInstance(),
) : DiagnosticsSink {
override fun log(message: String) {
crashlytics.log(message)
}
override fun setUserId(userId: String) {
crashlytics.setUserId(userId)
}
override fun setCustomKey(key: String, value: String) {
crashlytics.setCustomKey(key, value)
}
override fun recordException(throwable: Throwable) {
crashlytics.recordException(throwable)
}
}
object AppDiagnostics {
@Volatile
private var sink: DiagnosticsSink = NoOpDiagnosticsSink
fun installFirebaseCrashlytics() {
installSink(FirebaseCrashlyticsDiagnosticsSink())
}
fun installSink(nextSink: DiagnosticsSink) {
sink = nextSink
}
fun setDriverId(driverId: String) {
sink.setUserId(driverId)
}
fun clearDriverId() {
sink.setUserId("")
}
fun log(message: String) {
sink.log(message)
}
fun reportNonFatal(
throwable: Throwable,
operation: String,
keys: Map<String, Any?> = emptyMap(),
) {
sink.setCustomKey("operation", operation)
keys.forEach { (key, value) ->
sink.setCustomKey(key, value?.toString().orEmpty())
}
sink.log("non_fatal: $operation: ${throwable.message ?: throwable::class.java.simpleName}")
sink.recordException(throwable)
}
}
@@ -17,6 +17,7 @@ import okhttp3.WebSocket
import okhttp3.WebSocketListener
import pl.firmatpp.kierowca.data.DriverRepository
import pl.firmatpp.kierowca.data.model.RealtimeConfigDto
import pl.firmatpp.kierowca.diagnostics.AppDiagnostics
class DriverLiveSyncClient(
private val repository: DriverRepository,
@@ -66,6 +67,7 @@ class DriverLiveSyncClient(
override fun onFailure(webSocket: WebSocket, t: Throwable, response: okhttp3.Response?) {
stopHeartbeat()
AppDiagnostics.log("realtime_error: ${t.message ?: response?.message ?: "unknown"}")
reportRealtimeStatus("error", t.message ?: response?.message)
started.set(false)
scheduleReconnect()
@@ -14,6 +14,7 @@ import java.util.concurrent.TimeUnit
import pl.firmatpp.kierowca.data.ApiErrorKind
import pl.firmatpp.kierowca.data.ApiErrorMapper
import pl.firmatpp.kierowca.data.sync.DriverSyncRepository
import pl.firmatpp.kierowca.diagnostics.AppDiagnostics
class DriverSyncWorker(
appContext: Context,
@@ -62,6 +63,18 @@ class DriverSyncWorker(
Result.success()
}.getOrElse { throwable ->
val error = ApiErrorMapper.map(throwable)
AppDiagnostics.reportNonFatal(
throwable = throwable,
operation = "driver_sync_worker",
keys = mapOf(
"date" to inputData.getString(KEY_DATE),
"route_id" to inputData.getString(KEY_ROUTE_ID),
"api_error_kind" to error.kind.name,
"api_error_code" to error.code,
"api_status_code" to error.statusCode,
"retryable" to error.retryable,
),
)
if (error.retryable && error.kind != ApiErrorKind.Auth) Result.retry() else Result.failure()
}
@@ -26,6 +26,7 @@ import pl.firmatpp.kierowca.R
import pl.firmatpp.kierowca.data.ApiErrorKind
import pl.firmatpp.kierowca.data.ApiErrorMapper
import pl.firmatpp.kierowca.data.DriverRepository
import pl.firmatpp.kierowca.diagnostics.AppDiagnostics
import retrofit2.HttpException
class NewRouteNotificationWorker(
@@ -76,6 +77,17 @@ class NewRouteNotificationWorker(
Result.success()
} else {
val error = ApiErrorMapper.map(throwable)
AppDiagnostics.reportNonFatal(
throwable = throwable,
operation = "new_route_notification_worker",
keys = mapOf(
"route_id" to routeId,
"api_error_kind" to error.kind.name,
"api_error_code" to error.code,
"api_status_code" to error.statusCode,
"retryable" to error.retryable,
),
)
if (error.retryable && error.kind != ApiErrorKind.Auth) Result.retry() else Result.success()
}
}
@@ -74,6 +74,8 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.DateRangePicker
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@@ -86,6 +88,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberDateRangePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -127,6 +130,7 @@ import java.io.File
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import kotlinx.coroutines.delay
import java.util.Locale
@@ -210,7 +214,6 @@ fun DriverApp(viewModel: DriverViewModel, initialRouteId: String? = null, initia
onDate = viewModel::selectRouteDate,
onProfile = viewModel::openProfile,
onPhotoQueue = viewModel::openPhotoQueue,
onLeaveRequests = viewModel::openLeaveRequests,
onDispatchSheetUpload = viewModel::uploadDispatchSheetPhoto,
onRoute = viewModel::openRoute,
onDismissLiveUpdate = viewModel::dismissRouteDayLiveUpdate,
@@ -220,6 +223,7 @@ fun DriverApp(viewModel: DriverViewModel, initialRouteId: String? = null, initia
onRoutes = viewModel::refreshRoutes,
onProfile = viewModel::openProfile,
onLogout = viewModel::logout,
onLeaveRequests = viewModel::openLeaveRequests,
onNewRouteNotificationsChanged = { enabled ->
if (!enabled) {
viewModel.updateNewRouteNotifications(false)
@@ -574,7 +578,6 @@ private fun RoutesScreen(
onDate: (String) -> Unit,
onProfile: () -> Unit,
onPhotoQueue: () -> Unit,
onLeaveRequests: () -> Unit,
onDispatchSheetUpload: (Uri, String, PhotoUploadMetadata) -> Unit,
onRoute: (String) -> Unit,
onDismissLiveUpdate: () -> Unit,
@@ -686,9 +689,6 @@ private fun RoutesScreen(
onDate = onDate,
)
}
if (DriverLeaveRequestUiRules.isFeatureVisible(state.leaveRequestsConfig)) {
item { LeaveRequestsEntryCard(state.leaveRequests, onLeaveRequests) }
}
if (shouldShowDispatchSheetReminderCard(state.dispatchSheetReminder)) {
item {
DispatchSheetReminderCard(
@@ -833,15 +833,6 @@ private fun LeaveRequestsScreen(
Scaffold(
topBar = { SimpleTopBar("Wnioski urlopowe", onBack) },
containerColor = TppColors.Surface,
floatingActionButton = {
Button(
onClick = onAdd,
colors = ButtonDefaults.buttonColors(containerColor = TppColors.Forest),
shape = RoundedCornerShape(8.dp),
) {
Text("Nowy wniosek")
}
},
) { padding ->
Box(Modifier.fillMaxSize().padding(padding).pullRefresh(pullRefreshState)) {
LazyColumn(
@@ -850,6 +841,18 @@ private fun LeaveRequestsScreen(
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
item { FeedbackAndError(state.feedback, state.error) }
item {
Button(
onClick = onAdd,
modifier = Modifier.fillMaxWidth().height(56.dp),
colors = ButtonDefaults.buttonColors(containerColor = TppColors.Forest),
shape = RoundedCornerShape(8.dp),
) {
Icon(Icons.Outlined.CalendarToday, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text("Nowy wniosek", fontWeight = FontWeight.Bold)
}
}
if (state.leaveRequests.isEmpty()) {
item { EmptyState("Nie masz jeszcze wniosków urlopowych") }
} else {
@@ -958,7 +961,7 @@ private fun LeaveRequestDetailScreen(
) {
Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
"${event.action}${event.toStatus?.let { " · ${DriverLeaveRequestUiRules.statusLabel(it)}" } ?: ""}",
"${DriverLeaveRequestUiRules.eventActionLabel(event.action)}${event.toStatus?.let { " · ${DriverLeaveRequestUiRules.statusLabel(it)}" } ?: ""}",
color = TppColors.Ink,
fontWeight = FontWeight.Bold,
)
@@ -978,6 +981,18 @@ private fun AddLeaveRequestScreen(
onDraft: (String?, String?, String?, String?) -> Unit,
onSubmit: () -> Unit,
) {
var showDateRangePicker by remember { mutableStateOf(false) }
val selectedStartMillis = remember(state.leaveRequestDateFrom) {
localDateStringToUtcMillis(state.leaveRequestDateFrom)
}
val selectedEndMillis = remember(state.leaveRequestDateTo) {
localDateStringToUtcMillis(state.leaveRequestDateTo)
}
val dateRangePickerState = rememberDateRangePickerState(
initialSelectedStartDateMillis = selectedStartMillis,
initialSelectedEndDateMillis = selectedEndMillis,
)
Scaffold(topBar = { SimpleTopBar("Nowy wniosek", onBack) }, containerColor = TppColors.Surface) { padding ->
LazyColumn(
Modifier.fillMaxSize().padding(padding),
@@ -1007,18 +1022,33 @@ private fun AddLeaveRequestScreen(
}
}
item {
LeaveTextField(
label = "Data od (RRRR-MM-DD)",
value = state.leaveRequestDateFrom,
onValue = { onDraft(it, null, null, null) },
Card(
colors = CardDefaults.cardColors(containerColor = Color.White),
border = BorderStroke(1.dp, TppColors.Outline),
shape = RoundedCornerShape(8.dp),
modifier = Modifier.fillMaxWidth().clickable { showDateRangePicker = true },
) {
Row(
Modifier.fillMaxWidth().padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
Modifier.size(44.dp).background(Color(0xFFEAF7EF), RoundedCornerShape(8.dp)),
contentAlignment = Alignment.Center,
) {
Icon(Icons.Outlined.CalendarToday, contentDescription = null, tint = TppColors.Forest)
}
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text("Okres", color = TppColors.Ink, fontWeight = FontWeight.Bold)
Text(
leaveDateRange(state.leaveRequestDateFrom, state.leaveRequestDateTo),
color = TppColors.Muted,
)
}
item {
LeaveTextField(
label = "Data do (RRRR-MM-DD)",
value = state.leaveRequestDateTo,
onValue = { onDraft(null, it, null, null) },
)
Text("Zmień", color = TppColors.Forest, fontWeight = FontWeight.Bold)
}
}
}
item {
LeaveTextField(
@@ -1040,6 +1070,55 @@ private fun AddLeaveRequestScreen(
}
}
}
if (showDateRangePicker) {
DatePickerDialog(
onDismissRequest = { showDateRangePicker = false },
confirmButton = {
TextButton(
onClick = {
val start = dateRangePickerState.selectedStartDateMillis
val end = dateRangePickerState.selectedEndDateMillis ?: start
if (start != null && end != null) {
onDraft(utcMillisToLocalDateString(start), utcMillisToLocalDateString(end), null, null)
}
showDateRangePicker = false
},
enabled = dateRangePickerState.selectedStartDateMillis != null,
) {
Text("Ustaw", color = TppColors.Forest, fontWeight = FontWeight.Bold)
}
},
dismissButton = {
TextButton(onClick = { showDateRangePicker = false }) {
Text("Anuluj", color = TppColors.Muted)
}
},
) {
DateRangePicker(
state = dateRangePickerState,
title = {
Text(
"Wybierz okres urlopu",
modifier = Modifier.padding(start = 24.dp, end = 12.dp, top = 16.dp),
color = TppColors.Ink,
fontWeight = FontWeight.Bold,
)
},
headline = {
Text(
leaveDateRange(
dateRangePickerState.selectedStartDateMillis?.let(::utcMillisToLocalDateString),
dateRangePickerState.selectedEndDateMillis?.let(::utcMillisToLocalDateString),
),
modifier = Modifier.padding(start = 24.dp, end = 12.dp, bottom = 12.dp),
color = TppColors.Muted,
)
},
showModeToggle = false,
)
}
}
}
@Composable
@@ -1111,6 +1190,14 @@ private fun LeaveTextField(label: String, value: String, onValue: (String) -> Un
private fun leaveDateRange(from: String?, to: String?): String =
if (from == to) from ?: "-" else "${from ?: "?"} - ${to ?: "?"}"
private fun localDateStringToUtcMillis(value: String): Long? =
runCatching {
LocalDate.parse(value).atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli()
}.getOrNull()
private fun utcMillisToLocalDateString(value: Long): String =
Instant.ofEpochMilli(value).atZone(ZoneOffset.UTC).toLocalDate().toString()
private fun shortDateTime(value: String): String =
runCatching {
DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm", Locale("pl", "PL"))
@@ -1575,6 +1662,7 @@ private fun ProfileScreen(
onRoutes: () -> Unit,
onProfile: () -> Unit,
onLogout: () -> Unit,
onLeaveRequests: () -> Unit,
onNewRouteNotificationsChanged: (Boolean) -> Unit,
onOpenNotificationSettings: () -> Unit,
) {
@@ -1619,6 +1707,9 @@ private fun ProfileScreen(
)
}
}
if (DriverLeaveRequestUiRules.isFeatureVisible(state.leaveRequestsConfig)) {
LeaveRequestsEntryCard(state.leaveRequests, onLeaveRequests)
}
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.White),
@@ -19,7 +19,20 @@ object DriverLeaveRequestUiRules {
"cancel_requested" -> "Anulowanie do decyzji"
"cancelled" -> "Anulowany"
"revoked" -> "Cofnięto decyzję"
else -> status
else -> "Nieznany status"
}
fun eventActionLabel(action: String): String =
when (action) {
"submitted" -> "Wniosek złożony"
"approved" -> "Wniosek zatwierdzony"
"rejected" -> "Wniosek odrzucony"
"revoked" -> "Decyzja cofnięta"
"cancelled_by_driver" -> "Wniosek anulowany przez kierowcę"
"cancellation_requested" -> "Kierowca poprosił o anulowanie"
"cancellation_approved" -> "Anulowanie zatwierdzone"
"cancellation_rejected" -> "Anulowanie odrzucone"
else -> "Aktualizacja wniosku"
}
fun typeLabel(type: String): String =
@@ -26,6 +26,7 @@ import pl.firmatpp.kierowca.data.upload.DispatchSheetUploadEntity
import pl.firmatpp.kierowca.data.upload.DispatchSheetUploadOutbox
import pl.firmatpp.kierowca.data.upload.PhotoUploadEntity
import pl.firmatpp.kierowca.data.upload.PhotoUploadOutbox
import pl.firmatpp.kierowca.diagnostics.AppDiagnostics
import pl.firmatpp.kierowca.sync.DriverLiveSyncClient
import pl.firmatpp.kierowca.sync.DriverSyncHint
import pl.firmatpp.kierowca.sync.DriverSyncWorker
@@ -120,7 +121,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
}
}
fun requestOtp(phone: String) = runLoading {
fun requestOtp(phone: String) = runLoading("request_otp") {
val response = repository.requestOtp(phone)
otpAutoSubmitPolicy.reset()
_state.update {
@@ -134,8 +135,9 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
}
}
fun verifyOtp(code: String) = runLoading {
fun verifyOtp(code: String) = runLoading("verify_otp") {
val driver = repository.verifyOtp(_state.value.phone, code, android.os.Build.MODEL ?: "Android")
AppDiagnostics.setDriverId(driver.id)
_state.update { it.copy(driver = driver, imageAuthHeader = repository.imageAuthHeader()) }
refreshRoutes()
}
@@ -170,6 +172,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
val cached = syncRepository.bootstrap(date)
val response = cached.value
val settings = response.driverAppSettings
AppDiagnostics.setDriverId(response.session.driver.id)
_state.update {
it.copy(
driver = response.session.driver,
@@ -203,6 +206,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
}
observeDispatchSheetUploads(response.dispatchSheetReminder?.workDate)
}.onFailure { throwable ->
reportHandledException("load_routes", throwable, mapOf("date" to date))
_state.update {
val apiError = ApiErrorMapper.map(throwable)
it.copy(
@@ -223,7 +227,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
}
}
fun openRoute(routeId: String) = runLoading {
fun openRoute(routeId: String) = runLoading("open_route", mapOf("route_id" to routeId)) {
val cached = syncRepository.route(routeId)
val response = cached.value
photoUploadOutbox.discardConfirmedServerPhotos(response.route)
@@ -582,21 +586,27 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
}
}
fun logout() = runLoading {
fun logout() = runLoading("logout") {
photoUploadsJob?.cancel()
dispatchSheetUploadsJob?.cancel()
liveSyncClient.stop()
repository.logout()
syncRepository.clearCache()
pushTokenRegisteredForDriverId = null
AppDiagnostics.clearDriverId()
_state.update { DriverUiState(screen = DriverScreen.Phone, loading = false) }
}
private fun runLoading(block: suspend () -> Unit) {
private fun runLoading(
operation: String,
keys: Map<String, Any?> = emptyMap(),
block: suspend () -> Unit,
) {
viewModelScope.launch {
_state.update { it.copy(loading = true, error = null) }
runCatching { block() }
.onFailure { throwable ->
reportHandledException(operation, throwable, keys)
_state.update { it.copy(error = ApiErrorMapper.map(throwable).message) }
}
_state.update { it.copy(loading = false) }
@@ -696,10 +706,35 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
runCatching {
repository.storePushToken(token)
pushTokenRegisteredForDriverId = driverId
}.onFailure { throwable ->
reportHandledException("store_push_token", throwable, mapOf("driver_id" to driverId))
}
}
}.addOnFailureListener { throwable ->
reportHandledException("get_push_token", throwable, mapOf("driver_id" to driverId))
}
}.onFailure { throwable ->
reportHandledException("register_push_token", throwable, mapOf("driver_id" to driverId))
}
}
private fun reportHandledException(
operation: String,
throwable: Throwable,
keys: Map<String, Any?> = emptyMap(),
) {
val apiError = ApiErrorMapper.map(throwable)
AppDiagnostics.reportNonFatal(
throwable = throwable,
operation = operation,
keys = mapOf(
"screen" to _state.value.screen.name,
"api_error_kind" to apiError.kind.name,
"api_error_code" to apiError.code,
"api_status_code" to apiError.statusCode,
"retryable" to apiError.retryable,
) + keys,
)
}
override fun onCleared() {
@@ -0,0 +1,71 @@
package pl.firmatpp.kierowca.diagnostics
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertSame
import org.junit.Test
class AppDiagnosticsTest {
private val sink = RecordingDiagnosticsSink()
@After
fun tearDown() {
AppDiagnostics.installSink(NoOpDiagnosticsSink)
}
@Test
fun reportNonFatalAddsOperationContextBeforeRecordingException() {
AppDiagnostics.installSink(sink)
val throwable = IllegalStateException("upload failed")
AppDiagnostics.reportNonFatal(
throwable = throwable,
operation = "photo_upload",
keys = mapOf("route_id" to "R-42", "retryable" to true),
)
assertEquals(
listOf(
"key:operation=photo_upload",
"key:route_id=R-42",
"key:retryable=true",
"log:non_fatal: photo_upload: upload failed",
"exception:IllegalStateException",
),
sink.events,
)
assertSame(throwable, sink.throwables.single())
}
@Test
fun clearDriverIdClearsPreviouslySetUserIdentifier() {
AppDiagnostics.installSink(sink)
AppDiagnostics.setDriverId("driver-7")
AppDiagnostics.clearDriverId()
assertEquals(listOf("user:driver-7", "user:"), sink.events)
}
private class RecordingDiagnosticsSink : DiagnosticsSink {
val events = mutableListOf<String>()
val throwables = mutableListOf<Throwable>()
override fun log(message: String) {
events += "log:$message"
}
override fun setUserId(userId: String) {
events += "user:$userId"
}
override fun setCustomKey(key: String, value: String) {
events += "key:$key=$value"
}
override fun recordException(throwable: Throwable) {
events += "exception:${throwable::class.simpleName}"
throwables += throwable
}
}
}
@@ -17,8 +17,25 @@ class DriverLeaveRequestUiRulesTest {
@Test
fun exposesPolishStatusLabels() {
assertEquals("Oczekuje", DriverLeaveRequestUiRules.statusLabel("pending"))
assertEquals("Zatwierdzony", DriverLeaveRequestUiRules.statusLabel("approved"))
assertEquals("Odrzucony", DriverLeaveRequestUiRules.statusLabel("rejected"))
assertEquals("Anulowanie do decyzji", DriverLeaveRequestUiRules.statusLabel("cancel_requested"))
assertEquals("Anulowany", DriverLeaveRequestUiRules.statusLabel("cancelled"))
assertEquals("Cofnięto decyzję", DriverLeaveRequestUiRules.statusLabel("revoked"))
assertEquals("Nieznany status", DriverLeaveRequestUiRules.statusLabel("some_raw_status"))
}
@Test
fun exposesPolishEventActionLabels() {
assertEquals("Wniosek złożony", DriverLeaveRequestUiRules.eventActionLabel("submitted"))
assertEquals("Wniosek zatwierdzony", DriverLeaveRequestUiRules.eventActionLabel("approved"))
assertEquals("Wniosek odrzucony", DriverLeaveRequestUiRules.eventActionLabel("rejected"))
assertEquals("Decyzja cofnięta", DriverLeaveRequestUiRules.eventActionLabel("revoked"))
assertEquals("Wniosek anulowany przez kierowcę", DriverLeaveRequestUiRules.eventActionLabel("cancelled_by_driver"))
assertEquals("Kierowca poprosił o anulowanie", DriverLeaveRequestUiRules.eventActionLabel("cancellation_requested"))
assertEquals("Anulowanie zatwierdzone", DriverLeaveRequestUiRules.eventActionLabel("cancellation_approved"))
assertEquals("Anulowanie odrzucone", DriverLeaveRequestUiRules.eventActionLabel("cancellation_rejected"))
assertEquals("Aktualizacja wniosku", DriverLeaveRequestUiRules.eventActionLabel("some_raw_action"))
}
@Test
+1
View File
@@ -3,4 +3,5 @@ plugins {
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.google.services) apply false
alias(libs.plugins.firebase.crashlytics) apply false
}
+3
View File
@@ -20,6 +20,7 @@ playServicesAuthApiPhone = "18.3.0"
junit = "4.13.2"
firebaseBom = "33.7.0"
googleServices = "4.4.2"
firebaseCrashlyticsPlugin = "3.0.7"
[libraries]
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
@@ -55,6 +56,7 @@ retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref =
retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging-ktx" }
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
@@ -62,3 +64,4 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" }
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin" }