diff --git a/backend/src/routes/exports.ts b/backend/src/routes/exports.ts
index e849f97..e30ad07 100644
--- a/backend/src/routes/exports.ts
+++ b/backend/src/routes/exports.ts
@@ -177,11 +177,49 @@ export default async function exportRoutes(server: FastifyInstance) {
.from(generatedexports)
.where(eq(generatedexports.tenantId, req.user.tenant_id))
- console.log(data)
- reply.send(data)
+ reply.send(data.map(item => ({
+ ...item,
+ url: `/api/exports/${item.id}/download`,
+ })))
})
+ server.get("/exports/:id/download", async (req, reply) => {
+ const { id } = req.params as { id: string }
+ const exportId = Number(id)
+ if (!Number.isSafeInteger(exportId)) {
+ return reply.code(404).send({ error: "Export not found" })
+ }
+
+ const [item] = await server.db
+ .select()
+ .from(generatedexports)
+ .where(eq(generatedexports.id, exportId))
+
+ if (
+ !item ||
+ item.tenantId !== req.user.tenant_id ||
+ item.validUntil.getTime() <= Date.now()
+ ) {
+ return reply.code(404).send({ error: "Export not found" })
+ }
+
+ try {
+ const { Body, ContentType } = await s3.send(new GetObjectCommand({
+ Bucket: secrets.S3_BUCKET,
+ Key: item.filePath,
+ }))
+ const filename = item.filePath.split("/").pop() || `export-${item.id}`
+
+ reply.header("Content-Type", ContentType || "application/octet-stream")
+ reply.header("Content-Disposition", `attachment; filename="${filename}"`)
+ return reply.send(Body as any)
+ } catch (error) {
+ console.error(error)
+ return reply.code(500).send({ error: "Could not download export" })
+ }
+ })
+
diff --git a/backend/src/routes/files.ts b/backend/src/routes/files.ts
index cff9141..1494510 100644
--- a/backend/src/routes/files.ts
+++ b/backend/src/routes/files.ts
@@ -4,7 +4,6 @@ import { s3 } from "../utils/s3"
import {
GetObjectCommand
} from "@aws-sdk/client-s3"
-import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
import archiver from "archiver"
import { secrets } from "../utils/secrets"
import { saveFile } from "../utils/files"
@@ -241,6 +240,28 @@ export default async function fileRoutes(server: FastifyInstance) {
}
})
+ // Dateien über das Backend ausliefern, damit interne S3-Endpunkte wie
+ // http://minio:9000 niemals als URL an den Browser gelangen.
+ server.get("/files/content/:id", async (req, reply) => {
+ try {
+ const { id } = req.params as { id: string }
+ const file = await loadSingleFileForRequest(req, id)
+ if (!file) return reply.code(404).send({ error: "File not found" })
+
+ const { Body, ContentType } = await s3.send(new GetObjectCommand({
+ Bucket: secrets.S3_BUCKET,
+ Key: file.path!
+ }))
+
+ reply.header("Content-Type", ContentType || "application/octet-stream")
+ reply.header("Content-Disposition", "inline")
+ return reply.send(Body as any)
+ } catch (err) {
+ console.error(err)
+ return reply.code(500).send({ error: "Could not load file" })
+ }
+ })
+
// -------------------------------------------------------------
@@ -261,13 +282,7 @@ export default async function fileRoutes(server: FastifyInstance) {
const file = await loadSingleFileForRequest(req, id)
if (!file) return reply.code(404).send({ error: "Not found" })
- const url = await getSignedUrl(
- s3,
- new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: file.path! }),
- { expiresIn: 900 }
- )
-
- return { ...file, url }
+ return { ...file, url: `/api/files/content/${file.id}` }
} else {
// -------------------------------------------------
// MULTIPLE PRESIGNED URLs
@@ -283,26 +298,10 @@ export default async function fileRoutes(server: FastifyInstance) {
const selected = rows.filter(f => ids.includes(f.id) && f.path)
- console.log(selected)
-
- const url = await getSignedUrl(
- s3,
- new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: selected[0].path! }),
- { expiresIn: 900 }
- )
- console.log(url)
- console.log(selected.filter(f => !f.path))
-
- const output = await Promise.all(
- selected.map(async (file) => {
- const url = await getSignedUrl(
- s3,
- new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: file.path! }),
- { expiresIn: 900 }
- )
- return { ...file, url }
- })
- )
+ const output = selected.map(file => ({
+ ...file,
+ url: `/api/files/content/${file.id}`
+ }))
return { files: output }
}
diff --git a/frontend/pages/export/index.vue b/frontend/pages/export/index.vue
index 837d348..2baed11 100644
--- a/frontend/pages/export/index.vue
+++ b/frontend/pages/export/index.vue
@@ -62,13 +62,19 @@ const setupPage = async () => {
setupPage()
-function downloadFile(row) {
+async function downloadFile(row) {
+ const response = await useNuxtApp().$api.raw(row.url, {
+ method: "GET",
+ responseType: "blob"
+ })
+ const objectUrl = URL.createObjectURL(response._data as Blob)
const a = document.createElement("a")
- a.href = row.url
+ a.href = objectUrl
a.download = row.file_path.split("/").pop()
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
+ URL.revokeObjectURL(objectUrl)
}
const showCreateExportModal = ref(false)
diff --git a/mobile/app.json b/mobile/app.json
index 0ba9dad..9902d46 100644
--- a/mobile/app.json
+++ b/mobile/app.json
@@ -11,7 +11,7 @@
"ios": {
"supportsTablet": true,
"bundleIdentifier": "software.federspiel.fedeo",
- "buildNumber": "7",
+ "buildNumber": "11",
"infoPlist": {
"NSCameraUsageDescription": "Die Kamera wird benötigt, um Fotos zu Projekten und Objekten als Dokumente hochzuladen.",
"NSPhotoLibraryUsageDescription": "Der Zugriff auf Fotos wird benötigt, um Bilder als Dokumente hochzuladen.",
@@ -56,7 +56,21 @@
],
"react-native-ble-plx",
"expo-notifications",
- "expo-web-browser"
+ "expo-web-browser",
+ "./plugins/with-share-intent-multifile",
+ [
+ "expo-share-intent",
+ {
+ "iosShareExtensionName": "In FEDEO hochladen",
+ "iosAppGroupIdentifier": "group.software.federspiel.fedeo.share",
+ "iosActivationRules": {
+ "NSExtensionActivationSupportsFileWithMaxCount": 10,
+ "NSExtensionActivationSupportsImageWithMaxCount": 10,
+ "NSExtensionActivationSupportsMovieWithMaxCount": 10
+ },
+ "disableAndroid": true
+ }
+ ]
],
"experiments": {
"typedRoutes": true,
diff --git a/mobile/app/(tabs)/projects.tsx b/mobile/app/(tabs)/projects.tsx
index 2c2312b..edd6b5d 100644
--- a/mobile/app/(tabs)/projects.tsx
+++ b/mobile/app/(tabs)/projects.tsx
@@ -15,6 +15,7 @@ import {
import { router } from 'expo-router';
import { createProject, Customer, fetchCustomers, fetchPlants, fetchProjects, Plant, Project } from '@/src/lib/api';
+import { filterProjects, getActiveProjectPhase, isProjectCompleted } from '@/src/lib/resource-list-filters';
import { useAuth } from '@/src/providers/auth-provider';
const PRIMARY = '#69c350';
@@ -24,20 +25,6 @@ function getProjectLine(project: Project): string {
return project.name;
}
-function getActivePhaseLabel(project: Project): string {
- const explicit = String(project.active_phase || '').trim();
- if (explicit) return explicit;
-
- const phases = Array.isArray(project.phases) ? project.phases : [];
- const active = phases.find((phase: any) => phase?.active);
- return String(active?.label || '').trim();
-}
-
-function isProjectCompletedByPhase(project: Project): boolean {
- const phase = getActivePhaseLabel(project).toLowerCase();
- return phase === 'abgeschlossen';
-}
-
export default function ProjectsScreen() {
const { token } = useAuth();
@@ -64,31 +51,10 @@ export default function ProjectsScreen() {
const [customerSearch, setCustomerSearch] = useState('');
const [plantSearch, setPlantSearch] = useState('');
- const filteredProjects = useMemo(() => {
- const terms = search
- .trim()
- .toLowerCase()
- .split(/\s+/)
- .filter(Boolean);
-
- return projects.filter((project) => {
- if (!showArchived && isProjectCompletedByPhase(project)) return false;
- if (terms.length === 0) return true;
-
- const haystack = [
- project.name,
- project.projectNumber,
- project.notes,
- project.customerRef,
- project.active_phase,
- getActivePhaseLabel(project),
- ]
- .map((value) => String(value || '').toLowerCase())
- .join(' ');
-
- return terms.every((term) => haystack.includes(term));
- });
- }, [projects, search, showArchived]);
+ const filteredProjects = useMemo(
+ () => filterProjects(projects, search, showArchived),
+ [projects, search, showArchived]
+ );
const selectedCustomerLabel = useMemo(() => {
if (!selectedCustomerId) return 'Kunde auswählen (optional)';
@@ -241,10 +207,10 @@ export default function ProjectsScreen() {
router.push(`/project/${project.id}`)}>
{getProjectLine(project)}
- {isProjectCompletedByPhase(project) ? Abgeschlossen : null}
+ {isProjectCompleted(project) ? Abgeschlossen : null}
- {getActivePhaseLabel(project) ? (
- Phase: {getActivePhaseLabel(project)}
+ {getActiveProjectPhase(project) ? (
+ Phase: {getActiveProjectPhase(project)}
) : null}
{project.notes ? {String(project.notes)} : null}
diff --git a/mobile/app/+native-intent.ts b/mobile/app/+native-intent.ts
new file mode 100644
index 0000000..04e4aca
--- /dev/null
+++ b/mobile/app/+native-intent.ts
@@ -0,0 +1,13 @@
+import { getShareExtensionKey } from 'expo-share-intent';
+
+export function redirectSystemPath({ path }: { path: string; initial: boolean }) {
+ try {
+ if (path.includes(`dataUrl=${getShareExtensionKey()}`)) {
+ // Zuerst die normale Session laden; der Provider öffnet danach die Upload-Strecke.
+ return '/';
+ }
+ return path;
+ } catch {
+ return '/';
+ }
+}
diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx
index 7e88a18..230b103 100644
--- a/mobile/app/_layout.tsx
+++ b/mobile/app/_layout.tsx
@@ -1,17 +1,51 @@
-import { Stack } from 'expo-router';
+import { Href, Stack, useRouter } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
+import { ShareIntentProvider, useShareIntentContext } from 'expo-share-intent';
+import { useEffect, useRef } from 'react';
import 'react-native-reanimated';
-import { AuthProvider } from '@/src/providers/auth-provider';
+import { AuthProvider, useAuth } from '@/src/providers/auth-provider';
+
+function ShareIntentRouter() {
+ const router = useRouter();
+ const { hasShareIntent } = useShareIntentContext();
+ const { isBootstrapping, token } = useAuth();
+ const isOpeningRef = useRef(false);
+
+ useEffect(() => {
+ if (!hasShareIntent) {
+ isOpeningRef.current = false;
+ return;
+ }
+
+ if (!isBootstrapping && token && !isOpeningRef.current) {
+ isOpeningRef.current = true;
+ router.push('/share-upload' as Href);
+ }
+ }, [hasShareIntent, isBootstrapping, router, token]);
+
+ return null;
+}
export default function RootLayout() {
return (
-
-
+
+
+
+
+
-
-
-
+
+
+
+
);
}
diff --git a/mobile/app/more/customers.tsx b/mobile/app/more/customers.tsx
index 704b328..b6a2893 100644
--- a/mobile/app/more/customers.tsx
+++ b/mobile/app/more/customers.tsx
@@ -15,6 +15,7 @@ import {
import { useRouter } from 'expo-router';
import { createCustomer, Customer, fetchCustomers } from '@/src/lib/api';
+import { filterCustomers } from '@/src/lib/resource-list-filters';
import { useAuth } from '@/src/providers/auth-provider';
const PRIMARY = '#69c350';
@@ -37,20 +38,10 @@ export default function CustomersScreen() {
const [numberInput, setNumberInput] = useState('');
const [notesInput, setNotesInput] = useState('');
- const filtered = useMemo(() => {
- const terms = search.trim().toLowerCase().split(/\s+/).filter(Boolean);
-
- return customers.filter((customer) => {
- if (!showArchived && customer.archived) return false;
- if (terms.length === 0) return true;
-
- const haystack = [customer.name, customer.customerNumber, customer.notes]
- .map((value) => String(value || '').toLowerCase())
- .join(' ');
-
- return terms.every((term) => haystack.includes(term));
- });
- }, [customers, search, showArchived]);
+ const filtered = useMemo(
+ () => filterCustomers(customers, search, showArchived),
+ [customers, search, showArchived]
+ );
const load = useCallback(async (showSpinner = true) => {
if (!token) return;
diff --git a/mobile/app/more/plants.tsx b/mobile/app/more/plants.tsx
index 3fcdd75..5e86d49 100644
--- a/mobile/app/more/plants.tsx
+++ b/mobile/app/more/plants.tsx
@@ -15,16 +15,11 @@ import {
import { useRouter } from 'expo-router';
import { createPlant, Customer, fetchCustomers, fetchPlants, Plant } from '@/src/lib/api';
+import { filterPlants, getPlantCustomerName } from '@/src/lib/resource-list-filters';
import { useAuth } from '@/src/providers/auth-provider';
const PRIMARY = '#69c350';
-function getCustomerName(raw: Plant['customer']): string | null {
- if (!raw) return null;
- if (typeof raw === 'object') return raw.name ? String(raw.name) : null;
- return String(raw);
-}
-
export default function PlantsScreen() {
const { token } = useAuth();
const router = useRouter();
@@ -47,20 +42,10 @@ export default function PlantsScreen() {
const [pickerMode, setPickerMode] = useState<'customer' | null>(null);
const [customerSearch, setCustomerSearch] = useState('');
- const filtered = useMemo(() => {
- const terms = search.trim().toLowerCase().split(/\s+/).filter(Boolean);
-
- return plants.filter((plant) => {
- if (!showArchived && plant.archived) return false;
- if (terms.length === 0) return true;
-
- const haystack = [plant.name, plant.description, getCustomerName(plant.customer)]
- .map((value) => String(value || '').toLowerCase())
- .join(' ');
-
- return terms.every((term) => haystack.includes(term));
- });
- }, [plants, search, showArchived]);
+ const filtered = useMemo(
+ () => filterPlants(plants, search, showArchived),
+ [plants, search, showArchived]
+ );
const filteredCustomerOptions = useMemo(() => {
const terms = customerSearch.trim().toLowerCase().split(/\s+/).filter(Boolean);
@@ -189,8 +174,8 @@ export default function PlantsScreen() {
{plant.name}
{plant.archived ? Abgeschlossen : null}
- {getCustomerName(plant.customer) ? (
- Kunde: {getCustomerName(plant.customer)}
+ {getPlantCustomerName(plant.customer) ? (
+ Kunde: {getPlantCustomerName(plant.customer)}
) : null}
{plant.description ? {String(plant.description)} : null}
diff --git a/mobile/app/more/settings.tsx b/mobile/app/more/settings.tsx
index e6f3382..c7fbc58 100644
--- a/mobile/app/more/settings.tsx
+++ b/mobile/app/more/settings.tsx
@@ -5,6 +5,7 @@ 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 { getSiriPreferredTenantId, setSiriPreferredTenantId } from '@/src/lib/siri-settings';
import {
getApiBaseUrlSync,
hydrateApiBaseUrl,
@@ -21,13 +22,15 @@ function isValidServerUrl(value: string): boolean {
}
export default function SettingsScreen() {
- const { logout, token } = useAuth();
+ const { activeTenantId, logout, tenants, token } = useAuth();
const [serverUrl, setServerUrlInput] = useState(getApiBaseUrlSync());
const [savedUrl, setSavedUrl] = useState(getApiBaseUrlSync());
const [submitting, setSubmitting] = useState(false);
const [pushSubmitting, setPushSubmitting] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(null);
+ const [siriTenantId, setSiriTenantId] = useState(null);
+ const [siriSubmitting, setSiriSubmitting] = useState(false);
const loadConfig = useCallback(async () => {
const current = await hydrateApiBaseUrl();
@@ -37,8 +40,26 @@ export default function SettingsScreen() {
useEffect(() => {
void loadConfig();
+ void getSiriPreferredTenantId().then(setSiriTenantId);
}, [loadConfig]);
+ async function onSelectSiriTenant(tenantId: number | null) {
+ setSiriSubmitting(true);
+ setError(null);
+ setSuccess(null);
+ try {
+ await setSiriPreferredTenantId(tenantId);
+ setSiriTenantId(tenantId);
+ setSuccess(tenantId === null
+ ? 'Siri fragt beim nächsten Befehl wieder nach dem Tenant.'
+ : 'Siri-Standardtenant gespeichert.');
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Siri-Standardtenant konnte nicht gespeichert werden.');
+ } finally {
+ setSiriSubmitting(false);
+ }
+ }
+
async function onSave() {
setError(null);
setSuccess(null);
@@ -175,6 +196,43 @@ export default function SettingsScreen() {
+
+ Siri & Kurzbefehle
+
+ Der Siri-Standardtenant gilt nur für Sprachbefehle. Der aktuell in FEDEO geöffnete Tenant wird dadurch
+ nicht gewechselt. Ohne Auswahl fragt Siri beim nächsten Befehl nach.
+
+
+ {tenants.map((tenant) => {
+ const tenantId = Number(tenant.id);
+ const selected = tenantId === siriTenantId;
+ return (
+ onSelectSiriTenant(tenantId)}
+ disabled={siriSubmitting}>
+
+ {tenant.name}
+ {tenantId === activeTenantId ? Aktuell in FEDEO geöffnet : null}
+
+
+ {selected ? 'Siri-Standard' : 'Auswählen'}
+
+
+ );
+ })}
+
+ onSelectSiriTenant(null)}
+ disabled={siriSubmitting || siriTenantId === null}>
+ Bei nächstem Befehl nachfragen
+
+
+ Verfügbar: Todo erstellen · Offene Todos anzeigen · Todo erledigen
+
+
Mobile Push
@@ -247,6 +305,25 @@ const styles = StyleSheet.create({
color: '#6b7280',
fontSize: 12,
},
+ tenantButton: {
+ minHeight: 52,
+ borderWidth: 1,
+ borderColor: '#d1d5db',
+ borderRadius: 10,
+ paddingHorizontal: 12,
+ paddingVertical: 9,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ gap: 10,
+ backgroundColor: '#ffffff',
+ },
+ tenantButtonSelected: { borderColor: PRIMARY, backgroundColor: '#eff9ea' },
+ tenantTextWrap: { flex: 1 },
+ tenantName: { color: '#111827', fontSize: 14, fontWeight: '600' },
+ tenantNameSelected: { color: '#2f5f24' },
+ tenantAction: { color: '#6b7280', fontSize: 12, fontWeight: '600' },
+ tenantActionSelected: { color: '#3d7a30' },
actions: {
gap: 8,
marginTop: 6,
diff --git a/mobile/app/share-upload.tsx b/mobile/app/share-upload.tsx
new file mode 100644
index 0000000..92c3d5c
--- /dev/null
+++ b/mobile/app/share-upload.tsx
@@ -0,0 +1,306 @@
+import { Ionicons } from '@expo/vector-icons';
+import { useRouter } from 'expo-router';
+import { useShareIntentContext } from 'expo-share-intent';
+import { useEffect, useMemo, useState } from 'react';
+import {
+ ActivityIndicator,
+ Alert,
+ Pressable,
+ ScrollView,
+ StyleSheet,
+ Text,
+ TextInput,
+ View,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+
+import {
+ Customer,
+ fetchCustomers,
+ fetchPlants,
+ fetchProjects,
+ Plant,
+ Project,
+ uploadCustomerFile,
+ uploadPlantFile,
+ uploadProjectFile,
+} from '@/src/lib/api';
+import { useAuth } from '@/src/providers/auth-provider';
+import {
+ filterCustomers,
+ filterPlants,
+ filterProjects,
+ getActiveProjectPhase,
+ getPlantCustomerName,
+} from '@/src/lib/resource-list-filters';
+
+type TargetType = 'project' | 'customer' | 'plant';
+type UploadTarget = Project | Customer | Plant;
+
+const TARGET_TYPES: { key: TargetType; label: string; plural: string; icon: keyof typeof Ionicons.glyphMap }[] = [
+ { key: 'project', label: 'Projekt', plural: 'Projekte', icon: 'folder-outline' },
+ { key: 'customer', label: 'Kunde', plural: 'Kunden', icon: 'people-outline' },
+ { key: 'plant', label: 'Objekt', plural: 'Objekte', icon: 'business-outline' },
+];
+
+const PRIMARY = '#69c350';
+
+export default function ShareUploadScreen() {
+ const router = useRouter();
+ const { token } = useAuth();
+ const { shareIntent, resetShareIntent } = useShareIntentContext();
+ const [targetType, setTargetType] = useState(null);
+ const [targets, setTargets] = useState([]);
+ const [search, setSearch] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [isUploading, setIsUploading] = useState(false);
+ const [uploadedCount, setUploadedCount] = useState(0);
+ const [loadError, setLoadError] = useState(null);
+
+ const files = shareIntent.files || [];
+ const selectedType = TARGET_TYPES.find((type) => type.key === targetType);
+
+ useEffect(() => {
+ if (!targetType || !token) return;
+
+ let isActive = true;
+ setIsLoading(true);
+ setLoadError(null);
+ setSearch('');
+
+ const request = targetType === 'project'
+ ? fetchProjects(token, true)
+ : targetType === 'customer'
+ ? fetchCustomers(token, true)
+ : fetchPlants(token, true);
+
+ void request
+ .then((rows) => {
+ if (isActive) setTargets(rows.sort((a, b) => a.name.localeCompare(b.name, 'de')));
+ })
+ .catch((error) => {
+ if (isActive) setLoadError(error instanceof Error ? error.message : 'Die Ziele konnten nicht geladen werden.');
+ })
+ .finally(() => {
+ if (isActive) setIsLoading(false);
+ });
+
+ return () => {
+ isActive = false;
+ };
+ }, [targetType, token]);
+
+ const filteredTargets = useMemo(() => {
+ if (targetType === 'project') return filterProjects(targets as Project[], search);
+ if (targetType === 'customer') return filterCustomers(targets as Customer[], search);
+ if (targetType === 'plant') return filterPlants(targets as Plant[], search);
+ return [];
+ }, [search, targets, targetType]);
+
+ const close = () => {
+ resetShareIntent();
+ router.replace('/');
+ };
+
+ const confirmCancel = () => {
+ Alert.alert('Upload abbrechen?', 'Die geteilten Dateien werden nicht hochgeladen.', [
+ { text: 'Weiter auswählen', style: 'cancel' },
+ { text: 'Abbrechen', style: 'destructive', onPress: close },
+ ]);
+ };
+
+ const uploadTo = async (target: UploadTarget) => {
+ if (!token || !targetType || files.length === 0 || isUploading) return;
+
+ setIsUploading(true);
+ setUploadedCount(0);
+ try {
+ for (const [index, file] of files.entries()) {
+ const common = {
+ uri: file.path,
+ filename: file.fileName || `Geteilte Datei ${index + 1}`,
+ mimeType: file.mimeType || undefined,
+ };
+
+ if (targetType === 'project') {
+ await uploadProjectFile(token, { ...common, projectId: target.id });
+ } else if (targetType === 'customer') {
+ await uploadCustomerFile(token, { ...common, customerId: target.id });
+ } else {
+ await uploadPlantFile(token, { ...common, plantId: target.id });
+ }
+ setUploadedCount(index + 1);
+ }
+
+ resetShareIntent();
+ Alert.alert('Upload abgeschlossen', `${files.length} ${files.length === 1 ? 'Datei wurde' : 'Dateien wurden'} zu „${target.name}“ hochgeladen.`, [
+ { text: 'Fertig', onPress: () => router.replace('/') },
+ ]);
+ } catch (error) {
+ Alert.alert('Upload fehlgeschlagen', error instanceof Error ? error.message : 'Die Datei konnte nicht hochgeladen werden.');
+ } finally {
+ setIsUploading(false);
+ }
+ };
+
+ if (files.length === 0) {
+ return (
+
+
+
+
+
+ Keine Dateien gefunden
+ Teile eine Datei oder ein Foto erneut mit FEDEO.
+
+ Zurück zu FEDEO
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ {files.length} {files.length === 1 ? 'Datei' : 'Dateien'} ausgewählt
+ {files.map((file) => file.fileName).join(', ')}
+
+
+
+ {!targetType ? (
+ <>
+ Wohin möchtest du hochladen?
+ Wähle zuerst die Art des Ziels aus.
+
+ {TARGET_TYPES.map((type) => (
+ setTargetType(type.key)}>
+
+ {type.label}
+
+
+ ))}
+
+ >
+ ) : (
+ <>
+ setTargetType(null)} disabled={isUploading}>
+
+ Andere Zielart wählen
+
+ {selectedType?.label} auswählen
+
+
+
+
+
+ {isLoading ? (
+
+ ) : loadError ? (
+ {loadError}
+ ) : filteredTargets.length === 0 ? (
+ Keine passenden {selectedType?.plural.toLocaleLowerCase('de')} gefunden.
+ ) : (
+
+ {filteredTargets.map((target) => {
+ const number = 'projectNumber' in target
+ ? target.projectNumber
+ : 'customerNumber' in target
+ ? target.customerNumber
+ : null;
+ const detail = targetType === 'project'
+ ? getActiveProjectPhase(target as Project)
+ : targetType === 'plant'
+ ? getPlantCustomerName((target as Plant).customer)
+ : null;
+ return (
+ [styles.targetRow, pressed ? styles.targetRowPressed : null]}
+ onPress={() => void uploadTo(target)}
+ disabled={isUploading}>
+
+ {target.name}
+ {number ? Nr.: {String(number)} : null}
+ {detail ? (
+
+ {targetType === 'project' ? `Phase: ${detail}` : `Kunde: ${detail}`}
+
+ ) : null}
+
+
+
+
+
+ );
+ })}
+
+ )}
+ >
+ )}
+
+ {isUploading ? (
+
+
+ Upload läuft: {uploadedCount} von {files.length}
+
+ ) : null}
+
+
+ Abbrechen
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safeArea: { flex: 1, backgroundColor: '#ffffff' },
+ content: { paddingBottom: 28, gap: 14 },
+ flex: { flex: 1 },
+ fileSummary: { flexDirection: 'row', gap: 12, alignItems: 'center', paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: '#d8edcf', backgroundColor: '#eff9ea' },
+ summaryIcon: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center', backgroundColor: '#ffffff' },
+ summaryTitle: { fontSize: 15, fontWeight: '700', color: '#2f5f24' },
+ summaryText: { marginTop: 2, fontSize: 12, color: '#4f6f47' },
+ heading: { marginTop: 4, paddingHorizontal: 16, fontSize: 20, fontWeight: '700', color: '#111827' },
+ description: { marginTop: -8, paddingHorizontal: 16, fontSize: 14, color: '#6b7280' },
+ typeGrid: { borderTopWidth: 1, borderTopColor: '#e5e7eb' },
+ typeCard: { flexDirection: 'row', alignItems: 'center', gap: 14, paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: '#e5e7eb', backgroundColor: '#ffffff' },
+ iconCircle: { width: 42, height: 42, borderRadius: 21, alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff9ea' },
+ typeLabel: { flex: 1, fontSize: 15, fontWeight: '600', color: '#111827' },
+ backLink: { flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', marginHorizontal: 12, marginTop: 2, padding: 4 },
+ backLinkText: { fontSize: 14, fontWeight: '600', color: '#3d7a30' },
+ searchBox: { minHeight: 44, marginHorizontal: 16, flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 12, borderWidth: 1, borderColor: '#d1d5db', borderRadius: 10, backgroundColor: '#ffffff' },
+ searchInput: { flex: 1, paddingVertical: 10, fontSize: 15, color: '#111827' },
+ loader: { marginVertical: 32 },
+ errorText: { marginHorizontal: 16, padding: 12, borderRadius: 10, backgroundColor: '#fef2f2', color: '#b91c1c' },
+ emptyListText: { paddingVertical: 28, textAlign: 'center', color: '#6b7280' },
+ targetList: { borderTopWidth: 1, borderTopColor: '#e5e7eb', backgroundColor: '#ffffff' },
+ targetRow: { minHeight: 64, flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#e5e7eb', backgroundColor: '#ffffff' },
+ targetRowPressed: { backgroundColor: '#f3f4f6' },
+ targetName: { fontSize: 15, fontWeight: '600', color: '#111827' },
+ targetNumber: { marginTop: 3, fontSize: 13, color: '#6b7280' },
+ uploadIcon: { width: 38, height: 38, borderRadius: 19, alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff9ea' },
+ uploadOverlay: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10, marginHorizontal: 16, padding: 14, borderRadius: 10, backgroundColor: '#eff9ea' },
+ uploadText: { fontWeight: '600', color: '#2f5f24' },
+ cancelButton: { alignItems: 'center', padding: 14 },
+ cancelButtonText: { fontSize: 16, fontWeight: '600', color: '#b91c1c' },
+ emptyState: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 28 },
+ emptyIcon: { width: 64, height: 64, borderRadius: 32, alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff9ea' },
+ emptyTitle: { marginTop: 14, fontSize: 20, fontWeight: '700', color: '#111827' },
+ emptyText: { marginTop: 6, textAlign: 'center', color: '#6b7280' },
+ primaryButton: { marginTop: 22, paddingHorizontal: 18, paddingVertical: 13, borderRadius: 10, backgroundColor: PRIMARY },
+ primaryButtonText: { fontSize: 15, fontWeight: '700', color: '#fff' },
+});
diff --git a/mobile/package-lock.json b/mobile/package-lock.json
index 874a3a9..18c6528 100644
--- a/mobile/package-lock.json
+++ b/mobile/package-lock.json
@@ -12,7 +12,7 @@
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
- "expo": "~54.0.35",
+ "expo": "~54.0.36",
"expo-camera": "~17.0.10",
"expo-constants": "~18.0.13",
"expo-document-picker": "^14.0.8",
@@ -25,6 +25,7 @@
"expo-notifications": "~0.32.17",
"expo-router": "~6.0.24",
"expo-secure-store": "^15.0.8",
+ "expo-share-intent": "^5.1.1",
"expo-splash-screen": "~31.0.13",
"expo-status-bar": "~3.0.9",
"expo-symbols": "~1.0.8",
@@ -53,9 +54,9 @@
}
},
"node_modules/@0no-co/graphql.web": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.3.2.tgz",
- "integrity": "sha512-Q1+pRlLhE31GOY/2c9BAEnFTNxO7Awtc6fhhEDlxyCBQ2N0IhD32cPVvPChrK9mwBNSgRdW/sF1kd2e0ojHj1Q==",
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.3.3.tgz",
+ "integrity": "sha512-4gFGBdyaFmQ6n9euhp5JtIGS4ZeivwDr1tCPENUxTvy5wyv532yOtFCr9zzYAJh1s6uibgC+TRXUcay+mxzCoQ==",
"license": "MIT",
"peerDependencies": {
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0"
@@ -1301,9 +1302,9 @@
}
},
"node_modules/@babel/plugin-transform-regenerator": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz",
- "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==",
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.8.tgz",
+ "integrity": "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==",
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7"
@@ -1351,9 +1352,9 @@
}
},
"node_modules/@babel/plugin-transform-spread": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz",
- "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==",
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.8.tgz",
+ "integrity": "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA==",
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.29.7",
@@ -1771,15 +1772,15 @@
}
},
"node_modules/@expo/config": {
- "version": "12.0.13",
- "resolved": "https://registry.npmjs.org/@expo/config/-/config-12.0.13.tgz",
- "integrity": "sha512-Cu52arBa4vSaupIWsF0h7F/Cg//N374nYb7HAxV0I4KceKA7x2UXpYaHOL7EEYYvp7tZdThBjvGpVmr8ScIvaQ==",
+ "version": "12.0.14",
+ "resolved": "https://registry.npmjs.org/@expo/config/-/config-12.0.14.tgz",
+ "integrity": "sha512-3dfbBd9LnPDgyylhCgkOsaG8Adg52uOVOTQYH5lf23a/t8M5eQpXKCvzUarrf62B78057n2NnkiofK9TfPgvzw==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "~7.10.4",
- "@expo/config-plugins": "~54.0.4",
+ "@expo/config-plugins": "~54.0.5",
"@expo/config-types": "^54.0.10",
- "@expo/json-file": "^10.0.8",
+ "@expo/json-file": "^10.0.16",
"deepmerge": "^4.3.1",
"getenv": "^2.0.0",
"glob": "^13.0.0",
@@ -1792,14 +1793,14 @@
}
},
"node_modules/@expo/config-plugins": {
- "version": "54.0.4",
- "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-54.0.4.tgz",
- "integrity": "sha512-g2yXGICdoOw5i3LkQSDxl2Q5AlQCrG7oniu0pCPPO+UxGb7He4AFqSvPSy8HpRUj55io17hT62FTjYRD+d6j3Q==",
+ "version": "54.0.5",
+ "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-54.0.5.tgz",
+ "integrity": "sha512-aWQ3sViNRoQWw6So4A2qhWCt24CuGBK6MrRHI1AG+V6/NQAjIZCHaSvcXK2gXKpmisRhTSUWaKPIgLJQFB+AeQ==",
"license": "MIT",
"dependencies": {
"@expo/config-types": "^54.0.10",
- "@expo/json-file": "~10.0.8",
- "@expo/plist": "^0.4.8",
+ "@expo/json-file": "~10.0.16",
+ "@expo/plist": "^0.4.9",
"@expo/sdk-runtime-versions": "^1.0.0",
"chalk": "^4.1.2",
"debug": "^4.3.5",
@@ -1840,6 +1841,30 @@
"@babel/highlight": "^7.10.4"
}
},
+ "node_modules/@expo/config/node_modules/@expo/json-file": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.2.0.tgz",
+ "integrity": "sha512-S6XzKe3R9GQeHiUPXc3xJjOv2VJhOEwFYf7xdC2z2cUqt3kZJ9mSO877sNQloVdnW/SUCtPY3bexlM7nwq+CAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.20.0",
+ "json5": "^2.2.3"
+ }
+ },
+ "node_modules/@expo/config/node_modules/@expo/json-file/node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@expo/config/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
@@ -1893,9 +1918,9 @@
}
},
"node_modules/@expo/env": {
- "version": "2.0.8",
- "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.0.8.tgz",
- "integrity": "sha512-5VQD6GT8HIMRaSaB5JFtOXuvfDVU80YtZIuUT/GDhUF782usIXY13Tn3IdDz1Tm/lqA9qnRZQ1BF4t7LlvdJPA==",
+ "version": "2.0.12",
+ "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.0.12.tgz",
+ "integrity": "sha512-wVfzeBGlUohZG5kS8QCqXurpuWZFJEkBB1wXCifai3EZ/Llcg/VMTiUCpAgHImD3lI7GIU3V1uI64c04XIo98Q==",
"license": "MIT",
"dependencies": {
"chalk": "^4.0.0",
@@ -2006,15 +2031,24 @@
}
},
"node_modules/@expo/json-file": {
- "version": "10.0.14",
- "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.14.tgz",
- "integrity": "sha512-yWwBFywFv+SxkJp/pIzzA416JVYflNUh7pqQzgaA6nXDqRyK7KfrqVzk8PdUfDnqbBcaZZxpzNssfQZzp5KHrA==",
+ "version": "10.0.16",
+ "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.16.tgz",
+ "integrity": "sha512-fcVkWEj+hLuP2yt5W0aw6LmDRqSPWDLUSxOMcmFeV+algmIF59sQVKCwB9btjQLd4V6x9N0pISkQEkBubUHrCw==",
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.20.0",
+ "@babel/code-frame": "~7.10.4",
"json5": "^2.2.3"
}
},
+ "node_modules/@expo/json-file/node_modules/@babel/code-frame": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
+ "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/highlight": "^7.10.4"
+ }
+ },
"node_modules/@expo/metro": {
"version": "54.2.0",
"resolved": "https://registry.npmjs.org/@expo/metro/-/metro-54.2.0.tgz",
@@ -2061,9 +2095,9 @@
}
},
"node_modules/@expo/osascript": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.7.0.tgz",
- "integrity": "sha512-wKIXL8UtbuX4KwavPasIW3CUcgTbYfjzLcgUhjyKUAYDEqMaf6gmU1bqz3ffBPTokmX+G8/vFG1ZuI9etQWukA==",
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.7.1.tgz",
+ "integrity": "sha512-Zn03EX6In7ts2lPUW2ESUSkEhEWQN1qqsiXjadtZMJOuZRkMiAg1ZQHuvz9DjByDWNJ2pBwAGyrts9lj9k389g==",
"license": "MIT",
"dependencies": {
"@expo/spawn-async": "^1.8.0"
@@ -2073,12 +2107,12 @@
}
},
"node_modules/@expo/package-manager": {
- "version": "1.13.0",
- "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.13.0.tgz",
- "integrity": "sha512-s3W3eZafJDEyVL7W/jxj2Nz3eONKxSCU604S5xj8ijrVaRz83x0DnZznLf/UXQEI1w+FyibH68nHeQyk767b1A==",
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.13.1.tgz",
+ "integrity": "sha512-y/K+CaYYpZpNGZhSX4HyLT/vyIunFjNfyoxNysPBCefeLKI/VCx6f9LNPzrxayr3rCYO5bl9O8H+HRQK265Nkg==",
"license": "MIT",
"dependencies": {
- "@expo/json-file": "^11.0.0",
+ "@expo/json-file": "^11.0.1",
"@expo/spawn-async": "^1.8.0",
"chalk": "^4.0.0",
"npm-package-arg": "^11.0.0",
@@ -2087,9 +2121,9 @@
}
},
"node_modules/@expo/package-manager/node_modules/@expo/json-file": {
- "version": "11.0.0",
- "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-11.0.0.tgz",
- "integrity": "sha512-pHJCETqFL5x5BzNV6cEPwjwuECgGmnl0bNmfHIJ6LM1tlh2eVXi5HEdit3zby/JO/B8Otk5cgcqtJXgvvUat3A==",
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-11.0.1.tgz",
+ "integrity": "sha512-zxHWj4MKKMAL29ZQSY/Fssx4Thluk40JmuGNaeS078wy/NhlFhnVi+rHHunulE3xJAJ0CM73m8VK2+GkF9eRwQ==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.20.0",
@@ -2141,9 +2175,9 @@
}
},
"node_modules/@expo/schema-utils": {
- "version": "0.1.8",
- "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-0.1.8.tgz",
- "integrity": "sha512-9I6ZqvnAvKKDiO+ZF8BpQQFYWXOJvTAL5L/227RUbWG1OVZDInFifzCBiqAZ3b67NRfeAgpgvbA7rejsqhY62A==",
+ "version": "0.1.9",
+ "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-0.1.9.tgz",
+ "integrity": "sha512-t9bYwG4Z0yCVzHYJoDMci1OFq2FkBkhStlfUGSkspKYTwB/84+x6sY+CXCgdhkQNQtvWaugW5KUs9YZfAXq9Sg==",
"license": "MIT"
},
"node_modules/@expo/sdk-runtime-versions": {
@@ -2259,6 +2293,102 @@
"integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==",
"license": "MIT"
},
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "license": "MIT"
+ },
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
@@ -2565,6 +2695,16 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
@@ -4440,9 +4580,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.10.0",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
- "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
+ "version": "2.11.13",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz",
+ "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
@@ -4533,9 +4673,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.28.1",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
- "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "version": "4.28.8",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
+ "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
"funding": [
{
"type": "opencollective",
@@ -4552,11 +4692,11 @@
],
"license": "MIT",
"dependencies": {
- "baseline-browser-mapping": "^2.9.0",
- "caniuse-lite": "^1.0.30001759",
- "electron-to-chromium": "^1.5.263",
- "node-releases": "^2.0.27",
- "update-browserslist-db": "^1.2.0"
+ "baseline-browser-mapping": "^2.11.12",
+ "caniuse-lite": "^1.0.30001809",
+ "electron-to-chromium": "^1.5.402",
+ "node-releases": "^2.0.53",
+ "update-browserslist-db": "^1.3.0"
},
"bin": {
"browserslist": "cli.js"
@@ -4683,9 +4823,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001770",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz",
- "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==",
+ "version": "1.0.30001809",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz",
+ "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==",
"funding": [
{
"type": "opencollective",
@@ -4965,12 +5105,15 @@
"license": "MIT"
},
"node_modules/core-js-compat": {
- "version": "3.49.0",
- "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
- "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==",
+ "version": "3.50.0",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.50.0.tgz",
+ "integrity": "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==",
"license": "MIT",
"dependencies": {
- "browserslist": "^4.28.1"
+ "browserslist": "^4.28.7"
+ },
+ "engines": {
+ "node": ">=6.4.0"
},
"funding": {
"type": "opencollective",
@@ -5273,6 +5416,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "license": "MIT"
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -5280,9 +5429,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.286",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
- "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
+ "version": "1.5.402",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.402.tgz",
+ "integrity": "sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==",
"license": "ISC"
},
"node_modules/emoji-regex": {
@@ -5955,22 +6104,22 @@
}
},
"node_modules/expo": {
- "version": "54.0.35",
- "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.35.tgz",
- "integrity": "sha512-E+tXpQwjGm5fK/uwa55p0Xx/kuo5dXDKfVJ95IargTNa5KiFt26lSTXXa9KnHbI4EDLwFD38/xTKZvzPTlGTdg==",
+ "version": "54.0.36",
+ "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.36.tgz",
+ "integrity": "sha512-HMHp1H+actmnX85NJE6lILKzSJV6pTDNkwghq9EMOP3zTynjvBYVqJGSLlm6sEVzJCC5Z2ZiKgLvtsHrqlY0dg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.0",
- "@expo/cli": "54.0.25",
- "@expo/config": "~12.0.13",
- "@expo/config-plugins": "~54.0.4",
+ "@expo/cli": "54.0.26",
+ "@expo/config": "~12.0.14",
+ "@expo/config-plugins": "~54.0.5",
"@expo/devtools": "0.1.8",
"@expo/fingerprint": "0.15.5",
"@expo/metro": "~54.2.0",
- "@expo/metro-config": "54.0.16",
+ "@expo/metro-config": "54.0.17",
"@expo/vector-icons": "^15.0.3",
"@ungap/structured-clone": "^1.3.0",
- "babel-preset-expo": "~54.0.11",
+ "babel-preset-expo": "~54.0.12",
"expo-asset": "~12.0.13",
"expo-constants": "~18.0.13",
"expo-file-system": "~19.0.23",
@@ -6488,6 +6637,168 @@
"node": ">=20.16.0"
}
},
+ "node_modules/expo-share-intent": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/expo-share-intent/-/expo-share-intent-5.1.1.tgz",
+ "integrity": "sha512-0sEf34+4w/ySQd7xZmnog/oOm1q+PUBHFoGU97mxTXIGijY4LNmSX806efrDkYMwCxgRA1iHxG/zBib4zBPmYw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/achorein"
+ },
+ "https://www.buymeacoffee.com/achorein"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@expo/config-plugins": "~10.1.1",
+ "expo-constants": "~18.0.10",
+ "expo-linking": "~8.0.9"
+ },
+ "peerDependencies": {
+ "expo": "^54",
+ "expo-constants": ">=18.0.8",
+ "expo-linking": ">=8.0.8",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/expo-share-intent/node_modules/@babel/code-frame": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
+ "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/highlight": "^7.10.4"
+ }
+ },
+ "node_modules/expo-share-intent/node_modules/@expo/config-plugins": {
+ "version": "10.1.2",
+ "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-10.1.2.tgz",
+ "integrity": "sha512-IMYCxBOcnuFStuK0Ay+FzEIBKrwW8OVUMc65+v0+i7YFIIe8aL342l7T4F8lR4oCfhXn7d6M5QPgXvjtc/gAcw==",
+ "license": "MIT",
+ "dependencies": {
+ "@expo/config-types": "^53.0.5",
+ "@expo/json-file": "~9.1.5",
+ "@expo/plist": "^0.3.5",
+ "@expo/sdk-runtime-versions": "^1.0.0",
+ "chalk": "^4.1.2",
+ "debug": "^4.3.5",
+ "getenv": "^2.0.0",
+ "glob": "^10.4.2",
+ "resolve-from": "^5.0.0",
+ "semver": "^7.5.4",
+ "slash": "^3.0.0",
+ "slugify": "^1.6.6",
+ "xcode": "^3.0.1",
+ "xml2js": "0.6.0"
+ }
+ },
+ "node_modules/expo-share-intent/node_modules/@expo/config-types": {
+ "version": "53.0.5",
+ "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-53.0.5.tgz",
+ "integrity": "sha512-kqZ0w44E+HEGBjy+Lpyn0BVL5UANg/tmNixxaRMLS6nf37YsDrLk2VMAmeKMMk5CKG0NmOdVv3ngeUjRQMsy9g==",
+ "license": "MIT"
+ },
+ "node_modules/expo-share-intent/node_modules/@expo/json-file": {
+ "version": "9.1.5",
+ "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-9.1.5.tgz",
+ "integrity": "sha512-prWBhLUlmcQtvN6Y7BpW2k9zXGd3ySa3R6rAguMJkp1z22nunLN64KYTUWfijFlprFoxm9r2VNnGkcbndAlgKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "~7.10.4",
+ "json5": "^2.2.3"
+ }
+ },
+ "node_modules/expo-share-intent/node_modules/@expo/plist": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.3.5.tgz",
+ "integrity": "sha512-9RYVU1iGyCJ7vWfg3e7c/NVyMFs8wbl+dMWZphtFtsqyN9zppGREU3ctlD3i8KUE0sCUTVnLjCWr+VeUIDep2g==",
+ "license": "MIT",
+ "dependencies": {
+ "@xmldom/xmldom": "^0.8.8",
+ "base64-js": "^1.2.3",
+ "xmlbuilder": "^15.1.1"
+ }
+ },
+ "node_modules/expo-share-intent/node_modules/brace-expansion": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
+ "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/expo-share-intent/node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/expo-share-intent/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/expo-share-intent/node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/expo-share-intent/node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/expo-share-intent/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/expo-splash-screen": {
"version": "31.0.13",
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-31.0.13.tgz",
@@ -6557,26 +6868,26 @@
}
},
"node_modules/expo/node_modules/@expo/cli": {
- "version": "54.0.25",
- "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.25.tgz",
- "integrity": "sha512-WnUqIb8oMBhtwSfIqdCHCzcaDIpLNXItRVd5miuvWi4GO0SGo89PSsAkbVJ+LJgcaY+v5rbgMELJS9I/CqOulA==",
+ "version": "54.0.26",
+ "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.26.tgz",
+ "integrity": "sha512-BjsAoKINLEo3LRE+sDC6FCgjxuOWsyfOFOKz0txrbEcxSatzIjJDVuX8XaTdmeicZdcoN524yl1sfwCWfxhYMw==",
"license": "MIT",
"dependencies": {
"@0no-co/graphql.web": "^1.0.8",
"@expo/code-signing-certificates": "^0.0.6",
- "@expo/config": "~12.0.13",
- "@expo/config-plugins": "~54.0.4",
+ "@expo/config": "~12.0.14",
+ "@expo/config-plugins": "~54.0.5",
"@expo/devcert": "^1.2.1",
- "@expo/env": "~2.0.8",
+ "@expo/env": "~2.0.12",
"@expo/image-utils": "^0.8.8",
"@expo/json-file": "^10.0.16",
"@expo/metro": "~54.2.0",
- "@expo/metro-config": "~54.0.16",
+ "@expo/metro-config": "~54.0.17",
"@expo/osascript": "^2.3.8",
"@expo/package-manager": "^1.9.10",
"@expo/plist": "^0.4.9",
- "@expo/prebuild-config": "^54.0.8",
- "@expo/schema-utils": "^0.1.8",
+ "@expo/prebuild-config": "^54.0.9",
+ "@expo/schema-utils": "^0.1.9",
"@expo/spawn-async": "^1.7.2",
"@expo/ws-tunnel": "^1.0.1",
"@expo/xcpretty": "^4.3.0",
@@ -6643,27 +6954,38 @@
}
}
},
- "node_modules/expo/node_modules/@expo/json-file": {
- "version": "10.2.0",
- "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.2.0.tgz",
- "integrity": "sha512-S6XzKe3R9GQeHiUPXc3xJjOv2VJhOEwFYf7xdC2z2cUqt3kZJ9mSO877sNQloVdnW/SUCtPY3bexlM7nwq+CAQ==",
+ "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/prebuild-config": {
+ "version": "54.0.9",
+ "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-54.0.9.tgz",
+ "integrity": "sha512-3/Rmyzt8vduPjnSVHbnc0wYFrlhwLWn2g596rDyKcLGeqN2WTLJbzVeznsrUwyzhBNXgnTomZWO5HDzbZ/4E7g==",
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.20.0",
- "json5": "^2.2.3"
+ "@expo/config": "~12.0.14",
+ "@expo/config-plugins": "~54.0.5",
+ "@expo/config-types": "^54.0.10",
+ "@expo/image-utils": "^0.8.8",
+ "@expo/json-file": "^10.0.16",
+ "@react-native/normalize-colors": "0.81.5",
+ "debug": "^4.3.1",
+ "resolve-from": "^5.0.0",
+ "semver": "^7.6.0",
+ "xml2js": "0.6.0"
+ },
+ "peerDependencies": {
+ "expo": "*"
}
},
"node_modules/expo/node_modules/@expo/metro-config": {
- "version": "54.0.16",
- "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-54.0.16.tgz",
- "integrity": "sha512-3LLb9ZQl0VlqSlsalJ7+CYjfz60PBoSDHvpE1UF71aTM1Nx0Vb4LhXo7bCCC+PYP9q/GPB58LLbIROQ8PjKX2w==",
+ "version": "54.0.17",
+ "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-54.0.17.tgz",
+ "integrity": "sha512-PQFgQCZGY0DffZUvBzJttDPreZfHrQakaBlKjnvOUMNXbDna+TYmg1IFZuIDUYJezLcdp+TvVFTLjNi1+mqaVw==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.20.0",
"@babel/core": "^7.20.0",
"@babel/generator": "^7.20.5",
- "@expo/config": "~12.0.13",
- "@expo/env": "~2.0.8",
+ "@expo/config": "~12.0.14",
+ "@expo/env": "~2.0.12",
"@expo/json-file": "~10.0.16",
"@expo/metro": "~54.2.0",
"@expo/spawn-async": "^1.7.2",
@@ -6690,29 +7012,10 @@
}
}
},
- "node_modules/expo/node_modules/@expo/metro-config/node_modules/@expo/json-file": {
- "version": "10.0.16",
- "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.16.tgz",
- "integrity": "sha512-fcVkWEj+hLuP2yt5W0aw6LmDRqSPWDLUSxOMcmFeV+algmIF59sQVKCwB9btjQLd4V6x9N0pISkQEkBubUHrCw==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "~7.10.4",
- "json5": "^2.2.3"
- }
- },
- "node_modules/expo/node_modules/@expo/metro-config/node_modules/@expo/json-file/node_modules/@babel/code-frame": {
- "version": "7.10.4",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
- "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
- "license": "MIT",
- "dependencies": {
- "@babel/highlight": "^7.10.4"
- }
- },
"node_modules/expo/node_modules/babel-preset-expo": {
- "version": "54.0.11",
- "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-54.0.11.tgz",
- "integrity": "sha512-dEpeFDtYEFzmWtWVwvt7sUCZH0fxXPfbJlgXd7XNZSQDa/Ki/hTOj9exMTzqR2oyPHDNcE9VxYCJ4oS6xw4Pjg==",
+ "version": "54.0.12",
+ "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-54.0.12.tgz",
+ "integrity": "sha512-6xeSkdaixmQhWSYL7tfLu0pOS0BY+8ftwmdNSHtpEFSizrXYZkCjk/B6Dxr+6nwNRihixMcS0aBlWS1wlDl3pw==",
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.25.9",
@@ -6753,9 +7056,9 @@
}
},
"node_modules/expo/node_modules/brace-expansion": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
- "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
+ "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -6816,9 +7119,9 @@
}
},
"node_modules/expo/node_modules/ws": {
- "version": "8.21.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
- "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
+ "version": "8.21.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
+ "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@@ -7032,6 +7335,34 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/foreground-child/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/freeport-async": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz",
@@ -8218,6 +8549,21 @@
"node": ">= 0.4"
}
},
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
"node_modules/jest-environment-node": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz",
@@ -8559,9 +8905,9 @@
"license": "MIT"
},
"node_modules/lightningcss": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
- "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
@@ -8574,23 +8920,23 @@
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
- "lightningcss-android-arm64": "1.32.0",
- "lightningcss-darwin-arm64": "1.32.0",
- "lightningcss-darwin-x64": "1.32.0",
- "lightningcss-freebsd-x64": "1.32.0",
- "lightningcss-linux-arm-gnueabihf": "1.32.0",
- "lightningcss-linux-arm64-gnu": "1.32.0",
- "lightningcss-linux-arm64-musl": "1.32.0",
- "lightningcss-linux-x64-gnu": "1.32.0",
- "lightningcss-linux-x64-musl": "1.32.0",
- "lightningcss-win32-arm64-msvc": "1.32.0",
- "lightningcss-win32-x64-msvc": "1.32.0"
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
}
},
"node_modules/lightningcss-android-arm64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
- "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
"cpu": [
"arm64"
],
@@ -8608,9 +8954,9 @@
}
},
"node_modules/lightningcss-darwin-arm64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
- "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
"cpu": [
"arm64"
],
@@ -8628,9 +8974,9 @@
}
},
"node_modules/lightningcss-darwin-x64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
- "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
"cpu": [
"x64"
],
@@ -8648,9 +8994,9 @@
}
},
"node_modules/lightningcss-freebsd-x64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
- "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
"cpu": [
"x64"
],
@@ -8668,9 +9014,9 @@
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
- "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
"cpu": [
"arm"
],
@@ -8688,9 +9034,9 @@
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
- "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
"cpu": [
"arm64"
],
@@ -8708,9 +9054,9 @@
}
},
"node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
- "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
"cpu": [
"arm64"
],
@@ -8728,9 +9074,9 @@
}
},
"node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
- "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
"cpu": [
"x64"
],
@@ -8748,9 +9094,9 @@
}
},
"node_modules/lightningcss-linux-x64-musl": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
- "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
"cpu": [
"x64"
],
@@ -8768,9 +9114,9 @@
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
- "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
"cpu": [
"arm64"
],
@@ -8788,9 +9134,9 @@
}
},
"node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
- "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
"cpu": [
"x64"
],
@@ -9524,10 +9870,13 @@
"license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.27",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
- "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
- "license": "MIT"
+ "version": "2.0.53",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz",
+ "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
},
"node_modules/normalize-path": {
"version": "3.0.0",
@@ -9962,6 +10311,12 @@
"node": ">=6"
}
},
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "license": "BlueOak-1.0.0"
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -11626,6 +11981,21 @@
"node": ">=8"
}
},
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/string.prototype.matchall": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
@@ -11736,6 +12106,19 @@
"node": ">=8"
}
},
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@@ -11840,9 +12223,9 @@
}
},
"node_modules/tar": {
- "version": "7.5.19",
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz",
- "integrity": "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==",
+ "version": "7.5.22",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
+ "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
@@ -12273,9 +12656,9 @@
}
},
"node_modules/undici": {
- "version": "6.27.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
- "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
+ "version": "6.28.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
+ "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
"license": "MIT",
"engines": {
"node": ">=18.17"
@@ -12384,9 +12767,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
- "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.0.tgz",
+ "integrity": "sha512-x/M6q3w4Ybp91CNaS4S69UnliqR3BzRpOT6LWbksjth0S/+jhfaPJsWjt/TewpT8j9eLIojUf5jr29WextHroA==",
"funding": [
{
"type": "opencollective",
@@ -12934,6 +13317,24 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
diff --git a/mobile/package.json b/mobile/package.json
index 62011ac..6550d81 100644
--- a/mobile/package.json
+++ b/mobile/package.json
@@ -25,7 +25,7 @@
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
- "expo": "~54.0.35",
+ "expo": "~54.0.36",
"expo-camera": "~17.0.10",
"expo-constants": "~18.0.13",
"expo-document-picker": "^14.0.8",
@@ -38,6 +38,7 @@
"expo-notifications": "~0.32.17",
"expo-router": "~6.0.24",
"expo-secure-store": "^15.0.8",
+ "expo-share-intent": "^5.1.1",
"expo-splash-screen": "~31.0.13",
"expo-status-bar": "~3.0.9",
"expo-symbols": "~1.0.8",
diff --git a/mobile/plugins/ios/FEDEOSiriIntents.swift b/mobile/plugins/ios/FEDEOSiriIntents.swift
new file mode 100644
index 0000000..452d175
--- /dev/null
+++ b/mobile/plugins/ios/FEDEOSiriIntents.swift
@@ -0,0 +1,421 @@
+import AppIntents
+import Foundation
+import Security
+
+private enum FEDEOSiriError: LocalizedError {
+ case notSignedIn
+ case noTenant
+ case invalidResponse
+ case server(String)
+
+ var errorDescription: String? {
+ switch self {
+ case .notSignedIn: return "Bitte öffne FEDEO und melde dich zuerst an."
+ case .noTenant: return "Für dieses Konto ist kein Tenant verfügbar."
+ case .invalidResponse: return "Der FEDEO-Server hat eine ungültige Antwort geliefert."
+ case .server(let message): return message
+ }
+ }
+}
+
+private struct FEDEOMeResponse: Decodable {
+ struct User: Decodable { let id: String }
+ let user: User
+ let tenants: [FEDEOTenant]
+ let activeTenant: Int?
+
+ enum CodingKeys: String, CodingKey {
+ case user, tenants, activeTenant
+ }
+
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ user = try container.decode(User.self, forKey: .user)
+ tenants = try container.decodeIfPresent([FEDEOTenant].self, forKey: .tenants) ?? []
+ if let number = try? container.decode(Int.self, forKey: .activeTenant) {
+ activeTenant = number
+ } else if let text = try? container.decode(String.self, forKey: .activeTenant) {
+ activeTenant = Int(text)
+ } else {
+ activeTenant = nil
+ }
+ }
+}
+
+private struct FEDEOTenant: Codable, Sendable {
+ let id: Int
+ let name: String
+ let short: String?
+}
+
+private struct FEDEOTask: Codable, Sendable {
+ let id: Int
+ let name: String
+ let categorie: String?
+ let archived: Bool?
+ let userId: String?
+ let user_id: String?
+ let profile: String?
+}
+
+private struct FEDEOTokenResponse: Decodable { let token: String }
+
+private actor FEDEOSiriClient {
+ static let shared = FEDEOSiriClient()
+
+ private let tokenKey = "fedeo.mobile.auth.token"
+ private let serverKey = "fedeo.mobile.server.base"
+ private let preferredTenantKey = "fedeo.mobile.siri.preferred-tenant"
+ private let defaultServer = "https://app.fedeo.de/backend"
+
+ func loadContext() async throws -> (me: FEDEOMeResponse, token: String) {
+ guard let token = keychainValue(for: tokenKey), !token.isEmpty else {
+ throw FEDEOSiriError.notSignedIn
+ }
+ let me: FEDEOMeResponse = try await request(path: "/api/me", token: token)
+ guard !me.tenants.isEmpty else { throw FEDEOSiriError.noTenant }
+ return (me, token)
+ }
+
+ func preferredTenantID() -> Int? {
+ keychainValue(for: preferredTenantKey).flatMap(Int.init)
+ }
+
+ func setPreferredTenantID(_ tenantID: Int) {
+ setKeychainValue(String(tenantID), for: preferredTenantKey)
+ }
+
+ func accessToken(for tenantID: Int, context: (me: FEDEOMeResponse, token: String)) async throws -> String {
+ if context.me.activeTenant == tenantID { return context.token }
+ let response: FEDEOTokenResponse = try await request(
+ path: "/api/tenant/switch",
+ method: "POST",
+ token: context.token,
+ body: ["tenant_id": String(tenantID)]
+ )
+ return response.token
+ }
+
+ func openTasks(tenantID: Int, context: (me: FEDEOMeResponse, token: String)? = nil) async throws -> [FEDEOTask] {
+ let loadedContext: (me: FEDEOMeResponse, token: String)
+ if let context {
+ loadedContext = context
+ } else {
+ loadedContext = try await loadContext()
+ }
+ let token = try await accessToken(for: tenantID, context: loadedContext)
+ let tasks: [FEDEOTask] = try await request(path: "/api/resource/tasks", token: token)
+ return tasks.filter { task in
+ guard task.archived != true, task.categorie != "Abgeschlossen" else { return false }
+ let assignedUser = task.userId ?? task.user_id ?? task.profile
+ return assignedUser == nil || assignedUser == loadedContext.me.user.id
+ }
+ }
+
+ func createTask(name: String, tenantID: Int, context: (me: FEDEOMeResponse, token: String)) async throws -> FEDEOTask {
+ let token = try await accessToken(for: tenantID, context: context)
+ return try await request(
+ path: "/api/resource/tasks",
+ method: "POST",
+ token: token,
+ body: ["name": name, "categorie": "Offen", "userId": context.me.user.id]
+ )
+ }
+
+ @available(iOS 16.0, *)
+ func completeTask(_ task: FEDEOTodoEntity, context: (me: FEDEOMeResponse, token: String)) async throws {
+ let token = try await accessToken(for: task.tenantID, context: context)
+ let _: FEDEOTask = try await request(
+ path: "/api/resource/tasks/\(task.taskID)",
+ method: "PUT",
+ token: token,
+ body: ["categorie": "Abgeschlossen"]
+ )
+ }
+
+ private func request(
+ path: String,
+ method: String = "GET",
+ token: String,
+ body: [String: String]? = nil
+ ) async throws -> T {
+ let base = (keychainValue(for: serverKey) ?? defaultServer).trimmingCharacters(in: CharacterSet(charactersIn: "/"))
+ guard let url = URL(string: base + path) else { throw FEDEOSiriError.invalidResponse }
+ var request = URLRequest(url: url)
+ request.httpMethod = method
+ request.timeoutInterval = 20
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
+ request.setValue("application/json", forHTTPHeaderField: "Accept")
+ if let body {
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.httpBody = try JSONSerialization.data(withJSONObject: body)
+ }
+
+ let (data, response) = try await URLSession.shared.data(for: request)
+ guard let httpResponse = response as? HTTPURLResponse else { throw FEDEOSiriError.invalidResponse }
+ guard (200..<300).contains(httpResponse.statusCode) else {
+ let payload = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
+ let message = payload?["message"] as? String ?? payload?["error"] as? String ?? "FEDEO-Anfrage fehlgeschlagen."
+ throw FEDEOSiriError.server(message)
+ }
+ return try JSONDecoder().decode(T.self, from: data)
+ }
+
+ private func keychainValue(for key: String) -> String? {
+ let keyData = Data(key.utf8)
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "app:no-auth",
+ kSecAttrAccount as String: keyData,
+ kSecReturnData as String: true,
+ kSecMatchLimit as String: kSecMatchLimitOne,
+ ]
+ var result: CFTypeRef?
+ guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
+ let data = result as? Data else { return nil }
+ return String(data: data, encoding: .utf8)
+ }
+
+ private func setKeychainValue(_ value: String, for key: String) {
+ let keyData = Data(key.utf8)
+ let baseQuery: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "app:no-auth",
+ kSecAttrAccount as String: keyData,
+ ]
+ let valueData = Data(value.utf8)
+ if SecItemUpdate(baseQuery as CFDictionary, [kSecValueData as String: valueData] as CFDictionary) == errSecItemNotFound {
+ var insert = baseQuery
+ insert[kSecAttrGeneric as String] = keyData
+ insert[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
+ insert[kSecValueData as String] = valueData
+ SecItemAdd(insert as CFDictionary, nil)
+ }
+ }
+}
+
+@available(iOS 16.0, *)
+struct FEDEOTenantEntity: AppEntity {
+ static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "FEDEO-Tenant")
+ static var defaultQuery = FEDEOTenantQuery()
+
+ let id: Int
+ let name: String
+
+ var displayRepresentation: DisplayRepresentation {
+ DisplayRepresentation(title: "\(name)")
+ }
+}
+
+@available(iOS 16.0, *)
+struct FEDEOTenantQuery: EntityStringQuery {
+ func entities(for identifiers: [Int]) async throws -> [FEDEOTenantEntity] {
+ let context = try await FEDEOSiriClient.shared.loadContext()
+ return context.me.tenants.filter { identifiers.contains($0.id) }.map { .init(id: $0.id, name: $0.name) }
+ }
+
+ func suggestedEntities() async throws -> [FEDEOTenantEntity] {
+ let context = try await FEDEOSiriClient.shared.loadContext()
+ let preferred = await FEDEOSiriClient.shared.preferredTenantID() ?? context.me.activeTenant
+ return context.me.tenants
+ .sorted { ($0.id == preferred ? 0 : 1, $0.name) < ($1.id == preferred ? 0 : 1, $1.name) }
+ .map { .init(id: $0.id, name: $0.name) }
+ }
+
+ func entities(matching string: String) async throws -> [FEDEOTenantEntity] {
+ let terms = string.lowercased().split(separator: " ")
+ return try await suggestedEntities().filter { tenant in
+ let name = tenant.name.lowercased()
+ return terms.allSatisfy(name.contains)
+ }
+ }
+}
+
+@available(iOS 16.0, *)
+struct FEDEOTodoEntity: AppEntity {
+ static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "FEDEO-Todo")
+ static var defaultQuery = FEDEOTodoQuery()
+
+ let id: String
+ let taskID: Int
+ let tenantID: Int
+ let name: String
+ let tenantName: String
+
+ var displayRepresentation: DisplayRepresentation {
+ DisplayRepresentation(title: "\(name)", subtitle: "\(tenantName)")
+ }
+}
+
+@available(iOS 16.0, *)
+struct FEDEOTodoQuery: EntityStringQuery {
+ func entities(for identifiers: [String]) async throws -> [FEDEOTodoEntity] {
+ let context = try await FEDEOSiriClient.shared.loadContext()
+ var result: [FEDEOTodoEntity] = []
+ for tenant in context.me.tenants {
+ let requested = identifiers.filter { $0.hasPrefix("\(tenant.id):") }
+ guard !requested.isEmpty else { continue }
+ let tasks = try await FEDEOSiriClient.shared.openTasks(tenantID: tenant.id, context: context)
+ result += tasks.filter { requested.contains("\(tenant.id):\($0.id)") }.map {
+ .init(id: "\(tenant.id):\($0.id)", taskID: $0.id, tenantID: tenant.id, name: $0.name, tenantName: tenant.name)
+ }
+ }
+ return result
+ }
+
+ func suggestedEntities() async throws -> [FEDEOTodoEntity] {
+ let context = try await FEDEOSiriClient.shared.loadContext()
+ let preferredID = await FEDEOSiriClient.shared.preferredTenantID() ?? context.me.activeTenant
+ guard let tenant = context.me.tenants.first(where: { $0.id == preferredID }) ?? context.me.tenants.first else { return [] }
+ let tasks = try await FEDEOSiriClient.shared.openTasks(tenantID: tenant.id, context: context)
+ return tasks.map {
+ .init(id: "\(tenant.id):\($0.id)", taskID: $0.id, tenantID: tenant.id, name: $0.name, tenantName: tenant.name)
+ }
+ }
+
+ func entities(matching string: String) async throws -> [FEDEOTodoEntity] {
+ let terms = string.lowercased().split(separator: " ")
+ return try await suggestedEntities().filter { todo in
+ let name = todo.name.lowercased()
+ return terms.allSatisfy(name.contains)
+ }
+ }
+
+}
+
+@available(iOS 16.0, *)
+private protocol FEDEOTenantResolving {}
+
+@available(iOS 16.0, *)
+extension FEDEOTenantResolving {
+ func availableTenants(context: (me: FEDEOMeResponse, token: String)) -> [FEDEOTenantEntity] {
+ context.me.tenants.map { .init(id: $0.id, name: $0.name) }
+ }
+}
+
+@available(iOS 16.0, *)
+struct FEDEOCreateTodoIntent: AppIntent, FEDEOTenantResolving {
+ static var title: LocalizedStringResource = "Todo in FEDEO erstellen"
+ static var description = IntentDescription("Erstellt ein neues Todo im ausgewählten FEDEO-Tenant.")
+
+ @Parameter(title: "Todo") var name: String
+ @Parameter(title: "Tenant") var tenant: FEDEOTenantEntity?
+
+ static var parameterSummary: some ParameterSummary {
+ Summary("Todo \(\.$name) in \(\.$tenant) erstellen")
+ }
+
+ func perform() async throws -> some IntentResult & ProvidesDialog {
+ let context = try await FEDEOSiriClient.shared.loadContext()
+ let selected = try await resolveTenant(context: context)
+ let task = try await FEDEOSiriClient.shared.createTask(name: name, tenantID: selected.id, context: context)
+ await FEDEOSiriClient.shared.setPreferredTenantID(selected.id)
+ return .result(dialog: "Todo „\(task.name)“ wurde bei \(selected.name) erstellt.")
+ }
+
+ private func resolveTenant(context: (me: FEDEOMeResponse, token: String)) async throws -> FEDEOTenantEntity {
+ if let tenant { return tenant }
+ if let preferredID = await FEDEOSiriClient.shared.preferredTenantID(),
+ let preferred = context.me.tenants.first(where: { $0.id == preferredID }) {
+ return .init(id: preferred.id, name: preferred.name)
+ }
+ let currentName = context.me.tenants.first { $0.id == context.me.activeTenant }?.name ?? context.me.tenants[0].name
+ return try await $tenant.requestDisambiguation(
+ among: availableTenants(context: context),
+ dialog: "Soll das Todo bei \(currentName) erstellt werden oder möchtest du den Tenant wechseln?"
+ )
+ }
+}
+
+@available(iOS 16.0, *)
+struct FEDEOListOpenTodosIntent: AppIntent, FEDEOTenantResolving {
+ static var title: LocalizedStringResource = "Offene FEDEO-Todos anzeigen"
+ static var description = IntentDescription("Zeigt deine offenen Todos in einem FEDEO-Tenant.")
+
+ @Parameter(title: "Tenant") var tenant: FEDEOTenantEntity?
+
+ func perform() async throws -> some IntentResult & ReturnsValue<[FEDEOTodoEntity]> & ProvidesDialog {
+ let context = try await FEDEOSiriClient.shared.loadContext()
+ let selected = try await resolveTenant(context: context)
+ let tasks = try await FEDEOSiriClient.shared.openTasks(tenantID: selected.id, context: context)
+ await FEDEOSiriClient.shared.setPreferredTenantID(selected.id)
+ let todos = tasks.map {
+ FEDEOTodoEntity(id: "\(selected.id):\($0.id)", taskID: $0.id, tenantID: selected.id, name: $0.name, tenantName: selected.name)
+ }
+ let names = todos.prefix(5).map(\.name).joined(separator: ", ")
+ let dialog = todos.isEmpty
+ ? "Du hast bei \(selected.name) keine offenen Todos."
+ : "Bei \(selected.name) sind \(todos.count) Todos offen: \(names)."
+ return .result(value: todos, dialog: IntentDialog(stringLiteral: dialog))
+ }
+
+ private func resolveTenant(context: (me: FEDEOMeResponse, token: String)) async throws -> FEDEOTenantEntity {
+ if let tenant { return tenant }
+ if let preferredID = await FEDEOSiriClient.shared.preferredTenantID(),
+ let preferred = context.me.tenants.first(where: { $0.id == preferredID }) {
+ return .init(id: preferred.id, name: preferred.name)
+ }
+ return try await $tenant.requestDisambiguation(among: availableTenants(context: context), dialog: "Für welchen Tenant soll ich die offenen Todos anzeigen?")
+ }
+}
+
+@available(iOS 16.0, *)
+struct FEDEOCompleteTodoIntent: AppIntent {
+ static var title: LocalizedStringResource = "FEDEO-Todo erledigen"
+ static var description = IntentDescription("Markiert ein offenes FEDEO-Todo als abgeschlossen.")
+
+ @Parameter(title: "Todo") var todo: FEDEOTodoEntity
+
+ static var parameterSummary: some ParameterSummary {
+ Summary("\(\.$todo) erledigen")
+ }
+
+ func perform() async throws -> some IntentResult & ProvidesDialog {
+ let context = try await FEDEOSiriClient.shared.loadContext()
+ try await FEDEOSiriClient.shared.completeTask(todo, context: context)
+ await FEDEOSiriClient.shared.setPreferredTenantID(todo.tenantID)
+ return .result(dialog: "Todo „\(todo.name)“ wurde bei \(todo.tenantName) erledigt.")
+ }
+}
+
+@available(iOS 16.0, *)
+struct FEDEOAppShortcuts: AppShortcutsProvider {
+ static var appShortcuts: [AppShortcut] {
+ AppShortcut(
+ intent: FEDEOCreateTodoIntent(),
+ phrases: [
+ "Erstelle ein Todo in \(.applicationName)",
+ "Erstelle ein Todo mit \(.applicationName)",
+ "Neues Todo in \(.applicationName)",
+ "Neues Todo mit \(.applicationName)",
+ ],
+ shortTitle: "Todo erstellen",
+ systemImageName: "checklist"
+ )
+ AppShortcut(
+ intent: FEDEOListOpenTodosIntent(),
+ phrases: [
+ "Zeige meine offenen Todos in \(.applicationName)",
+ "Zeige meine offenen Todos mit \(.applicationName)",
+ "Meine Todos in \(.applicationName)",
+ "Meine Todos bei \(.applicationName)",
+ "Was sind meine offenen Todos in \(.applicationName)",
+ ],
+ shortTitle: "Offene Todos",
+ systemImageName: "list.bullet"
+ )
+ AppShortcut(
+ intent: FEDEOCompleteTodoIntent(),
+ phrases: [
+ "Erledige ein Todo in \(.applicationName)",
+ "Erledige ein Todo mit \(.applicationName)",
+ "Todo abschließen in \(.applicationName)",
+ "Todo abschließen mit \(.applicationName)",
+ "Erledige \(\.$todo) in \(.applicationName)",
+ "Schließe \(\.$todo) in \(.applicationName) ab",
+ ],
+ shortTitle: "Todo erledigen",
+ systemImageName: "checkmark.circle"
+ )
+ }
+}
diff --git a/mobile/plugins/with-share-intent-multifile.js b/mobile/plugins/with-share-intent-multifile.js
new file mode 100644
index 0000000..68fd28e
--- /dev/null
+++ b/mobile/plugins/with-share-intent-multifile.js
@@ -0,0 +1,158 @@
+const fs = require('node:fs');
+const path = require('node:path');
+const { withAppDelegate, withInfoPlist, withXcodeProject } = require('@expo/config-plugins');
+
+const SHARE_EXTENSION_DIRECTORY = 'InFEDEOhochladen';
+const SIRI_INTENTS_FILE = 'FEDEOSiriIntents.swift';
+
+function replaceOnce(source, search, replacement, label) {
+ if (!source.includes(search)) {
+ throw new Error(`Multi-File-Patch konnte ${label} nicht finden.`);
+ }
+ return source.replace(search, replacement);
+}
+
+module.exports = function withShareIntentMultifile(config) {
+ config = withInfoPlist(config, (modConfig) => {
+ modConfig.modResults.INAlternativeAppNames = [
+ {
+ INAlternativeAppName: 'Fedeo',
+ INAlternativeAppNamePronunciationHint: 'Feh-deh-oh',
+ },
+ {
+ INAlternativeAppName: 'FEDEO Aufgaben',
+ INAlternativeAppNamePronunciationHint: 'Feh-deh-oh Aufgaben',
+ },
+ ];
+ return modConfig;
+ });
+
+ config = withAppDelegate(config, (modConfig) => {
+ let source = modConfig.modResults.contents;
+ if (!source.includes('import AppIntents')) {
+ source = source.replace('import Expo\n', 'import AppIntents\nimport Expo\n');
+ }
+ if (!source.includes('FEDEOAppShortcuts.updateAppShortcutParameters()')) {
+ source = replaceOnce(
+ source,
+ ' ) -> Bool {\n',
+ ' ) -> Bool {\n if #available(iOS 16.0, *) {\n FEDEOAppShortcuts.updateAppShortcutParameters()\n }\n',
+ 'die Siri-Shortcut-Aktualisierung im AppDelegate'
+ );
+ }
+ modConfig.modResults.contents = source;
+ return modConfig;
+ });
+
+ return withXcodeProject(config, async (modConfig) => {
+ const siriTemplatePath = path.join(__dirname, 'ios', SIRI_INTENTS_FILE);
+ const siriDestinationPath = path.join(modConfig.modRequest.platformProjectRoot, 'FEDEO', SIRI_INTENTS_FILE);
+ fs.copyFileSync(siriTemplatePath, siriDestinationPath);
+
+ const project = modConfig.modResults;
+ const appTarget = project.pbxTargetByName('FEDEO');
+ const appGroup = project.pbxGroupByName('FEDEO');
+ const appTargetUUID = Object.entries(project.pbxNativeTargetSection()).find(
+ ([key, entry]) => !key.endsWith('_comment') && entry === appTarget
+ )?.[0];
+ const appGroupUUID = Object.entries(project.hash.project.objects.PBXGroup).find(
+ ([key, entry]) => !key.endsWith('_comment') && entry === appGroup
+ )?.[0];
+ const fileReferences = project.pbxFileReferenceSection();
+ const siriFileExists = Object.values(fileReferences).some(
+ (entry) => entry && typeof entry === 'object' && String(entry.path || entry.name || '').includes(SIRI_INTENTS_FILE)
+ );
+ if (!siriFileExists) {
+ if (!appTargetUUID || !appGroupUUID) {
+ throw new Error('FEDEO-App-Target für Siri App Intents wurde nicht gefunden.');
+ }
+ project.addSourceFile(
+ `FEDEO/${SIRI_INTENTS_FILE}`,
+ { target: appTargetUUID },
+ appGroupUUID
+ );
+ }
+
+ const controllerPath = path.join(
+ modConfig.modRequest.platformProjectRoot,
+ SHARE_EXTENSION_DIRECTORY,
+ 'ShareViewController.swift'
+ );
+
+ let source = fs.readFileSync(controllerPath, 'utf8');
+
+ source = replaceOnce(
+ source,
+ ' var sharedText: [String] = []\n',
+ ' var sharedText: [String] = []\n var processedMediaAttachmentCount = 0\n',
+ 'den Attachment-Zähler'
+ );
+
+ const imageCompletion = ` // If this is the last item, save imagesData in userDefaults and redirect to host app
+ if index == (content.attachments?.count)! - 1 {
+ let userDefaults = UserDefaults(suiteName: self.hostAppGroupIdentifier)
+ userDefaults?.set(self.toData(data: self.sharedMedia), forKey: self.sharedKey)
+ userDefaults?.synchronize()
+ self.redirectToHostApp(type: .media)
+ }`;
+
+ source = replaceOnce(
+ source,
+ imageCompletion,
+ ' self.finishMediaAttachment(content: content, type: .media)',
+ 'den Abschluss der Bildverarbeitung'
+ );
+
+ source = replaceOnce(
+ source,
+ imageCompletion,
+ ' self.finishMediaAttachment(content: content, type: .media)',
+ 'den Abschluss der Videoverarbeitung'
+ );
+
+ const fileCompletion = ` if index == (content.attachments?.count)! - 1 {
+ let userDefaults = UserDefaults(suiteName: self.hostAppGroupIdentifier)
+ userDefaults?.set(self.toData(data: self.sharedMedia), forKey: self.sharedKey)
+ userDefaults?.synchronize()
+ self.redirectToHostApp(type: .file)
+ }`;
+
+ source = replaceOnce(
+ source,
+ fileCompletion,
+ ' self.finishMediaAttachment(content: content, type: .file)',
+ 'den Abschluss der Dateiverarbeitung'
+ );
+
+ const redirectMethod = ' private func redirectToHostApp(type: RedirectType) {';
+ const completionMethod = ` private func finishMediaAttachment(content: NSExtensionItem, type: RedirectType) {
+ processedMediaAttachmentCount += 1
+ let expectedMediaAttachmentCount = content.attachments?.filter { attachment in
+ attachment.hasItemConformingToTypeIdentifier(imageContentType)
+ || attachment.hasItemConformingToTypeIdentifier(videoContentType)
+ || attachment.hasItemConformingToTypeIdentifier(vcardContentType)
+ || attachment.hasItemConformingToTypeIdentifier(fileURLType)
+ || attachment.hasItemConformingToTypeIdentifier(pkpassContentType)
+ || attachment.hasItemConformingToTypeIdentifier(pdfContentType)
+ }.count ?? 0
+ guard processedMediaAttachmentCount == expectedMediaAttachmentCount else { return }
+
+ let userDefaults = UserDefaults(suiteName: hostAppGroupIdentifier)
+ userDefaults?.set(toData(data: sharedMedia), forKey: sharedKey)
+ userDefaults?.synchronize()
+ redirectToHostApp(type: type)
+ }
+
+`;
+
+ source = replaceOnce(
+ source,
+ redirectMethod,
+ completionMethod + redirectMethod,
+ 'die Weiterleitungsmethode'
+ );
+
+ fs.writeFileSync(controllerPath, source);
+ return modConfig;
+ });
+};
diff --git a/mobile/src/lib/resource-list-filters.ts b/mobile/src/lib/resource-list-filters.ts
new file mode 100644
index 0000000..33fa773
--- /dev/null
+++ b/mobile/src/lib/resource-list-filters.ts
@@ -0,0 +1,63 @@
+import { Customer, Plant, Project } from './api';
+
+function searchTerms(search: string): string[] {
+ return search.trim().toLocaleLowerCase('de').split(/\s+/).filter(Boolean);
+}
+
+function matchesTerms(values: unknown[], terms: string[]): boolean {
+ if (terms.length === 0) return true;
+ const haystack = values
+ .map((value) => String(value || '').toLocaleLowerCase('de'))
+ .join(' ');
+ return terms.every((term) => haystack.includes(term));
+}
+
+export function getActiveProjectPhase(project: Project): string {
+ const explicit = String(project.active_phase || '').trim();
+ if (explicit) return explicit;
+
+ const phases = Array.isArray(project.phases) ? project.phases : [];
+ const active = phases.find((phase: any) => phase?.active);
+ return String(active?.label || '').trim();
+}
+
+export function isProjectCompleted(project: Project): boolean {
+ return getActiveProjectPhase(project).toLocaleLowerCase('de') === 'abgeschlossen';
+}
+
+export function filterProjects(projects: Project[], search: string, showCompleted = false): Project[] {
+ const terms = searchTerms(search);
+ return projects.filter((project) => {
+ if (!showCompleted && isProjectCompleted(project)) return false;
+ return matchesTerms([
+ project.name,
+ project.projectNumber,
+ project.notes,
+ project.customerRef,
+ project.active_phase,
+ getActiveProjectPhase(project),
+ ], terms);
+ });
+}
+
+export function filterCustomers(customers: Customer[], search: string, showArchived = false): Customer[] {
+ const terms = searchTerms(search);
+ return customers.filter((customer) => {
+ if (!showArchived && customer.archived) return false;
+ return matchesTerms([customer.name, customer.customerNumber, customer.notes], terms);
+ });
+}
+
+export function getPlantCustomerName(raw: Plant['customer']): string | null {
+ if (!raw) return null;
+ if (typeof raw === 'object') return raw.name ? String(raw.name) : null;
+ return String(raw);
+}
+
+export function filterPlants(plants: Plant[], search: string, showArchived = false): Plant[] {
+ const terms = searchTerms(search);
+ return plants.filter((plant) => {
+ if (!showArchived && plant.archived) return false;
+ return matchesTerms([plant.name, plant.description, getPlantCustomerName(plant.customer)], terms);
+ });
+}
diff --git a/mobile/src/lib/siri-settings.ts b/mobile/src/lib/siri-settings.ts
new file mode 100644
index 0000000..c16b196
--- /dev/null
+++ b/mobile/src/lib/siri-settings.ts
@@ -0,0 +1,18 @@
+import * as SecureStore from 'expo-secure-store';
+
+const SIRI_PREFERRED_TENANT_KEY = 'fedeo.mobile.siri.preferred-tenant';
+
+export async function getSiriPreferredTenantId(): Promise {
+ const stored = await SecureStore.getItemAsync(SIRI_PREFERRED_TENANT_KEY);
+ if (!stored) return null;
+ const tenantId = Number(stored);
+ return Number.isFinite(tenantId) ? tenantId : null;
+}
+
+export async function setSiriPreferredTenantId(tenantId: number | null): Promise {
+ if (tenantId === null) {
+ await SecureStore.deleteItemAsync(SIRI_PREFERRED_TENANT_KEY);
+ return;
+ }
+ await SecureStore.setItemAsync(SIRI_PREFERRED_TENANT_KEY, String(tenantId));
+}
diff --git a/mobile/src/lib/token-storage.ts b/mobile/src/lib/token-storage.ts
index 766142a..6e34e67 100644
--- a/mobile/src/lib/token-storage.ts
+++ b/mobile/src/lib/token-storage.ts
@@ -2,6 +2,7 @@ import * as SecureStore from 'expo-secure-store';
const TOKEN_KEY = 'fedeo.mobile.auth.token';
const REFRESH_TOKEN_KEY = 'fedeo.mobile.auth.refresh-token';
+const SIRI_PREFERRED_TENANT_KEY = 'fedeo.mobile.siri.preferred-tenant';
let memoryToken: string | null = null;
let memoryRefreshToken: string | null = null;
@@ -71,6 +72,7 @@ export async function clearStoredToken(): Promise {
await Promise.all([
SecureStore.deleteItemAsync(TOKEN_KEY),
SecureStore.deleteItemAsync(REFRESH_TOKEN_KEY),
+ SecureStore.deleteItemAsync(SIRI_PREFERRED_TENANT_KEY),
]);
}
}