From 96a47f45b8a52dabcc3725630cc79ad220bc71d4 Mon Sep 17 00:00:00 2001 From: diffwireauto <330573775+diffwireauto@users.noreply.github.com> Date: Tue, 10 Nov 2026 09:00:50 -0500 Subject: [PATCH 1/2] fix(#1140): name both Apple apps Offband MeshCore; flag iPhone app on Mac Both Apple builds kept Flutter's default bundle names. The iPhone app, installed on a Mac from the App Store, was labeled "Runner", and the notarized Mac app showed as "offband_meshcore" in Finder. PRODUCT_NAME is now "Offband MeshCore" for the iOS Runner target and the macOS app. Bundle identifiers are unchanged, so app data stays put. CI uses the new paths and fails if the name regresses: CFBundleName on macOS, the .app folder in the IPA on iOS. When the iPhone app runs on a Mac (ProcessInfo.isiOSAppOnMac, read over a small platform channel), a one-button notice says it is the iPhone version in a fixed phone-sized window. Once per app version, with no link elsewhere, by the owner's decision. Co-Authored-By: Claude Opus 5.5 --- .github/scripts/ios-assert-frameworks.sh | 4 +- .github/workflows/build.yml | 11 +- .github/workflows/ios-signed.yml | 7 +- .github/workflows/macos-signed.yml | 11 +- ios/Runner.xcodeproj/project.pbxproj | 14 +-- ios/Runner/AppDelegate.swift | 10 ++ lib/l10n/app_en.arb | 2 + lib/l10n/app_localizations.dart | 12 ++ lib/l10n/app_localizations_bg.dart | 7 ++ lib/l10n/app_localizations_de.dart | 7 ++ lib/l10n/app_localizations_en.dart | 7 ++ lib/l10n/app_localizations_es.dart | 7 ++ lib/l10n/app_localizations_fr.dart | 7 ++ lib/l10n/app_localizations_hu.dart | 7 ++ lib/l10n/app_localizations_it.dart | 7 ++ lib/l10n/app_localizations_ja.dart | 7 ++ lib/l10n/app_localizations_ko.dart | 7 ++ lib/l10n/app_localizations_nl.dart | 7 ++ lib/l10n/app_localizations_pl.dart | 7 ++ lib/l10n/app_localizations_pt.dart | 7 ++ lib/l10n/app_localizations_ru.dart | 7 ++ lib/l10n/app_localizations_sk.dart | 7 ++ lib/l10n/app_localizations_sl.dart | 7 ++ lib/l10n/app_localizations_sv.dart | 7 ++ lib/l10n/app_localizations_uk.dart | 7 ++ lib/l10n/app_localizations_zh.dart | 7 ++ lib/main.dart | 5 +- lib/storage/scoping_inventory.dart | 8 ++ lib/widgets/ios_on_mac_notice.dart | 89 ++++++++++++++ macos/Runner.xcodeproj/project.pbxproj | 8 +- macos/Runner/Configs/AppInfo.xcconfig | 2 +- test/widgets/ios_on_mac_notice_test.dart | 150 +++++++++++++++++++++++ untranslated.json | 34 +++++ 33 files changed, 471 insertions(+), 22 deletions(-) create mode 100644 lib/widgets/ios_on_mac_notice.dart create mode 100644 test/widgets/ios_on_mac_notice_test.dart diff --git a/.github/scripts/ios-assert-frameworks.sh b/.github/scripts/ios-assert-frameworks.sh index 2e286f59..0e7bb17c 100644 --- a/.github/scripts/ios-assert-frameworks.sh +++ b/.github/scripts/ios-assert-frameworks.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash -# Fails unless a built Runner.app holds the native-asset libraries (#1037). +# Fails unless a built iOS app holds the native-asset libraries (#1037). # An iOS build can compile with them missing, and the app then fails only at # runtime, when it opens storage or loads translation. set -eu -app="${1:?usage: ios-assert-frameworks.sh }" +app="${1:?usage: ios-assert-frameworks.sh }" frameworks="${app}/Frameworks" if [ ! -d "${frameworks}" ]; then diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1885c4ac..d8712a43 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -152,7 +152,7 @@ jobs: echo "Refused, as required." - name: Assert the app holds its native-asset frameworks (#1037) if: needs.classify.outputs.docs_only != 'true' - run: bash .github/scripts/ios-assert-frameworks.sh build/ios/iphoneos/Runner.app + run: bash .github/scripts/ios-assert-frameworks.sh "build/ios/iphoneos/Offband MeshCore.app" # ios-signed.yml cannot run on a PR, so its output check is proven here, # with no secrets: every case that must not count as success (#1040). - name: "Negative: the altool check refuses anything but success" @@ -265,8 +265,13 @@ jobs: if: needs.classify.outputs.docs_only != 'true' run: | set -eu - APP=build/macos/Build/Products/Release/offband_meshcore.app - lipo -archs "${APP}/Contents/MacOS/offband_meshcore" || true + APP="build/macos/Build/Products/Release/Offband MeshCore.app" + NAME="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleName' "${APP}/Contents/Info.plist")" + if [ "${NAME}" != "Offband MeshCore" ]; then + echo "::error::CFBundleName is '${NAME}', expected 'Offband MeshCore' (#1140)." + exit 1 + fi + lipo -archs "${APP}/Contents/MacOS/Offband MeshCore" || true ditto -c -k --keepParent "${APP}" offband-macos.zip - name: Upload macOS build if: needs.classify.outputs.docs_only != 'true' diff --git a/.github/workflows/ios-signed.yml b/.github/workflows/ios-signed.yml index 442cc7ad..8d97f727 100644 --- a/.github/workflows/ios-signed.yml +++ b/.github/workflows/ios-signed.yml @@ -198,7 +198,12 @@ jobs: UNPACKED="${RUNNER_TEMP}/ipa" mkdir -p "${UNPACKED}" unzip -q "${IPA}" -d "${UNPACKED}" - echo "APP=${UNPACKED}/Payload/Runner.app" >> "$GITHUB_ENV" + if [ ! -d "${UNPACKED}/Payload/Offband MeshCore.app" ]; then + echo "::error::The IPA holds no 'Offband MeshCore.app' (#1140):" + ls -1 "${UNPACKED}/Payload" + exit 1 + fi + echo "APP=${UNPACKED}/Payload/Offband MeshCore.app" >> "$GITHUB_ENV" - name: Assert the IPA holds its native-asset frameworks (#1037) run: bash .github/scripts/ios-assert-frameworks.sh "${APP}" diff --git a/.github/workflows/macos-signed.yml b/.github/workflows/macos-signed.yml index 05450d8e..2c6e34e3 100644 --- a/.github/workflows/macos-signed.yml +++ b/.github/workflows/macos-signed.yml @@ -119,7 +119,12 @@ jobs: - name: Sign with Developer ID run: | set -eu - APP=build/macos/Build/Products/Release/offband_meshcore.app + APP="build/macos/Build/Products/Release/Offband MeshCore.app" + NAME="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleName' "${APP}/Contents/Info.plist")" + if [ "${NAME}" != "Offband MeshCore" ]; then + echo "::error::CFBundleName is '${NAME}', expected 'Offband MeshCore' (#1140)." + exit 1 + fi echo "APP=${APP}" >> "$GITHUB_ENV" SIGN=(codesign --force --options runtime --timestamp --sign "${IDENTITY}" --keychain "${KC}") while IFS= read -r -d '' f; do @@ -132,7 +137,7 @@ jobs: done < <(find "${APP}/Contents/Frameworks" -depth -name '*.framework' -type d -print0) "${SIGN[@]}" --entitlements macos/Runner/Release.entitlements "${APP}" codesign --verify --deep --strict --verbose=2 "${APP}" - lipo -archs "${APP}/Contents/MacOS/offband_meshcore" + lipo -archs "${APP}/Contents/MacOS/Offband MeshCore" # ditto, not zip: it preserves the .app bundle's symlinks (#1108). The # same zip is what Apple notarizes and what job 3 staples. @@ -218,7 +223,7 @@ jobs: run: | set -eu ditto -x -k offband-macos-signed.zip . - APP=offband_meshcore.app + APP="Offband MeshCore.app" xcrun stapler staple "${APP}" xcrun stapler validate "${APP}" codesign --verify --deep --strict --verbose=2 "${APP}" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 90a6822a..7f9974fa 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -55,7 +55,7 @@ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Offband MeshCore.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -454,7 +454,7 @@ "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = app.offband.meshcore.ios; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "Offband MeshCore"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -474,7 +474,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Offband MeshCore.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Offband MeshCore"; }; name = Debug; }; @@ -489,7 +489,7 @@ PRODUCT_BUNDLE_IDENTIFIER = app.offband.meshcore.ios.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Offband MeshCore.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Offband MeshCore"; }; name = Release; }; @@ -504,7 +504,7 @@ PRODUCT_BUNDLE_IDENTIFIER = app.offband.meshcore.ios.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Offband MeshCore.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Offband MeshCore"; }; name = Profile; }; @@ -633,7 +633,7 @@ "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = app.offband.meshcore.ios; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "Offband MeshCore"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -655,7 +655,7 @@ "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = app.offband.meshcore.ios; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "Offband MeshCore"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index c30b367e..75e386da 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -12,5 +12,15 @@ import UIKit func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + if let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "OffbandPlatform") { + FlutterMethodChannel(name: "offband/platform", binaryMessenger: registrar.messenger()) + .setMethodCallHandler { call, result in + if call.method == "isiOSAppOnMac" { + result(ProcessInfo.processInfo.isiOSAppOnMac) + } else { + result(FlutterMethodNotImplemented) + } + } + } } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b2f3fda3..c6278a38 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -509,6 +509,8 @@ "sponsorPrompt_anytime": "You can support Offband anytime from About in Settings, or at offband.org/donate.", "sponsorPrompt_notNow": "Not now", "sponsorPrompt_never": "Never Show Again", + "iosOnMac_title": "iPhone app on a Mac", + "iosOnMac_body": "This is the iPhone version of Offband MeshCore. It's made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.", "settings_aboutLinkFailed": "Could not open the link", "settings_aboutOpenMeteoAttribution": "LOS elevation data: Open-Meteo (CC BY 4.0)", "settings_aboutPresetsAttribution": "Regional radio presets: MeshCore's suggested settings, maintained by Liam Cottle (api.meshcore.nz), with Offband additions.", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index cd0b9837..74af28ed 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1774,6 +1774,18 @@ abstract class AppLocalizations { /// **'Never Show Again'** String get sponsorPrompt_never; + /// No description provided for @iosOnMac_title. + /// + /// In en, this message translates to: + /// **'iPhone app on a Mac'** + String get iosOnMac_title; + + /// No description provided for @iosOnMac_body. + /// + /// In en, this message translates to: + /// **'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'** + String get iosOnMac_body; + /// No description provided for @settings_aboutLinkFailed. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 16074096..5d8113c7 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -985,6 +985,13 @@ class AppLocalizationsBg extends AppLocalizations { @override String get sponsorPrompt_never => 'Never Show Again'; + @override + String get iosOnMac_title => 'iPhone app on a Mac'; + + @override + String get iosOnMac_body => + 'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'; + @override String get settings_aboutLinkFailed => 'Could not open the link'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 86d2d4ac..a8f3b763 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -980,6 +980,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get sponsorPrompt_never => 'Never Show Again'; + @override + String get iosOnMac_title => 'iPhone app on a Mac'; + + @override + String get iosOnMac_body => + 'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'; + @override String get settings_aboutLinkFailed => 'Could not open the link'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index c31afc87..c811df90 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -971,6 +971,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get sponsorPrompt_never => 'Never Show Again'; + @override + String get iosOnMac_title => 'iPhone app on a Mac'; + + @override + String get iosOnMac_body => + 'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'; + @override String get settings_aboutLinkFailed => 'Could not open the link'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 4a731d04..9f17dd69 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -981,6 +981,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get sponsorPrompt_never => 'Never Show Again'; + @override + String get iosOnMac_title => 'iPhone app on a Mac'; + + @override + String get iosOnMac_body => + 'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'; + @override String get settings_aboutLinkFailed => 'Could not open the link'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index ca1692eb..0e8898e2 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -985,6 +985,13 @@ class AppLocalizationsFr extends AppLocalizations { @override String get sponsorPrompt_never => 'Never Show Again'; + @override + String get iosOnMac_title => 'iPhone app on a Mac'; + + @override + String get iosOnMac_body => + 'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'; + @override String get settings_aboutLinkFailed => 'Could not open the link'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index f8276eeb..8644cccb 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -983,6 +983,13 @@ class AppLocalizationsHu extends AppLocalizations { @override String get sponsorPrompt_never => 'Never Show Again'; + @override + String get iosOnMac_title => 'iPhone app on a Mac'; + + @override + String get iosOnMac_body => + 'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'; + @override String get settings_aboutLinkFailed => 'Could not open the link'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index aa91e014..57e89a1d 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -983,6 +983,13 @@ class AppLocalizationsIt extends AppLocalizations { @override String get sponsorPrompt_never => 'Never Show Again'; + @override + String get iosOnMac_title => 'iPhone app on a Mac'; + + @override + String get iosOnMac_body => + 'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'; + @override String get settings_aboutLinkFailed => 'Could not open the link'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index e7acd1d5..833e649f 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -956,6 +956,13 @@ class AppLocalizationsJa extends AppLocalizations { @override String get sponsorPrompt_never => 'Never Show Again'; + @override + String get iosOnMac_title => 'iPhone app on a Mac'; + + @override + String get iosOnMac_body => + 'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'; + @override String get settings_aboutLinkFailed => 'Could not open the link'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 0739a01d..33106347 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -956,6 +956,13 @@ class AppLocalizationsKo extends AppLocalizations { @override String get sponsorPrompt_never => 'Never Show Again'; + @override + String get iosOnMac_title => 'iPhone app on a Mac'; + + @override + String get iosOnMac_body => + 'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'; + @override String get settings_aboutLinkFailed => 'Could not open the link'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index f54677a7..3c91073a 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -977,6 +977,13 @@ class AppLocalizationsNl extends AppLocalizations { @override String get sponsorPrompt_never => 'Never Show Again'; + @override + String get iosOnMac_title => 'iPhone app on a Mac'; + + @override + String get iosOnMac_body => + 'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'; + @override String get settings_aboutLinkFailed => 'Could not open the link'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 455aa673..85f1a6b2 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -985,6 +985,13 @@ class AppLocalizationsPl extends AppLocalizations { @override String get sponsorPrompt_never => 'Never Show Again'; + @override + String get iosOnMac_title => 'iPhone app on a Mac'; + + @override + String get iosOnMac_body => + 'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'; + @override String get settings_aboutLinkFailed => 'Could not open the link'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index c46b31ee..cbf5b0bc 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -983,6 +983,13 @@ class AppLocalizationsPt extends AppLocalizations { @override String get sponsorPrompt_never => 'Never Show Again'; + @override + String get iosOnMac_title => 'iPhone app on a Mac'; + + @override + String get iosOnMac_body => + 'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'; + @override String get settings_aboutLinkFailed => 'Could not open the link'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index a9332831..8a52b42e 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -982,6 +982,13 @@ class AppLocalizationsRu extends AppLocalizations { @override String get sponsorPrompt_never => 'Never Show Again'; + @override + String get iosOnMac_title => 'iPhone app on a Mac'; + + @override + String get iosOnMac_body => + 'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'; + @override String get settings_aboutLinkFailed => 'Could not open the link'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 18142185..699b6fa0 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -977,6 +977,13 @@ class AppLocalizationsSk extends AppLocalizations { @override String get sponsorPrompt_never => 'Never Show Again'; + @override + String get iosOnMac_title => 'iPhone app on a Mac'; + + @override + String get iosOnMac_body => + 'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'; + @override String get settings_aboutLinkFailed => 'Could not open the link'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index f2ea851f..32c8ada2 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -974,6 +974,13 @@ class AppLocalizationsSl extends AppLocalizations { @override String get sponsorPrompt_never => 'Never Show Again'; + @override + String get iosOnMac_title => 'iPhone app on a Mac'; + + @override + String get iosOnMac_body => + 'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'; + @override String get settings_aboutLinkFailed => 'Could not open the link'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 84f5f16a..39ce744b 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -972,6 +972,13 @@ class AppLocalizationsSv extends AppLocalizations { @override String get sponsorPrompt_never => 'Never Show Again'; + @override + String get iosOnMac_title => 'iPhone app on a Mac'; + + @override + String get iosOnMac_body => + 'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'; + @override String get settings_aboutLinkFailed => 'Could not open the link'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 2877c165..ac6f78df 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -980,6 +980,13 @@ class AppLocalizationsUk extends AppLocalizations { @override String get sponsorPrompt_never => 'Never Show Again'; + @override + String get iosOnMac_title => 'iPhone app on a Mac'; + + @override + String get iosOnMac_body => + 'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'; + @override String get settings_aboutLinkFailed => 'Could not open the link'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index d23c2d3d..b896227c 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -948,6 +948,13 @@ class AppLocalizationsZh extends AppLocalizations { @override String get sponsorPrompt_never => 'Never Show Again'; + @override + String get iosOnMac_title => 'iPhone app on a Mac'; + + @override + String get iosOnMac_body => + 'This is the iPhone version of Offband MeshCore. It\'s made for iPhone and iPad, so on a Mac it runs in a fixed phone-sized window.'; + @override String get settings_aboutLinkFailed => 'Could not open the link'; diff --git a/lib/main.dart b/lib/main.dart index 5a042e93..8c9af68a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -43,6 +43,7 @@ import 'widgets/storage_unavailable_banner.dart'; import 'storage/prefs_manager.dart'; import 'utils/app_logger.dart'; import 'widgets/keep_screen_awake.dart'; +import 'widgets/ios_on_mac_notice.dart'; import 'widgets/sponsor_prompt.dart'; void main() async { @@ -394,7 +395,9 @@ class MeshCoreApp extends StatelessWidget { }, home: (PlatformInfo.isWeb && !PlatformInfo.isChrome) ? const ChromeRequiredScreen() - : const SponsorPromptGate(child: ScannerScreen()), + : const IosOnMacNoticeGate( + child: SponsorPromptGate(child: ScannerScreen()), + ), ); }, ), diff --git a/lib/storage/scoping_inventory.dart b/lib/storage/scoping_inventory.dart index 3afb895a..7235edf6 100644 --- a/lib/storage/scoping_inventory.dart +++ b/lib/storage/scoping_inventory.dart @@ -315,6 +315,14 @@ const List storageSurfaces = [ 'for (#1129). Follows the installed app, not a radio', owner: 'lib/widgets/sponsor_prompt.dart', ), + StorageSurface( + 'ios_on_mac_notice_shown_for', + verdict: ScopingVerdict.globalOnPurpose, + reason: + 'the app version the iPhone-on-a-Mac notice last showed for (#1140). ' + 'A fact about the installed app, not a radio', + owner: 'lib/widgets/ios_on_mac_notice.dart', + ), StorageSurface( 'anr_last_exit_ts_ms', verdict: ScopingVerdict.globalOnPurpose, diff --git a/lib/widgets/ios_on_mac_notice.dart b/lib/widgets/ios_on_mac_notice.dart new file mode 100644 index 00000000..6dd5d5ac --- /dev/null +++ b/lib/widgets/ios_on_mac_notice.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../l10n/l10n.dart'; +import '../storage/prefs_manager.dart'; +import '../utils/app_logger.dart'; +import '../utils/platform_info.dart'; + +/// Tells a Mac user they are running the iPhone app (#1140). Shown once per +/// app version. No link to the macOS download, by the owner's decision. +class IosOnMacNotice { + static const String shownForKey = 'ios_on_mac_notice_shown_for'; + static const MethodChannel _channel = MethodChannel('offband/platform'); + + static Future onLaunch( + BuildContext context, { + bool? isIOS, + Future Function()? isOnMac, + SharedPreferences? prefs, + Future Function()? installedVersion, + }) async { + if (!(isIOS ?? PlatformInfo.isIOS)) return; + if (!await (isOnMac ?? _isOnMac)()) return; + final version = await (installedVersion ?? _installedVersion)(); + if (version == null) return; + final store = prefs ?? PrefsManager.instance; + if (store.getString(shownForKey) == version) return; + await store.setString(shownForKey, version); + if (!context.mounted) return; + final l10n = context.l10n; + await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(l10n.iosOnMac_title), + content: Text(l10n.iosOnMac_body), + actions: [ + FilledButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: Text(l10n.common_ok), + ), + ], + ), + ); + } + + static Future _isOnMac() async { + try { + return await _channel.invokeMethod('isiOSAppOnMac') ?? false; + } catch (e) { + appLogger.warn('iOS-on-Mac check failed: $e', tag: 'Platform'); + return false; + } + } + + static Future _installedVersion() async { + try { + final info = await PackageInfo.fromPlatform(); + return '${info.version}+${info.buildNumber}'; + } catch (e) { + appLogger.error('iOS-on-Mac notice: no app version: $e', tag: 'Platform'); + return null; + } + } +} + +/// Wraps the app's first screen and offers the notice after it has drawn. +class IosOnMacNoticeGate extends StatefulWidget { + const IosOnMacNoticeGate({super.key, required this.child}); + + final Widget child; + + @override + State createState() => _IosOnMacNoticeGateState(); +} + +class _IosOnMacNoticeGateState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) IosOnMacNotice.onLaunch(context); + }); + } + + @override + Widget build(BuildContext context) => widget.child; +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index defe6932..237b3679 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -68,7 +68,7 @@ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* meshcore_open.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = meshcore_open.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* meshcore_open.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Offband MeshCore.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -482,7 +482,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/meshcore_open.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/meshcore_open"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Offband MeshCore.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Offband MeshCore"; }; name = Debug; }; @@ -497,7 +497,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/meshcore_open.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/meshcore_open"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Offband MeshCore.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Offband MeshCore"; }; name = Release; }; @@ -512,7 +512,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/meshcore_open.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/meshcore_open"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Offband MeshCore.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Offband MeshCore"; }; name = Profile; }; diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index b7622d77..7fb472ce 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -5,7 +5,7 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = offband_meshcore +PRODUCT_NAME = Offband MeshCore // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = app.offband.meshcore diff --git a/test/widgets/ios_on_mac_notice_test.dart b/test/widgets/ios_on_mac_notice_test.dart new file mode 100644 index 00000000..a8a7390e --- /dev/null +++ b/test/widgets/ios_on_mac_notice_test.dart @@ -0,0 +1,150 @@ +// The iPhone-app-on-a-Mac notice (#1140): shown only for the iOS app running +// on a Mac, once per app version, with no link to another download. + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/l10n/app_localizations.dart'; +import 'package:meshcore_open/widgets/ios_on_mac_notice.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +Future _prefs() { + SharedPreferences.setMockInitialValues({}); + return SharedPreferences.getInstance(); +} + +/// One app launch. A button stands in for the gate's post-frame call. +Future _launch( + WidgetTester tester, + SharedPreferences prefs, { + bool isIOS = true, + bool onMac = true, + String version = '1.8.1+70', +}) async { + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Builder( + builder: (context) => Scaffold( + body: TextButton( + onPressed: () => IosOnMacNotice.onLaunch( + context, + isIOS: isIOS, + isOnMac: () async => onMac, + prefs: prefs, + installedVersion: () async => version, + ), + child: const Text('launch'), + ), + ), + ), + ), + ); + await tester.tap(find.text('launch')); + await tester.pumpAndSettle(); +} + +final _title = find.text('iPhone app on a Mac'); + +Future _dismiss(WidgetTester tester) async { + await tester.tap(find.text('OK')); + await tester.pumpAndSettle(); +} + +void main() { + testWidgets('the iOS app on a Mac shows it, with OK as its only button', ( + tester, + ) async { + await _launch(tester, await _prefs()); + expect(_title, findsOneWidget); + expect(find.byType(TextButton), findsOneWidget); // the test's own button + expect(find.byType(FilledButton), findsOneWidget); + await _dismiss(tester); + expect(_title, findsNothing); + }); + + testWidgets('an iPhone or iPad never shows it', (tester) async { + await _launch(tester, await _prefs(), onMac: false); + expect(_title, findsNothing); + }); + + testWidgets('no other platform asks or shows it', (tester) async { + var asked = false; + final prefs = await _prefs(); + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Builder( + builder: (context) => TextButton( + onPressed: () => IosOnMacNotice.onLaunch( + context, + isIOS: false, + isOnMac: () async => asked = true, + prefs: prefs, + installedVersion: () async => '1.8.1+70', + ), + child: const Text('launch'), + ), + ), + ), + ); + await tester.tap(find.text('launch')); + await tester.pumpAndSettle(); + expect(asked, isFalse); + expect(_title, findsNothing); + }); + + testWidgets('asks the iOS app over offband/platform, isiOSAppOnMac', ( + tester, + ) async { + final calls = []; + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + const MethodChannel('offband/platform'), + (call) async { + calls.add(call.method); + return true; + }, + ); + addTearDown( + () => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + const MethodChannel('offband/platform'), + null, + ), + ); + final prefs = await _prefs(); + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Builder( + builder: (context) => TextButton( + onPressed: () => IosOnMacNotice.onLaunch( + context, + isIOS: true, + prefs: prefs, + installedVersion: () async => '1.8.1+70', + ), + child: const Text('launch'), + ), + ), + ), + ); + await tester.tap(find.text('launch')); + await tester.pumpAndSettle(); + expect(calls, ['isiOSAppOnMac']); + expect(_title, findsOneWidget); + }); + + testWidgets('once per version: not again for the same one, again after an ' + 'update', (tester) async { + final prefs = await _prefs(); + await _launch(tester, prefs); + await _dismiss(tester); + await _launch(tester, prefs); + expect(_title, findsNothing); + await _launch(tester, prefs, version: '1.8.2+71'); + expect(_title, findsOneWidget); + }); +} diff --git a/untranslated.json b/untranslated.json index 0bce3ab7..dc0f1162 100644 --- a/untranslated.json +++ b/untranslated.json @@ -118,6 +118,8 @@ "sponsorPrompt_anytime", "sponsorPrompt_notNow", "sponsorPrompt_never", + "iosOnMac_title", + "iosOnMac_body", "settings_aboutLinkFailed", "settings_aboutPresetsAttribution", "settings_infoFirmware", @@ -518,6 +520,8 @@ "sponsorPrompt_anytime", "sponsorPrompt_notNow", "sponsorPrompt_never", + "iosOnMac_title", + "iosOnMac_body", "settings_aboutLinkFailed", "settings_aboutPresetsAttribution", "settings_infoFirmware", @@ -918,6 +922,8 @@ "sponsorPrompt_anytime", "sponsorPrompt_notNow", "sponsorPrompt_never", + "iosOnMac_title", + "iosOnMac_body", "settings_aboutLinkFailed", "settings_aboutPresetsAttribution", "settings_infoFirmware", @@ -1318,6 +1324,8 @@ "sponsorPrompt_anytime", "sponsorPrompt_notNow", "sponsorPrompt_never", + "iosOnMac_title", + "iosOnMac_body", "settings_aboutLinkFailed", "settings_aboutPresetsAttribution", "settings_infoFirmware", @@ -1718,6 +1726,8 @@ "sponsorPrompt_anytime", "sponsorPrompt_notNow", "sponsorPrompt_never", + "iosOnMac_title", + "iosOnMac_body", "settings_aboutLinkFailed", "settings_aboutPresetsAttribution", "settings_infoFirmware", @@ -2118,6 +2128,8 @@ "sponsorPrompt_anytime", "sponsorPrompt_notNow", "sponsorPrompt_never", + "iosOnMac_title", + "iosOnMac_body", "settings_aboutLinkFailed", "settings_aboutPresetsAttribution", "settings_infoFirmware", @@ -2518,6 +2530,8 @@ "sponsorPrompt_anytime", "sponsorPrompt_notNow", "sponsorPrompt_never", + "iosOnMac_title", + "iosOnMac_body", "settings_aboutLinkFailed", "settings_aboutPresetsAttribution", "settings_infoFirmware", @@ -2918,6 +2932,8 @@ "sponsorPrompt_anytime", "sponsorPrompt_notNow", "sponsorPrompt_never", + "iosOnMac_title", + "iosOnMac_body", "settings_aboutLinkFailed", "settings_aboutPresetsAttribution", "settings_infoFirmware", @@ -3318,6 +3334,8 @@ "sponsorPrompt_anytime", "sponsorPrompt_notNow", "sponsorPrompt_never", + "iosOnMac_title", + "iosOnMac_body", "settings_aboutLinkFailed", "settings_aboutPresetsAttribution", "settings_infoFirmware", @@ -3718,6 +3736,8 @@ "sponsorPrompt_anytime", "sponsorPrompt_notNow", "sponsorPrompt_never", + "iosOnMac_title", + "iosOnMac_body", "settings_aboutLinkFailed", "settings_aboutPresetsAttribution", "settings_infoFirmware", @@ -4118,6 +4138,8 @@ "sponsorPrompt_anytime", "sponsorPrompt_notNow", "sponsorPrompt_never", + "iosOnMac_title", + "iosOnMac_body", "settings_aboutLinkFailed", "settings_aboutPresetsAttribution", "settings_infoFirmware", @@ -4518,6 +4540,8 @@ "sponsorPrompt_anytime", "sponsorPrompt_notNow", "sponsorPrompt_never", + "iosOnMac_title", + "iosOnMac_body", "settings_aboutLinkFailed", "settings_aboutPresetsAttribution", "settings_infoFirmware", @@ -4918,6 +4942,8 @@ "sponsorPrompt_anytime", "sponsorPrompt_notNow", "sponsorPrompt_never", + "iosOnMac_title", + "iosOnMac_body", "settings_aboutLinkFailed", "settings_aboutPresetsAttribution", "settings_infoFirmware", @@ -5318,6 +5344,8 @@ "sponsorPrompt_anytime", "sponsorPrompt_notNow", "sponsorPrompt_never", + "iosOnMac_title", + "iosOnMac_body", "settings_aboutLinkFailed", "settings_aboutPresetsAttribution", "settings_infoFirmware", @@ -5718,6 +5746,8 @@ "sponsorPrompt_anytime", "sponsorPrompt_notNow", "sponsorPrompt_never", + "iosOnMac_title", + "iosOnMac_body", "settings_aboutLinkFailed", "settings_aboutPresetsAttribution", "settings_infoFirmware", @@ -6118,6 +6148,8 @@ "sponsorPrompt_anytime", "sponsorPrompt_notNow", "sponsorPrompt_never", + "iosOnMac_title", + "iosOnMac_body", "settings_aboutLinkFailed", "settings_aboutPresetsAttribution", "settings_infoFirmware", @@ -6518,6 +6550,8 @@ "sponsorPrompt_anytime", "sponsorPrompt_notNow", "sponsorPrompt_never", + "iosOnMac_title", + "iosOnMac_body", "settings_aboutLinkFailed", "settings_aboutPresetsAttribution", "settings_infoFirmware", From 9c5c136570c306835fc54802481b640ea334f95a Mon Sep 17 00:00:00 2001 From: diffwireauto <330573775+diffwireauto@users.noreply.github.com> Date: Wed, 11 Nov 2026 12:24:58 -0500 Subject: [PATCH 2/2] fix(#1143): let the sandboxed Mac app open the Save dialog The Mac app is sandboxed but never declared files.user-selected.read-write, so macOS refused the native Save dialog and every desktop export (logs, serial captures, config) failed on the notarized build. Both entitlement files gain the key; the sandbox stays on by the owner's decision. build.yml and macos-signed.yml now read the signed app's entitlements with PlistBuddy and fail if the key is not true, so a regression stops CI instead of reaching a tester. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 6 ++++++ .github/workflows/macos-signed.yml | 6 ++++++ macos/Runner/DebugProfile.entitlements | 2 ++ macos/Runner/Release.entitlements | 2 ++ 4 files changed, 16 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d8712a43..13fe7bdf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -271,6 +271,12 @@ jobs: echo "::error::CFBundleName is '${NAME}', expected 'Offband MeshCore' (#1140)." exit 1 fi + codesign -d --entitlements - --xml "${APP}" > "${RUNNER_TEMP}/entitlements.plist" + SAVE="$(/usr/libexec/PlistBuddy -c 'Print :com.apple.security.files.user-selected.read-write' "${RUNNER_TEMP}/entitlements.plist" || true)" + if [ "${SAVE}" != "true" ]; then + echo "::error::App lacks files.user-selected.read-write; the sandbox will refuse the Save dialog (#1143)." + exit 1 + fi lipo -archs "${APP}/Contents/MacOS/Offband MeshCore" || true ditto -c -k --keepParent "${APP}" offband-macos.zip - name: Upload macOS build diff --git a/.github/workflows/macos-signed.yml b/.github/workflows/macos-signed.yml index 2c6e34e3..2e95393f 100644 --- a/.github/workflows/macos-signed.yml +++ b/.github/workflows/macos-signed.yml @@ -137,6 +137,12 @@ jobs: done < <(find "${APP}/Contents/Frameworks" -depth -name '*.framework' -type d -print0) "${SIGN[@]}" --entitlements macos/Runner/Release.entitlements "${APP}" codesign --verify --deep --strict --verbose=2 "${APP}" + codesign -d --entitlements - --xml "${APP}" > "${RUNNER_TEMP}/entitlements.plist" + SAVE="$(/usr/libexec/PlistBuddy -c 'Print :com.apple.security.files.user-selected.read-write' "${RUNNER_TEMP}/entitlements.plist" || true)" + if [ "${SAVE}" != "true" ]; then + echo "::error::Signed app lacks files.user-selected.read-write; the sandbox will refuse the Save dialog (#1143)." + exit 1 + fi lipo -archs "${APP}/Contents/MacOS/Offband MeshCore" # ditto, not zip: it preserves the .app bundle's symlinks (#1108). The diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index 2fcad1b4..f9e0ad11 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -22,5 +22,7 @@ com.apple.security.device.camera + com.apple.security.files.user-selected.read-write + diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index 2b1c6948..73d0c856 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -18,5 +18,7 @@ com.apple.security.device.camera + com.apple.security.files.user-selected.read-write +