Füge Unterstützung für die Registrierung von Geräten hinzu: Implementiere die Registrierung des aktuellen Geräts und verbessere die Fehlerbehandlung für Push-Token.
This commit is contained in:
Binary file not shown.
@@ -27,7 +27,10 @@ class PushTokenRepository @Inject constructor(
|
|||||||
appVersion = "${BuildConfig.VERSION_NAME}+${BuildConfig.VERSION_CODE}",
|
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 ->
|
}.onFailure { error ->
|
||||||
Log.w("PushTokenRepository", "Push-Token Registrierung fehlgeschlagen", error)
|
Log.w("PushTokenRepository", "Push-Token Registrierung fehlgeschlagen", error)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import de.harheimertc.notifications.HarheimerNotifications
|
import de.harheimertc.notifications.HarheimerNotifications
|
||||||
|
import de.harheimertc.BuildConfig
|
||||||
import de.harheimertc.repositories.Mannschaft
|
import de.harheimertc.repositories.Mannschaft
|
||||||
import de.harheimertc.repositories.NotificationPreferences
|
import de.harheimertc.repositories.NotificationPreferences
|
||||||
import de.harheimertc.ui.components.LoadingState
|
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) {
|
if (state.loading) {
|
||||||
item { LoadingState("Benachrichtigungseinstellungen werden geladen...") }
|
item { LoadingState("Benachrichtigungseinstellungen werden geladen...") }
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import de.harheimertc.repositories.LoginRepository
|
|||||||
import de.harheimertc.repositories.MannschaftenRepository
|
import de.harheimertc.repositories.MannschaftenRepository
|
||||||
import de.harheimertc.repositories.NotificationPreferences
|
import de.harheimertc.repositories.NotificationPreferences
|
||||||
import de.harheimertc.repositories.NotificationPreferencesRepository
|
import de.harheimertc.repositories.NotificationPreferencesRepository
|
||||||
|
import de.harheimertc.repositories.PushTokenRepository
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -22,6 +23,9 @@ data class NotificationSettingsUiState(
|
|||||||
val seasons: List<String> = emptyList(),
|
val seasons: List<String> = emptyList(),
|
||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
val saveError: String? = null,
|
val saveError: String? = null,
|
||||||
|
val deviceRegistrationMessage: String? = null,
|
||||||
|
val deviceRegistrationError: Boolean = false,
|
||||||
|
val deviceRegistrationInProgress: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -29,6 +33,7 @@ class NotificationSettingsViewModel @Inject constructor(
|
|||||||
private val preferencesRepository: NotificationPreferencesRepository,
|
private val preferencesRepository: NotificationPreferencesRepository,
|
||||||
private val mannschaftenRepository: MannschaftenRepository,
|
private val mannschaftenRepository: MannschaftenRepository,
|
||||||
private val loginRepository: LoginRepository,
|
private val loginRepository: LoginRepository,
|
||||||
|
private val pushTokenRepository: PushTokenRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _state = MutableStateFlow(NotificationSettingsUiState())
|
private val _state = MutableStateFlow(NotificationSettingsUiState())
|
||||||
val state: StateFlow<NotificationSettingsUiState> = _state
|
val state: StateFlow<NotificationSettingsUiState> = _state
|
||||||
@@ -82,6 +87,30 @@ class NotificationSettingsViewModel @Inject constructor(
|
|||||||
update(current.copy(selectedTeamSlugs = nextTeams))
|
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<String>, currentUserName: String, syncRemote: Boolean = false) {
|
private suspend fun loadTeams(settings: NotificationPreferences, seasons: List<String>, currentUserName: String, syncRemote: Boolean = false) {
|
||||||
mannschaftenRepository.fetchMannschaften(settings.selectedTeamSeason)
|
mannschaftenRepository.fetchMannschaften(settings.selectedTeamSeason)
|
||||||
.onSuccess { teams ->
|
.onSuccess { teams ->
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ LOCAL_API_BASE_URL=https://harheimertc.tsschulz.de/
|
|||||||
PRODUCTION_API_BASE_URL=https://harheimertc.de/
|
PRODUCTION_API_BASE_URL=https://harheimertc.de/
|
||||||
|
|
||||||
# Android app versioning for Play Store uploads
|
# Android app versioning for Play Store uploads
|
||||||
ANDROID_VERSION_CODE=34
|
ANDROID_VERSION_CODE=35
|
||||||
ANDROID_VERSION_NAME=0.9.29
|
ANDROID_VERSION_NAME=0.9.30
|
||||||
|
|
||||||
# Temporary hotfix: disable R8 minification for release to avoid Retrofit generic signature stripping.
|
# Temporary hotfix: disable R8 minification for release to avoid Retrofit generic signature stripping.
|
||||||
RELEASE_MINIFY_ENABLED=false
|
RELEASE_MINIFY_ENABLED=false
|
||||||
|
|||||||
@@ -47,6 +47,6 @@ const formattedLastImport = computed(() => status.value.lastSuccessfulImportAt ?
|
|||||||
async function load() { status.value = await $fetch('/api/cms/mytischtennis') }
|
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 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 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)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getUserFromToken, hasRole } from '../../../utils/auth.js'
|
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) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
|
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 (!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.' })
|
if (!hasRole(user, 'admin')) throw createError({ statusCode: 403, statusMessage: 'Nur Administratoren dürfen einen Push-Test auslösen.' })
|
||||||
|
|
||||||
const result = await sendTestPushToUser(user.id)
|
const registeredTokenCount = await androidPushTokenCountForUser(user.id)
|
||||||
return { success: true, ...result }
|
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.'
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -102,6 +102,11 @@ export function upsertPushToken(user, { token, platform = 'android', appVersion
|
|||||||
return user
|
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 = {} }) {
|
async function sendFcmMessage({ serviceAccount, accessToken, token, data = {} }) {
|
||||||
const projectId = projectIdFromServiceAccount(serviceAccount)
|
const projectId = projectIdFromServiceAccount(serviceAccount)
|
||||||
if (!projectId) throw new Error('FCM project_id fehlt.')
|
if (!projectId) throw new Error('FCM project_id fehlt.')
|
||||||
|
|||||||
Reference in New Issue
Block a user