64 Commits

Author SHA1 Message Date
MineTec 15791423ea added fileFetchedAt to TickerPageResponse and updated the Ticker UI to use it as the data currency timestamp for proxied files 2026-07-13 00:32:27 +02:00
MineTec 2f5a6b4ce0 split file_viewer into focused sub-widgets 2026-07-13 00:01:07 +02:00
MineTec 53bc6d5360 share weekKey extension and log splash precache failures 2026-07-12 23:56:47 +02:00
MineTec f50359b4eb use AppProgressIndicator for plain loading spinners 2026-07-12 23:45:55 +02:00
MineTec 9994a1f3fa extract SettingsDropdownTile for settings dropdowns 2026-07-12 23:42:56 +02:00
MineTec 4aa31a2e44 extract SettingsCheckboxTile for settings toggles 2026-07-12 23:41:12 +02:00
MineTec dfce3e7b5c add PromptDialog helper for text input dialogs 2026-07-12 23:36:46 +02:00
MineTec 564a334cdc route avatar crop and profile view through AppRoutes 2026-07-12 23:32:48 +02:00
MineTec db329c7299 adopt MarianumConnectQuery base in remaining queries 2026-07-12 23:28:51 +02:00
MineTec 0a2ff5c3fb add MarianumConnectQuery base and adopt it in list queries 2026-07-12 23:24:21 +02:00
MineTec 9b5198c6db add ticker page bloc and avatar disk cache 2026-07-12 23:18:53 +02:00
MineTec 94794ff092 remove unused dead code files 2026-07-12 23:17:47 +02:00
MineTec fe2b3c43b2 implemented custom subject colors for the timetable and unified the color palette system 2026-07-12 21:30:08 +02:00
MineTec a7111844b1 bugfixes, better background operation robustness and platform-specific error handling 2026-07-12 19:17:49 +02:00
MineTec babc347b18 implemented global user search integration for Talk chats with role-coded badges and direct chat creation logic 2026-07-12 19:05:27 +02:00
MineTec 44e45c9b78 implemented full interactive poll support in Talk, including creation, voting, and closing functionality 2026-07-12 17:54:30 +02:00
MineTec 3f44e9302f implemented dynamic quick reactions by surfacing emojis from message content in the chat options dialog 2026-07-12 16:42:30 +02:00
MineTec 59501d3b45 implemented enlarged emoji rendering for chat messages containing up to three emojis 2026-07-12 16:26:22 +02:00
MineTec 91a6216f66 migrated custom timetable events to Marianum-Connect and refactored push notification handling 2026-07-12 14:31:48 +02:00
MineTec c444ed54a5 migrated Marianum Message module to the Marianum-Connect API and implemented push notification deep linking 2026-07-11 17:37:50 +02:00
MineTec ff23199345 downgrade AGP and Gradle versions, update Ticker icon, and upgrade dependencies 2026-07-11 15:32:09 +02:00
MineTec 9545d2a946 upgraded Android Gradle Plugin to 9.2.1 and Gradle wrapper to 9.4.1 2026-07-10 19:16:07 +02:00
MineTec 37608e59b3 implemented Ticker page selection persistence and enhanced ProseMirror table display modes 2026-07-10 19:15:52 +02:00
MineTec 0d01f6b631 fixed push-registration not activated on app-login 2026-07-10 15:35:56 +02:00
MineTec b0d2e7a34b fixed inconsistent placement of appbar title on iOS 2026-07-09 12:04:20 +02:00
MineTec cedeb06569 implemented the Ticker module with a native ProseMirror document renderer and integrated API support for structured content, navigation trees, and proxied files 2026-07-09 00:51:08 +02:00
MineTec 1114291313 Add Fastlane Play Store integration with automated screenshots
Fastlane supply config (android/fastlane) with de-DE metadata structure,
integration_test-driven screenshot automation (screenshot_test.dart,
test_driver, tool/screenshots.sh) using stable keys on the login form,
refreshed materials/screenshots (phone + 7-inch) and app banner/symbol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 23:43:10 +02:00
MineTec 9b74c9fd81 implemented a comprehensive client-side demo mode triggered by a "demo@" username prefix, serving curated fixture data for all app modules 2026-07-07 20:24:16 +02:00
MineTec 2668b111f1 updated login branding and the about dialog with updated slogan. 2026-07-07 19:21:11 +02:00
MineTec a19b67eb84 refactored and condensed technical documentation and comments across the codebase to improve readability 2026-07-06 23:17:06 +02:00
MineTec c48f5ef215 migrated feedback submission from the legacy mhsl.eu endpoint to the MarianumConnect API 2026-07-06 22:36:11 +02:00
MineTec ddbb3fbc19 implemented a new telemetry heartbeat and client-side error reporting system for MarianumConnect; replaced legacy mhsl.eu user index updates with a POST me/telemetry heartbeat using stable, securely-stored device identifiers; integrated global Flutter and platform error handlers to report uncaught exceptions with session-based deduplication and rate limiting. 2026-07-06 22:11:23 +02:00
MineTec 2be5051d12 migrated the breaker system to MarianumConnect and refactored the API response to a rule-based model with support for version-specific blocks using maxBuild. 2026-07-06 21:15:39 +02:00
MineTec 38a271929c implemented a guided notification permission flow triggered on the first Talk visit 2026-07-06 20:01:45 +02:00
MineTec 3be0113f93 fixed bug on leaving talk chat, spamming console 2026-07-06 15:55:27 +02:00
MineTec 2b59f11ea5 fixed z-index issue with background-loading indicator being behind talk-messages in chats 2026-07-06 15:51:08 +02:00
MineTec 124b1a5177 added support for simultan downloads in files and talk, support for background downloads, enhanced loading in files with retry 2026-07-06 15:43:17 +02:00
MineTec 614ab159af updated notification subject parsing to support newline separators for sender and message, while maintaining a fallback for legacy colon-delimited formats. 2026-07-05 23:57:48 +02:00
MineTec 5952c6619f fixed a race condition in long-poll initialization by adding a token validation guard to prevent stale chat sessions from hijacking the current state during navigation or app resumes. 2026-07-05 23:55:01 +02:00
MineTec dab208877f improved reliability and error handling for background notification actions by migrating to a top-level entry point for stable AOT callback resolution; implemented failure notifications to preserve undelivered reply text and surface technical errors; resolved a race condition that could resurrect dismissed notifications during silent re-renders; updated the Android manifest with the required broadcast receiver for notification actions; and refactored internal OCS methods to return detailed success/failure records for improved logging and field debugging. 2026-07-05 23:43:58 +02:00
MineTec 483fea62ba implemented dual Nextcloud push registration with separate general and talk apptypes to ensure reliable Talk notification delivery; introduced stacked MessagingStyle notifications for chat threads with support for circular conversation avatars and disk caching 2026-07-05 22:48:04 +02:00
MineTec 35e144799e Bumped Gradle to 8.14 and fixed build warnings 2026-07-05 00:17:20 +02:00
MineTec b14e8c8ea2 improved timetable date range calculation to ensure the current week remains accessible during holiday gaps or when persisted bounds are stale, and updated holiday labels to prioritize long names over short names in the calendar view. 2026-07-04 23:54:50 +02:00
MineTec 74a2ddd17f implemented an E2E-encrypted Nextcloud push-v2 notification system with support for RSA decryption and signature verification; introduced an iOS Notification Service Extension and native AppDelegate handlers for Talk actions (inline reply and mark-as-read); replaced the legacy notification registration with a new lifecycle managing app passwords and secure keypair storage; added background message handling with tray synchronization and a test notification utility in the settings. 2026-07-04 22:50:18 +02:00
MineTec 32f7c311bc fixed pending share race error on warm app start 2026-07-02 15:28:05 +02:00
MineTec 4bc7ffd37a implemented post-login splash screen with Lottie animations and integrated background data prefetching during the login transition 2026-06-29 12:00:26 +02:00
MineTec 13f4f79829 removed broad media access permissions from the manifest to comply with Google Play policies, and bumped the app version to 1.2.2+57. 2026-06-23 18:41:18 +02:00
MineTec a76b09af26 updated pods and SPM dependencies (Firebase 12.15, PhoneNumberKit)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:13:00 +02:00
Marianum 4fd09204ec iOS build 2026-06-23 12:06:22 +02:00
MineTec f554c57d8d updated version to 1.2.1+56 2026-06-22 22:44:54 +02:00
MineTec baa26a6e79 implemented a comprehensive Nextcloud file sharing system with support for user, group, and public link shares with gating based on server-side permissions; added sharing management interfaces including a share sheet; updated the file list with visual badges for incoming shares and improved OCS API response handling. 2026-06-02 21:42:08 +02:00
MineTec b6d06dd3b4 implemented foreign timetable support for students, teachers, rooms, and classes, including a searchable element picker with favorites support, introduced a capabilities system for feature gating, refactored the timetable UI into a reusable TimetableCalendarView component, and redesigned the chat input field with a unified emoji picker and integrated attachment actions. 2026-05-31 21:29:16 +02:00
MineTec 6e12da08c0 implemented a customizable chat background system with support for patterns, solid colors, and gallery images; added a dedicated settings page with live preview and adjustable blur/dim effects, updated the image cropper to support flexible aspect ratios for wallpapers, and integrated file cleanup logic during account logout. 2026-05-31 19:20:18 +02:00
MineTec 5ebf5bccdb implemented avatar management for user profiles and chat rooms, including 1:1 cropping, integrated OCS and Spreed avatar APIs, added cache invalidation logic, and updated the account settings view to display user info and profile pictures. 2026-05-31 18:42:30 +02:00
MineTec f966cf302b implemented favorite and leave actions for chat/rooms info view 2026-05-30 14:05:00 +02:00
MineTec 582432dbb9 implemented support for viewing large group profile pictures 2026-05-30 13:57:26 +02:00
MineTec ece0669f7d implemented a central haptic feedback system with configurable levels (off, reduced, full), added a Haptics facade providing semantic feedback methods, integrated haptic cues across navigation, settings toggles, and async action results, and updated version to 1.1.0+54 2026-05-30 13:54:19 +02:00
MineTec 01b4b44010 migrated holidays module to MarianumConnect API, replaced local Holiday model. 2026-05-24 17:49:25 +02:00
MineTec 93b9929f8f migrated timetable integration from WebUntis to the MarianumConnect API, implementing a Dio-based client with bearer token authentication, background session validation, and auto-refresh logic. 2026-05-23 17:32:42 +02:00
MineTec 2858f910c9 implemented DST-safe date arithmetic with new addDays and subtractDays extensions, updated timetable state to reset view and scroll boundaries on initialization to prevent stale views, added hard caps to calendar navigation, and updated version to 1.0.3+52 2026-05-22 15:08:30 +02:00
MineTec f185b3273a updated version to 1.0.2+50 2026-05-17 11:55:40 +02:00
MineTec 831ea56869 Merge pull request 'updated app icons and splash screen assets, updated sort logic' (#100) from develop-logo into develop
Reviewed-on: #100
2026-05-17 09:54:59 +00:00
MineTec 215911cf29 refactored room and file sorting to use direct comparators instead of temporary sort strings, removed obsolete 'sort' properties from API models, and improved file list sorting with case-insensitive name comparisons and null-safe date handling 2026-05-17 00:27:17 +02:00
MineTec e5873f73b9 updated app icons and splash screen assets across Android, iOS, and Web platforms, added a new brand SVG asset, refined Android adaptive icon scaling with insets, and updated iOS asset catalog configurations and launch screen dimensions 2026-05-16 14:33:22 +02:00
640 changed files with 30604 additions and 6588 deletions
+12
View File
@@ -350,3 +350,15 @@ hs_err_pid*
*.idea* *.idea*
**/.DS_store **/.DS_store
# Fastlane (Play-Store-Upload)
android/fastlane/report.xml
android/fastlane/Preview.html
android/fastlane/play-service-account.json
android/fastlane/README.md.bak
# lokale Screenshots außerhalb der Fastlane-Metadaten
/screenshots/
# Fastlane-Bild-PNGs sind generierte Kopien; kanonisch/versioniert liegen die
# Screenshots unter materials/screenshots/. Nur die leeren Ordner (.gitkeep)
# bleiben eingecheckt, damit supply die Struktur vorfindet.
android/fastlane/metadata/android/**/images/**/*.png
@@ -4,4 +4,10 @@
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<!-- Allow cleartext HTTP in debug builds so developers can point the
Marianum-Connect custom endpoint at a local backend (e.g.
http://10.0.2.2:8080 from the Android emulator). Release builds
keep the default cleartext block. -->
<application android:usesCleartextTraffic="true" />
</manifest> </manifest>
+46 -1
View File
@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application <application
android:label="Marianum Fulda" android:label="Marianum Fulda"
android:name="${applicationName}" android:name="${applicationName}"
@@ -47,6 +48,35 @@
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
<!-- Fallback small icon + accent for anything FCM ever renders itself
(e.g. plain notification messages). Launcher mipmaps are unusable
as small icons: Android only draws their alpha silhouette, which
shows up as a solid circle in the status bar. -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_stat_notification" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/notification_accent" />
<!-- flutter_local_notifications liefert Notification-Action-Taps (Talk:
Antworten / Gelesen) als expliziten Broadcast an diesen Receiver.
Das Plugin deklariert ihn NICHT selbst — ohne diesen Eintrag kommt
der Tap nirgends an und der Background-Handler startet nie. -->
<receiver
android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver"
android:exported="false" />
<!-- background_downloader runs large/long downloads in a foreground
service (WorkManager) so they survive the app being backgrounded and
beat the default 9-minute WorkManager timeout. On API 34+ the
dataSync foreground service type must be declared explicitly here;
see the FOREGROUND_SERVICE_DATA_SYNC permission below. -->
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
tools:node="merge" />
<!-- Receiver classes live at the package root (NOT under .widgets) because <!-- Receiver classes live at the package root (NOT under .widgets) because
the home_widget Flutter plugin resolves them as <app-package>.<name>. --> the home_widget Flutter plugin resolves them as <app-package>.<name>. -->
<receiver <receiver
@@ -103,7 +133,22 @@
</intent> </intent>
</queries> </queries>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<!-- Android 13+ runtime notification permission. Requested at login via
FirebaseMessaging.requestPermission(); without this declaration the
locally rendered push notifications are silently dropped. -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- background_downloader foreground service for large/long downloads that
must keep running while the app is backgrounded. DATA_SYNC is the
required type declaration on Android 14+ (API 34+). -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<!-- Workmanager periodic widget refresh needs to reschedule after device <!-- Workmanager periodic widget refresh needs to reschedule after device
reboot, otherwise the widget freezes until the user opens the app. --> reboot, otherwise the widget freezes until the user opens the app. -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<!-- Aus open_filex gemergt: die App öffnet nur selbst heruntergeladene Dateien
über dessen FileProvider (content://-URIs) und braucht keinen Zugriff auf
geteilte Medien. Entfernt wegen Google-Play "Photo and Video Permissions". -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" tools:node="remove"/>
</manifest> </manifest>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 B

After

Width:  |  Height:  |  Size: 69 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 1021 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 B

After

Width:  |  Height:  |  Size: 69 B

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Status-bar small icon: monochrome Marianum M, ported from
marianum_m_watermark.xml (same path + nested transforms). Android renders
only the alpha channel of small icons, so the fill is hard white and never
tinted at runtime. 24dp with the source's non-square viewport — the system
fits it into the icon bounds.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="70.000168"
android:viewportHeight="82.227348">
<group
android:scaleX="0.26458333"
android:scaleY="0.26458334"
android:translateX="107.44411"
android:translateY="-80.482198">
<group
android:translateX="-749.41293"
android:translateY="290.52252">
<group
android:scaleX="0.13333333"
android:scaleY="-0.13333333"
android:translateX="0"
android:translateY="632.14667">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M 3499.67,4594.33 c -16.43,-106.19 -29.71,-199.97 -43.79,-293.49 86.83,-19 138.5,-27.61 223.38,-43.82 63.81,-12.18 175.24,-20.4 179.64,-83.23 6.46,-92.69 -124.69,-55.41 -188.38,-43.81 -84.33,15.36 -159.13,28.84 -232.2,43.81 -13.68,-60.72 -26.83,-118.68 -39.43,-179.61 -36.76,-178.32 -73.67,-368.16 -105.11,-551.97 18.09,25.66 30.84,42.72 43.8,65.7 66.7,118.26 140.39,245.04 227.76,354.83 33.49,42.05 76.86,94.81 118.31,113.91 98.42,45.36 166.68,-22.2 170.87,-118.28 3.68,-85.28 -23.09,-181.17 -35.08,-275.99 -12.4,-98.19 -22.89,-194.93 -35.03,-275.98 72.44,102.69 147.93,269.64 240.95,381.12 27.51,33 73.55,80.61 118.27,87.62 218.76,34.33 126.58,-312.17 127.05,-473.13 0.4,-144.9 44.01,-255.37 175.21,-271.59 43.02,-5.31 105.84,11.16 112.7,-26.34 8.67,-47.38 -78.15,-60.52 -125.84,-61.28 -291.34,-4.51 -322.06,262.33 -311.01,573.88 -19.85,-18.57 -35.71,-47.53 -52.57,-74.47 -97.59,-155.88 -203.95,-327.22 -297.92,-503.79 -25.93,-48.79 -53.68,-114.7 -135.8,-83.23 -17.27,6.63 -48.25,44.39 -52.56,56.96 -19.58,57.19 1.55,137.42 8.76,205.89 21.54,203.72 57.81,389.09 78.87,587.01 -26.3,0.51 -43.93,-30.07 -56.96,-48.2 -46.9,-65.27 -86.02,-140.76 -127.04,-214.64 -52.84,-95.15 -108.23,-192.84 -157.71,-293.52 -75.25,-153.09 -188.6,-501.89 -242.12,-678.81 -8.67,-28.67 -17.7,-58.08 -26.3,-87.64 -7.48,-25.72 -10.39,-57.68 -35.05,-74.46 -100.02,18.93 -89.71,104.89 -70.09,205.9 47.35,243.43 170.89,706.45 211.48,946.04 -72.97,-70.97 -153.99,-207.41 -289.14,-236.55 -136.47,-29.44 -217.95,47.68 -271.6,122.66 -17.14,23.96 -41.43,49.54 -26.29,78.84 83.96,35.51 113.37,-65.2 197.15,-74.47 22.65,-2.5 54.56,2.4 74.46,8.78 132.4,42.34 237.57,218.76 297.87,346.07 74.16,156.45 125.32,330.5 148.95,490.64 -65.71,11.4 -142.96,22.15 -219.25,36.52 -109.8,20.72 -158.81,10.75 -201.29,59.86 9.15,41.95 41.41,60.8 70.1,83.24 126.26,-16.84 252.45,-33.77 372.36,-56.97 20.43,89.25 51.98,218.51 74.45,311.05 40.53,26.02 88.88,-8.43 105.17,-35.06 z" />
</group>
</group>
</group>
</vector>
@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon> </adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Brand accent for notifications; mirrors LightAppTheme.marianumRed
(Color.fromARGB(255, 153, 51, 51)). Used as the FCM default
notification color and must stay in sync with the Dart constant. -->
<color name="notification_accent">#993333</color>
</resources>
+5
View File
@@ -22,6 +22,11 @@ subprojects { sub ->
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
} }
// Some plugins (e.g. app_settings) hard-pin an older compileSdk
// than their transitive AndroidX deps require, which fails the
// AAR metadata check. Align every plugin with the app's
// compileSdk (flutter.compileSdkVersion) so the build passes.
compileSdkVersion 36
} }
} }
sub.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { sub.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
+6
View File
@@ -0,0 +1,6 @@
package_name("eu.mhsl.marianum.mobile.client")
# Service-Account-JSON für die Google Play Developer API. Pfad über die
# Umgebungsvariable SUPPLY_JSON_KEY setzen (die Datei selbst wird per
# .gitignore nicht eingecheckt).
json_key_file(ENV["SUPPLY_JSON_KEY"] || "fastlane/play-service-account.json")
+29
View File
@@ -0,0 +1,29 @@
default_platform(:android)
# Play-Store-Auslieferung über fastlane supply. Die Screenshots erzeugt
# ../tool/screenshots.sh (Demo-Login + integration_test) direkt in
# metadata/android/<locale>/images/. Texte/Changelogs liegen daneben in
# metadata/android/<locale>/.
platform :android do
desc "Nur Screenshots hochladen (verändert keine Texte/Binaries)"
lane :upload_screenshots do
upload_to_play_store(
skip_upload_apk: true,
skip_upload_aab: true,
skip_upload_metadata: true,
skip_upload_changelogs: true,
skip_upload_images: false,
skip_upload_screenshots: false,
metadata_path: "./fastlane/metadata/android",
)
end
desc "Store-Texte, Changelogs und Screenshots hochladen (keine Binaries)"
lane :upload_metadata do
upload_to_play_store(
skip_upload_apk: true,
skip_upload_aab: true,
metadata_path: "./fastlane/metadata/android",
)
end
end
+2
View File
@@ -0,0 +1,2 @@
# fastlane-Plugins (aktuell keine). Datei vorhanden, damit `fastlane` das
# Plugin-Handling initialisiert.
+35
View File
@@ -0,0 +1,35 @@
# Fastlane Play-Store-Screenshots & Upload
Schritt 2 der Play-Store-Automatisierung. Die App muss dafür **nicht** neu
gebaut werden der Demo-Modus (`demo@`-Login) liefert die kompletten Inhalte
aus Fixtures.
## 1. Screenshots aufnehmen
Ein Gerät/Emulator (≥ Android 8) per `adb devices` sichtbar, dann aus dem
**Client**-Wurzelverzeichnis:
```bash
tool/screenshots.sh
```
Fährt Phone (1080×1920), 7″ (1200×1920) und 10″ (1600×2560) nacheinander durch
(setzt die Displaygröße per `adb shell wm size/density`) und legt die PNGs in
`metadata/android/de-DE/images/{phone,sevenInch,tenInch}Screenshots/` ab.
- Nur eine Größe: `PROFILES="phone" tool/screenshots.sh`
- Profile-Build (flüssigere Frames): `BUILD_MODE=profile tool/screenshots.sh`
## 2. Hochladen
Service-Account-JSON der Google Play Developer API besorgen und referenzieren:
```bash
export SUPPLY_JSON_KEY=/pfad/zu/play-service-account.json
cd android
fastlane upload_screenshots # nur Bilder
# oder
fastlane upload_metadata # Bilder + Texte/Changelogs, keine Binaries
```
Der Service-Account-Key wird **nicht** eingecheckt (siehe `.gitignore`).
+4
View File
@@ -4,3 +4,7 @@ android.enableJetifier=true
android.defaults.buildfeatures.buildconfig=true android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false android.nonTransitiveRClass=false
android.nonFinalResIds=false android.nonFinalResIds=false
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="69.999985mm"
height="82.227501mm"
viewBox="0 0 69.999985 82.227501"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="g1"
transform="matrix(0.26458333,0,0,0.26458334,120.1078,-90.601937)"><g
id="group-R5"
transform="translate(-749.41293,290.52252)"><path
id="path2"
d="m 418.75901,57.817748 c -2.19013,14.15999 -3.96053,26.66133 -5.83906,39.13333 11.5776,2.532 18.4688,3.680002 29.7848,5.841332 8.50666,1.62133 23.36533,2.71733 23.952,11.096 0.86133,12.35866 -16.62534,7.388 -25.11734,5.84133 -11.244,-2.04667 -21.21693,-3.844 -30.9596,-5.84133 -1.82346,8.09866 -3.57813,15.82533 -5.25733,23.948 -4.90253,23.77866 -9.824,49.08933 -14.01666,73.59866 2.4136,-3.42133 4.11413,-5.69733 5.84226,-8.76267 8.89267,-15.76799 18.71867,-32.67066 30.37027,-47.31066 4.46106,-5.60667 10.24506,-12.64133 15.77306,-15.188 13.12267,-6.048 22.224,2.96133 22.78,15.77067 0.49334,11.372 -3.07733,24.15733 -4.676,36.80133 -1.65333,13.08933 -3.05066,25.98933 -4.66933,36.796 9.65733,-13.69067 19.72267,-35.95334 32.12533,-50.81733 3.668,-4.4 9.80667,-10.748 15.76934,-11.68134 29.16799,-4.57866 16.87733,41.62267 16.94133,63.084 0.0547,19.32001 5.86667,34.04934 23.36133,36.21068 5.73333,0.70933 14.10933,-1.48667 15.02533,3.51466 1.15467,6.31467 -10.42133,8.068 -16.77866,8.168 -38.84667,0.60267 -42.94133,-34.97734 -41.46933,-76.51601 -2.64534,2.476 -4.76134,6.336 -7.00934,9.93067 -13.00933,20.78267 -27.192,43.62801 -39.72133,67.17068 -3.45467,6.504 -7.15867,15.29333 -18.10667,11.09733 -2.30133,-0.88267 -6.43466,-5.92 -7.008,-7.59467 -2.60786,-7.62666 0.20667,-18.32533 1.168,-27.45333 2.87334,-27.16134 7.70934,-51.87868 10.516,-78.26534 -3.50533,-0.0707 -5.85733,4.008 -7.59333,6.42667 -6.2548,8.70266 -11.47147,18.76666 -16.94133,28.61866 -7.04214,12.684 -14.42814,25.71067 -21.02867,39.13201 -10.03173,20.41333 -25.14373,66.92133 -32.28013,90.50933 -1.15573,3.82133 -2.35933,7.744 -3.5052,11.68667 -0.998,3.428 -1.38693,7.69066 -4.67347,9.928 -13.33853,-2.52534 -11.962,-13.98534 -9.3464,-27.45467 6.31414,-32.45733 22.78547,-94.19334 28.19587,-126.13867 -9.72813,9.464 -20.53067,27.65466 -38.552,31.54133 -18.1928,3.92667 -29.05788,-6.35733 -36.2136,-16.35333 -2.28489,-3.19467 -5.52188,-6.60534 -3.50417,-10.51467 11.19375,-4.73333 15.11457,8.69467 26.28537,9.93067 3.0204,0.33199 7.2756,-0.32001 9.93027,-1.17067 17.65253,-5.64667 31.67346,-29.168 39.71506,-46.14133 9.88747,-20.86267 16.70987,-44.06667 19.86094,-65.42133 -8.76254,-1.51867 -19.064,-2.95467 -29.2344,-4.86933 -14.64054,-2.76134 -21.17334,-1.432 -26.83947,-7.980002 1.2228,-5.592 5.5228,-8.108 9.34627,-11.09867 16.83653,2.24533 33.6584,4.504 49.64946,7.59333 2.72454,-11.9 6.92973,-29.13199 9.92813,-41.47199 5.40267,-3.468 11.84947,1.12533 14.0204,4.676"
style="fill:#941a1f;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.133333" /></g></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+215
View File
@@ -0,0 +1,215 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:marianum_mobile/app.dart';
import 'package:marianum_mobile/main.dart' as app;
import 'package:marianum_mobile/routing/app_routes.dart';
import 'package:marianum_mobile/state/app/modules/app_modules.dart';
import 'package:marianum_mobile/state/app/modules/settings/bloc/settings_cubit.dart';
import 'package:marianum_mobile/view/pages/talk/widgets/chat_tile.dart';
/// Marketing-Screenshot-Lauf für die Stores. Loggt sich über den Demo-Login
/// (`demo@…`, siehe DemoMode) ein und nimmt einen kuratierten Satz Screens auf:
/// Stundenplan (hell + dunkel), Talk-Liste, Talk-Chat, Dateien, „Mehr"-Bereich
/// und Einstellungen. Ausgeführt über `flutter drive` mit
/// test_driver/integration_test.dart, orchestriert von tool/screenshots.sh
/// (Phone / 7″ / 10″).
///
/// Kein Netzwerk: der Demo-Modus beantwortet allen Backend-Verkehr aus
/// Fixtures, daher ist der Lauf deterministisch.
Future<void> main() async {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Store-Screenshots über den Demo-Login', (tester) async {
// Der integration_test-Harness installiert eigene Error-Handler. app()
// überschreibt sie in main() durch die produktiven (ClientErrorReporter),
// was die End-of-Test-Buchführung des Harness sprengt (_pendingExceptionDetails).
// Daher vorher sichern und nach main() zurücksetzen.
final harnessFlutterOnError = FlutterError.onError;
final harnessDispatcherOnError = PlatformDispatcher.instance.onError;
_log('main() starten');
await app.main();
FlutterError.onError = harnessFlutterOnError;
PlatformDispatcher.instance.onError = harnessDispatcherOnError;
await tester.pump(const Duration(seconds: 1));
_log('Login');
await _login(tester);
_log('warte auf App-Shell');
await _pumpUntil(
tester,
find.byType(App),
timeout: const Duration(seconds: 20),
);
_log('warte auf Post-Login-Splash-Ende');
await _pumpUntilGone(
tester,
find.byKey(const ValueKey('post-login-splash')),
timeout: const Duration(seconds: 12),
);
await tester.pump(const Duration(seconds: 1));
// Screenshots auf Android brauchen eine einmalige Umwandlung der
// Flutter-Surface in eine lesbare Image-Texture. Erst hier nach Login und
// Splash , damit der sichtbare Screen erst kurz vor der Aufnahme „einfriert".
if (Platform.isAndroid) {
_log('convertFlutterSurfaceToImage');
await binding.convertFlutterSurfaceToImage();
}
// 1 + 2: Stundenplan in beiden Themes.
await _goTo(tester, Modules.timetable);
_setTheme(tester, ThemeMode.light);
await _capture(tester, binding, '1_stundenplan_hell');
_setTheme(tester, ThemeMode.dark);
await _capture(tester, binding, '2_stundenplan_dunkel');
// Restlicher Satz einheitlich im hellen Theme.
_setTheme(tester, ThemeMode.light);
// 3: Talk-Liste.
await _goTo(tester, Modules.talk);
await _capture(tester, binding, '3_talk_liste');
// 4: Talk-Chat (ersten Chat der Liste öffnen).
await tester.tap(find.byType(ChatTile).first);
await _capture(tester, binding, '4_talk_chat');
_popPushedPage(tester);
await tester.pump(const Duration(milliseconds: 500));
// 5: Dateien.
await _goTo(tester, Modules.files);
await _capture(tester, binding, '5_dateien');
// 6: „Mehr"-Bereich (letzter Tab, hinter den Modul-Tabs).
_goToMore(tester);
await _capture(tester, binding, '6_mehr');
// 7: Einstellungen (Vollbild-Push).
AppRoutes.openSettings(tester.element(find.byType(App)));
await _capture(tester, binding, '7_einstellungen');
_popPushedPage(tester);
_log('fertig');
});
}
void _log(String message) => debugPrint('SHOTS: $message');
Future<void> _login(WidgetTester tester) async {
final loginVisible = await _pumpUntil(
tester,
find.byKey(const Key('login-username-field')),
);
if (!loginVisible) {
_log('kein Login-Screen sichtbar bereits angemeldet, überspringe Login');
return;
}
await tester.enterText(
find.byKey(const Key('login-username-field')),
'demo@screenshots',
);
await tester.enterText(find.byKey(const Key('login-password-field')), 'demo');
await tester.pump();
await tester.tap(find.byKey(const Key('login-submit-button')));
await tester.pump(const Duration(milliseconds: 500));
}
/// Setzt das App-Theme über den [SettingsCubit] (MaterialApp folgt
/// `settings.appTheme`). `val(write: true)` plant den Emit als Microtask.
void _setTheme(WidgetTester tester, ThemeMode mode) {
final context = tester.element(find.byType(App));
context.read<SettingsCubit>().val(write: true).appTheme = mode;
}
/// Navigiert zu [module] über den Bottom-Tab, wenn er in der Leiste liegt,
/// sonst als Vollbild-Push. Größenunabhängig, da die Tab-Anzahl je Gerät variiert.
Future<void> _goTo(WidgetTester tester, Modules module) async {
final appFinder = find.byType(App);
if (appFinder.evaluate().isEmpty) {
_log('WARN: App-Shell fehlt, kann $module nicht ansteuern');
return;
}
final context = tester.element(appFinder);
if (!AppRoutes.goToTab(context, module)) {
final resolved = AppModule.modules(context)[module];
if (resolved != null) AppRoutes.openModule(context, resolved);
}
}
/// Springt auf den „Mehr"-Tab. Der liegt hinter den Modul-Tabs, sein Index ist
/// also die Anzahl der Bottom-Bar-Module.
void _goToMore(WidgetTester tester) {
final context = tester.element(find.byType(App));
final moreIndex = AppModule.getBottomBarModules(context).length;
app.Main.bottomNavigator.jumpToTab(moreIndex);
}
/// Schließt einen per [AppRoutes] gepushten Vollbild-Screen wieder.
void _popPushedPage(WidgetTester tester) {
final navigator = AppRoutes.rootNavigatorKey.currentState;
if (navigator != null && navigator.canPop()) navigator.pop();
}
/// Lässt die Oberfläche zur Ruhe kommen und legt den Screenshot ab.
Future<void> _capture(
WidgetTester tester,
IntegrationTestWidgetsFlutterBinding binding,
String name,
) async {
// Feste Setzzeit statt „warte auf Spinner-Ende": die Demo-Fixtures kommen
// synchron (keine künstlichen Delays), aber Module wie Dateien/Chat laden erst
// beim Navigieren nach. Eine globale CircularProgressIndicator-Prüfung taugt
// nicht, weil die PersistentTabView alle Tabs am Leben hält und
// Avatar-Platzhalter dauerhaft einen Spinner zeigen. In 500ms-Schritten
// pumpen, damit Timer/Microtasks (u.a. der Theme-Emit) durchlaufen.
await _settle(tester, const Duration(seconds: 6));
_log('-> $name: takeScreenshot');
await binding.takeScreenshot(name);
}
/// Pumpt [duration] in 500ms-Schritten ab, damit die App async Fixtures laden
/// und ihre Frames rendern kann (Ersatz für pumpAndSettle, das an den
/// Dauer-Animationen hängen bleibt).
Future<void> _settle(WidgetTester tester, Duration duration) async {
final steps = duration.inMilliseconds ~/ 500;
for (var i = 0; i < steps; i++) {
await tester.pump(const Duration(milliseconds: 500));
}
}
/// Pumpt in kurzen Schritten, bis [finder] erscheint oder [timeout] abläuft.
/// Gibt zurück, ob [finder] gefunden wurde.
Future<bool> _pumpUntil(
WidgetTester tester,
Finder finder, {
Duration timeout = const Duration(seconds: 10),
}) async {
final end = DateTime.now().add(timeout);
while (DateTime.now().isBefore(end)) {
await tester.pump(const Duration(milliseconds: 200));
if (finder.evaluate().isNotEmpty) return true;
}
return false;
}
/// Gegenstück zu [_pumpUntil]: pumpt, bis [finder] nichts mehr trifft.
Future<bool> _pumpUntilGone(
WidgetTester tester,
Finder finder, {
Duration timeout = const Duration(seconds: 10),
}) async {
final end = DateTime.now().add(timeout);
while (DateTime.now().isBefore(end)) {
await tester.pump(const Duration(milliseconds: 200));
if (finder.evaluate().isEmpty) return true;
}
return false;
}
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>NotificationServiceExtension</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>
</dict>
</plist>
@@ -0,0 +1,256 @@
import UserNotifications
import Security
/// Notification Service Extension for Nextcloud push-v2.
///
/// Architecture: Nextcloud pushes an E2E-encrypted notification to the
/// MarianumConnect proxy, which forwards it via FCM to this device as an
/// `alert` push with `mutable-content: 1`. That flag makes iOS spin up this
/// extension *before* showing the notification, giving us ~30 s (wall clock,
/// shared with the CPU budget) to decrypt the payload and replace the
/// placeholder alert with the real content.
///
/// The FCM proxy places the two relevant fields at the top level of the APNs
/// payload (mirrored into `request.content.userInfo`):
/// - `subject` base64( RSA-encrypted subject JSON ) encrypted with THIS
/// device's public key, so we decrypt with our private key.
/// - `signature` base64( SHA-512-with-RSA over the *encrypted* subject bytes )
/// signed by the per-user server key we stored at registration.
///
/// Key material lives in the shared (App Group) keychain, written by the Dart
/// side via flutter_secure_storage with
/// `IOSOptions(groupId: kPushKeychainGroup, accessibility: first_unlock)`:
/// - `push_device_private_key_pem` crypton PKCS#8-in-"RSA PRIVATE KEY" PEM
/// - `push_server_public_key_pem` SPKI "PUBLIC KEY" PEM (from NC)
///
/// Robustness contract: on ANY failure (missing keys, bad signature, decrypt
/// failure, malformed JSON, timeout) we deliver the untouched placeholder
/// content. We never crash and never deliver empty content.
class NotificationService: UNNotificationServiceExtension {
/// Must exactly match `kPushKeychainGroup` in lib/push/push_secure_storage.dart
/// and the `keychain-access-groups` entitlement of BOTH the Runner and this
/// extension. Wrong value here => keychain reads return nil => placeholder.
private static let keychainAccessGroup = "group.eu.mhsl.marianum.mobile.client.widget"
private static let devicePrivateKeyAccount = "push_device_private_key_pem"
private static let serverPublicKeyAccount = "push_server_public_key_pem"
private static let talkCategoryId = "TALK_MESSAGE"
private var contentHandler: ((UNNotificationContent) -> Void)?
private var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(
_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
) {
self.contentHandler = contentHandler
self.bestAttemptContent =
request.content.mutableCopy() as? UNMutableNotificationContent
guard let content = bestAttemptContent else {
contentHandler(request.content)
return
}
// Any early return delivers the placeholder unchanged that is the
// intended fallback, never an error path that hides the notification.
let userInfo = content.userInfo
guard
let subjectB64 = userInfo["subject"] as? String,
let signatureB64 = userInfo["signature"] as? String,
let encrypted = Data(base64Encoded: subjectB64),
let signature = Data(base64Encoded: signatureB64)
else {
deliver(content)
return
}
guard let privateKey = loadPrivateKey() else {
NSLog("[NSE] no device private key in keychain, delivering placeholder")
deliver(content)
return
}
// Signature verification is defence-in-depth (the proxy already verified
// it). Skip only if we somehow have no server key; never deliver on a
// *failed* verification.
if let serverKey = loadServerPublicKey() {
if !verifySignature(signature, over: encrypted, with: serverKey) {
NSLog("[NSE] signature verification failed, delivering placeholder")
deliver(content)
return
}
}
guard
let plaintext = decrypt(encrypted, with: privateKey),
let subject = try? JSONSerialization.jsonObject(with: plaintext) as? [String: Any]
else {
NSLog("[NSE] could not decrypt/parse subject, delivering placeholder")
deliver(content)
return
}
apply(subject: subject, to: content)
deliver(content)
}
override func serviceExtensionTimeWillExpire() {
// Ran out of time hand back whatever we have (at worst the placeholder).
if let handler = contentHandler, let content = bestAttemptContent {
handler(content)
}
}
private func deliver(_ content: UNNotificationContent) {
contentHandler?(content)
}
// MARK: - Content shaping
private func apply(subject: [String: Any], to content: UNMutableNotificationContent) {
// Delete pushes normally arrive as silent `background` pushes that never
// invoke this extension. If one reaches us anyway, an NSE cannot suppress
// the banner (iOS always shows *something*), so we leave the placeholder.
// Actual delete cleanup is done by the Flutter app on next open. See
// ios/PUSH_NSE_SETUP.md "Delete-Handling".
let isDelete = (subject["delete"] as? Bool == true)
|| (subject["delete-multiple"] as? Bool == true)
|| (subject["delete-all"] as? Bool == true)
if isDelete {
NSLog("[NSE] delete push reached NSE — cannot suppress, leaving placeholder")
return
}
let app = subject["app"] as? String
let text = subject["subject"] as? String ?? "Neue Benachrichtigung"
let objectId = stringValue(subject["id"])
let nid = subject["nid"]
if app == "spreed" {
// Talk: "Sender: message" -> title = Sender, body = message.
let (sender, message) = splitSender(text)
content.title = sender
content.body = message
content.categoryIdentifier = NotificationService.talkCategoryId
if let token = objectId, !token.isEmpty {
content.threadIdentifier = "talk_\(token)"
var info = content.userInfo
info["chatToken"] = token
if let nid = nid { info["nid"] = nid }
content.userInfo = info
}
} else {
content.title = text
content.body = ""
}
// Badge intentionally left untouched (the app recomputes it from the
// delivered notifications).
}
/// Splits a Talk subject into header and message. nextcloud/spreed's
/// Notifier separates them with a NEWLINE `"{user}\n{message}"` for 1:1
/// chats, `"{user} in {call}\n{message}"` for groups (verified against
/// spreed; mirrors parseTalkSubject in lib/push/push_subject.dart). The
/// legacy `": "` form is kept as fallback for older payloads.
private func splitSender(_ subject: String) -> (String, String) {
if let range = subject.range(of: "\n") {
let sender = String(subject[..<range.lowerBound])
.trimmingCharacters(in: .whitespaces)
let message = String(subject[range.upperBound...])
if !sender.isEmpty && !message.isEmpty {
return (sender, message)
}
}
if let range = subject.range(of: ": ") {
let sender = String(subject[..<range.lowerBound])
let message = String(subject[range.upperBound...])
if !sender.isEmpty && !message.isEmpty {
return (sender, message)
}
}
return ("Talk", subject)
}
private func stringValue(_ value: Any?) -> String? {
if let s = value as? String { return s }
if let n = value as? NSNumber { return n.stringValue }
return nil
}
// MARK: - Crypto
private func verifySignature(_ signature: Data, over message: Data, with key: SecKey) -> Bool {
var error: Unmanaged<CFError>?
let ok = SecKeyVerifySignature(
key,
.rsaSignatureMessagePKCS1v15SHA512,
message as CFData,
signature as CFData,
&error
)
return ok
}
private func decrypt(_ data: Data, with key: SecKey) -> Data? {
// NC 32 defaults to OAEP(SHA-1); older instances use PKCS#1 v1.5. Try
// OAEP first, fall back to PKCS#1 mirrors the Dart PushDecryptor.
for algorithm in [SecKeyAlgorithm.rsaEncryptionOAEPSHA1,
SecKeyAlgorithm.rsaEncryptionPKCS1] {
var error: Unmanaged<CFError>?
if let plain = SecKeyCreateDecryptedData(key, algorithm, data as CFData, &error) as Data? {
return plain
}
}
return nil
}
// MARK: - Key loading
private func loadPrivateKey() -> SecKey? {
guard let pem = keychainString(NotificationService.devicePrivateKeyAccount),
let der = PEM.der(fromPem: pem),
// crypton labels it "RSA PRIVATE KEY" but the body is PKCS#8.
let pkcs1 = PEM.pkcs1PrivateKey(fromPkcs8: der)
else { return nil }
return PEM.rsaKey(pkcs1: pkcs1, isPrivate: true)
}
private func loadServerPublicKey() -> SecKey? {
guard let pem = keychainString(NotificationService.serverPublicKeyAccount),
let der = PEM.der(fromPem: pem),
let pkcs1 = PEM.pkcs1PublicKey(fromSpki: der)
else { return nil }
return PEM.rsaKey(pkcs1: pkcs1, isPrivate: false)
}
// MARK: - Keychain
/// Reads a flutter_secure_storage entry from the shared keychain. Must match
/// exactly how flutter_secure_storage_darwin 0.3.2 stores items:
/// kSecClass = kSecClassGenericPassword
/// kSecAttrAccount = the Dart key, verbatim
/// kSecAttrService = (unset the Dart IOSOptions set no accountName)
/// kSecAttrAccessGroup = the App Group id
/// value = raw UTF-8 bytes of the string
private func keychainString(_ account: String) -> String? {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: account,
kSecAttrAccessGroup: NotificationService.keychainAccessGroup,
kSecReturnData: true,
kSecMatchLimit: kSecMatchLimitOne,
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess, let data = result as? Data else {
if status != errSecItemNotFound {
NSLog("[NSE] keychain read '\(account)' failed: \(status)")
}
return nil
}
return String(data: data, encoding: .utf8)
}
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.eu.mhsl.marianum.mobile.client.widget</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>group.eu.mhsl.marianum.mobile.client.widget</string>
</array>
</dict>
</plist>
+105
View File
@@ -0,0 +1,105 @@
import Foundation
import Security
/// Minimal PEM/DER helpers to turn the crypton-produced PEM strings into
/// `SecKey`s. iOS's `SecKeyCreateWithData` only accepts *raw PKCS#1* DER for
/// RSA, so we must strip the two wrapper formats crypton/Nextcloud use:
///
/// - Private key: crypton's `toPEM()` emits the `RSA PRIVATE KEY` label but the
/// DER body is actually **PKCS#8** `PrivateKeyInfo`
/// (SEQUENCE { INTEGER version, SEQUENCE algId, OCTET STRING pkcs1 }).
/// We extract the OCTET STRING content = the PKCS#1 `RSAPrivateKey`.
///
/// - Public key: `PUBLIC KEY` label, **SPKI** `SubjectPublicKeyInfo`
/// (SEQUENCE { SEQUENCE algId, BIT STRING spki }). We extract the BIT STRING
/// content and drop its leading 0x00 "unused bits" byte = the PKCS#1
/// `RSAPublicKey`.
///
/// Only DER lengths up to 4 bytes are handled more than enough for RSA-2048.
enum PEM {
/// Strips the PEM armor and returns the base64-decoded DER.
static func der(fromPem pem: String) -> Data? {
let body = pem
.split(whereSeparator: { $0 == "\n" || $0 == "\r" })
.filter { !$0.hasPrefix("-----") }
.joined()
return Data(base64Encoded: body)
}
/// PKCS#8 `PrivateKeyInfo` -> inner PKCS#1 `RSAPrivateKey`.
static func pkcs1PrivateKey(fromPkcs8 der: Data) -> Data? {
var reader = DERReader(der)
guard let outer = reader.readTLV(), outer.tag == 0x30 else { return nil }
var inner = DERReader(Data(outer.value))
guard let version = inner.readTLV(), version.tag == 0x02 else { return nil } // INTEGER version
guard let algId = inner.readTLV(), algId.tag == 0x30 else { return nil } // SEQUENCE algId
guard let octet = inner.readTLV(), octet.tag == 0x04 else { return nil } // OCTET STRING
return Data(octet.value)
}
/// SPKI `SubjectPublicKeyInfo` -> inner PKCS#1 `RSAPublicKey`.
static func pkcs1PublicKey(fromSpki der: Data) -> Data? {
var reader = DERReader(der)
guard let outer = reader.readTLV(), outer.tag == 0x30 else { return nil }
var inner = DERReader(Data(outer.value))
guard let algId = inner.readTLV(), algId.tag == 0x30 else { return nil } // SEQUENCE algId
guard let bitString = inner.readTLV(), bitString.tag == 0x03 else { return nil } // BIT STRING
var bytes = Array(bitString.value)
guard let first = bytes.first, first == 0x00 else { return nil } // unused-bits count
bytes.removeFirst()
return Data(bytes)
}
/// Builds an RSA `SecKey` from raw PKCS#1 DER.
static func rsaKey(pkcs1 der: Data, isPrivate: Bool) -> SecKey? {
let attributes: [CFString: Any] = [
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeyClass: isPrivate ? kSecAttrKeyClassPrivate : kSecAttrKeyClassPublic,
kSecAttrKeySizeInBits: 2048,
]
var error: Unmanaged<CFError>?
let key = SecKeyCreateWithData(der as CFData, attributes as CFDictionary, &error)
if key == nil {
NSLog("[NSE] SecKeyCreateWithData failed: \(String(describing: error?.takeRetainedValue()))")
}
return key
}
}
/// Tiny non-recursive DER TLV reader over a byte buffer.
private struct DERReader {
private let bytes: [UInt8]
private var pos = 0
init(_ data: Data) { bytes = [UInt8](data) }
/// Reads one tag-length-value triple, advancing past the value. Returns the
/// tag byte and the value bytes, or nil on a malformed/truncated buffer.
mutating func readTLV() -> (tag: UInt8, value: ArraySlice<UInt8>)? {
guard pos < bytes.count else { return nil }
let tag = bytes[pos]
pos += 1
guard pos < bytes.count else { return nil }
var length = Int(bytes[pos])
pos += 1
if length & 0x80 != 0 {
let numLengthBytes = length & 0x7F
guard numLengthBytes > 0, numLengthBytes <= 4, pos + numLengthBytes <= bytes.count else {
return nil
}
length = 0
for _ in 0..<numLengthBytes {
length = (length << 8) | Int(bytes[pos])
pos += 1
}
}
guard pos + length <= bytes.count else { return nil }
let value = bytes[pos..<pos + length]
pos += length
return (tag, value)
}
}
+286
View File
@@ -0,0 +1,286 @@
# iOS Notification Service Extension (NSE) — Setup & Übergabe
> **Für wen:** Eine spätere (kontextlose) Claude-Session **und** den Menschen, der
> auf einem Mac mit Xcode weiterbaut. Dieses Dokument ist die einzige Quelle der
> Wahrheit für die iOS-Push-Fertigstellung. Die Swift-/Dart-Dateien existieren
> bereits (auf Linux textuell erstellt, **nie kompiliert**). Was fehlt, ist die
> reine Xcode-Verdrahtung (Target anlegen, Capabilities, Signing) — das geht
> **nur** auf dem Mac.
---
## 1. Architektur-Kurzfassung
```
Nextcloud (cloud.marianum-fulda.de)
│ push-v2: E2E-verschlüsselte Notification (mit Device-Public-Key verschlüsselt)
MarianumConnect Push-Proxy (connect.marianum-fulda.de/push-proxy/notifications)
│ verifiziert Signatur, leitet per FCM HTTP v1 weiter
FCM → APNs → iOS-Gerät
│ alert-Push mit `mutable-content: 1`, `subject` + `signature` top-level im Payload
NotificationServiceExtension (DIESES Target)
│ liest Device-Private-Key + Server-Public-Key aus dem geteilten Keychain
│ verifiziert Signatur → entschlüsselt subject → setzt Titel/Body/Category
iOS zeigt die fertige Notification
```
- **Verschlüsselung:** OAEP-SHA1 (NC-32-Default) mit Fallback PKCS#1 v1.5.
- **Signatur:** `SHA512withRSA` über die **base64-dekodierten (verschlüsselten)**
subject-Bytes, geprüft mit dem per-User-Server-Public-Key aus der Registrierung.
- **Dart-Referenz:** `lib/push/` — insb. `push_decryptor.dart` (Krypto-Spiegel),
`push_secure_storage.dart` (Keychain-Optionen), `push_keypair.dart` (Keyformat),
`push_registration_store.dart` (was group-scoped gespeichert wird),
`push_registration.dart` (`_persistNativeAuthContext`).
- **Backend-Referenz:** MarianumConnect-Service `marMobileApi`, Endpunkte
`push-proxy/notifications` (NC→Proxy) und `PUT/DELETE me/push-device`.
- **Talk-Actions (Antworten / Als gelesen markieren):** werden **nativ** im
`AppDelegate` per `URLSession` an die Talk-OCS-API geschickt, weil bei einer
Notification-Action nicht garantiert ist, dass die Flutter-Engine läuft.
---
## 2. Dateiinventar
| Datei | Status | Anmerkung |
|-------|--------|-----------|
| `ios/NotificationServiceExtension/NotificationService.swift` | **existiert** | NSE-Hauptklasse (entschlüsseln + Content setzen) |
| `ios/NotificationServiceExtension/PEM.swift` | **existiert** | PEM/DER→`SecKey` (PKCS#8/SPKI-Stripping) |
| `ios/NotificationServiceExtension/Info.plist` | **existiert** | `NSExtensionPointIdentifier = com.apple.usernotifications.service` |
| `ios/NotificationServiceExtension/NotificationServiceExtension.entitlements` | **existiert** | App Group + keychain-access-groups |
| `ios/Runner/Runner.entitlements` | **geändert** | `keychain-access-groups` (widget-Group) ergänzt |
| `ios/Runner/AppDelegate.swift` | **geändert** | TALK_MESSAGE-Category + native Action-Behandlung |
| `lib/push/push_registration_store.dart` | **geändert** | schreibt `nextcloud_username` + `nextcloud_base_url` group-scoped |
| `lib/push/push_registration.dart` | **geändert** | `_persistNativeAuthContext()` bei `register()` |
| **Xcode-Target „NotificationServiceExtension"** | **FEHLT** | muss in Xcode angelegt werden (Abschnitt 3) |
| `ios/Runner.xcodeproj/project.pbxproj` | **unverändert** | bewusst NICHT von Hand editiert — Xcode legt das Target an |
> **Wichtig:** Die vier Dateien unter `ios/NotificationServiceExtension/` liegen
> schon auf der Platte. Beim Anlegen des Targets erzeugt Xcode eigene
> Platzhalter-Dateien — **diese löschen und die vorhandenen Dateien zuordnen**
> (siehe 3.2), sonst überschreibst du die fertige Implementierung.
---
## 3. Xcode-Checkliste (auf dem Mac)
### 3.1 Target anlegen
1. `ios/Runner.xcworkspace` in Xcode öffnen (nicht `.xcodeproj`).
2. **File → New → Target… → iOS → Notification Service Extension**.
3. **Product Name exakt: `NotificationServiceExtension`** (der
`NSExtensionPrincipalClass` in der Info.plist ist
`$(PRODUCT_MODULE_NAME).NotificationService` — bei abweichendem Namen zerbricht
das). Language: Swift. „Embed in Application": Runner.
4. Beim Dialog „Activate scheme?" → Activate.
5. **Bundle Identifier:** `eu.mhsl.marianum.mobile.client.NotificationServiceExtension`
(Muster wie bei den bestehenden Extensions).
6. **Deployment Target = 15.0** (identisch zum Runner; im `post_install` des
Podfiles wird ohnehin alles auf 15.0 gezwungen).
### 3.2 Generierte Dateien durch die vorhandenen ersetzen
- Xcode legt eine eigene `NotificationService.swift` und `Info.plist` im
Target-Ordner an. **Beide aus dem Projekt entfernen** („Move to Trash" für die
frisch generierten) und stattdessen die bereits vorhandenen Dateien hinzufügen:
- `NotificationService.swift`, `PEM.swift`**Target Membership: nur NSE**.
- `Info.plist` → in den **Build Settings** des NSE-Targets als
`INFOPLIST_FILE = NotificationServiceExtension/Info.plist` setzen (bzw. die
vorhandene Datei als Info.plist des Targets referenzieren).
- `NotificationServiceExtension.entitlements` → in Build Settings
`CODE_SIGN_ENTITLEMENTS = NotificationServiceExtension/NotificationServiceExtension.entitlements`.
### 3.3 Capabilities (in **beiden** Targets: Runner **und** NSE)
- **App Groups:** `group.eu.mhsl.marianum.mobile.client.widget` aktivieren.
(Beim Runner ist die Group bereits vorhanden; beim NSE neu hinzufügen.)
- **Keychain Sharing:** Keychain-Group `group.eu.mhsl.marianum.mobile.client.widget`
hinzufügen. Die Entitlement-Dateien enthalten das schon als
`keychain-access-groups`; über die Capabilities-UI stellst du sicher, dass das
Provisioning-Profil es abdeckt.
- Prüfen, dass die Capabilities-UI **keine** doppelten/abweichenden Einträge
erzeugt hat (Xcode schreibt gern in die `.entitlements`; danach Abschnitt 4
gegenchecken).
### 3.4 Signing
- Beide Targets: **Team** = dasselbe wie Runner. Automatic Signing an, oder die
passenden Profile wählen. Der App-Identifier-Prefix (Team-ID) muss identisch
sein, sonst greift die Keychain-Group nicht.
### 3.5 Pods / Podfile
- Die NSE ist **komplett selbst-enthalten** (`Foundation`, `Security`,
`UserNotifications` — alles System-Frameworks). **Kein** Pod-Target nötig.
- Prüfen, ob irgendein Flutter-Plugin explizit ein NSE-Target im Podfile verlangt
(aktuell **nein**`firebase_messaging` braucht keins). Falls doch mal nötig:
im Podfile analog zur `Share Extension` einen `target
'NotificationServiceExtension' do inherit! :search_paths end`-Block ergänzen,
danach `pod install`.
- Nach dem Anlegen einmal `cd ios && pod install` laufen lassen (aktualisiert die
Workspace-Referenzen).
### 3.6 aps-environment (Production!)
- `ios/Runner/Runner.entitlements` hat aktuell `aps-environment = development`.
**Für TestFlight/Release muss `production` aktiv sein.** Das wurde hier bewusst
**nicht** hart umgestellt, um den Debug-Flow nicht zu brechen.
- Empfehlung: über die **Capabilities → Push Notifications** und die
Build-Konfiguration steuern (Xcode setzt beim Archive automatisch `production`,
wenn das Profil ein Distribution-Profil ist). Beim Archive-Export kontrollieren,
dass in der finalen `.ipa` `aps-environment = production` steht (siehe 5.2).
---
## 4. Ermittelte Keychain-Details (verbindlich)
Die Dart-Seite schreibt mit
`IOSOptions(groupId: 'group.eu.mhsl.marianum.mobile.client.widget', accessibility: first_unlock)`.
Aus dem Quellcode von **`flutter_secure_storage_darwin` 0.3.2** (gepinnt in
`pubspec.lock`) ergibt sich die exakte Ablage im Keychain:
| Keychain-Attribut | Wert |
|-------------------|------|
| `kSecClass` | `kSecClassGenericPassword` |
| `kSecAttrAccount` | der Dart-**Key**, **wortwörtlich** (kein Hash, kein Prefix) |
| `kSecAttrService` | **nicht gesetzt** (die `IOSOptions` setzen kein `accountName`) |
| `kSecAttrAccessGroup` | `group.eu.mhsl.marianum.mobile.client.widget` |
| `kSecAttrAccessible` | `kSecAttrAccessibleAfterFirstUnlock` (aus `first_unlock`) |
| Wert (`kSecValueData`) | **rohe UTF-8-Bytes** des Strings (PEM/Passwort im Klartext) |
**Deshalb** fragt die Swift-Seite (`keychainString(...)` in NSE **und**
AppDelegate) exakt so ab: `GenericPassword` + `kSecAttrAccount = <key>` +
`kSecAttrAccessGroup` + `kSecMatchLimit=One` + `kSecReturnData=true`, **ohne**
`kSecAttrService`. (Ein gesetztes `kSecAttrService` würde nicht matchen.)
### Konkrete Einträge (Account-Namen)
| Account (`kSecAttrAccount`) | Inhalt | Geschrieben von | Genutzt von |
|-----------------------------|--------|-----------------|-------------|
| `push_device_private_key_pem` | Device-**Private**-Key, PEM | `push_keypair.dart` | **NSE** (entschlüsseln) |
| `push_device_public_key_pem` | Device-Public-Key, SPKI-PEM | `push_keypair.dart` | — (nur Registrierung) |
| `push_server_public_key_pem` | **Server**-Public-Key (per User), SPKI-PEM | `push_registration_store.dart` | **NSE** (Signatur prüfen) |
| `push_device_identifier` | NC Device-Identifier | `push_registration_store.dart` | Dart |
| `push_registered_fcm_token` | FCM-Token der Registrierung | `push_registration_store.dart` | Dart |
| `nextcloud_app_password` | NC App-Password | `account_data.dart` | **AppDelegate** (Basic-Auth) |
| `nextcloud_username` | NC Username | `push_registration_store.dart` (**neu**) | **AppDelegate** (Basic-Auth) |
| `nextcloud_base_url` | z.B. `https://cloud.marianum-fulda.de` | `push_registration_store.dart` (**neu**) | **AppDelegate** (OCS-URL) |
> `username` und `password` (Realpasswort) liegen in AccountDatas **Default**-Storage
> **ohne** `groupId` → nur im primären Access-Group der App, **für die NSE
> unsichtbar**. Deshalb werden `nextcloud_username` + `nextcloud_base_url` bei
> jeder `register()`-Runde zusätzlich **group-scoped** geschrieben. Das
> App-Password lag schon immer group-scoped.
### Schlüsselformate (kritisch für das PEM-Parsing)
`crypton` (2.2.1) erzeugt die Keys:
- **Private Key:** Label `-----BEGIN RSA PRIVATE KEY-----`, der DER-Body ist aber
**PKCS#8** `PrivateKeyInfo` (SEQ{ INTEGER version, SEQ algId, OCTET STRING
pkcs1 }) — **nicht** rohes PKCS#1! `SecKeyCreateWithData` will bei RSA aber
**rohes PKCS#1**. `PEM.pkcs1PrivateKey(fromPkcs8:)` strippt den PKCS#8-Wrapper
(extrahiert den OCTET-STRING-Inhalt).
- **Public Key** (Device **und** der von NC gelieferte Server-Key): Label
`-----BEGIN PUBLIC KEY-----`, DER = **SPKI** `SubjectPublicKeyInfo`.
`PEM.pkcs1PublicKey(fromSpki:)` extrahiert den BIT-STRING-Inhalt und wirft das
führende `0x00`-Byte (unused bits) weg → PKCS#1 `RSAPublicKey`.
---
## 5. Verifikation auf dem Mac
### 5.1 Build
- `flutter clean && flutter pub get`
- `cd ios && pod install`
- In Xcode: **beide** Targets bauen. NSE-Target-Scheme separat bauen, um Swift-
Fehler früh zu sehen.
- Auf echtem Gerät installieren (Push funktioniert **nicht** im Simulator).
### 5.2 aps-environment im Archive prüfen
- Product → Archive → Distribute (oder `.ipa` exportieren) → `.ipa` entpacken →
`codesign -d --entitlements :- Payload/Runner.app` → muss
`aps-environment = production` zeigen.
### 5.3 Testbenachrichtigung (schneller Smoke-Test, ohne NC-Krypto)
- In den App-Einstellungen → Benachrichtigungen → **Testbenachrichtigung**
(Backend `POST me/push-device/test`). Erwartung: Alert „Testbenachrichtigung /
Push-Benachrichtigungen funktionieren! 🎉". Testet Registrierung + FCM +
Rendering — die NSE ist hier **nicht** beteiligt (Connect-Push, kein
`mutable-content`).
### 5.4 Echte Talk-Nachricht (End-to-End inkl. NSE)
- Gerät sperren. Von einem Zweitaccount eine Talk-Nachricht schicken.
- **Erwartung:** Banner mit **echtem Absender + Nachrichtentext** (nicht der
Platzhalter „Neue Benachrichtigung / Tippen zum Öffnen"). Long-press →
Actions „Antworten" + „Als gelesen markieren".
- „Antworten" → Text senden → Nachricht erscheint im Chat (nativ via URLSession),
Chat wird als gelesen markiert.
- Alternativ auf der NC-Instanz: `occ notification:test-push --talk <user> -v`.
### 5.5 Woran erkenne ich, ob die NSE lief?
- **NSE lief & OK:** echter Absender/Text im Banner.
- **NSE lief nicht / Fehler:** Platzhalter-Text bleibt stehen. Das ist der
bewusste Fallback (nie leer, nie Crash).
- **Logs:** Über die macOS-**Console.app** das Gerät wählen und nach `[NSE]` bzw.
`[Talk action]` filtern (`NSLog`-Ausgaben). Oder in Xcode: Debug → Attach to
Process → `NotificationServiceExtension`.
---
## 6. Bekannte Fehlerbilder + Fixes
| Symptom | Wahrscheinliche Ursache | Fix |
|---------|-------------------------|-----|
| NSE greift gar nicht (immer Platzhalter, auch bei Talk) | `mutable-content` fehlt / Signing des NSE-Targets kaputt / Bundle-ID falsch | Backend-`FcmMessageFactory` setzt `mutable-content:1` (prüfen). NSE-Target-Signing + Provisioning prüfen. Bundle-ID = `…client.NotificationServiceExtension`. |
| Banner zeigt Platzhalter trotz Talk | PEM-Parsing/Padding schlägt fehl | Console.app nach `[NSE] SecKeyCreateWithData failed` / `could not decrypt`. Prüfen, dass Private-Key wirklich als PKCS#8-in-„RSA PRIVATE KEY" ankommt (crypton). OAEP↔PKCS1-Fallback ist schon drin. |
| `[NSE] no device private key in keychain` | Keychain-Group greift nicht | In **beiden** Targets Keychain Sharing + App Group aktiv? Team-ID identisch? Access-Group-String exakt `group.eu.mhsl.marianum.mobile.client.widget`? |
| Signatur schlägt immer fehl | Falscher/kein Server-Public-Key | `push_server_public_key_pem` vorhanden? (wird bei `register()` gesetzt). Notfalls: ohne Server-Key überspringt die NSE die Prüfung — bleibt sie hängen, ist der Key da aber falsch geparst. |
| Reply-Action tut nichts | Credentials fehlen / Delegate-Konflikt (s.u.) | Console.app nach `[Talk action]`. `nextcloud_username`/`_app_password`/`_base_url` im Keychain? App einmal neu einloggen (schreibt sie via `register()`). |
| Reply wird **doppelt** gesendet | AppDelegate **und** ein Plugin behandeln dieselbe Action | Der AppDelegate returnt bei TALK_REPLY/TALK_MARK_READ **ohne** `super`-Aufruf, das Plugin sieht die Action also nicht. Falls doch doppelt: prüfen, ob `flutter_local_notifications` auf iOS separat als Delegate registriert ist. |
### ⚠️ Delegate-Ownership (wichtigster Verifikationspunkt)
`FlutterAppDelegate` konformiert (über `FlutterAppLifeCycleProvider`) zu
`UNUserNotificationCenterDelegate` und **leitet** `userNotificationCenter(...)` an
die registrierten Plugins weiter (firebase_messaging,
flutter_local_notifications). Der `AppDelegate` setzt sich in
`didFinishLaunching` als `UNUserNotificationCenter.current().delegate = self`,
**überschreibt** die beiden Delegate-Methoden und ruft für alles **außer** den
zwei Talk-Actions `super` auf — dadurch bleibt das Plugin-Forwarding intakt. Das
ist der fragilste Teil und **muss auf dem Gerät verifiziert werden**:
- Normale Push-Taps müssen weiterhin ins richtige Chat/Ziel navigieren
(läuft über `super` → Plugins).
- Foreground-Darstellung von FCM darf nicht kaputtgehen (`willPresent` wird hier
bewusst **nicht** überschrieben, läuft komplett über `super`/Plugins).
- Sollte ein Plugin den Delegate **nach** `didFinishLaunching` erneut übernehmen
(und damit unsere Talk-Actions abfangen), das Setzen von
`UNUserNotificationCenter.current().delegate = self` an einen späteren Punkt
verschieben (z. B. nach dem ersten Dart-Frame). Ohne Gerät nicht abschließend
testbar.
> **Compile-Hinweis:** `override` an den beiden `userNotificationCenter`-Methoden
> ist korrekt, weil die Methoden über das von `FlutterAppDelegate` adoptierte
> Protokoll `FlutterAppLifeCycleProvider : UNUserNotificationCenterDelegate`
> sichtbar sind (verifiziert im Flutter-3.44-Engine-Header). `super.userNotification…`
> ist daher aufrufbar.
---
## 7. Offene TODOs / bewusste Einschränkungen
1. **Delete-Handling auf iOS (best effort).** Delete-Pushes kommen als silent
`background`-Pushes (`content-available`, **kein** `mutable-content`) → die NSE
läuft dafür **nicht**. Das Wegräumen erledigt der Flutter-Background-Handler
(`PushMessageHandler`) bzw. ein Sweep beim App-Öffnen. Erreicht doch mal ein
Delete-Subject die NSE, **kann** sie die Notification **nicht** unterdrücken
(eine NSE muss immer *irgendeinen* Content liefern) — sie lässt den Platzhalter
stehen und loggt. Kein Fix möglich, dokumentierte iOS-Limitierung.
2. **Rich-Detail via OCS (spätere Erweiterung).** v1 lädt **nichts** nach. Optional
könnte die NSE bei Talk `GET …/ocs/v2.php/apps/notifications/api/v2/notifications/{nid}`
mit dem App-Password abrufen, um Avatar/Rich-Subject anzuzeigen (Timeout < 25 s,
innerhalb des NSE-Budgets). Nicht implementiert.
3. **`aps-environment = production`** ist noch nicht hart gesetzt (Abschnitt 3.6) —
vor dem Release erledigen und im Archive gegenchecken (5.2).
4. **Keychain-Access-Group-Schreibweise.** Die Entitlements listen die App-Group
ohne `$(AppIdentifierPrefix)` als `keychain-access-groups`. Das ist das von
`flutter_secure_storage` erwartete Verhalten (Access-Group == App-Group-ID).
Sollte der Keychain-Zugriff wider Erwarten scheitern (Status `-34018` /
`errSecMissingEntitlement`), in **beiden** Targets die Keychain-Sharing-
Capability über die Xcode-UI neu setzen und Provisioning-Profile erneuern.
+3
View File
@@ -46,5 +46,8 @@ end
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target) flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
end
end end
end end
+2 -249
View File
@@ -1,311 +1,64 @@
PODS: PODS:
- connectivity_plus (0.0.1):
- Flutter
- device_info_plus (0.0.1):
- Flutter
- DKImagePickerController/Core (4.3.9):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.9)
- DKImagePickerController/PhotoGallery (4.3.9):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.9)
- DKPhotoGallery (0.0.19):
- DKPhotoGallery/Core (= 0.0.19)
- DKPhotoGallery/Model (= 0.0.19)
- DKPhotoGallery/Preview (= 0.0.19)
- DKPhotoGallery/Resource (= 0.0.19)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.19):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.19):
- SDWebImage
- SwiftyGif
- emoji_picker_flutter (0.0.1):
- Flutter
- eraser (0.0.1): - eraser (0.0.1):
- Flutter - Flutter
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase/CoreOnly (12.12.0):
- FirebaseCore (~> 12.12.0)
- Firebase/Messaging (12.12.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.12.0)
- firebase_core (4.7.0):
- Firebase/CoreOnly (= 12.12.0)
- Flutter
- firebase_messaging (16.2.0):
- Firebase/Messaging (= 12.12.0)
- firebase_core
- Flutter
- FirebaseCore (12.12.1):
- FirebaseCoreInternal (~> 12.12.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreInternal (12.12.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseInstallations (12.12.0):
- FirebaseCore (~> 12.12.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (12.12.0):
- FirebaseCore (~> 12.12.0)
- FirebaseInstallations (~> 12.12.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_app_badge (2.0.0): - flutter_app_badge (2.0.0):
- Flutter - Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_native_splash (2.4.3):
- Flutter
- flutter_secure_storage_darwin (10.0.0):
- Flutter
- FlutterMacOS
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- home_widget (0.0.1): - home_widget (0.0.1):
- Flutter - Flutter
- image_picker_ios (0.0.1):
- Flutter
- in_app_review (2.0.0):
- Flutter
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- open_filex (0.0.2): - open_filex (0.0.2):
- Flutter - Flutter
- package_info_plus (0.4.5):
- Flutter
- PhoneNumberKit (3.7.11): - PhoneNumberKit (3.7.11):
- PhoneNumberKit/PhoneNumberKitCore (= 3.7.11) - PhoneNumberKit/PhoneNumberKitCore (= 3.7.11)
- PhoneNumberKit/UIKit (= 3.7.11) - PhoneNumberKit/UIKit (= 3.7.11)
- PhoneNumberKit/PhoneNumberKitCore (3.7.11) - PhoneNumberKit/PhoneNumberKitCore (3.7.11)
- PhoneNumberKit/UIKit (3.7.11): - PhoneNumberKit/UIKit (3.7.11):
- PhoneNumberKit/PhoneNumberKitCore - PhoneNumberKit/PhoneNumberKitCore
- PromisesObjC (2.4.0)
- receive_sharing_intent (1.8.1): - receive_sharing_intent (1.8.1):
- Flutter - Flutter
- screen_brightness_ios (2.1.3):
- Flutter
- SDWebImage (5.21.7):
- SDWebImage/Core (= 5.21.7)
- SDWebImage/Core (5.21.7)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- SwiftyGif (5.4.5)
- syncfusion_flutter_pdfviewer (0.0.1):
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
- wakelock_plus (0.0.1):
- Flutter
- workmanager_apple (0.0.1): - workmanager_apple (0.0.1):
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`)
- eraser (from `.symlinks/plugins/eraser/ios`) - eraser (from `.symlinks/plugins/eraser/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_app_badge (from `.symlinks/plugins/flutter_app_badge/ios`) - flutter_app_badge (from `.symlinks/plugins/flutter_app_badge/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
- home_widget (from `.symlinks/plugins/home_widget/ios`) - home_widget (from `.symlinks/plugins/home_widget/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
- open_filex (from `.symlinks/plugins/open_filex/ios`) - open_filex (from `.symlinks/plugins/open_filex/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- PhoneNumberKit (~> 3.7.6) - PhoneNumberKit (~> 3.7.6)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`) - workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- DKImagePickerController
- DKPhotoGallery
- Firebase
- FirebaseCore
- FirebaseCoreInternal
- FirebaseInstallations
- FirebaseMessaging
- GoogleDataTransport
- GoogleUtilities
- nanopb
- PhoneNumberKit - PhoneNumberKit
- PromisesObjC
- SDWebImage
- SwiftyGif
EXTERNAL SOURCES: EXTERNAL SOURCES:
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
emoji_picker_flutter:
:path: ".symlinks/plugins/emoji_picker_flutter/ios"
eraser: eraser:
:path: ".symlinks/plugins/eraser/ios" :path: ".symlinks/plugins/eraser/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_messaging:
:path: ".symlinks/plugins/firebase_messaging/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_app_badge: flutter_app_badge:
:path: ".symlinks/plugins/flutter_app_badge/ios" :path: ".symlinks/plugins/flutter_app_badge/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage_darwin:
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
home_widget: home_widget:
:path: ".symlinks/plugins/home_widget/ios" :path: ".symlinks/plugins/home_widget/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
in_app_review:
:path: ".symlinks/plugins/in_app_review/ios"
open_filex: open_filex:
:path: ".symlinks/plugins/open_filex/ios" :path: ".symlinks/plugins/open_filex/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
receive_sharing_intent: receive_sharing_intent:
:path: ".symlinks/plugins/receive_sharing_intent/ios" :path: ".symlinks/plugins/receive_sharing_intent/ios"
screen_brightness_ios:
:path: ".symlinks/plugins/screen_brightness_ios/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
syncfusion_flutter_pdfviewer:
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
workmanager_apple: workmanager_apple:
:path: ".symlinks/plugins/workmanager_apple/ios" :path: ".symlinks/plugins/workmanager_apple/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc
eraser: 83a4b06985f3702aa3d8dec816f9693266012937 eraser: 83a4b06985f3702aa3d8dec816f9693266012937
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: aa154fee4e9b8eac17aa42344988865b3e857d33
firebase_core: 9156a152117c843440b0b990c785aa0259bc5447
firebase_messaging: 0d962ab44ff24ed36deb8fa2ee043c4671858269
FirebaseCore: 86241206e656f5c80c995e370e6c975913b9b284
FirebaseCoreInternal: 7c12fc3011d889085e765e317d7b9fd1cef97af9
FirebaseInstallations: 4e6e162aa4abaaeeeb01dd00179dfc5ad9c2194e
FirebaseMessaging: 341004946fa7ffc741344b20f1b667514fc93e31
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_app_badge: ca742dd659a157c1090ef7cd881cb78f48f3bcdf flutter_app_badge: ca742dd659a157c1090ef7cd881cb78f48f3bcdf
flutter_local_notifications: 643a3eda1ce1c0599413ca31672536d423dee214
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 PhoneNumberKit: 9ff0c5ae9fe4770193b68a3d3e6c938fe976788c
PhoneNumberKit: ced55861269312a5e3bc2ef82a58d6255b1c976a
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
screen_brightness_ios: 212d950bb99c915eee971c884f4a6c87c92cd13d
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
video_player_avfoundation: 3453f792138786248960ca029747fcd9f318ef52
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778 workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
PODFILE CHECKSUM: 424a9b4c0fe81d8ebeaa9cb0dfedb60a68b19a0d PODFILE CHECKSUM: 087d168982f24fb137e2d46f893b771b4b9955c6
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2
+28 -24
View File
@@ -19,6 +19,7 @@
AA0101070000000011111111 /* TimetableWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = AA0101020000000011111111 /* TimetableWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; AA0101070000000011111111 /* TimetableWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = AA0101020000000011111111 /* TimetableWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
AA0102010000000022222222 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0102020000000022222222 /* SceneDelegate.swift */; }; AA0102010000000022222222 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0102020000000022222222 /* SceneDelegate.swift */; };
B8263932DB64B022CCEE7A53 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90960A132A5F91779B3FBE28 /* Pods_Runner.framework */; }; B8263932DB64B022CCEE7A53 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90960A132A5F91779B3FBE28 /* Pods_Runner.framework */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -96,6 +97,7 @@
C7E1879BE78835C7E3256316 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; C7E1879BE78835C7E3256316 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
DD904D7C0FC0AD11449CEB80 /* Pods-Share Extension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.debug.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.debug.xcconfig"; sourceTree = "<group>"; }; DD904D7C0FC0AD11449CEB80 /* Pods-Share Extension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.debug.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.debug.xcconfig"; sourceTree = "<group>"; };
EF5279D9BF8FCBB117AF998E /* Pods-Share Extension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.profile.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.profile.xcconfig"; sourceTree = "<group>"; }; EF5279D9BF8FCBB117AF998E /* Pods-Share Extension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.profile.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.profile.xcconfig"; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -155,6 +157,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
B8263932DB64B022CCEE7A53 /* Pods_Runner.framework in Frameworks */, B8263932DB64B022CCEE7A53 /* Pods_Runner.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -194,6 +197,7 @@
9740EEB11CF90186004384FC /* Flutter */ = { 9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
@@ -274,6 +278,9 @@
productType = "com.apple.product-type.app-extension"; productType = "com.apple.product-type.app-extension";
}; };
97C146ED1CF9000F007C117D /* Runner */ = { 97C146ED1CF9000F007C117D /* Runner */ = {
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
@@ -286,7 +293,6 @@
3321F8102FB1C00C0011C712 /* Embed Foundation Extensions */, 3321F8102FB1C00C0011C712 /* Embed Foundation Extensions */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
174B54D80220E5F588BD9737 /* [CP] Embed Pods Frameworks */, 174B54D80220E5F588BD9737 /* [CP] Embed Pods Frameworks */,
859FAB4E05FAC31B7B1A62D7 /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@@ -323,6 +329,9 @@
/* Begin PBXProject section */ /* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = { 97C146E61CF9000F007C117D /* Project object */ = {
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
BuildIndependentTargetsInParallel = YES; BuildIndependentTargetsInParallel = YES;
@@ -425,23 +434,6 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
}; };
859FAB4E05FAC31B7B1A62D7 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;
@@ -647,7 +639,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = BB0001010000000011111111 /* ShareExtension-Debug.xcconfig */; baseConfigurationReference = BB0001010000000011111111 /* ShareExtension-Debug.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
@@ -691,7 +683,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = BB0001020000000011111111 /* ShareExtension-Release.xcconfig */; baseConfigurationReference = BB0001020000000011111111 /* ShareExtension-Release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
@@ -732,7 +724,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = BB0001030000000011111111 /* ShareExtension-Profile.xcconfig */; baseConfigurationReference = BB0001030000000011111111 /* ShareExtension-Profile.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES; CLANG_ENABLE_OBJC_WEAK = YES;
@@ -941,7 +933,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = BB0001040000000011111111 /* TimetableWidget-Debug.xcconfig */; baseConfigurationReference = BB0001040000000011111111 /* TimetableWidget-Debug.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
@@ -983,7 +975,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = BB0001050000000011111111 /* TimetableWidget-Release.xcconfig */; baseConfigurationReference = BB0001050000000011111111 /* TimetableWidget-Release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
@@ -1023,7 +1015,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = BB0001060000000011111111 /* TimetableWidget-Profile.xcconfig */; baseConfigurationReference = BB0001060000000011111111 /* TimetableWidget-Profile.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
@@ -1103,6 +1095,18 @@
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
}; };
rootObject = 97C146E61CF9000F007C117D /* Project object */; rootObject = 97C146E61CF9000F007C117D /* Project object */;
} }
@@ -0,0 +1,176 @@
{
"pins" : [
{
"identity" : "abseil-cpp-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/abseil-cpp-binary.git",
"state" : {
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
"version" : "1.2024072200.0"
}
},
{
"identity" : "app-check",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/app-check.git",
"state" : {
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
"version" : "11.2.0"
}
},
{
"identity" : "dkcamera",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKCamera",
"state" : {
"branch" : "master",
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
}
},
{
"identity" : "dkimagepickercontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKImagePickerController",
"state" : {
"branch" : "4.3.9",
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
}
},
{
"identity" : "dkphotogallery",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
"state" : {
"branch" : "master",
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
}
},
{
"identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk",
"state" : {
"revision" : "d10045cace0b4c335c4efa8f7df7e9a9fc5a7c60",
"version" : "12.13.0"
}
},
{
"identity" : "google-ads-on-device-conversion-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
"state" : {
"revision" : "19dffda9a9caf8d86570ff846535902d8509d7bf",
"version" : "3.5.0"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "c2c76bebcfbb90d90ea10599f934f9af160e1604",
"version" : "12.13.0"
}
},
{
"identity" : "googledatatransport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleDataTransport.git",
"state" : {
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
"version" : "10.1.0"
}
},
{
"identity" : "googleutilities",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "60da361632d0de02786f709bdc0c4df340f7613e",
"version" : "8.1.0"
}
},
{
"identity" : "grpc-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/grpc-binary.git",
"state" : {
"revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
"version" : "1.69.1"
}
},
{
"identity" : "gtm-session-fetcher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/gtm-session-fetcher.git",
"state" : {
"revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a",
"version" : "5.3.0"
}
},
{
"identity" : "interop-ios-for-google-sdks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
"state" : {
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
"version" : "101.0.0"
}
},
{
"identity" : "leveldb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/leveldb.git",
"state" : {
"revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b",
"version" : "1.22.2"
}
},
{
"identity" : "nanopb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/nanopb.git",
"state" : {
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
"version" : "2.30910.0"
}
},
{
"identity" : "promises",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/promises.git",
"state" : {
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
"version" : "2.4.0"
}
},
{
"identity" : "sdwebimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
"version" : "5.21.7"
}
},
{
"identity" : "swiftygif",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kirualex/SwiftyGif.git",
"state" : {
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
"version" : "5.4.5"
}
},
{
"identity" : "tocropviewcontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TimOliver/TOCropViewController",
"state" : {
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
"version" : "2.8.0"
}
}
],
"version" : 2
}
@@ -5,6 +5,24 @@
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -5,8 +5,44 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/google/abseil-cpp-binary.git", "location" : "https://github.com/google/abseil-cpp-binary.git",
"state" : { "state" : {
"revision" : "bfc0b6f81adc06ce5121eb23f628473638d67c5c", "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
"version" : "1.2022062300.0" "version" : "1.2024072200.0"
}
},
{
"identity" : "app-check",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/app-check.git",
"state" : {
"revision" : "bb4002485ff867768dec13bf904a2ddb050bd1b1",
"version" : "11.3.0"
}
},
{
"identity" : "dkcamera",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKCamera",
"state" : {
"branch" : "master",
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
}
},
{
"identity" : "dkimagepickercontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKImagePickerController",
"state" : {
"branch" : "4.3.9",
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
}
},
{
"identity" : "dkphotogallery",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
"state" : {
"branch" : "master",
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
} }
}, },
{ {
@@ -14,8 +50,17 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk", "location" : "https://github.com/firebase/firebase-ios-sdk",
"state" : { "state" : {
"revision" : "df2171b0c6afb9e9d4f7e07669d558c510b9f6be", "revision" : "42e81d245e30e49ea6a5830cf2842d44a1591270",
"version" : "10.13.0" "version" : "12.15.0"
}
},
{
"identity" : "google-ads-on-device-conversion-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
"state" : {
"revision" : "9bfcc6cf435b2e7c5562c1900b8680c594fa9a64",
"version" : "3.6.0"
} }
}, },
{ {
@@ -23,8 +68,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git", "location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : { "state" : {
"revision" : "03b9beee1a61f62d32c521e172e192a1663a5e8b", "revision" : "144855f40d8668927f256a3045f7fdc4c3f4338b",
"version" : "10.13.0" "version" : "12.15.0"
} }
}, },
{ {
@@ -32,8 +77,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleDataTransport.git", "location" : "https://github.com/google/GoogleDataTransport.git",
"state" : { "state" : {
"revision" : "aae45a320fd0d11811820335b1eabc8753902a40", "revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
"version" : "9.2.5" "version" : "10.1.0"
} }
}, },
{ {
@@ -41,8 +86,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git", "location" : "https://github.com/google/GoogleUtilities.git",
"state" : { "state" : {
"revision" : "c38ce365d77b04a9a300c31061c5227589e5597b", "revision" : "c46e5f8b7c23265f17c24ca7f9fa1b13ded7a822",
"version" : "7.11.5" "version" : "8.1.1"
} }
}, },
{ {
@@ -50,8 +95,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/google/grpc-binary.git", "location" : "https://github.com/google/grpc-binary.git",
"state" : { "state" : {
"revision" : "f1b366129d1125be7db83247e003fc333104b569", "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
"version" : "1.50.2" "version" : "1.69.1"
} }
}, },
{ {
@@ -59,8 +104,17 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/google/gtm-session-fetcher.git", "location" : "https://github.com/google/gtm-session-fetcher.git",
"state" : { "state" : {
"revision" : "d415594121c9e8a4f9d79cecee0965cf35e74dbd", "revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a",
"version" : "3.1.1" "version" : "5.3.0"
}
},
{
"identity" : "interop-ios-for-google-sdks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
"state" : {
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
"version" : "101.0.0"
} }
}, },
{ {
@@ -68,8 +122,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/leveldb.git", "location" : "https://github.com/firebase/leveldb.git",
"state" : { "state" : {
"revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b", "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
"version" : "1.22.2" "version" : "1.22.5"
} }
}, },
{ {
@@ -77,8 +131,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/nanopb.git", "location" : "https://github.com/firebase/nanopb.git",
"state" : { "state" : {
"revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692", "revision" : "3851d94a41890dea16dc3db34caf60e585cb4163",
"version" : "2.30909.0" "version" : "2.30910.1"
} }
}, },
{ {
@@ -86,17 +140,35 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/google/promises.git", "location" : "https://github.com/google/promises.git",
"state" : { "state" : {
"revision" : "e70e889c0196c76d22759eb50d6a0270ca9f1d9e", "revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837",
"version" : "2.3.1" "version" : "2.4.1"
} }
}, },
{ {
"identity" : "swift-protobuf", "identity" : "sdwebimage",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git", "location" : "https://github.com/SDWebImage/SDWebImage",
"state" : { "state" : {
"revision" : "ce20dc083ee485524b802669890291c0d8090170", "revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
"version" : "1.22.1" "version" : "5.21.7"
}
},
{
"identity" : "swiftygif",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kirualex/SwiftyGif.git",
"state" : {
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
"version" : "5.4.5"
}
},
{
"identity" : "tocropviewcontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TimOliver/TOCropViewController",
"state" : {
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
"version" : "2.8.0"
} }
} }
], ],
+188 -1
View File
@@ -1,16 +1,203 @@
import Flutter import Flutter
import UIKit import UIKit
import UserNotifications
@main @main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
// Kept in sync with lib/push/push_actions.dart (kTalkReplyActionId /
// kTalkMarkReadActionId) and lib/push/push_renderer.dart (iosTalkCategory).
private let talkCategoryId = "TALK_MESSAGE"
private let replyActionId = "TALK_REPLY"
private let markReadActionId = "TALK_MARK_READ"
// Shared (App Group) keychain same group as the NSE and the Dart side.
private let keychainAccessGroup = "group.eu.mhsl.marianum.mobile.client.widget"
private let usernameAccount = "nextcloud_username"
private let appPasswordAccount = "nextcloud_app_password"
private let baseUrlAccount = "nextcloud_base_url"
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions) let result = super.application(application, didFinishLaunchingWithOptions: launchOptions)
registerTalkCategory()
// FlutterAppDelegate conforms to UNUserNotificationCenterDelegate and
// forwards these callbacks to the plugins (firebase_messaging,
// flutter_local_notifications). We route Talk actions natively here the
// Flutter engine is not guaranteed to run for a background action and
// forward everything else to `super` so plugin behaviour is preserved.
UNUserNotificationCenter.current().delegate = self
return result
} }
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
} }
private func registerTalkCategory() {
let reply = UNTextInputNotificationAction(
identifier: replyActionId,
title: "Antworten",
options: [],
textInputButtonTitle: "Senden",
textInputPlaceholder: "Nachricht"
)
let markRead = UNNotificationAction(
identifier: markReadActionId,
title: "Als gelesen markieren",
options: []
)
let category = UNNotificationCategory(
identifier: talkCategoryId,
actions: [reply, markRead],
intentIdentifiers: [],
options: []
)
UNUserNotificationCenter.current().setNotificationCategories([category])
}
// MARK: - UNUserNotificationCenterDelegate
override func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let actionId = response.actionIdentifier
if actionId == replyActionId || actionId == markReadActionId {
// Handled natively; deliberately NOT forwarded to super so the plugins
// don't also process it (which would double-send the reply).
handleTalkAction(response, completion: completionHandler)
return
}
super.userNotificationCenter(
center, didReceive: response, withCompletionHandler: completionHandler)
}
// MARK: - Native Talk action handling
private func handleTalkAction(
_ response: UNNotificationResponse,
completion: @escaping () -> Void
) {
guard
let token = chatToken(from: response),
let credentials = loadCredentials()
else {
completion()
return
}
let group = DispatchGroup()
if response.actionIdentifier == replyActionId,
let text = (response as? UNTextInputNotificationResponse)?.userText
.trimmingCharacters(in: .whitespacesAndNewlines),
!text.isEmpty {
group.enter()
ocsPost(
credentials: credentials,
path: "apps/spreed/api/v1/chat/\(token)",
form: ["message": text]
) { group.leave() }
}
// Both reply and mark-read clear the unread marker on the chat.
group.enter()
ocsPost(
credentials: credentials,
path: "apps/spreed/api/v1/chat/\(token)/read",
form: nil
) { group.leave() }
group.notify(queue: .main) { completion() }
}
private func chatToken(from response: UNNotificationResponse) -> String? {
let userInfo = response.notification.request.content.userInfo
if let token = userInfo["chatToken"] as? String, !token.isEmpty {
return token
}
let thread = response.notification.request.content.threadIdentifier
if thread.hasPrefix("talk_") {
let token = String(thread.dropFirst("talk_".count))
return token.isEmpty ? nil : token
}
return nil
}
private struct Credentials {
let baseUrl: String
let authorization: String
}
private func loadCredentials() -> Credentials? {
guard
let username = keychainString(usernameAccount),
let appPassword = keychainString(appPasswordAccount),
let baseUrl = keychainString(baseUrlAccount),
let token = "\(username):\(appPassword)".data(using: .utf8)
else { return nil }
let authorization = "Basic \(token.base64EncodedString())"
let trimmed = baseUrl.hasSuffix("/") ? String(baseUrl.dropLast()) : baseUrl
return Credentials(baseUrl: trimmed, authorization: authorization)
}
private func ocsPost(
credentials: Credentials,
path: String,
form: [String: String]?,
completion: @escaping () -> Void
) {
guard let url = URL(string: "\(credentials.baseUrl)/ocs/v2.php/\(path)") else {
completion()
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue(credentials.authorization, forHTTPHeaderField: "Authorization")
request.setValue("true", forHTTPHeaderField: "OCS-APIRequest")
request.setValue("application/json", forHTTPHeaderField: "Accept")
if let form = form {
request.setValue(
"application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpBody = form
.map { "\($0.key)=\(formEncode($0.value))" }
.joined(separator: "&")
.data(using: .utf8)
}
URLSession.shared.dataTask(with: request) { _, response, error in
if let error = error {
NSLog("[Talk action] \(path) failed: \(error.localizedDescription)")
} else if let http = response as? HTTPURLResponse,
!(200..<300).contains(http.statusCode) {
NSLog("[Talk action] \(path) -> HTTP \(http.statusCode)")
}
completion()
}.resume()
}
private func formEncode(_ value: String) -> String {
var allowed = CharacterSet.alphanumerics
allowed.insert(charactersIn: "-._~")
return value.addingPercentEncoding(withAllowedCharacters: allowed) ?? value
}
// MARK: - Keychain (shared App Group)
private func keychainString(_ account: String) -> String? {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: account,
kSecAttrAccessGroup: keychainAccessGroup,
kSecReturnData: true,
kSecMatchLimit: kSecMatchLimitOne,
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess, let data = result as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
} }
@@ -1,122 +1 @@
{ {"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 B

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 B

After

Width:  |  Height:  |  Size: 69 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 12 KiB

@@ -38,7 +38,7 @@
</scene> </scene>
</scenes> </scenes>
<resources> <resources>
<image name="LaunchImage" width="800" height="1131"/> <image name="LaunchImage" width="512" height="512"/>
<image name="LaunchBackground" width="1" height="1"/> <image name="LaunchBackground" width="1" height="1"/>
</resources> </resources>
</document> </document>
+82 -82
View File
@@ -1,90 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict>
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Marianum Fulda</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>client</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Um Fotos direkt aus der App aufnehmen und teilen zu können wird Zugriff auf die Kamera benötigt.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Um Medien mit anderen zu teilen wird Zugriff zu deine Dateien benötigt.</string>
<key>UIApplicationSceneManifest</key>
<dict> <dict>
<key>UIApplicationSupportsMultipleScenes</key> <key>AppGroupId</key>
<false/> <string>$(CUSTOM_GROUP_ID)</string>
<key>UISceneConfigurations</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Marianum Fulda</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>client</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Um Fotos direkt aus der App aufnehmen und teilen zu können wird Zugriff auf die Kamera benötigt.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Um Medien mit anderen zu teilen wird Zugriff zu deine Dateien benötigt.</string>
<key>UIApplicationSceneManifest</key>
<dict> <dict>
<key>UIWindowSceneSessionRoleApplication</key> <key>UIApplicationSupportsMultipleScenes</key>
<array> <false/>
<dict> <key>UISceneConfigurations</key>
<key>UISceneClassName</key> <dict>
<string>UIWindowScene</string> <key>UIWindowSceneSessionRoleApplication</key>
<key>UISceneConfigurationName</key> <array>
<string>flutter</string> <dict>
<key>UISceneDelegateClassName</key> <key>UISceneClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string> <string>UIWindowScene</string>
<key>UISceneStoryboardFile</key> <key>UISceneConfigurationName</key>
<string>Main</string> <string>flutter</string>
</dict> <key>UISceneDelegateClassName</key>
</array> <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict> </dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict> </dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist> </plist>
+4
View File
@@ -9,5 +9,9 @@
<string>group.eu.mhsl.marianum.mobile.client.widget</string> <string>group.eu.mhsl.marianum.mobile.client.widget</string>
<string>group.eu.mhsl.marianum.mobile.client.share</string> <string>group.eu.mhsl.marianum.mobile.client.share</string>
</array> </array>
<key>keychain-access-groups</key>
<array>
<string>group.eu.mhsl.marianum.mobile.client.widget</string>
</array>
</dict> </dict>
</plist> </plist>

Some files were not shown because too many files have changed in this diff Show More