diff --git a/android-app/app/production/release/app-production-release.aab b/android-app/app/production/release/app-production-release.aab index 7c2c007..efeffed 100644 Binary files a/android-app/app/production/release/app-production-release.aab and b/android-app/app/production/release/app-production-release.aab differ diff --git a/android-app/app/src/main/java/de/harheimertc/repositories/PushTokenRepository.kt b/android-app/app/src/main/java/de/harheimertc/repositories/PushTokenRepository.kt index ed1ef33..4cecdf3 100755 --- a/android-app/app/src/main/java/de/harheimertc/repositories/PushTokenRepository.kt +++ b/android-app/app/src/main/java/de/harheimertc/repositories/PushTokenRepository.kt @@ -27,7 +27,10 @@ class PushTokenRepository @Inject constructor( appVersion = "${BuildConfig.VERSION_NAME}+${BuildConfig.VERSION_CODE}", ), ) - if (!response.isSuccessful) error("Push-Token konnte nicht registriert werden.") + if (!response.isSuccessful) { + val detail = response.errorBody()?.string().orEmpty().replace(Regex("\\s+"), " ").take(180) + error("Push-Token konnte nicht registriert werden (HTTP ${response.code()})${if (detail.isBlank()) "" else ": $detail"}") + } } }.onFailure { error -> Log.w("PushTokenRepository", "Push-Token Registrierung fehlgeschlagen", error) diff --git a/android-app/app/src/main/java/de/harheimertc/ui/screens/notifications/NotificationSettingsScreen.kt b/android-app/app/src/main/java/de/harheimertc/ui/screens/notifications/NotificationSettingsScreen.kt index 1a85207..4fb09da 100755 --- a/android-app/app/src/main/java/de/harheimertc/ui/screens/notifications/NotificationSettingsScreen.kt +++ b/android-app/app/src/main/java/de/harheimertc/ui/screens/notifications/NotificationSettingsScreen.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import de.harheimertc.notifications.HarheimerNotifications +import de.harheimertc.BuildConfig import de.harheimertc.repositories.Mannschaft import de.harheimertc.repositories.NotificationPreferences import de.harheimertc.ui.components.LoadingState @@ -103,6 +104,23 @@ fun NotificationSettingsScreen( } } + item { + NotificationCard("Geräte-Registrierung") { + Text("Der Push-Token wird dem aktuell angemeldeten Konto auf diesem Server zugeordnet:", color = Accent700) + Text(BuildConfig.API_BASE_URL, color = Accent900, style = MaterialTheme.typography.bodySmall) + Button( + onClick = viewModel::registerCurrentDevice, + enabled = !state.deviceRegistrationInProgress, + modifier = Modifier.fillMaxWidth(), + ) { + Text(if (state.deviceRegistrationInProgress) "Gerät wird registriert..." else "Android-Gerät jetzt registrieren") + } + state.deviceRegistrationMessage?.let { message -> + Text(message, color = if (state.deviceRegistrationError) MaterialTheme.colorScheme.error else Color(0xFF166534)) + } + } + } + if (state.loading) { item { LoadingState("Benachrichtigungseinstellungen werden geladen...") } } else { diff --git a/android-app/app/src/main/java/de/harheimertc/ui/screens/notifications/NotificationSettingsViewModel.kt b/android-app/app/src/main/java/de/harheimertc/ui/screens/notifications/NotificationSettingsViewModel.kt index 7b6ce87..906a22f 100755 --- a/android-app/app/src/main/java/de/harheimertc/ui/screens/notifications/NotificationSettingsViewModel.kt +++ b/android-app/app/src/main/java/de/harheimertc/ui/screens/notifications/NotificationSettingsViewModel.kt @@ -8,6 +8,7 @@ import de.harheimertc.repositories.LoginRepository import de.harheimertc.repositories.MannschaftenRepository import de.harheimertc.repositories.NotificationPreferences import de.harheimertc.repositories.NotificationPreferencesRepository +import de.harheimertc.repositories.PushTokenRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -22,6 +23,9 @@ data class NotificationSettingsUiState( val seasons: List = emptyList(), val error: String? = null, val saveError: String? = null, + val deviceRegistrationMessage: String? = null, + val deviceRegistrationError: Boolean = false, + val deviceRegistrationInProgress: Boolean = false, ) @HiltViewModel @@ -29,6 +33,7 @@ class NotificationSettingsViewModel @Inject constructor( private val preferencesRepository: NotificationPreferencesRepository, private val mannschaftenRepository: MannschaftenRepository, private val loginRepository: LoginRepository, + private val pushTokenRepository: PushTokenRepository, ) : ViewModel() { private val _state = MutableStateFlow(NotificationSettingsUiState()) val state: StateFlow = _state @@ -82,6 +87,30 @@ class NotificationSettingsViewModel @Inject constructor( update(current.copy(selectedTeamSlugs = nextTeams)) } + fun registerCurrentDevice() { + viewModelScope.launch { + _state.value = _state.value.copy( + deviceRegistrationInProgress = true, + deviceRegistrationMessage = null, + deviceRegistrationError = false, + ) + pushTokenRepository.registerCurrentDevice() + .onSuccess { + _state.value = _state.value.copy( + deviceRegistrationInProgress = false, + deviceRegistrationMessage = "Dieses Android-Gerät wurde beim Server registriert.", + ) + } + .onFailure { error -> + _state.value = _state.value.copy( + deviceRegistrationInProgress = false, + deviceRegistrationError = true, + deviceRegistrationMessage = error.message ?: "Android-Gerät konnte nicht registriert werden.", + ) + } + } + } + private suspend fun loadTeams(settings: NotificationPreferences, seasons: List, currentUserName: String, syncRemote: Boolean = false) { mannschaftenRepository.fetchMannschaften(settings.selectedTeamSeason) .onSuccess { teams -> diff --git a/android-app/gradle.properties b/android-app/gradle.properties index 7d124f1..db6024a 100755 --- a/android-app/gradle.properties +++ b/android-app/gradle.properties @@ -8,8 +8,8 @@ LOCAL_API_BASE_URL=https://harheimertc.tsschulz.de/ PRODUCTION_API_BASE_URL=https://harheimertc.de/ # Android app versioning for Play Store uploads -ANDROID_VERSION_CODE=34 -ANDROID_VERSION_NAME=0.9.29 +ANDROID_VERSION_CODE=35 +ANDROID_VERSION_NAME=0.9.30 # Temporary hotfix: disable R8 minification for release to avoid Retrofit generic signature stripping. RELEASE_MINIFY_ENABLED=false diff --git a/pages/cms/mytischtennis.vue b/pages/cms/mytischtennis.vue index d5c363b..0788767 100644 --- a/pages/cms/mytischtennis.vue +++ b/pages/cms/mytischtennis.vue @@ -47,6 +47,6 @@ const formattedLastImport = computed(() => status.value.lastSuccessfulImportAt ? async function load() { status.value = await $fetch('/api/cms/mytischtennis') } async function save() { busy.value = true; message.value = ''; error.value = false; try { status.value = await $fetch('/api/cms/mytischtennis', { method: 'PUT', body: form }); form.password = ''; message.value = 'Zugang verschlüsselt gespeichert.' } catch { error.value = true; message.value = 'Zugang konnte nicht gespeichert werden.' } finally { busy.value = false } } async function runImport() { busy.value = true; message.value = ''; error.value = false; try { const result = await $fetch('/api/cms/mytischtennis/import', { method: 'POST' }); message.value = `${result.rowCount} Werte wurden aktualisiert.`; await load() } catch (caught) { error.value = true; message.value = caught?.data?.message || 'Abruf fehlgeschlagen. Details stehen im Serverlog.'; await load().catch(() => {}) } finally { busy.value = false } } -async function sendTestPush() { busy.value = true; message.value = ''; error.value = false; try { const result = await $fetch('/api/cms/notifications/test', { method: 'POST' }); message.value = result.sent > 0 ? 'Push-Test wurde an deine registrierten Android-Geräte gesendet.' : 'Kein registriertes Android-Gerät gefunden. Bitte App einmal öffnen und Benachrichtigungen erlauben.' } catch (caught) { error.value = true; message.value = caught?.data?.statusMessage || caught?.data?.message || 'Push-Test konnte nicht gesendet werden.' } finally { busy.value = false } } +async function sendTestPush() { busy.value = true; message.value = ''; error.value = false; try { const result = await $fetch('/api/cms/notifications/test', { method: 'POST' }); if (result.registeredTokenCount === 0) { message.value = 'Kein Android-Token für dieses Konto gespeichert.'; error.value = true } else if (result.skipped) { message.value = `${result.registeredTokenCount} Android-Gerät(e) registriert, aber der FCM-Push-Dienst ist auf dem Server nicht konfiguriert.`; error.value = true } else if (result.deliveryError) { message.value = `${result.registeredTokenCount} Android-Gerät(e) registriert, aber: ${result.deliveryError}`; error.value = true } else if (result.sent > 0) { message.value = `Push-Test wurde an ${result.sent} registrierte(s) Android-Gerät(e) gesendet.` } else { message.value = `${result.registeredTokenCount} Android-Gerät(e) registriert, Zustellung fehlgeschlagen. Bitte Serverlog prüfen.`; error.value = true } } catch (caught) { error.value = true; message.value = caught?.data?.statusMessage || caught?.data?.message || 'Push-Test konnte nicht gesendet werden.' } finally { busy.value = false } } onMounted(load) diff --git a/server/api/cms/notifications/test.post.js b/server/api/cms/notifications/test.post.js index 9fdb391..1dcaec0 100644 --- a/server/api/cms/notifications/test.post.js +++ b/server/api/cms/notifications/test.post.js @@ -1,5 +1,5 @@ import { getUserFromToken, hasRole } from '../../../utils/auth.js' -import { sendTestPushToUser } from '../../../utils/push-notifications.js' +import { androidPushTokenCountForUser, sendTestPushToUser } from '../../../utils/push-notifications.js' export default defineEventHandler(async (event) => { const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '') @@ -7,6 +7,16 @@ export default defineEventHandler(async (event) => { if (!user) throw createError({ statusCode: 401, statusMessage: 'Nicht authentifiziert' }) if (!hasRole(user, 'admin')) throw createError({ statusCode: 403, statusMessage: 'Nur Administratoren dürfen einen Push-Test auslösen.' }) - const result = await sendTestPushToUser(user.id) - return { success: true, ...result } + const registeredTokenCount = await androidPushTokenCountForUser(user.id) + try { + const result = await sendTestPushToUser(user.id) + return { success: true, registeredTokenCount, ...result } + } catch (error) { + console.error('FCM Test-Push konnte nicht vorbereitet werden:', error) + return { + success: false, + registeredTokenCount, + deliveryError: 'Die FCM-Serververbindung konnte nicht aufgebaut werden. Details stehen im Serverlog.' + } + } }) diff --git a/server/utils/push-notifications.js b/server/utils/push-notifications.js index 168c145..f2d946e 100755 --- a/server/utils/push-notifications.js +++ b/server/utils/push-notifications.js @@ -102,6 +102,11 @@ export function upsertPushToken(user, { token, platform = 'android', appVersion return user } +export async function androidPushTokenCountForUser(userId) { + const user = (await readUsers()).find(entry => entry?.id === userId) + return pushTokensForUser(user).length +} + async function sendFcmMessage({ serviceAccount, accessToken, token, data = {} }) { const projectId = projectIdFromServiceAccount(serviceAccount) if (!projectId) throw new Error('FCM project_id fehlt.')