diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..245aaef --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: MegaManSec diff --git a/Magic Switch.xcodeproj/project.pbxproj b/Magic Switch.xcodeproj/project.pbxproj index ed8afd5..63a65cb 100644 --- a/Magic Switch.xcodeproj/project.pbxproj +++ b/Magic Switch.xcodeproj/project.pbxproj @@ -468,6 +468,66 @@ }; name = Release; }; + 0A0000A000000000000000C1 /* AppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = LA3A27SJGR; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_STORE $(inherited)"; + SWIFT_COMPILATION_MODE = wholemodule; + VERSIONING_SYSTEM = ""; + }; + name = AppStore; + }; 0774741A2CD8426F00571393 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -524,6 +584,34 @@ }; name = Release; }; + 0A0000A000000000000000C2 /* AppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "Magic Switch/Magic_Switch.entitlements"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 2; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_ASSET_PATHS = "\"Magic Switch/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Magic Switch/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.0; + MARKETING_VERSION = 0.0.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.megamansec.magic-switch"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = AppStore; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -532,6 +620,7 @@ buildConfigurations = ( 077474172CD8426F00571393 /* Debug */, 077474182CD8426F00571393 /* Release */, + 0A0000A000000000000000C1 /* AppStore */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -541,6 +630,7 @@ buildConfigurations = ( 0774741A2CD8426F00571393 /* Debug */, 0774741B2CD8426F00571393 /* Release */, + 0A0000A000000000000000C2 /* AppStore */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/Magic Switch.xcodeproj/xcshareddata/xcschemes/Magic Switch.xcscheme b/Magic Switch.xcodeproj/xcshareddata/xcschemes/Magic Switch.xcscheme index 81eafaf..0919707 100644 --- a/Magic Switch.xcodeproj/xcshareddata/xcschemes/Magic Switch.xcscheme +++ b/Magic Switch.xcodeproj/xcshareddata/xcschemes/Magic Switch.xcscheme @@ -71,7 +71,7 @@ buildConfiguration = "Debug"> diff --git a/Magic Switch/Manager/UpdateChecker.swift b/Magic Switch/Manager/UpdateChecker.swift index a7bfba5..7e0200e 100644 --- a/Magic Switch/Manager/UpdateChecker.swift +++ b/Magic Switch/Manager/UpdateChecker.swift @@ -96,9 +96,22 @@ final class UpdateChecker: ObservableObject { return Self.isNewer(latest, than: currentVersion) } + /// App Store builds must update through the App Store, so they never check GitHub. + static let isEnabled: Bool = { + #if APP_STORE + return false + #else + return true + #endif + }() + // MARK: - Initialization private init() { + guard Self.isEnabled else { + NotificationManager.removeNotification(identifier: Constants.updateNotificationID) + return + } // Surface the cached result immediately so the menu / Settings reflect the // last successful check without waiting for a network round trip. latestVersion = UserDefaults.standard.string(forKey: Constants.latestVersionKey) @@ -161,7 +174,7 @@ final class UpdateChecker: ObservableObject { /// The actual network fetch. A `manual` check surfaces success/failure on the /// button; automatic ones stay silent. One in-flight request at a time. private func performCheck(manual: Bool) { - guard !isChecking else { return } + guard Self.isEnabled, !isChecking else { return } guard let url = URL(string: Constants.latestReleaseAPI) else { return } isChecking = true if manual { lastCheckFailed = false } diff --git a/Magic Switch/View/Settings/OtherSettingsView.swift b/Magic Switch/View/Settings/OtherSettingsView.swift index d2ddbc7..e5499b1 100644 --- a/Magic Switch/View/Settings/OtherSettingsView.swift +++ b/Magic Switch/View/Settings/OtherSettingsView.swift @@ -116,25 +116,32 @@ struct OtherSettingsView: View { Text(updateChecker.currentVersion) .foregroundColor(.secondary) } - HStack { - Button { - updateChecker.checkNow() - } label: { - if updateChecker.isChecking { - HStack(spacing: 6) { - ProgressView() - .controlSize(.small) - Text("Checking…") + #if !APP_STORE + HStack { + Button { + updateChecker.checkNow() + } label: { + if updateChecker.isChecking { + HStack(spacing: 6) { + ProgressView() + .controlSize(.small) + Text("Checking…") + } + } else { + Text("Check for Updates") } - } else { - Text("Check for Updates") } + .disabled(updateChecker.isChecking) + .help("Check GitHub for a newer release right now.") + Spacer() + updateStatus } - .disabled(updateChecker.isChecking) - .help("Check GitHub for a newer release right now.") - Spacer() - updateStatus - } + SettingsRowView( + title: "Sponsor on GitHub", + help: "Support Magic Switch's development. Opens GitHub Sponsors in your browser.", + action: openSponsorPage + ) + #endif } } .onAppear(perform: refreshOnAppear) @@ -246,6 +253,11 @@ struct OtherSettingsView: View { openURL(url) } + private func openSponsorPage() { + guard let url = URL(string: "https://github.com/sponsors/MegaManSec") else { return } + openURL(url) + } + private func openLatestRelease() { guard let url = updateChecker.releasePageURL else { return } openURL(url) diff --git a/PRIVACY.md b/PRIVACY.md index e6ab68b..f4f1665 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -25,9 +25,11 @@ So your Macs can find each other, the app announces itself on the local network ## Update checks -At most once a day while the app is running, and whenever you click **Check for Updates**, the app asks GitHub (`api.github.com`) for the latest Magic Switch release. The request contains no identifiers or personal data, but GitHub sees your IP address as it would for any web request. GitHub's handling of that is covered by the [GitHub General Privacy Statement](https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement). +The version of Magic Switch downloaded from GitHub checks for updates; the Mac App Store version doesn't, since the App Store updates it. -Links in the app, such as the release page and the license, open in your web browser. +At most once a day while the app is running, and whenever you click **Check for Updates**, the GitHub version asks GitHub (`api.github.com`) for the latest Magic Switch release. The request contains no identifiers or personal data, but GitHub sees your IP address as it would for any web request. GitHub's handling of that is covered by the [GitHub General Privacy Statement](https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement). + +Links in the app, such as the release page, the license and GitHub Sponsors, open in your web browser. ## Permissions diff --git a/README.md b/README.md index 31adaab..0f56477 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,10 @@ Known limits: - On some systems an ad-hoc-signed build can't register with Notification Center at all, so failure notifications never fire no matter what the permission says; the Pairing and Macs tabs surface their errors inline as a fallback. - Sixty bits of entropy in the pairing code is fine against an online attacker (rate limit makes brute force infeasible) but theoretically grindable offline if someone captures ciphertext. PBKDF2 stretching pushes the cost up but doesn't eliminate it; a PAKE would close the gap and is the obvious next step. +## Support + +If Magic Switch is useful to you, you can support its development by [sponsoring me on GitHub](https://github.com/sponsors/MegaManSec). + ## License GNU GPL v3.0. See [LICENSE](LICENSE).