KI-AGENT: Mobile Push Registrierung anbinden
This commit is contained in:
@@ -55,6 +55,7 @@
|
||||
}
|
||||
],
|
||||
"react-native-ble-plx",
|
||||
"expo-notifications",
|
||||
"expo-web-browser"
|
||||
],
|
||||
"experiments": {
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-
|
||||
import { router } from 'expo-router';
|
||||
|
||||
import { DEFAULT_API_BASE_URL } from '@/src/config/env';
|
||||
import { sendMobileTestPush } from '@/src/lib/api';
|
||||
import { registerDeviceForPush } from '@/src/lib/push-registration';
|
||||
import {
|
||||
getApiBaseUrlSync,
|
||||
hydrateApiBaseUrl,
|
||||
@@ -23,6 +25,7 @@ export default function SettingsScreen() {
|
||||
const [serverUrl, setServerUrlInput] = useState(getApiBaseUrlSync());
|
||||
const [savedUrl, setSavedUrl] = useState(getApiBaseUrlSync());
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [pushSubmitting, setPushSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
||||
@@ -85,6 +88,46 @@ export default function SettingsScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
async function onRegisterPush() {
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
if (!token) {
|
||||
setError('Bitte zuerst anmelden, um dieses Gerät für Push zu registrieren.');
|
||||
return;
|
||||
}
|
||||
|
||||
setPushSubmitting(true);
|
||||
try {
|
||||
const result = await registerDeviceForPush(token);
|
||||
setSuccess(`Push registriert: ${result.centralDeviceId || result.id || 'aktiv'}`);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Push-Registrierung fehlgeschlagen.');
|
||||
} finally {
|
||||
setPushSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onSendTestPush() {
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
if (!token) {
|
||||
setError('Bitte zuerst anmelden, um eine Testnachricht zu senden.');
|
||||
return;
|
||||
}
|
||||
|
||||
setPushSubmitting(true);
|
||||
try {
|
||||
const result = await sendMobileTestPush(token);
|
||||
setSuccess(`Testnachricht angefordert: ${result.accepted} akzeptiert, ${result.rejected} abgelehnt.`);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Testnachricht konnte nicht gesendet werden.');
|
||||
} finally {
|
||||
setPushSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
const isDirty = serverUrl.trim() !== savedUrl;
|
||||
|
||||
return (
|
||||
@@ -131,6 +174,30 @@ export default function SettingsScreen() {
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.title}>Mobile Push</Text>
|
||||
<Text style={styles.hint}>
|
||||
Registriert dieses Gerät bei deiner FEDEO-Instanz und leitet den nativen Push-Token an den zentralen
|
||||
Push-Server weiter.
|
||||
</Text>
|
||||
|
||||
<View style={styles.actions}>
|
||||
<Pressable
|
||||
style={[styles.saveButton, (!token || pushSubmitting) ? styles.buttonDisabled : null]}
|
||||
onPress={onRegisterPush}
|
||||
disabled={!token || pushSubmitting}>
|
||||
<Text style={styles.saveButtonText}>{pushSubmitting ? 'Bitte warten...' : 'Gerät registrieren'}</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
style={[styles.resetButton, (!token || pushSubmitting) ? styles.buttonDisabled : null]}
|
||||
onPress={onSendTestPush}
|
||||
disabled={!token || pushSubmitting}>
|
||||
<Text style={styles.resetButtonText}>Testnachricht senden</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
@@ -140,6 +207,7 @@ const styles = StyleSheet.create({
|
||||
flexGrow: 1,
|
||||
backgroundColor: '#f9fafb',
|
||||
padding: 16,
|
||||
gap: 12,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#ffffff',
|
||||
|
||||
117
mobile/package-lock.json
generated
117
mobile/package-lock.json
generated
@@ -21,6 +21,7 @@
|
||||
"expo-image": "~3.0.11",
|
||||
"expo-image-picker": "~17.0.11",
|
||||
"expo-linking": "~8.0.12",
|
||||
"expo-notifications": "^56.0.12",
|
||||
"expo-router": "~6.0.23",
|
||||
"expo-secure-store": "^15.0.8",
|
||||
"expo-splash-screen": "~31.0.13",
|
||||
@@ -2149,6 +2150,25 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/require-utils": {
|
||||
"version": "56.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-56.1.2.tgz",
|
||||
"integrity": "sha512-j+zlUQK7xPTKlR9honSLN4umd4czOpNBPibJhOQVxSfT3IP8UJR+7aFvccj5dbuYiBCzDy8vxuDm3AGa0onR8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.20.0",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.24.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0 || ^5.0.0-0 || ^6.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/schema-utils": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-0.1.8.tgz",
|
||||
@@ -2162,12 +2182,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@expo/spawn-async": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@expo/spawn-async/-/spawn-async-1.7.2.tgz",
|
||||
"integrity": "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@expo/spawn-async/-/spawn-async-1.8.0.tgz",
|
||||
"integrity": "sha512-eb9xxd/LbuEGSdua4NumCu/McVB9EM+F/JxB9pWgnERw4HQ9XyTNH1KapG6oqLWR8TuRK2LQfzJlmNi94CVobw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.3"
|
||||
"cross-spawn": "^7.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -4441,6 +4461,12 @@
|
||||
"@babel/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/badgin": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz",
|
||||
"integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -6043,6 +6069,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/expo-application": {
|
||||
"version": "56.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expo-application/-/expo-application-56.0.3.tgz",
|
||||
"integrity": "sha512-DdGGPlMuM6cSTeKhbvh6OeLr2O/+EI5BHKYrD+Do8sJPYgLwzGrgESELfyjJCpEhFzT+TgKIdmLmWXhNUQnHiw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-asset": {
|
||||
"version": "12.0.13",
|
||||
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.13.tgz",
|
||||
@@ -6225,6 +6260,78 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-notifications": {
|
||||
"version": "56.0.12",
|
||||
"resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-56.0.12.tgz",
|
||||
"integrity": "sha512-ZGFeA6vs1dt+9IcFtriIf2sEgBSEXGZ6OnWIYzUkdYqKpJFv1/zigUyquAMEvGbAAjGC0Uwf8qXNYJc1pyxFfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/image-utils": "^0.10.0",
|
||||
"abort-controller": "^3.0.0",
|
||||
"badgin": "^1.1.5",
|
||||
"expo-application": "~56.0.3",
|
||||
"expo-constants": "~56.0.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-notifications/node_modules/@expo/env": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@expo/env/-/env-2.3.0.tgz",
|
||||
"integrity": "sha512-9HnnIbzwTTdbwSjNLXTk0fPm9ZwMJ7c1/31tsni8HZ8Q62KzYCyspahH+V365vg5J6lr001DzNwBxVWSaYCQLg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^4.0.0",
|
||||
"debug": "^4.3.4",
|
||||
"getenv": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-notifications/node_modules/@expo/image-utils": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.10.0.tgz",
|
||||
"integrity": "sha512-iV1J+F5KpVqfdYsuot+5b8ZBDH6m/jQN2EzQSoa+qOmHqPNck17AihA4X3sso7ghn7p+AHeOKgftwT64amgmkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/require-utils": "^56.1.2",
|
||||
"@expo/spawn-async": "^1.8.0",
|
||||
"chalk": "^4.0.0",
|
||||
"getenv": "^2.0.0",
|
||||
"jimp-compact": "0.16.1",
|
||||
"parse-png": "^2.1.0",
|
||||
"semver": "^7.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-notifications/node_modules/expo-constants": {
|
||||
"version": "56.0.14",
|
||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-56.0.14.tgz",
|
||||
"integrity": "sha512-NeFIFXi+RAB5ayR/CPiQXRab0HczkA+BQfF8uci4G3RMBSy+uzd+1skRx/uqLUo3OYjSfs6LUQ8JDVbRgJRRQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/env": "~2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-notifications/node_modules/semver": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
|
||||
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-router": {
|
||||
"version": "6.0.23",
|
||||
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-6.0.23.tgz",
|
||||
@@ -12086,7 +12193,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"expo-image": "~3.0.11",
|
||||
"expo-image-picker": "~17.0.11",
|
||||
"expo-linking": "~8.0.12",
|
||||
"expo-notifications": "^56.0.12",
|
||||
"expo-router": "~6.0.23",
|
||||
"expo-secure-store": "^15.0.8",
|
||||
"expo-splash-screen": "~31.0.13",
|
||||
|
||||
@@ -150,6 +150,21 @@ export type PrintLabelResponse = {
|
||||
base64?: string;
|
||||
};
|
||||
|
||||
export type MobilePushRegistrationInput = {
|
||||
localDeviceId: string;
|
||||
platform: 'ios' | 'android';
|
||||
providerToken: string;
|
||||
deviceLabel?: string | null;
|
||||
meta?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type MobilePushRegistrationResponse = {
|
||||
success: boolean;
|
||||
id?: string;
|
||||
centralDeviceId?: string;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export type WikiTreeItem = {
|
||||
id: string;
|
||||
parentId?: string | null;
|
||||
@@ -582,6 +597,24 @@ export async function fetchMe(token: string): Promise<MeResponse> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function registerMobilePushDevice(
|
||||
token: string,
|
||||
payload: MobilePushRegistrationInput
|
||||
): Promise<MobilePushRegistrationResponse> {
|
||||
return apiRequest<MobilePushRegistrationResponse>('/api/notifications/push/mobile/register', {
|
||||
method: 'POST',
|
||||
token,
|
||||
body: payload,
|
||||
});
|
||||
}
|
||||
|
||||
export async function sendMobileTestPush(token: string): Promise<{ accepted: number; rejected: number; deliveryJobId: string }> {
|
||||
return apiRequest<{ accepted: number; rejected: number; deliveryJobId: string }>('/api/notifications/test-mobile-push', {
|
||||
method: 'POST',
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
export async function switchTenantRequest(tenantId: number, token: string): Promise<string> {
|
||||
const payload = await apiRequest<{ token?: string }>('/api/tenant/switch', {
|
||||
method: 'POST',
|
||||
|
||||
76
mobile/src/lib/push-registration.ts
Normal file
76
mobile/src/lib/push-registration.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import Constants from 'expo-constants';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
import { registerMobilePushDevice } from '@/src/lib/api';
|
||||
|
||||
const DEVICE_ID_KEY = 'fedeo.mobilePush.localDeviceId';
|
||||
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => ({
|
||||
shouldPlaySound: true,
|
||||
shouldSetBadge: true,
|
||||
shouldShowBanner: true,
|
||||
shouldShowList: true,
|
||||
}),
|
||||
});
|
||||
|
||||
function createLocalDeviceId(): string {
|
||||
const random = typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
|
||||
return `mobile-${random}`;
|
||||
}
|
||||
|
||||
async function getLocalDeviceId() {
|
||||
const existing = await SecureStore.getItemAsync(DEVICE_ID_KEY);
|
||||
if (existing) return existing;
|
||||
|
||||
const created = createLocalDeviceId();
|
||||
await SecureStore.setItemAsync(DEVICE_ID_KEY, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
async function ensurePushPermission() {
|
||||
const current = await Notifications.getPermissionsAsync();
|
||||
if (hasPushPermission(current)) return;
|
||||
|
||||
const requested = await Notifications.requestPermissionsAsync();
|
||||
if (!hasPushPermission(requested)) {
|
||||
throw new Error('Push-Mitteilungen wurden nicht erlaubt.');
|
||||
}
|
||||
}
|
||||
|
||||
function hasPushPermission(status: unknown): boolean {
|
||||
const permission = status as { granted?: boolean; status?: string };
|
||||
return permission.granted === true || permission.status === 'granted';
|
||||
}
|
||||
|
||||
export async function registerDeviceForPush(token: string) {
|
||||
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
|
||||
throw new Error('Mobile Push ist nur auf iOS und Android verfügbar.');
|
||||
}
|
||||
|
||||
await ensurePushPermission();
|
||||
|
||||
const deviceToken = await Notifications.getDevicePushTokenAsync();
|
||||
const providerToken = String(deviceToken.data || '');
|
||||
if (!providerToken) {
|
||||
throw new Error('Es wurde kein nativer Push-Token zurückgegeben.');
|
||||
}
|
||||
|
||||
return await registerMobilePushDevice(token, {
|
||||
localDeviceId: await getLocalDeviceId(),
|
||||
platform: Platform.OS,
|
||||
providerToken,
|
||||
deviceLabel: Constants.deviceName || `${Platform.OS} Gerät`,
|
||||
meta: {
|
||||
os: Platform.OS,
|
||||
osVersion: Platform.Version,
|
||||
expoRuntimeVersion: Constants.expoConfig?.runtimeVersion || null,
|
||||
appVersion: Constants.expoConfig?.version || null,
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user