Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pretixscan/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ plugins {
alias(libs.plugins.jetbrainsCompose) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.kotlinJvm) apply false
alias(libs.plugins.kotlinKapt) apply false
alias(libs.plugins.google.protobuf) apply false
alias(libs.plugins.app.cash.sqldelight) apply false
alias(libs.plugins.osdetector) apply false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
</plurals>
<string name="sync_status_date_format">dd.MM.yyyy HH:mm:ss</string>
<string name="short_datetime_format">dd.MM. HH:mm</string>
<string name="first_scanned">Zuerst gescannt: %s</string>
<string name="first_scanned">Zuerst gescannt: </string>
<string name="datetime_format">%1$td.%1$tm.%1$tY %1$tH:%1$tM</string>
<string name="ok">OK</string>
<string name="tutorial_setup">Legen Sie bitte im Abschnitt "Geräte" Ihres Veranstalterskonto im pretix-Backend ein neues Gerät an um mit der Einrichtung zu beginnen. Es wird Ihnen dann ein QR-Code zum scannen angezeigt.</string>
Expand Down Expand Up @@ -159,6 +159,11 @@
<string name="scan_result_invalid_time">Ticket momentan nicht gültig</string>
<string name="scan_result_blocked">Ticket gesperrt</string>
<string name="scan_result_canceled">Ticket storniert</string>
<string name="scan_result_already_exchanged">Ticket bereits in ein Medium umgetauscht</string>
<string name="scan_result_medium_invalid">Wiederverwendbares Medium nicht gültig</string>
<string name="scan_result_medium_exists">Wiederverwendbares Medium existiert bereits</string>
<string name="scan_result_medium_exchange_required">Dieses Ticket muss in ein wiederverwendbares Medium getauscht werden. Leider unterstützt diese Version von pretixSCAN dies noch nicht.</string>
<string name="scan_result_medium_exchange_required_offline">Ticket muss in ein Medium umgetauscht werden, dies ist jedoch Offline nicht möglich</string>
<string name="error_sync_in_background">Eine Synchronisation läuft bereits im Hintergrund.</string>
<string name="dialog_unpaid_title">Unbezahlte Bestellung</string>
<string name="dialog_unpaid_text">Dieses Ticket gehört zu einer Bestellung die noch nicht bezahlt wurde. Möchten Sie den Teilnehmer trotzdem einchecken\? Die Bestellung wird dadurch NICHT auf bezahlt gestellt.</string>
Expand Down Expand Up @@ -228,8 +233,10 @@
<string name="badge_printing_not_available">Badge-Druck nicht verfügbar</string>
<string name="badge_printing_not_possible_no_available_printer">Es sind keine Drucker auf diesem System vorhanden. Bitte konfigurieren Sie einen und versuchen Sie es erneut.</string>
<string name="badge_printing_selected_printer_not_available">Der ausgewählte Drucker ist nicht verfügbar. Bitte prüfen Sie dass das Gerät online ist und mit dem System verbunden ist.</string>
<string name="badge_printing_no_printer_selected">Es ist kein Badge-Drucker ausgewählt. Bitte wählen Sie in den Einstellungen einen Badge-Drucker aus.</string>
<string name="settings_section_camera">Kamera</string>
<string name="settings_preferred_camera_label">Bevorzugte Kamera</string>
<string name="settings_camera_auto">Auto</string>
<string name="no_available_cameras">Keine Kamera verfügbar</string>
<string name="migration_token_rejected">Die Verbindung aus der vorherigen Version von pretixSCAN ist nicht mehr gültig. Bitte verbinden Sie dieses Gerät erneut mit einem neuen Token aus Ihrem pretix-Veranstalterkonto.</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@
<string name="scan_result_invalid_time">Ticket currently not valid</string>
<string name="scan_result_blocked">Ticket blocked</string>
<string name="scan_result_canceled">Order canceled</string>
<string name="scan_result_already_exchanged">Ticket already exchanged</string>
<string name="scan_result_medium_invalid">Reusable medium invalid</string>
<string name="scan_result_medium_exists">Reusable medium already exists</string>
<string name="scan_result_medium_exchange_required">This ticket requires to be exchanged to a reusable medium. Sadly this version of pretixSCAN isn't able to do this yet.</string>
<string name="scan_result_medium_exchange_required_offline">This ticket needs to be exchanged, but this isn't possible while offline</string>
<string name="error_sync_in_background">A synchronization is already running in the background.</string>
<string name="dialog_unpaid_title">Unpaid order</string>
<string name="dialog_unpaid_text">This ticket belongs to an order that has not yet been paid. Do you still want to check it in? This will not mark the order as paid.</string>
Expand Down Expand Up @@ -237,11 +242,11 @@
<string name="badge_printing_not_possible_no_available_printer">There are no available printers on the system.
Please, configure at least one printer and try again.
</string>
<string name="badge_printing_selected_printer_not_available">The selected printer is currently unavailable. Please
check that the device is online and connected to the system.
</string>
<string name="badge_printing_selected_printer_not_available">The selected printer is currently unavailable. Please check that the device is online and connected to the system.</string>
<string name="badge_printing_no_printer_selected">No badge printer is selected. Please choose a badge printer in the settings.</string>
<string name="settings_section_camera">Camera</string>
<string name="settings_preferred_camera_label">Preferred camera</string>
<string name="settings_camera_auto">Auto</string>
<string name="no_available_cameras">No available cameras</string>
<string name="migration_token_rejected">The connection from the previous version of pretixSCAN is no longer valid. Please connect this device again using a new token from your pretix organizer account.</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ expect fun GlobalScanSetup(
stateFlow: StateFlow<MainUiState<MainUiStateData>>,
onHandleDirectScan: suspend (String) -> Unit
)

@Composable
expect fun rememberDiscardGlobalScanInput(): () -> Unit
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
import eu.pretix.desktop.cache.AppCache
import eu.pretix.desktop.cache.DataStoreConfigStore
import eu.pretix.libpretixsync.models.db.toModel
import eu.pretix.libpretixsync.sync.SyncException
import eu.pretix.libpretixsync.sync.SyncManager
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
Expand Down Expand Up @@ -100,17 +101,18 @@ class SyncRootService(

log.info("performing sync for ${events.size} events")

// Initialize event sync states
_eventSyncStates.value = events.associate {
it.eventSlug to EventSyncState.Pending
}

_syncState.value = SyncState.InProgress("Syncing ${events.size} event(s)...")
val syncManager = GlobalContext.get().get<SyncManager>()
activeSyncManager = syncManager

var started = false
val syncResult = try {
syncManager.sync(force) { message ->
if (!started) {
started = true
_eventSyncStates.value = events.associate {
it.eventSlug to EventSyncState.Pending
}
}
_syncState.value = SyncState.InProgress(message)
events.forEach { event ->
if (message.contains(event.eventSlug, ignoreCase = true) ||
Expand All @@ -130,16 +132,17 @@ class SyncRootService(
throw syncResult.exception
}

if (!syncResult.isDataUploaded && !syncResult.isDataDownloaded) {
return
}

// Mark all as success
_eventSyncStates.value = events.associate {
it.eventSlug to EventSyncState.Success
}

_syncState.value = SyncState.Success(lastSync = nowMillis)
logEventSummary()
appConfig.lastFailedSync = 0L
appConfig.lastSync = nowMillis
appConfig.lastDownload = nowMillis
} catch (e: Exception) {
if (cancellationRequested) {
cancellationRequested = false
Expand All @@ -162,8 +165,10 @@ class SyncRootService(
}

_syncState.value = SyncState.Error(e.localizedMessage ?: "Unknown error")
appConfig.lastFailedSync = nowMillis
appConfig.lastFailedSyncMsg = e.localizedMessage ?: "Unknown error"
if (e !is SyncException) {
appConfig.lastFailedSync = System.currentTimeMillis()
appConfig.lastFailedSyncMsg = e.localizedMessage ?: "Unknown error"
}
}
}

Expand Down Expand Up @@ -213,8 +218,7 @@ class SyncRootService(
suspend fun minimalSync(nowMillis: Long = System.currentTimeMillis()) {
log.info("Running a minimal sync")
val shouldResume = skipFutureSyncs()
cancellationRequested = true
activeSyncManager?.cancel()
cancelActiveSync()
_minimumSyncState.value = SyncState.InProgress("")

withContext(Dispatchers.IO) {
Expand Down Expand Up @@ -256,10 +260,18 @@ class SyncRootService(
}
}

suspend fun runWithSyncStopped(action: () -> Unit) {
log.info("Stopping sync")
skipFutureSyncs()
cancelActiveSync()
viewModelScope.launch(Dispatchers.IO) {
syncMutex.withLock { action() }
}.join()
}

fun forceSync(nowMillis: Long = System.currentTimeMillis()) {
log.info("Full sync requested")
cancellationRequested = true
activeSyncManager?.cancel()
cancelActiveSync()
viewModelScope.launch(Dispatchers.IO) {
syncMutex.withLock {
executeSync(force = true, nowMillis = nowMillis)
Expand All @@ -269,6 +281,10 @@ class SyncRootService(

override fun onCleared() {
super.onCleared()
cancelActiveSync()
}

private fun cancelActiveSync() {
cancellationRequested = true
activeSyncManager?.cancel()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,12 @@ class AppCache(val cacheFactory: LocalCacheFactory) {

private var _db: SyncDatabase? = null
val db: SyncDatabase
get() {
if (_db == null) {
_db = cacheFactory.getSyncDataSource()
}
return _db!!
}
@Synchronized get() = _db ?: cacheFactory.getSyncDataSource().also { _db = it }

@Synchronized
fun reset() {
cacheFactory.deleteDataSource()
_db = null
cacheFactory.deleteDataSource()
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ fun createSyncDatabase(
driver = driver,
CheckInAdapter = CheckIn.Adapter(
datetimeAdapter = dateAdapter,
local_annulledAdapter = dateAdapter,
),
ClosingAdapter = Closing.Adapter(
cash_countedAdapter = bigDecimalAdapter,
Expand All @@ -36,6 +37,7 @@ fun createSyncDatabase(
tax_rateAdapter = bigDecimalAdapter,
tax_valueAdapter = bigDecimalAdapter,
line_price_grossAdapter = bigDecimalAdapter,
manual_discount_percentAdapter = bigDecimalAdapter,
),
ReceiptAdapter = Receipt.Adapter(
datetime_closedAdapter = dateAdapter,
Expand All @@ -48,10 +50,16 @@ fun createSyncDatabase(
date_fromAdapter = dateAdapter,
date_toAdapter = dateAdapter,
),
QueuedCheckInAdapter = QueuedCheckIn.Adapter(datetimeAdapter = dateAdapter),
QueuedCheckInAdapter = QueuedCheckIn.Adapter(
datetimeAdapter = dateAdapter,
annulledAdapter = dateAdapter,
),
DiscountAdapter = Discount.Adapter(
available_fromAdapter = dateAdapter,
available_untilAdapter = dateAdapter,
),
ReusableMediumAdapter = ReusableMedium.Adapter(
expiresAdapter = dateAdapter,
),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ class ConfigMigration(
oldConfig.resetEventConfig()
}

suspend fun discardMigratedSettings() {
newConfig.resetEventConfig()
deleteOldConfig()
}

/**
* Check if old configuration exists and hasn't been migrated yet.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package eu.pretix.desktop.migration

import eu.pretix.libpretixsync.api.DeviceAccessRevokedException
import eu.pretix.libpretixsync.api.UnauthorizedApiException
import kotlinx.coroutines.CancellationException
import java.util.logging.Logger

sealed class MigrationResult {
object Success : MigrationResult()
object TokenRejected : MigrationResult()
data class Failure(val error: String, val canRetry: Boolean) : MigrationResult()
}

class MigrationCoordinator(
private val configMigration: ConfigMigration,
private val tokenRoller: TokenRoller,
private val v1DirectoryLocator: V1DirectoryLocator = V1DirectoryLocator
private val v1DirectoryLocator: V1DirectoryLocator = V1DirectoryLocator,
private val cleanupV1Storage: () -> CleanupResult = V1StorageCleaner::cleanup
) {
private val logger = Logger.getLogger(MigrationCoordinator::class.java.name)

Expand Down Expand Up @@ -38,7 +43,11 @@ class MigrationCoordinator(
logger.info("Settings migration completed successfully")

// Step 2: Roll API token
rollToken()
if (!rollToken()) {
configMigration.discardMigratedSettings()
logger.warning("Migrated settings discarded, device must be set up again")
return MigrationResult.TokenRejected
}

// Step 3: Cleanup V1 storage
performCleanup()
Expand All @@ -58,23 +67,33 @@ class MigrationCoordinator(
/**
* Roll device API token to invalidate old app access.
*/
private suspend fun rollToken() {
try {
private suspend fun rollToken(): Boolean {
return try {
logger.info("Attempting to roll device API token")
val result = tokenRoller.rollApiToken()

result.fold(
onSuccess = { newKey ->
tokenRoller.updateApiKey(newKey)
logger.info("Token rolled successfully - old app access revoked")
true
},
onFailure = { error ->
logger.warning("Token rolling failed (non-fatal): ${error.message}")
logger.warning("Old app may still have access - user can manually revoke device")
if (error is UnauthorizedApiException || error is DeviceAccessRevokedException) {
logger.warning("Server rejected the migrated token: ${error.message}")
false
} else {
logger.warning("Token rolling failed (non-fatal): ${error.message}")
logger.warning("Old app may still have access - user can manually revoke device")
true
}
}
)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
logger.warning("Token rolling failed (non-fatal): ${e.message}")
true
}
}

Expand All @@ -94,7 +113,7 @@ class MigrationCoordinator(

logger.info("Starting automatic cleanup of V1 storage")

when (val result = V1StorageCleaner.cleanup()) {
when (val result = cleanupV1Storage()) {
is CleanupResult.Success -> {
logger.info("V1 storage cleanup successful: from $dataPath and $cachePath")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package eu.pretix.desktop.migration
import eu.pretix.desktop.cache.DataStoreConfig
import eu.pretix.libpretixsync.api.HttpClientFactory
import eu.pretix.libpretixsync.api.PretixApi
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
Expand Down Expand Up @@ -41,6 +42,8 @@ class TokenRoller(

logger.info("Token rolled successfully, new key received")
Result.success(newKey)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
logger.warning("Token rolling failed (non-fatal): ${e.message}")
Result.failure(e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ interface BadgeFactory {
suspend fun setup()

fun printBadges(layout: BadgeLayout?, position: JSONObject)
}

sealed class BadgePrinterUnavailableException(message: String) : Exception(message) {
class NotSelected : BadgePrinterUnavailableException("No badge printer selected in settings.")
class NotFound(printerName: String) : BadgePrinterUnavailableException("Badge printer $printerName is not available on this system.")
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@ data class MainUiStateData(
)

fun MainUiStateData.secret(secret: String?): MainUiStateData {
return this.copy(secret = secret)
return this.copy(secret = secret, resultState = null)
}
Original file line number Diff line number Diff line change
Expand Up @@ -371,9 +371,11 @@ fun SettingsScreen(
PrimaryButton(
onClick = {
// TODO: confirm reset
viewModel.logout()
navHostController.popBackStack()
navHostController.navigate(Route.Welcome.route)
coroutineScope.launch {
viewModel.logout()
navHostController.popBackStack()
navHostController.navigate(Route.Welcome.route)
}
},
label = stringResource(Res.string.full_delete_action)
)
Expand Down
Loading
Loading