mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 19:51:17 +00:00
fix pwa
This commit is contained in:
parent
711b02dffb
commit
669c769878
6 changed files with 282 additions and 81 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "Kumasy",
|
"name": "Kumasy",
|
||||||
"short_name": "Kumasy",
|
"short_name": "Kumasy",
|
||||||
"description": "Kumasy – Kurse buchen und verwalten",
|
"description": "Kumasy – Kurse buchen und verwalten",
|
||||||
"start_url": "/",
|
"start_url": "/?source=pwa",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#1E293B",
|
"background_color": "#1E293B",
|
||||||
"theme_color": "#F4C00C",
|
"theme_color": "#F4C00C",
|
||||||
|
|
|
||||||
|
|
@ -4,38 +4,10 @@ import { useEffect, useState, useSyncExternalStore } from "react";
|
||||||
import { X, BellRing } from "lucide-react";
|
import { X, BellRing } from "lucide-react";
|
||||||
import { Button } from "@/src/components/ui/button";
|
import { Button } from "@/src/components/ui/button";
|
||||||
import { INSTALL_PROMPT_SETTLED_EVENT, isInstallPromptPending } from "@/src/lib/pwa";
|
import { INSTALL_PROMPT_SETTLED_EVENT, isInstallPromptPending } from "@/src/lib/pwa";
|
||||||
import {
|
import { enablePushOnThisDevice, isPushBaseSupported } from "@/src/lib/push-client";
|
||||||
subscribeToPush,
|
|
||||||
unsubscribeFromPush,
|
|
||||||
} from "@/src/modules/notifications/actions/push-subscription-actions";
|
|
||||||
|
|
||||||
const DISMISSED_KEY = "kumasy-notification-prompt-dismissed";
|
const DISMISSED_KEY = "kumasy-notification-prompt-dismissed";
|
||||||
|
|
||||||
function urlBase64ToUint8Array(base64String: string): Uint8Array<ArrayBuffer> {
|
|
||||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
|
||||||
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
|
||||||
const rawData = window.atob(base64);
|
|
||||||
const output = new Uint8Array(new ArrayBuffer(rawData.length));
|
|
||||||
for (let i = 0; i < rawData.length; i++) output[i] = rawData.charCodeAt(i);
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPushSupported(): boolean {
|
|
||||||
return (
|
|
||||||
typeof window !== "undefined" &&
|
|
||||||
"serviceWorker" in navigator &&
|
|
||||||
"PushManager" in window &&
|
|
||||||
"Notification" in window
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basis-Voraussetzungen, die sich zur Laufzeit nicht ändern (API-Support,
|
|
||||||
// VAPID-Konfiguration) - anders als Notification.permission/localStorage
|
|
||||||
// unten, die erst nach einem Nutzer-Entscheid entstehen.
|
|
||||||
function isBaseSupported(): boolean {
|
|
||||||
return Boolean(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY) && isPushSupported();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Anders als in InstallPrompt.tsx gibt es hier ein echtes externes Signal,
|
// Anders als in InstallPrompt.tsx gibt es hier ein echtes externes Signal,
|
||||||
// auf das reagiert werden muss: computeBaseEligible() hängt (über
|
// auf das reagiert werden muss: computeBaseEligible() hängt (über
|
||||||
// isInstallPromptPending()) vom Install-Prompt-Status ab, der sich NACH
|
// isInstallPromptPending()) vom Install-Prompt-Status ab, der sich NACH
|
||||||
|
|
@ -60,7 +32,7 @@ function subscribeToInstallPromptSettled(onChange: () => void) {
|
||||||
*/
|
*/
|
||||||
function computeBaseEligible(): boolean {
|
function computeBaseEligible(): boolean {
|
||||||
if (typeof window === "undefined") return false;
|
if (typeof window === "undefined") return false;
|
||||||
if (!isBaseSupported()) return false;
|
if (!isPushBaseSupported()) return false;
|
||||||
if (Notification.permission === "denied") return false;
|
if (Notification.permission === "denied") return false;
|
||||||
// Wartet, bis InstallPrompt nicht mehr sichtbar ist (installiert oder
|
// Wartet, bis InstallPrompt nicht mehr sichtbar ist (installiert oder
|
||||||
// weggeklickt) - sonst überlappen sich beide "fixed bottom-4"-Banner.
|
// weggeklickt) - sonst überlappen sich beide "fixed bottom-4"-Banner.
|
||||||
|
|
@ -89,7 +61,7 @@ export function NotificationPrompt() {
|
||||||
const eligible = (baseEligible || missingServerRegistration) && !dismissed;
|
const eligible = (baseEligible || missingServerRegistration) && !dismissed;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isBaseSupported() && Notification.permission === "granted") {
|
if (isPushBaseSupported() && Notification.permission === "granted") {
|
||||||
navigator.serviceWorker.ready
|
navigator.serviceWorker.ready
|
||||||
.then((registration) => registration.pushManager.getSubscription())
|
.then((registration) => registration.pushManager.getSubscription())
|
||||||
.then((subscription) => {
|
.then((subscription) => {
|
||||||
|
|
@ -113,44 +85,22 @@ export function NotificationPrompt() {
|
||||||
const handleEnable = async () => {
|
const handleEnable = async () => {
|
||||||
setIsSubscribing(true);
|
setIsSubscribing(true);
|
||||||
try {
|
try {
|
||||||
const permission = await Notification.requestPermission();
|
const result = await enablePushOnThisDevice();
|
||||||
if (permission !== "granted") {
|
if (result.success) {
|
||||||
// "denied" wird schon von computeBaseEligible() dauerhaft
|
dismiss();
|
||||||
// ausgeschlossen, kein zusätzliches localStorage-Flag nötig.
|
} else {
|
||||||
|
// Technischer Fehler ODER Permission nicht erteilt: Prompt nur
|
||||||
|
// ausblenden, NICHT dauerhaft in localStorage merken (siehe
|
||||||
|
// hideWithoutPersisting) - eine "denied"-Permission wird bereits von
|
||||||
|
// computeBaseEligible() dauerhaft ausgeschlossen, ein Retry-Pfad für
|
||||||
|
// technische Fehler (Server/Netzwerk) muss aber bestehen bleiben.
|
||||||
|
// Best-effort: schlägt die Server-Registrierung fehl, bleibt evtl.
|
||||||
|
// eine verwaiste Browser-Subscription zurück, die der useEffect-Check
|
||||||
|
// oben beim nächsten Laden über getSubscription() erkennt und den
|
||||||
|
// Prompt trotzdem wieder anbietet.
|
||||||
|
console.error("Push-Aktivierung fehlgeschlagen:", result.error);
|
||||||
hideWithoutPersisting();
|
hideWithoutPersisting();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const registration = await navigator.serviceWorker.ready;
|
|
||||||
const subscription = await registration.pushManager.subscribe({
|
|
||||||
userVisibleOnly: true,
|
|
||||||
applicationServerKey: urlBase64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!),
|
|
||||||
});
|
|
||||||
|
|
||||||
const json = subscription.toJSON();
|
|
||||||
if (!json.endpoint || !json.keys?.p256dh || !json.keys?.auth) {
|
|
||||||
throw new Error("Unvollständige Push-Subscription vom Browser erhalten.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await subscribeToPush(
|
|
||||||
{ endpoint: json.endpoint, keys: { p256dh: json.keys.p256dh, auth: json.keys.auth } },
|
|
||||||
navigator.userAgent
|
|
||||||
);
|
|
||||||
if (!result.success) {
|
|
||||||
// Serverseitig nicht gespeichert - Subscription auf dem Gerät nicht
|
|
||||||
// verwaist zurücklassen, sonst denkt der Browser dauerhaft, er sei
|
|
||||||
// angemeldet, ohne dass Push je zugestellt werden könnte. Best-effort:
|
|
||||||
// schlägt das fehl, bleibt eine verwaiste Browser-Subscription zurück,
|
|
||||||
// die der useEffect-Check oben beim nächsten Laden über
|
|
||||||
// getSubscription() erkennt und den Prompt trotzdem wieder anbietet.
|
|
||||||
await subscription.unsubscribe().catch(() => {});
|
|
||||||
throw new Error(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
dismiss();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Push-Aktivierung fehlgeschlagen:", error);
|
|
||||||
hideWithoutPersisting();
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubscribing(false);
|
setIsSubscribing(false);
|
||||||
}
|
}
|
||||||
|
|
@ -194,16 +144,3 @@ export function NotificationPrompt() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wird von einer Einstellungen-UI importiert, um Push wieder zu deaktivieren
|
|
||||||
// (z.B. künftige "Geräte verwalten"-Seite) - hier co-located, weil sie
|
|
||||||
// denselben Browser-API-Zugriff (serviceWorker.ready) braucht wie handleEnable.
|
|
||||||
export async function disablePushOnThisDevice(): Promise<void> {
|
|
||||||
if (!isPushSupported()) return;
|
|
||||||
const registration = await navigator.serviceWorker.ready;
|
|
||||||
const subscription = await registration.pushManager.getSubscription();
|
|
||||||
if (!subscription) return;
|
|
||||||
|
|
||||||
await unsubscribeFromPush(subscription.endpoint).catch(() => {});
|
|
||||||
await subscription.unsubscribe().catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
109
src/lib/push-client.ts
Normal file
109
src/lib/push-client.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
/**
|
||||||
|
* Client-seitige Web-Push-Helfer, gemeinsam genutzt von NotificationPrompt
|
||||||
|
* (Erst-Opt-in-Banner) und der Push-Sektion in /settings/personal
|
||||||
|
* (nachträgliches Aktivieren/Status/Deaktivieren).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
subscribeToPush,
|
||||||
|
unsubscribeFromPush,
|
||||||
|
} from "@/src/modules/notifications/actions/push-subscription-actions";
|
||||||
|
|
||||||
|
function urlBase64ToUint8Array(base64String: string): Uint8Array<ArrayBuffer> {
|
||||||
|
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||||
|
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
const rawData = window.atob(base64);
|
||||||
|
const output = new Uint8Array(new ArrayBuffer(rawData.length));
|
||||||
|
for (let i = 0; i < rawData.length; i++) output[i] = rawData.charCodeAt(i);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPushSupported(): boolean {
|
||||||
|
return (
|
||||||
|
typeof window !== "undefined" &&
|
||||||
|
"serviceWorker" in navigator &&
|
||||||
|
"PushManager" in window &&
|
||||||
|
"Notification" in window
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API-Support + VAPID-Konfiguration - ändert sich zur Laufzeit nicht. */
|
||||||
|
export function isPushBaseSupported(): boolean {
|
||||||
|
return Boolean(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY) && isPushSupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnablePushResult = { success: true } | { success: false; error: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragt die Browser-Permission an (falls nötig), erstellt eine
|
||||||
|
* PushSubscription und registriert sie serverseitig. Wirft nie - Fehler
|
||||||
|
* kommen als { success: false, error }.
|
||||||
|
*/
|
||||||
|
export async function enablePushOnThisDevice(): Promise<EnablePushResult> {
|
||||||
|
if (!isPushBaseSupported()) {
|
||||||
|
return { success: false, error: "Push wird von diesem Browser nicht unterstützt." };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const permission = await Notification.requestPermission();
|
||||||
|
if (permission !== "granted") {
|
||||||
|
return { success: false, error: "Berechtigung wurde nicht erteilt." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
const subscription = await registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: urlBase64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!),
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = subscription.toJSON();
|
||||||
|
if (!json.endpoint || !json.keys?.p256dh || !json.keys?.auth) {
|
||||||
|
throw new Error("Unvollständige Push-Subscription vom Browser erhalten.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await subscribeToPush(
|
||||||
|
{ endpoint: json.endpoint, keys: { p256dh: json.keys.p256dh, auth: json.keys.auth } },
|
||||||
|
navigator.userAgent
|
||||||
|
);
|
||||||
|
if (!result.success) {
|
||||||
|
// Serverseitig nicht gespeichert - Subscription auf dem Gerät nicht
|
||||||
|
// verwaist zurücklassen, sonst denkt der Browser dauerhaft, er sei
|
||||||
|
// angemeldet, ohne dass Push je zugestellt werden könnte.
|
||||||
|
await subscription.unsubscribe().catch(() => {});
|
||||||
|
return { success: false, error: result.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("enablePushOnThisDevice fehlgeschlagen:", error);
|
||||||
|
return { success: false, error: "Push-Aktivierung fehlgeschlagen." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function disablePushOnThisDevice(): Promise<void> {
|
||||||
|
if (!isPushSupported()) return;
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
const subscription = await registration.pushManager.getSubscription();
|
||||||
|
if (!subscription) return;
|
||||||
|
|
||||||
|
await unsubscribeFromPush(subscription.endpoint).catch(() => {});
|
||||||
|
await subscription.unsubscribe().catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PushDeviceStatus = "unsupported" | "denied" | "disabled" | "enabled";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ermittelt den aktuellen Push-Status DIESES Geräts/Browsers. "enabled"
|
||||||
|
* verlangt eine tatsächlich beim Server registrierte Subscription, nicht nur
|
||||||
|
* Browser-Permission="granted" - sonst zeigt die Settings-Seite fälschlich
|
||||||
|
* "aktiv" an, wenn die Server-Registrierung zuvor fehlgeschlagen ist.
|
||||||
|
*/
|
||||||
|
export async function getPushDeviceStatus(): Promise<PushDeviceStatus> {
|
||||||
|
if (!isPushBaseSupported()) return "unsupported";
|
||||||
|
if (Notification.permission === "denied") return "denied";
|
||||||
|
if (Notification.permission !== "granted") return "disabled";
|
||||||
|
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
const subscription = await registration.pushManager.getSubscription();
|
||||||
|
return subscription ? "enabled" : "disabled";
|
||||||
|
}
|
||||||
|
|
@ -47,6 +47,22 @@ export async function middleware(request: NextRequest) {
|
||||||
return redirectResponse;
|
return redirectResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// manifest.json's start_url ("/?source=pwa") landet nur hier, wenn das
|
||||||
|
// Betriebssystem die installierte PWA vom Homescreen öffnet - ein normaler
|
||||||
|
// Browser-Aufruf von "/" hat diesen Parameter nie. Die Marketing-
|
||||||
|
// Landingpage ist für Homescreen-Nutzer nutzlos (kein neuer Besucher würde
|
||||||
|
// sie so erreichen), daher direkt zu Login/Dashboard weiterleiten.
|
||||||
|
if (request.nextUrl.pathname === "/" && request.nextUrl.searchParams.get("source") === "pwa") {
|
||||||
|
const url = request.nextUrl.clone();
|
||||||
|
url.pathname = user ? "/dashboard" : "/login";
|
||||||
|
url.searchParams.delete("source");
|
||||||
|
const redirectResponse = NextResponse.redirect(url);
|
||||||
|
supabaseResponse.cookies.getAll().forEach(({ name, value, ...options }) => {
|
||||||
|
redirectResponse.cookies.set(name, value, options);
|
||||||
|
});
|
||||||
|
return redirectResponse;
|
||||||
|
}
|
||||||
|
|
||||||
return supabaseResponse;
|
return supabaseResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Bell, BellOff, BellRing, ShieldAlert } from "lucide-react";
|
||||||
|
import { Button } from "@/src/components/ui/button";
|
||||||
|
import {
|
||||||
|
disablePushOnThisDevice,
|
||||||
|
enablePushOnThisDevice,
|
||||||
|
getPushDeviceStatus,
|
||||||
|
type PushDeviceStatus,
|
||||||
|
} from "@/src/lib/push-client";
|
||||||
|
|
||||||
|
const STATUS_CONTENT: Record<
|
||||||
|
PushDeviceStatus,
|
||||||
|
{ icon: typeof Bell; iconBg: string; iconColor: string; title: string; description: string }
|
||||||
|
> = {
|
||||||
|
unsupported: {
|
||||||
|
icon: BellOff,
|
||||||
|
iconBg: "bg-gray-100",
|
||||||
|
iconColor: "text-gray-400",
|
||||||
|
title: "Nicht unterstützt",
|
||||||
|
description: "Dieser Browser unterstützt keine Push-Benachrichtigungen.",
|
||||||
|
},
|
||||||
|
denied: {
|
||||||
|
icon: ShieldAlert,
|
||||||
|
iconBg: "bg-orange-50",
|
||||||
|
iconColor: "text-orange-600",
|
||||||
|
title: "Blockiert",
|
||||||
|
description:
|
||||||
|
"Du hast Benachrichtigungen für diese Seite im Browser blockiert. Erlaube sie in den Website-Einstellungen deines Browsers, um sie hier wieder zu aktivieren.",
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
icon: Bell,
|
||||||
|
iconBg: "bg-yellow-50",
|
||||||
|
iconColor: "text-yellow-600",
|
||||||
|
title: "Deaktiviert",
|
||||||
|
description:
|
||||||
|
"Aktiviere Benachrichtigungen, um über Buchungsbestätigungen und freie Wartelisten-Plätze informiert zu werden.",
|
||||||
|
},
|
||||||
|
enabled: {
|
||||||
|
icon: BellRing,
|
||||||
|
iconBg: "bg-green-50",
|
||||||
|
iconColor: "text-green-600",
|
||||||
|
title: "Aktiv auf diesem Gerät",
|
||||||
|
description: "Du erhältst Push-Benachrichtigungen auf diesem Gerät/Browser.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push-Status + (De-)Aktivierung für DIESES Gerät/Browser - unabhängig von
|
||||||
|
* NotificationPrompt (dem Erst-Opt-in-Banner), der einmalig weggeklickt
|
||||||
|
* werden kann. Hier kann Push jederzeit nachträglich aktiviert werden, ohne
|
||||||
|
* auf den Banner warten oder Browser-Daten zurücksetzen zu müssen.
|
||||||
|
*/
|
||||||
|
const NotificationPermissionSettings = () => {
|
||||||
|
// null = wird noch ermittelt (getPushDeviceStatus ist async, siehe
|
||||||
|
// getSubscription() in push-client.ts) - erst nach dem Mount bekannt, um
|
||||||
|
// serverseitig identisch mit dem ersten Client-Render zu bleiben.
|
||||||
|
const [status, setStatus] = useState<PushDeviceStatus | null>(null);
|
||||||
|
const [isWorking, setIsWorking] = useState(false);
|
||||||
|
|
||||||
|
// getPushDeviceStatus() deckt den "unsupported"-Fall (isPushBaseSupported())
|
||||||
|
// bereits selbst ab - kein synchroner Kurzschluss hier nötig, sonst würde
|
||||||
|
// setState synchron im Effect-Body statt in der .then()-Callback laufen.
|
||||||
|
const refreshStatus = () => {
|
||||||
|
getPushDeviceStatus()
|
||||||
|
.then(setStatus)
|
||||||
|
.catch(() => setStatus("unsupported"));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleEnable = async () => {
|
||||||
|
setIsWorking(true);
|
||||||
|
try {
|
||||||
|
const result = await enablePushOnThisDevice();
|
||||||
|
if (!result.success) {
|
||||||
|
console.error("Push-Aktivierung fehlgeschlagen:", result.error);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
refreshStatus();
|
||||||
|
setIsWorking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisable = async () => {
|
||||||
|
setIsWorking(true);
|
||||||
|
try {
|
||||||
|
await disablePushOnThisDevice();
|
||||||
|
} finally {
|
||||||
|
refreshStatus();
|
||||||
|
setIsWorking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (status === null) return null;
|
||||||
|
|
||||||
|
const content = STATUS_CONTENT[status];
|
||||||
|
const Icon = content.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-white/60 bg-white/55 backdrop-blur-xl rounded-2xl shadow-sm p-6 space-y-5">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-base font-semibold text-gray-900">Push-Benachrichtigungen</h2>
|
||||||
|
<p className="text-xs text-gray-400 mt-0.5">Für dieses Gerät/diesen Browser</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div
|
||||||
|
className={`shrink-0 flex items-center justify-center size-9 rounded-lg ${content.iconBg} ${content.iconColor}`}
|
||||||
|
>
|
||||||
|
<Icon size={16} />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-sm font-medium text-gray-900">{content.title}</p>
|
||||||
|
<p className="text-xs text-gray-500 mt-0.5">{content.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status === "disabled" && (
|
||||||
|
<Button size="sm" onClick={handleEnable} disabled={isWorking}>
|
||||||
|
{isWorking ? "Aktiviert…" : "Benachrichtigungen aktivieren"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === "enabled" && (
|
||||||
|
<Button size="sm" variant="outline" onClick={handleDisable} disabled={isWorking}>
|
||||||
|
{isWorking ? "Deaktiviert…" : "Auf diesem Gerät deaktivieren"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationPermissionSettings;
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { requireAuthWithData, isManager, isEmployee, isUser } from "@/src/lib/auth/getUser";
|
import { requireAuthWithData, isManager, isEmployee, isUser } from "@/src/lib/auth/getUser";
|
||||||
import PersonalSettingsForm, { PersonalSettingsProps } from "../components/PersonalSettingsForm";
|
import PersonalSettingsForm, { PersonalSettingsProps } from "../components/PersonalSettingsForm";
|
||||||
|
import NotificationPermissionSettings from "../components/NotificationPermissionSettings";
|
||||||
|
|
||||||
const SettingsPersonalPageView = async () => {
|
const SettingsPersonalPageView = async () => {
|
||||||
const userData = await requireAuthWithData();
|
const userData = await requireAuthWithData();
|
||||||
|
|
@ -51,6 +52,7 @@ const SettingsPersonalPageView = async () => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<PersonalSettingsForm {...props} />
|
<PersonalSettingsForm {...props} />
|
||||||
|
<NotificationPermissionSettings />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue