diff --git a/.gitignore b/.gitignore index ab7ac1c..b266df0 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,8 @@ GoogleService-Info.plist **/fastlane/.env **/fastlane/.env.* fastlane/README.md +ios/.bundle/ +ios/vendor/ # Play Store release assets & scripts play-store/ diff --git a/integration_test/app_screenshots_test.dart b/integration_test/app_screenshots_test.dart new file mode 100644 index 0000000..aa6aba0 --- /dev/null +++ b/integration_test/app_screenshots_test.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:meshcore_sar_app/l10n/app_localizations.dart'; +import 'package:meshcore_sar_app/main.dart'; +import 'package:meshcore_sar_app/providers/contacts_provider.dart'; +import 'package:meshcore_sar_app/providers/messages_provider.dart'; +import 'package:meshcore_sar_app/screens/home_screen.dart'; +import 'package:meshcore_sar_app/services/wizard_preferences.dart'; +import 'package:meshcore_sar_app/utils/sample_data_generator.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + const screenshotPrefix = String.fromEnvironment('SCREENSHOT_PREFIX'); + + testWidgets('captures App Store screenshots', (tester) async { + SharedPreferences.setMockInitialValues({ + 'wizard_completed': true, + 'wizard_version': 1, + }); + await WizardPreferences.setWizardCompleted(true); + + await tester.pumpWidget(const MeshCoreSarApp()); + await _pumpUntilFound(tester, find.byType(HomeScreen)); + await binding.convertFlutterSurfaceToImage(); + + final homeContext = tester.element(find.byType(HomeScreen)); + _loadSampleData(homeContext); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + await _takeScreenshot(binding, tester, '${screenshotPrefix}01-messages'); + + await _tapTab(tester, 'Contacts'); + await _takeScreenshot(binding, tester, '${screenshotPrefix}02-contacts'); + + await _tapTab(tester, 'Map'); + await tester.pumpAndSettle(const Duration(seconds: 2)); + await _takeScreenshot(binding, tester, '${screenshotPrefix}03-map'); + + await _openOverflowItem(tester, Icons.more_vert, 'Settings'); + await _takeScreenshot(binding, tester, '${screenshotPrefix}04-settings'); + }); +} + +void _loadSampleData(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + const centerLocation = LatLng(46.0569, 14.5058); + final contacts = SampleDataGenerator.generateContacts( + centerLocation: centerLocation, + l10n: l10n, + teamMemberCount: 5, + channelCount: 2, + ); + final sampleMessages = SampleDataGenerator.generateAllMessages( + centerLocation: centerLocation, + l10n: l10n, + foundPersonCount: 2, + fireCount: 1, + stagingCount: 1, + objectCount: 1, + generalChannelMessages: 8, + emergencyChannelMessages: 5, + ); + context.read().addContacts(contacts); + for (final message in sampleMessages.messages) { + context.read().addMessage( + message, + contactLocationSnapshot: sampleMessages.contactLocations[message.id], + ); + } +} + +Future _pumpUntilFound( + 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: 100)); + if (finder.evaluate().isNotEmpty) { + return; + } + } + expect(finder, findsOneWidget); +} + +Future _tapTab(WidgetTester tester, String label) async { + final tab = find.text(label).last; + await tester.tap(tab); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); +} + +Future _openOverflowItem( + WidgetTester tester, + IconData buttonIcon, + String label, +) async { + await tester.tap(find.byIcon(buttonIcon)); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); + await tester.tap(find.text(label)); + await tester.pumpAndSettle(const Duration(seconds: 1)); +} + +Future _takeScreenshot( + IntegrationTestWidgetsFlutterBinding binding, + WidgetTester tester, + String name, +) async { + await tester.pumpAndSettle(const Duration(milliseconds: 500)); + final bytes = await binding.takeScreenshot(name); + expect(bytes.isNotEmpty, isTrue); +} diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 86709de..4b9cf7b 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -1,49 +1,47 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.7) - base64 - nkf - rexml - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) + CFPropertyList (3.0.9) + abbrev (0.1.2) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.4.0) - aws-partitions (1.1172.0) - aws-sdk-core (3.233.0) + aws-eventstream (1.3.2) + aws-partitions (1.1109.0) + aws-sdk-core (3.224.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) base64 - bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.113.0) - aws-sdk-core (~> 3, >= 3.231.0) + aws-sdk-kms (1.101.0) + aws-sdk-core (~> 3, >= 3.216.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.199.1) - aws-sdk-core (~> 3, >= 3.231.0) + aws-sdk-s3 (1.188.0) + aws-sdk-core (~> 3, >= 3.224.1) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.12.1) + aws-sigv4 (1.11.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.3.0) - bigdecimal (3.3.1) claide (1.1.0) colored (1.2) colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) + csv (3.3.5) declarative (0.0.20) digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20240107) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.112.0) - faraday (1.10.4) + excon (0.109.0) + faraday (1.10.5) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -55,25 +53,26 @@ GEM faraday-rack (~> 1.0) faraday-retry (~> 1.0) ruby2_keywords (>= 0.0.4) - faraday-cookie_jar (0.0.7) + faraday-cookie_jar (0.0.8) faraday (>= 0.8.0) - http-cookie (~> 1.0.0) + http-cookie (>= 1.0.0) faraday-em_http (1.0.0) faraday-em_synchrony (1.0.1) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.1.1) + faraday-multipart (1.2.0) multipart-post (~> 2.0) faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) - faraday-retry (1.0.3) + faraday-retry (1.0.4) faraday_middleware (1.2.1) faraday (~> 1.0) - fastimage (2.4.0) - fastlane (2.228.0) + fastimage (2.4.1) + fastlane (2.229.0) CFPropertyList (>= 2.3, < 4.0.0) + abbrev (~> 0.1.2) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) @@ -81,6 +80,7 @@ GEM bundler (>= 1.12.0, < 3.0.0) colored (~> 1.2) commander (~> 4.6) + csv (~> 3.3) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) excon (>= 0.71.0, < 1.0.0) @@ -100,6 +100,7 @@ GEM jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) + mutex_m (~> 0.3.0) naturally (~> 2.2) optparse (>= 0.1.1, < 1.0.0) plist (>= 3.1.0, < 4.0.0) @@ -114,8 +115,7 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.4.1) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) - fastlane-sirp (1.0.0) - sysrandom (~> 1.0) + fastlane-sirp (1.1.0) gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) @@ -131,19 +131,19 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-playcustomapp_v1 (0.13.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.31.0) + google-apis-storage_v1 (0.32.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.8.0) + google-cloud-core (1.6.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.5.0) - google-cloud-storage (1.47.0) + google-cloud-errors (1.3.1) + google-cloud-storage (1.37.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.31.0) + google-apis-storage_v1 (~> 0.1) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) @@ -159,42 +159,40 @@ GEM httpclient (2.9.0) mutex_m jmespath (1.6.2) - json (2.15.1) - jwt (2.10.2) + json (2.7.6) + jwt (2.10.3) base64 logger (1.7.0) mini_magick (4.13.2) mini_mime (1.1.5) - multi_json (1.17.0) + multi_json (1.15.0) multipart-post (2.4.1) mutex_m (0.3.0) nanaimo (0.4.0) naturally (2.3.0) - nkf (0.2.0) - optparse (0.6.0) + optparse (0.8.1) os (1.1.4) plist (3.7.2) - public_suffix (6.0.2) - rake (13.3.0) + public_suffix (5.1.1) + rake (13.4.2) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) - retriable (3.1.2) + retriable (3.8.0) rexml (3.4.4) rouge (3.28.0) ruby2_keywords (0.0.5) rubyzip (2.4.1) security (0.1.5) - signet (0.21.0) + signet (0.18.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 4.0) + jwt (>= 1.5, < 3.0) multi_json (~> 1.10) simctl (1.6.10) CFPropertyList naturally - sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -204,6 +202,7 @@ GEM tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) + unf (0.2.0) unicode-display_width (2.6.0) word_wrap (1.0.0) xcodeproj (1.27.0) @@ -226,4 +225,4 @@ DEPENDENCIES fastlane BUNDLED WITH - 2.7.2 + 2.4.22 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e993f0a..b45721b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -57,6 +57,8 @@ PODS: - FlutterMacOS - image_picker_ios (0.0.1): - Flutter + - integration_test (0.0.1): + - Flutter - nsd_ios (0.0.1): - Flutter - package_info_plus (0.4.5): @@ -100,6 +102,7 @@ DEPENDENCIES: - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) - nsd_ios (from `.symlinks/plugins/nsd_ios/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) @@ -144,6 +147,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/geolocator_apple/darwin" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" + integration_test: + :path: ".symlinks/plugins/integration_test/ios" nsd_ios: :path: ".symlinks/plugins/nsd_ios/ios" package_info_plus: @@ -182,6 +187,7 @@ SPEC CHECKSUMS: flutter_local_notifications: 643a3eda1ce1c0599413ca31672536d423dee214 geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e nsd_ios: 596ad79109ddd3e52d665f650f36428049e3653e package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 47360ef..7cb1c0a 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -16,17 +16,66 @@ default_platform(:ios) platform :ios do + def project_root + File.expand_path("../..", __dir__) + end + + def beta_app_info + { + "en-US" => { + description: "Field-ready SAR coordination over MeshCore with chat, voice, images, offline maps, and live team context.", + feedback_email: "hey@dz0ny.dev", + marketing_url: "https://github.com/dz0ny/meshcore-sar", + privacy_policy_url: "https://github.com/dz0ny/meshcore-sar" + } + } + end + + def beta_build_info + { + "en-US" => { + whats_new: "Test MeshCore SAR field coordination flows: messaging, maps, contacts, voice, images, and SAR markers." + } + } + end + desc "Push a new release build to the App Store" lane :release do increment_build_number(xcodeproj: "Runner.xcodeproj") build_app(workspace: "Runner.xcworkspace", scheme: "Runner") - upload_to_app_store(skip_metadata: true, skip_screenshots: true) + upload_to_testflight( + skip_waiting_for_build_processing: true, + localized_app_info: beta_app_info, + localized_build_info: beta_build_info, + changelog: "Test MeshCore SAR field coordination flows: messaging, maps, contacts, voice, images, and SAR markers." + ) end desc "Push a new beta build to TestFlight" lane :beta do increment_build_number(xcodeproj: "Runner.xcodeproj") build_app(workspace: "Runner.xcworkspace", scheme: "Runner") - upload_to_testflight(skip_waiting_for_build_processing: true) + upload_to_testflight( + skip_waiting_for_build_processing: true, + localized_app_info: beta_app_info, + localized_build_info: beta_build_info, + changelog: "Test MeshCore SAR field coordination flows: messaging, maps, contacts, voice, images, and SAR markers." + ) + end + + desc "Capture iOS screenshots for App Store Connect" + lane :screenshots do + sh("cd #{project_root} && scripts/take_screenshots.sh --ios") + end + + desc "Upload App Store screenshots" + lane :store_assets do + upload_to_app_store( + skip_binary_upload: true, + skip_metadata: true, + skip_screenshots: false, + submit_for_review: false, + force: true + ) end end diff --git a/ios/fastlane/README.md b/ios/fastlane/README.md index 054434a..0c9bf8b 100644 --- a/ios/fastlane/README.md +++ b/ios/fastlane/README.md @@ -31,6 +31,22 @@ Push a new release build to the App Store Push a new beta build to TestFlight +### ios screenshots + +```sh +[bundle exec] fastlane ios screenshots +``` + +Capture iOS screenshots for App Store Connect + +### ios store_assets + +```sh +[bundle exec] fastlane ios store_assets +``` + +Upload App Store screenshots + ---- This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. diff --git a/ios/fastlane/metadata/copyright.txt b/ios/fastlane/metadata/copyright.txt new file mode 100644 index 0000000..a79da20 --- /dev/null +++ b/ios/fastlane/metadata/copyright.txt @@ -0,0 +1 @@ +Copyright 2026 Janez Troha diff --git a/ios/fastlane/metadata/en-US/description.txt b/ios/fastlane/metadata/en-US/description.txt new file mode 100644 index 0000000..40521b9 --- /dev/null +++ b/ios/fastlane/metadata/en-US/description.txt @@ -0,0 +1,14 @@ +MeshCore SAR gives search and rescue teams a field-ready coordination workspace for low-connectivity operations. + +Use mesh chat, live location context, SAR markers, offline-first maps, tactical drawings, voice clips, and compressed image transfer in one app. The app is built around MeshCore and BLE so teams can coordinate over LoRa links when normal infrastructure is weak or unavailable. + +Key capabilities: +- Direct and group mesh messaging +- Live team contacts with signal, battery, and location context +- Offline maps with SAR markers and tactical overlays +- Voice clips optimized for low-bandwidth links +- Image transfer with compact encoding +- GPX trail import/export +- Incident, staging, and discovery workflows for field teams + +MeshCore SAR is intended for search and rescue field teams, incident command, and operators who need practical coordination tools outside normal coverage. diff --git a/ios/fastlane/metadata/en-US/keywords.txt b/ios/fastlane/metadata/en-US/keywords.txt new file mode 100644 index 0000000..d53e509 --- /dev/null +++ b/ios/fastlane/metadata/en-US/keywords.txt @@ -0,0 +1 @@ +meshcore,sar,search,rescue,mesh,lora,offline,maps,radio,gps diff --git a/ios/fastlane/metadata/en-US/marketing_url.txt b/ios/fastlane/metadata/en-US/marketing_url.txt new file mode 100644 index 0000000..2392c93 --- /dev/null +++ b/ios/fastlane/metadata/en-US/marketing_url.txt @@ -0,0 +1 @@ +https://github.com/dz0ny/meshcore-sar diff --git a/ios/fastlane/metadata/en-US/name.txt b/ios/fastlane/metadata/en-US/name.txt new file mode 100644 index 0000000..e9dc922 --- /dev/null +++ b/ios/fastlane/metadata/en-US/name.txt @@ -0,0 +1 @@ +MeshCore SAR diff --git a/ios/fastlane/metadata/en-US/privacy_url.txt b/ios/fastlane/metadata/en-US/privacy_url.txt new file mode 100644 index 0000000..2392c93 --- /dev/null +++ b/ios/fastlane/metadata/en-US/privacy_url.txt @@ -0,0 +1 @@ +https://github.com/dz0ny/meshcore-sar diff --git a/ios/fastlane/metadata/en-US/promotional_text.txt b/ios/fastlane/metadata/en-US/promotional_text.txt new file mode 100644 index 0000000..9729f7b --- /dev/null +++ b/ios/fastlane/metadata/en-US/promotional_text.txt @@ -0,0 +1 @@ +Field coordination for search and rescue teams using MeshCore, BLE, offline maps, messaging, voice, and SAR markers. diff --git a/ios/fastlane/metadata/en-US/release_notes.txt b/ios/fastlane/metadata/en-US/release_notes.txt new file mode 100644 index 0000000..3989c04 --- /dev/null +++ b/ios/fastlane/metadata/en-US/release_notes.txt @@ -0,0 +1 @@ +Adds App Store screenshots and TestFlight metadata for MeshCore SAR publishing. diff --git a/ios/fastlane/metadata/en-US/subtitle.txt b/ios/fastlane/metadata/en-US/subtitle.txt new file mode 100644 index 0000000..08f4779 --- /dev/null +++ b/ios/fastlane/metadata/en-US/subtitle.txt @@ -0,0 +1 @@ +SAR mesh field coordination diff --git a/ios/fastlane/metadata/en-US/support_url.txt b/ios/fastlane/metadata/en-US/support_url.txt new file mode 100644 index 0000000..f92d4d8 --- /dev/null +++ b/ios/fastlane/metadata/en-US/support_url.txt @@ -0,0 +1 @@ +https://github.com/dz0ny/meshcore-sar/issues diff --git a/ios/fastlane/report.xml b/ios/fastlane/report.xml index a64cc8d..f8a4821 100644 --- a/ios/fastlane/report.xml +++ b/ios/fastlane/report.xml @@ -5,22 +5,12 @@ - + - - - - - - - - - - - + diff --git a/ios/fastlane/screenshots/en-US/01-messages.png b/ios/fastlane/screenshots/en-US/01-messages.png new file mode 100644 index 0000000..43a8c7a Binary files /dev/null and b/ios/fastlane/screenshots/en-US/01-messages.png differ diff --git a/ios/fastlane/screenshots/en-US/02-contacts.png b/ios/fastlane/screenshots/en-US/02-contacts.png new file mode 100644 index 0000000..af0bb08 Binary files /dev/null and b/ios/fastlane/screenshots/en-US/02-contacts.png differ diff --git a/ios/fastlane/screenshots/en-US/03-map.png b/ios/fastlane/screenshots/en-US/03-map.png new file mode 100644 index 0000000..cf5677e Binary files /dev/null and b/ios/fastlane/screenshots/en-US/03-map.png differ diff --git a/ios/fastlane/screenshots/en-US/04-settings.png b/ios/fastlane/screenshots/en-US/04-settings.png new file mode 100644 index 0000000..1ddc182 Binary files /dev/null and b/ios/fastlane/screenshots/en-US/04-settings.png differ diff --git a/ios/fastlane/screenshots/en-US/ipad-01-messages.png b/ios/fastlane/screenshots/en-US/ipad-01-messages.png new file mode 100644 index 0000000..7990048 Binary files /dev/null and b/ios/fastlane/screenshots/en-US/ipad-01-messages.png differ diff --git a/ios/fastlane/screenshots/en-US/ipad-02-contacts.png b/ios/fastlane/screenshots/en-US/ipad-02-contacts.png new file mode 100644 index 0000000..31bd622 Binary files /dev/null and b/ios/fastlane/screenshots/en-US/ipad-02-contacts.png differ diff --git a/ios/fastlane/screenshots/en-US/ipad-03-map.png b/ios/fastlane/screenshots/en-US/ipad-03-map.png new file mode 100644 index 0000000..cc53bd6 Binary files /dev/null and b/ios/fastlane/screenshots/en-US/ipad-03-map.png differ diff --git a/ios/fastlane/screenshots/en-US/ipad-04-settings.png b/ios/fastlane/screenshots/en-US/ipad-04-settings.png new file mode 100644 index 0000000..9307995 Binary files /dev/null and b/ios/fastlane/screenshots/en-US/ipad-04-settings.png differ diff --git a/lib/main.dart b/lib/main.dart index f4d96ca..6ec5a73 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -50,6 +50,9 @@ class MeshCoreSarApp extends StatefulWidget { } class _MeshCoreSarAppState extends State { + static const bool _isScreenshotRun = bool.fromEnvironment( + 'MESHCORE_SCREENSHOTS', + ); final GlobalKey _navigatorKey = GlobalKey(); AppThemeMode _themeMode = AppThemeMode.system; Locale? _locale; @@ -87,7 +90,9 @@ class _MeshCoreSarAppState extends State { _pendingNotificationPayload = NotificationService().consumeLaunchPayload(); // Check if we need to request location permissions - await _checkLocationPermissions(); + if (!_isScreenshotRun) { + await _checkLocationPermissions(); + } // Check for app updates (Android only) - runs in background // Shows notification if update is available diff --git a/lib/providers/offline_tiles_provider.dart b/lib/providers/offline_tiles_provider.dart index 5aa9a40..b68a85a 100644 --- a/lib/providers/offline_tiles_provider.dart +++ b/lib/providers/offline_tiles_provider.dart @@ -56,6 +56,7 @@ class OfflineTilesProvider extends ChangeNotifier { TileDownloadService? _downloadService; StreamSubscription? _downloadSubscription; StreamSubscription>? _peersSubscription; + bool _isDisposed = false; // Drawing state DrawingMode _drawingMode = DrawingMode.none; @@ -436,7 +437,9 @@ class OfflineTilesProvider extends ChangeNotifier { _peersSubscription = null; await _sharing.stopPeerDiscovery(); _discoveredPeers = {}; - notifyListeners(); + if (!_isDisposed) { + notifyListeners(); + } } void addManualPeer(String ipAddress) { @@ -569,6 +572,7 @@ class OfflineTilesProvider extends ChangeNotifier { @override void dispose() { + _isDisposed = true; _downloadSubscription?.cancel(); _downloadService?.dispose(); _peersSubscription?.cancel(); diff --git a/lib/screens/contacts_tab.dart b/lib/screens/contacts_tab.dart index caee101..7f2c450 100644 --- a/lib/screens/contacts_tab.dart +++ b/lib/screens/contacts_tab.dart @@ -38,6 +38,9 @@ class ContactsTab extends StatefulWidget { } class _ContactsTabState extends State { + static const bool _isScreenshotRun = bool.fromEnvironment( + 'MESHCORE_SCREENSHOTS', + ); Position? _currentPosition; final Map _sectionFilters = { ContactSection.teamMembers: '', @@ -62,7 +65,9 @@ class _ContactsTabState extends State { for (final section in ContactSection.values) section: TextEditingController(text: _sectionFilters[section] ?? ''), }; - _getCurrentLocation(); + if (!_isScreenshotRun) { + _getCurrentLocation(); + } // Mark all contacts as viewed when tab is opened WidgetsBinding.instance.addPostFrameCallback((_) { context.read().markAllAsViewed(); diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index d190a04..9ce28e2 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -14,6 +14,9 @@ class NotificationService { factory NotificationService() => _instance; NotificationService._internal(); + static const bool _skipPermissionRequests = bool.fromEnvironment( + 'MESHCORE_SCREENSHOTS', + ); final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin(); static const String _prefMessagesEnabled = 'notifications_messages_enabled'; @@ -101,9 +104,9 @@ class NotificationService { // iOS initialization settings final darwinSettings = DarwinInitializationSettings( - requestAlertPermission: true, - requestBadgePermission: true, - requestSoundPermission: true, + requestAlertPermission: !_skipPermissionRequests, + requestBadgePermission: !_skipPermissionRequests, + requestSoundPermission: !_skipPermissionRequests, ); // Combined initialization settings @@ -125,7 +128,11 @@ class NotificationService { } // Request permissions - await _requestPermissions(); + if (_skipPermissionRequests) { + _permissionGranted = true; + } else { + await _requestPermissions(); + } await _loadPreferences(); // Create notification channels (Android) @@ -338,6 +345,8 @@ class NotificationService { String? notes, AppLocalizations? localizations, }) async { + if (_skipPermissionRequests) return; + if (!_isInitialized) { debugPrint( '⚠️ [NotificationService] Not initialized, skipping notification', @@ -520,6 +529,8 @@ class NotificationService { String? channelName, AppLocalizations? localizations, }) async { + if (_skipPermissionRequests) return; + if (!_isInitialized) { debugPrint( '⚠️ [NotificationService] Not initialized, skipping notification', @@ -722,6 +733,8 @@ class NotificationService { required String downloadUrl, AppLocalizations? localizations, }) async { + if (_skipPermissionRequests) return; + if (!_isInitialized) { debugPrint( '⚠️ [NotificationService] Not initialized, skipping notification', @@ -822,6 +835,8 @@ class NotificationService { required double batteryPercent, required bool isCurrentDevice, }) async { + if (_skipPermissionRequests) return false; + if (!_isInitialized) { debugPrint( '⚠️ [NotificationService] Not initialized, skipping notification', @@ -913,6 +928,8 @@ class NotificationService { required String contactKey, String? contactName, }) async { + if (_skipPermissionRequests) return false; + if (!_isInitialized) return false; if (!_permissionGranted) return false; if (!_discoveryNotificationsEnabled) return false; diff --git a/lib/services/voice_player_service.dart b/lib/services/voice_player_service.dart index cce5f95..ec4d2c2 100644 --- a/lib/services/voice_player_service.dart +++ b/lib/services/voice_player_service.dart @@ -11,6 +11,7 @@ class VoicePlayerService { final AudioPlayer _player = AudioPlayer(); final StreamController _events = StreamController.broadcast(); bool _isPlaying = false; + bool _isDisposed = false; Duration _position = Duration.zero; Duration _duration = Duration.zero; Timer? _fallbackTicker; @@ -30,22 +31,22 @@ class VoicePlayerService { } else { _stopFallbackTicker(); } - _events.add(null); + _emit(); }); _player.onLog.listen((msg) => debugPrint('🔊 [VoicePlayer] log: $msg')); _player.onPositionChanged.listen((position) { _position = position; - _events.add(null); + _emit(); }); _player.onDurationChanged.listen((duration) { _duration = duration; - _events.add(null); + _emit(); }); _player.onPlayerComplete.listen((_) { _isPlaying = false; _position = _duration; _stopFallbackTicker(); - _events.add(null); + _emit(); }); } @@ -57,7 +58,7 @@ class VoicePlayerService { milliseconds: (pcmSamples.length * 1000) ~/ sampleRateHz, ); _playbackStartedAt = DateTime.now(); - _events.add(null); + _emit(); final wavBytes = _buildWav(pcmSamples, sampleRate: sampleRateHz); final tmpDir = await getTemporaryDirectory(); @@ -70,14 +71,14 @@ class VoicePlayerService { try { _isPlaying = true; _startFallbackTicker(); - _events.add(null); + _emit(); await _player.play(DeviceFileSource(file.path)); debugPrint('🔊 [VoicePlayer] play() returned (audio playing)'); } catch (e, st) { debugPrint('❌ [VoicePlayer] play() error: $e\n$st'); _isPlaying = false; _stopFallbackTicker(); - _events.add(null); + _emit(); } } @@ -88,15 +89,22 @@ class VoicePlayerService { _position = Duration.zero; _playbackStartedAt = null; _stopFallbackTicker(); - _events.add(null); + _emit(); } void dispose() { + _isDisposed = true; _stopFallbackTicker(); _events.close(); _player.dispose(); } + void _emit() { + if (!_isDisposed) { + _events.add(null); + } + } + void _startFallbackTicker() { if (_fallbackTicker != null) return; _fallbackTicker = Timer.periodic(const Duration(milliseconds: 100), (_) { @@ -107,7 +115,7 @@ class VoicePlayerService { final clamped = elapsed > _duration ? _duration : elapsed; if (clamped > _position) { _position = clamped; - _events.add(null); + _emit(); } }); } diff --git a/pubspec.lock b/pubspec.lock index e70d805..316e695 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -503,6 +503,11 @@ packages: url: "https://pub.dev" source: hosted version: "0.8.1" + flutter_driver: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -591,6 +596,11 @@ packages: description: flutter source: sdk version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" geoclue: dependency: transitive description: @@ -767,6 +777,11 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" intl: dependency: "direct main" description: @@ -1112,6 +1127,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + process: + dependency: transitive + description: + name: process + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 + url: "https://pub.dev" + source: hosted + version: "5.0.5" proj4dart: dependency: "direct main" description: @@ -1405,6 +1428,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" synchronized: dependency: transitive description: @@ -1589,6 +1620,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" webserial: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 4196288..490acd4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -129,9 +129,15 @@ dependencies: webserial: ^1.2.0 dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: sdk: flutter + integration_test: + sdk: flutter + # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your diff --git a/scripts/take_screenshots.sh b/scripts/take_screenshots.sh index 29b441e..b5bf616 100755 --- a/scripts/take_screenshots.sh +++ b/scripts/take_screenshots.sh @@ -14,12 +14,18 @@ NC='\033[0m' # No Color # Configuration OUTPUT_DIR="screenshots" +IOS_OUTPUT_DIR="ios/fastlane/screenshots/en-US" +IOS_SCREENSHOT_WIDTH=1284 +IOS_SCREENSHOT_HEIGHT=2778 +IPAD_SCREENSHOT_WIDTH=2048 +IPAD_SCREENSHOT_HEIGHT=2732 INTEGRATION_TEST="integration_test/app_screenshots_test.dart" # Device configurations for App Store screenshots # iOS devices (required sizes: 6.7", 6.5", 5.5") IOS_DEVICES=( - "iPhone Air" # 6.3" - 1206x2622 (newer large format) + "iPhone 17 Pro Max" # 6.9" - App Store large phone format + "iPad Pro 13-inch (M5)" # 13" - required iPad format ) # Android devices (phone + tablet recommended) @@ -45,7 +51,7 @@ mkdir -p "$OUTPUT_DIR" # Function to list available devices list_devices() { echo -e "${YELLOW}📱 Available iOS Simulators:${NC}" - xcrun simctl list devices available | grep "iPhone" | grep -v "unavailable" + xcrun simctl list devices available | grep -E "iPhone|iPad" | grep -v "unavailable" echo "" echo -e "${YELLOW}🤖 Available Android Emulators:${NC}" emulator -list-avds @@ -74,23 +80,46 @@ take_ios_screenshots() { echo -e "${BLUE} Device ID: $device_id${NC}" - # Boot the simulator if not already booted + xcrun simctl shutdown "$device_id" 2>/dev/null || true xcrun simctl boot "$device_id" 2>/dev/null || true sleep 3 + xcrun simctl uninstall "$device_id" com.meshcore.sar.meshcoreSarApp 2>/dev/null || true + xcrun simctl privacy "$device_id" grant notifications com.meshcore.sar.meshcoreSarApp 2>/dev/null || true + xcrun simctl privacy "$device_id" grant location com.meshcore.sar.meshcoreSarApp 2>/dev/null || true # Create device-specific output directory - local device_dir="$OUTPUT_DIR/ios/${device_name// /_}" + local device_dir="$IOS_OUTPUT_DIR" + local screenshot_prefix="" + local screenshot_width="$IOS_SCREENSHOT_WIDTH" + local screenshot_height="$IOS_SCREENSHOT_HEIGHT" + local remove_pattern="[0-9][0-9]-*.png" + if [[ "$device_name" == *"iPad"* ]]; then + screenshot_prefix="ipad-" + screenshot_width="$IPAD_SCREENSHOT_WIDTH" + screenshot_height="$IPAD_SCREENSHOT_HEIGHT" + remove_pattern="ipad-*.png" + fi mkdir -p "$device_dir" + rm -f "$device_dir"/$remove_pattern # Run the integration test - flutter drive \ + SCREENSHOT_OUTPUT_DIR="$device_dir" flutter drive \ --driver=test_driver/integration_test.dart \ --target="$INTEGRATION_TEST" \ -d "$device_id" \ + --dart-define=MESHCORE_SCREENSHOTS=true \ + --dart-define=SCREENSHOT_PREFIX="$screenshot_prefix" \ --screenshot="$device_dir" || { - echo -e "${YELLOW}⚠️ Warning: Screenshot capture had issues on $device_name${NC}" + echo -e "${RED}❌ Screenshot capture failed on $device_name${NC}" + return 1 } + if command -v sips >/dev/null 2>&1; then + for screenshot in "$device_dir"/$remove_pattern; do + sips -z "$screenshot_height" "$screenshot_width" "$screenshot" >/dev/null + done + fi + echo -e "${GREEN}✅ Completed: $device_name${NC}" echo "" } @@ -129,12 +158,14 @@ take_android_screenshots() { mkdir -p "$device_dir" # Run the integration test - flutter drive \ + SCREENSHOT_OUTPUT_DIR="$device_dir" flutter drive \ --driver=test_driver/integration_test.dart \ --target="$INTEGRATION_TEST" \ -d emulator-5554 \ + --dart-define=MESHCORE_SCREENSHOTS=true \ --screenshot="$device_dir" || { - echo -e "${YELLOW}⚠️ Warning: Screenshot capture had issues on $device_name${NC}" + echo -e "${RED}❌ Screenshot capture failed on $device_name${NC}" + return 1 } # Kill emulator @@ -192,16 +223,11 @@ while [[ $# -gt 0 ]]; do esac done -# Create test driver if it doesn't exist +# Ensure test driver exists DRIVER_FILE="test_driver/integration_test.dart" -mkdir -p test_driver if [ ! -f "$DRIVER_FILE" ]; then - echo -e "${YELLOW}📝 Creating integration test driver...${NC}" - cat > "$DRIVER_FILE" << 'EOF' -import 'package:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); -EOF + echo -e "${RED}❌ Error: Integration test driver not found at $DRIVER_FILE${NC}" + exit 1 fi # Take screenshots @@ -239,10 +265,18 @@ echo -e "${GREEN}╔════════════════════ echo -e "${GREEN}║ ✅ Screenshot Capture Complete! ║${NC}" echo -e "${GREEN}╔═══════════════════════════════════════════╗${NC}" echo "" -echo -e "${BLUE}📁 Screenshots saved to: $OUTPUT_DIR${NC}" +if [ "$PLATFORM" = "ios" ]; then + echo -e "${BLUE}📁 Screenshots saved to: $IOS_OUTPUT_DIR${NC}" +else + echo -e "${BLUE}📁 Screenshots saved to: $OUTPUT_DIR${NC}" +fi echo "" echo -e "${YELLOW}Next steps:${NC}" -echo -e " 1. Review screenshots in $OUTPUT_DIR" +if [ "$PLATFORM" = "ios" ]; then + echo -e " 1. Review screenshots in $IOS_OUTPUT_DIR" +else + echo -e " 1. Review screenshots in $OUTPUT_DIR" +fi echo -e " 2. Organize by device size for App Store" echo -e " 3. Add captions and localization if needed" echo "" diff --git a/test_driver/integration_test.dart b/test_driver/integration_test.dart new file mode 100644 index 0000000..65e97d1 --- /dev/null +++ b/test_driver/integration_test.dart @@ -0,0 +1,19 @@ +import 'dart:io'; + +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:integration_test/integration_test_driver_extended.dart'; + +Future main() async { + final driver = await FlutterDriver.connect(); + await integrationDriver( + driver: driver, + onScreenshot: (name, image, [args]) async { + final outputDir = Platform.environment['SCREENSHOT_OUTPUT_DIR'] ?? + 'ios/fastlane/screenshots/en-US'; + final file = File('$outputDir/$name.png'); + await file.parent.create(recursive: true); + await file.writeAsBytes(image); + return true; + }, + ); +}