pull/1141/merge
Benjamin Wiechel 1 day ago committed by GitHub
commit 9b96a9b77b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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 <path to Runner.app>}"
app="${1:?usage: ios-assert-frameworks.sh <path to the .app>}"
frameworks="${app}/Frameworks"
if [ ! -d "${frameworks}" ]; then

@ -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,19 @@ 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
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
if: needs.classify.outputs.docs_only != 'true'

@ -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}"

@ -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,13 @@ 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"
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
# same zip is what Apple notarizes and what job 3 staples.
@ -218,7 +229,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}"

@ -55,7 +55,7 @@
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
@ -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";

@ -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)
}
}
}
}
}

@ -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.",

@ -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:

@ -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';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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()),
),
);
},
),

@ -315,6 +315,14 @@ const List<StorageSurface> 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,

@ -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<void> onLaunch(
BuildContext context, {
bool? isIOS,
Future<bool> Function()? isOnMac,
SharedPreferences? prefs,
Future<String?> 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<void>(
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<bool> _isOnMac() async {
try {
return await _channel.invokeMethod<bool>('isiOSAppOnMac') ?? false;
} catch (e) {
appLogger.warn('iOS-on-Mac check failed: $e', tag: 'Platform');
return false;
}
}
static Future<String?> _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<IosOnMacNoticeGate> createState() => _IosOnMacNoticeGateState();
}
class _IosOnMacNoticeGateState extends State<IosOnMacNoticeGate> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) IosOnMacNotice.onLaunch(context);
});
}
@override
Widget build(BuildContext context) => widget.child;
}

@ -68,7 +68,7 @@
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
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 = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
@ -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;
};

@ -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

@ -22,5 +22,7 @@
</array>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>

@ -18,5 +18,7 @@
</array>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>

@ -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<SharedPreferences> _prefs() {
SharedPreferences.setMockInitialValues({});
return SharedPreferences.getInstance();
}
/// One app launch. A button stands in for the gate's post-frame call.
Future<void> _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<void> _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 = <String>[];
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);
});
}

@ -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",

Loading…
Cancel
Save

Powered by TurnKey Linux.