From f58b8376e6a0e1d586df41bcf52f678aed8eefbe Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 18 Apr 2026 10:34:09 -0700 Subject: [PATCH] Additional onboarding cleanup --- Localizable.xcstrings | 413 ++++++++++++------ Meshtastic.xcodeproj/project.pbxproj | 10 +- Meshtastic/Extensions/View.swift | 20 + Meshtastic/Meshtastic.entitlements | 2 + Meshtastic/Views/Connect/Connect.swift | 9 +- .../Views/Onboarding/DeviceOnboarding.swift | 119 +++-- MeshtasticTests/ConnectViewTests.swift | 2 +- MeshtasticTests/DeviceOnboardingTests.swift | 181 ++++++++ 8 files changed, 567 insertions(+), 189 deletions(-) create mode 100644 MeshtasticTests/DeviceOnboardingTests.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 30e6245e..d9e465dd 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -2,7 +2,6 @@ "sourceLanguage" : "en", "strings" : { "" : { - "shouldTranslate" : false, "localizations" : { "da" : { "stringUnit" : { @@ -10,7 +9,8 @@ "value" : "" } } - } + }, + "shouldTranslate" : false }, "\t%@" : { "localizations" : { @@ -225,100 +225,104 @@ }, "shouldTranslate" : false }, - " : %@" : { + ": %@" : { "localizations" : { - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : " : %@" - } - }, "es" : { "stringUnit" : { "state" : "translated", - "value" : " : %@" + "value" : ": %@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : " : %@" + "value" : ": %@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : " : %@" + "value" : ": %@" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : " : %@" + "value" : ": %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : " : %@" + "value" : ": %@" } }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", - "value" : " : %@" + "value" : ": %@" } } }, "shouldTranslate" : false }, - " : %d" : { + ": %d" : { "localizations" : { - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : " : %d" - } - }, "es" : { "stringUnit" : { "state" : "translated", - "value" : " : %d" + "value" : ": %d" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : " : %d" + "value" : ": %d" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : " : %d" + "value" : ": %d" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : " : %d" + "value" : ": %d" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : " : %d" + "value" : ": %d" } }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", - "value" : " : %d" + "value" : ": %d" } } }, "shouldTranslate" : false }, + "\"Disconnect Meshtastic\" — disconnect from the connected BLE node." : { + "comment" : "A description of how to use the \"Disconnect Node\" Siri shortcut.", + "isCommentAutoGenerated" : true + }, + "\"Send a Meshtastic direct message\" — send a private message to a node." : { + "comment" : "A description of how to send a direct message to a node using Siri.", + "isCommentAutoGenerated" : true + }, + "\"Send a Meshtastic group message\" — send a message to a mesh channel." : { + "comment" : "A description of how to send a group message using Siri.", + "isCommentAutoGenerated" : true + }, + "\"Shut down my Meshtastic node\" or \"Restart my Meshtastic node\"." : { + "comment" : "A description of how to use Siri to restart or shut down a node.", + "isCommentAutoGenerated" : true + }, "(Re)define PIN_GPS_EN for your board." : { "localizations" : { "da" : { @@ -2352,6 +2356,7 @@ } }, "🦕 End of life Version 🦖 ☄️" : { + "extractionState" : "stale", "localizations" : { "da" : { "stringUnit" : { @@ -3018,7 +3023,9 @@ } } }, - "A default self-signed certificate is included for localhost connections. Import a custom .p12 if needed. Client CA (.pem) validates connecting TAK clients." : {}, + "A default self-signed certificate is included for localhost connections. Import a custom .p12 if needed. Client CA (.pem) validates connecting TAK clients." : { + + }, "A green lock means the channel is securely encrypted with either a 128 or 256 bit AES key." : { "localizations" : { "es" : { @@ -3863,7 +3870,9 @@ } } }, - "Add CA" : {}, + "Add CA" : { + + }, "Add Channel" : { "localizations" : { "da" : { @@ -4083,6 +4092,7 @@ } }, "Additional help" : { + "extractionState" : "stale", "localizations" : { "da" : { "stringUnit" : { @@ -4134,6 +4144,10 @@ } } }, + "Additional Help" : { + "comment" : "A button that opens a link to the Meshtastic FAQ.", + "isCommentAutoGenerated" : true + }, "Address" : { "localizations" : { "da" : { @@ -7008,6 +7022,14 @@ } } }, + "Background Activity" : { + "comment" : "A title for a screen that describes the benefits of enabling background location tracking.", + "isCommentAutoGenerated" : true + }, + "Background Mesh Tracking" : { + "comment" : "A description of the background mesh tracking feature.", + "isCommentAutoGenerated" : true + }, "Backup" : { "localizations" : { "ja" : { @@ -7717,6 +7739,10 @@ } } }, + "Battery Usage" : { + "comment" : "A description of the battery usage of enabling background activity.", + "isCommentAutoGenerated" : true + }, "Baud" : { "localizations" : { "da" : { @@ -9471,6 +9497,10 @@ } } }, + "CarPlay Messaging" : { + "comment" : "A description of how to send a message to a mesh channel using CarPlay.", + "isCommentAutoGenerated" : true + }, "Categories" : { "localizations" : { "da" : { @@ -11484,8 +11514,12 @@ } } }, - "Client CA Certificate" : {}, - "Client Configuration" : {}, + "Client CA Certificate" : { + + }, + "Client Configuration" : { + + }, "Client Hidden" : { "extractionState" : "stale", "localizations" : { @@ -12186,7 +12220,9 @@ } } }, - "Configuration" : {}, + "Configuration" : { + + }, "Configuration for: %@" : { "localizations" : { "da" : { @@ -12447,6 +12483,10 @@ } } }, + "Configure Siri & Shortcuts" : { + "comment" : "A button that will open the app's settings to configure Siri and Shortcuts.", + "isCommentAutoGenerated" : true + }, "Confirm" : { "localizations" : { "da" : { @@ -12653,6 +12693,14 @@ } } }, + "Connect to nodes on your local Wi-Fi network." : { + "comment" : "A description of how to connect to nodes on your local Wi-Fi network.", + "isCommentAutoGenerated" : true + }, + "Connect to your Meshtastic node via Bluetooth Low Energy for the best messaging experience." : { + "comment" : "A description of the Bluetooth connectivity feature.", + "isCommentAutoGenerated" : true + }, "Connected" : { "localizations" : { "da" : { @@ -12735,6 +12783,10 @@ } } }, + "Connected firmware: **%@**" : { + "comment" : "A label displaying the firmware version of a device. The argument is the firmware version.", + "isCommentAutoGenerated" : true + }, "Connected Node %@" : { "localizations" : { "da" : { @@ -13221,6 +13273,14 @@ } } }, + "Continue" : { + "comment" : "A button that will continue to the next step in the onboarding process.", + "isCommentAutoGenerated" : true + }, + "Continuous Location Updates" : { + "comment" : "A description of the continuous location updates feature.", + "isCommentAutoGenerated" : true + }, "Control Type" : { "localizations" : { "da" : { @@ -13990,6 +14050,7 @@ } }, "Current Firmware Version: %@, Latest Firmware Version: %@" : { + "extractionState" : "stale", "localizations" : { "da" : { "stringUnit" : { @@ -14047,6 +14108,16 @@ } } }, + "Current Firmware Version: %@, Minimum Required Version: %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Current Firmware Version: %1$@, Minimum Required Version: %2$@" + } + } + } + }, "Current: %lld" : { "localizations" : { "da" : { @@ -14570,7 +14641,9 @@ } } }, - "Delete All" : {}, + "Delete All" : { + + }, "Delete all config, keys and BLE bonds? " : { "localizations" : { "es" : { @@ -18174,7 +18247,9 @@ } } }, - "Download TAK Server Data Package" : {}, + "Download TAK Server Data Package" : { + + }, "Drag & Drop Firmware Update" : { "localizations" : { "da" : { @@ -18819,6 +18894,10 @@ } } }, + "Enable Background Activity" : { + "comment" : "A toggle to enable or disable background activity.", + "isCommentAutoGenerated" : true + }, "Enable broadcasting device metrics to the mesh network. When disabled, metrics are only sent to connected clients." : { "localizations" : { "es" : { @@ -18961,7 +19040,9 @@ } } }, - "Enable TAK Server" : {}, + "Enable TAK Server" : { + + }, "Enable this device as a Store and Forward server. Requires an ESP32 device with PSRAM." : { "localizations" : { "da" : { @@ -19351,6 +19432,10 @@ } } }, + "Enabling background activity may increase battery usage. You can toggle this at any time in the app settings." : { + "comment" : "A description of the battery usage of enabling background activity.", + "isCommentAutoGenerated" : true + }, "Enabling Ethernet will disable the bluetooth connection to the app." : { "localizations" : { "da" : { @@ -19728,8 +19813,12 @@ } } }, - "Enter P12 Password" : {}, - "Enter the password for the PKCS#12 file" : {}, + "Enter P12 Password" : { + + }, + "Enter the password for the PKCS#12 file" : { + + }, "environment" : { "extractionState" : "stale", "localizations" : { @@ -22118,6 +22207,7 @@ } }, "Firmware update docs" : { + "extractionState" : "stale", "localizations" : { "da" : { "stringUnit" : { @@ -22169,6 +22259,14 @@ } } }, + "Firmware Update Docs" : { + "comment" : "A link to the firmware update documentation.", + "isCommentAutoGenerated" : true + }, + "Firmware Update Required" : { + "comment" : "A title for a screen that displays a firmware update is required message.", + "isCommentAutoGenerated" : true + }, "Firmware Updates" : { "localizations" : { "da" : { @@ -23771,7 +23869,9 @@ } } }, - "Generate a data package (.zip) to configure TAK clients to connect to this server." : {}, + "Generate a data package (.zip) to configure TAK clients to connect to this server." : { + + }, "Generate a new private key to replace the one currently in use. The public key will automatically be regenerated from your private key." : { "localizations" : { "es" : { @@ -26181,6 +26281,10 @@ } } }, + "How to Update" : { + "comment" : "A label displayed above the list of available firmware update options.", + "isCommentAutoGenerated" : true + }, "How to update Firmware" : { "localizations" : { "da" : { @@ -27266,10 +27370,18 @@ } } }, - "Import" : {}, - "Import .pem" : {}, - "Import Custom .p12" : {}, - "Import Error" : {}, + "Import" : { + + }, + "Import .pem" : { + + }, + "Import Custom .p12" : { + + }, + "Import Error" : { + + }, "Import Route" : { "localizations" : { "da" : { @@ -28066,6 +28178,10 @@ } } }, + "Keep the mesh map updated and send your position to the mesh even while using other apps." : { + "comment" : "A description of the benefits of continuous location updates.", + "isCommentAutoGenerated" : true + }, "Key" : { "localizations" : { "da" : { @@ -31354,7 +31470,12 @@ "comment" : "A description of the read-only mode feature in TAK Server.", "isCommentAutoGenerated" : true }, + "Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app." : { + "comment" : "A description of how user data is used by Meshtastic.", + "isCommentAutoGenerated" : true + }, "Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. You can opt out under app settings." : { + "extractionState" : "stale", "localizations" : { "es" : { "stringUnit" : { @@ -31682,6 +31803,10 @@ } } }, + "Message Notifications" : { + "comment" : "A description of the message notifications feature.", + "isCommentAutoGenerated" : true + }, "Message received from the text message app." : { "extractionState" : "stale", "localizations" : { @@ -32201,6 +32326,10 @@ } } }, + "Minimum required: **%@**" : { + "comment" : "A label displaying the minimum required firmware version.", + "isCommentAutoGenerated" : true + }, "Minimum time between detection broadcasts" : { "extractionState" : "stale", "localizations" : { @@ -32997,7 +33126,9 @@ } } }, - "mTLS" : {}, + "mTLS" : { + + }, "Multiplier" : { "localizations" : { "da" : { @@ -36935,6 +37066,10 @@ } } }, + "Open Web Flasher" : { + "comment" : "A button that opens the Web Flasher app.", + "isCommentAutoGenerated" : true + }, "Optimized for 2 color displays" : { "extractionState" : "stale", "localizations" : { @@ -39159,7 +39294,9 @@ } } }, - "Port" : {}, + "Port" : { + + }, "Position" : { "localizations" : { "da" : { @@ -41905,6 +42042,10 @@ } } }, + "Read and reply to Meshtastic channel and direct messages directly from your car's display using CarPlay." : { + "comment" : "A description of how to use CarPlay with Meshtastic.", + "isCommentAutoGenerated" : true + }, "Read-Only Mode" : { "comment" : "A toggle that allows the user to enable or disable read-only mode for the TAK server.", "isCommentAutoGenerated" : true @@ -42212,6 +42353,14 @@ } } }, + "Receive notifications for incoming messages and critical alerts even when the app is in the background." : { + "comment" : "A description of the notification feature.", + "isCommentAutoGenerated" : true + }, + "Receive position updates from other nodes and maintain an accurate picture of the mesh while in the background." : { + "comment" : "A description of the benefits of enabling background mesh tracking.", + "isCommentAutoGenerated" : true + }, "Received a negative acknowledgment" : { "extractionState" : "stale", "localizations" : { @@ -42481,6 +42630,10 @@ } } }, + "Recommended secure version: **%@**" : { + "comment" : "A label displaying the recommended secure version of the connected device.", + "isCommentAutoGenerated" : true + }, "Recording route" : { "localizations" : { "da" : { @@ -42816,7 +42969,9 @@ } } }, - "Reload Bundled Certificates" : {}, + "Reload Bundled Certificates" : { + + }, "Remote administration for: %@" : { "localizations" : { "da" : { @@ -43623,7 +43778,9 @@ } } }, - "Reset to Default" : {}, + "Reset to Default" : { + + }, "Restart" : { "localizations" : { "da" : { @@ -43676,7 +43833,9 @@ } } }, - "Restart Server" : {}, + "Restart Server" : { + + }, "Restart to the node you are connected to" : { "localizations" : { "da" : { @@ -46448,8 +46607,6 @@ } } }, - "Secure mTLS connection on port 8089. Both server and client certificates are required." : {}, - "Secure mTLS connection on port 8089. Both server and client certificates are required. TAK Channel Index selects the channel index where TAK messages will be sent." : { "comment" : "A footer for the TAK Server configuration section.", "isCommentAutoGenerated" : true @@ -46512,6 +46669,10 @@ } } }, + "Security Advisory" : { + "comment" : "A title for a security advisory displayed in a card.", + "isCommentAutoGenerated" : true + }, "Security Config" : { "localizations" : { "da" : { @@ -46628,6 +46789,10 @@ } } }, + "Security Update Recommended" : { + "comment" : "A title for a view that warns the user that their device is running an outdated firmware version.", + "isCommentAutoGenerated" : true + }, "Select" : { "extractionState" : "stale", "localizations" : { @@ -47581,6 +47746,10 @@ } } }, + "Send and receive Meshtastic messages hands-free using Siri and CarPlay." : { + "comment" : "A description of how to use Siri and CarPlay with Meshtastic.", + "isCommentAutoGenerated" : true + }, "Send ASCII bell with alert message. Useful for triggering external notification on bell." : { "localizations" : { "da" : { @@ -49143,7 +49312,9 @@ } } }, - "Server Certificate" : {}, + "Server Certificate" : { + + }, "Server Option" : { "localizations" : { "da" : { @@ -49190,7 +49361,9 @@ } } }, - "Server Status" : {}, + "Server Status" : { + + }, "Set" : { "localizations" : { "da" : { @@ -49237,6 +49410,10 @@ } } }, + "Set a channel name" : { + "comment" : "A label for a button that sets a channel name.", + "isCommentAutoGenerated" : true + }, "Set LoRa Region" : { "localizations" : { "da" : { @@ -49856,6 +50033,10 @@ } } }, + "Share with TAK Buddies" : { + "comment" : "A button that shares the QR code with TAK buddies.", + "isCommentAutoGenerated" : true + }, "Share your location in real-time and keep your group coordinated with integrated GPS features." : { "localizations" : { "de" : { @@ -50614,6 +50795,10 @@ } } }, + "Shut Down / Restart Node" : { + "comment" : "A Siri shortcut to restart or shut down a node.", + "isCommentAutoGenerated" : true + }, "Shut Down Node?" : { "localizations" : { "da" : { @@ -50970,6 +51155,14 @@ } } }, + "Siri & CarPlay" : { + "comment" : "A description of how to use Siri and CarPlay with Meshtastic.", + "isCommentAutoGenerated" : true + }, + "Siri, Shortcuts & CarPlay" : { + "comment" : "A label displayed above the Siri, Shortcuts & CarPlay onboarding view.", + "isCommentAutoGenerated" : true + }, "Six Hours" : { "extractionState" : "stale", "localizations" : { @@ -52018,7 +52211,9 @@ } } }, - "Status" : {}, + "Status" : { + + }, "Stay Connected Anywhere" : { "localizations" : { "de" : { @@ -52656,9 +52851,18 @@ } } } + }, + "TAK Cannot Be Used on Public Channel" : { + "comment" : "A warning displayed when the user's primary channel is public.", + "isCommentAutoGenerated" : true + }, + "TAK Channel Index" : { + "comment" : "A label for the TAK channel index.", + "isCommentAutoGenerated" : true + }, + "TAK Server" : { }, - "TAK Server" : {}, "TAK Tracker" : { "extractionState" : "stale", "localizations" : { @@ -53942,7 +54146,12 @@ } } }, + "The Meshtastic Apple app requires firmware version %@ or later. Older firmware versions are no longer supported and may have compatibility issues or missing features." : { + "comment" : "A body text that explains that the app requires a certain version of the firmware.", + "isCommentAutoGenerated" : true + }, "The Meshtastic Apple apps support firmware version %@ and above." : { + "extractionState" : "stale", "localizations" : { "da" : { "stringUnit" : { @@ -55989,7 +56198,9 @@ } } }, - "TLS Certificates" : {}, + "TLS Certificates" : { + + }, "TLS Enabled" : { "localizations" : { "da" : { @@ -60062,6 +60273,7 @@ } }, "Version %@ includes substantial network optimizations and extensive changes to devices and client apps. Only nodes version %@ and above are supported." : { + "extractionState" : "stale", "localizations" : { "da" : { "stringUnit" : { @@ -61187,6 +61399,7 @@ } }, "Welcome to" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -61214,6 +61427,10 @@ } } }, + "Welcome to Meshtastic" : { + "comment" : "The title of the onboarding screen.", + "isCommentAutoGenerated" : true + }, "What does the lock mean?" : { "localizations" : { "da" : { @@ -62416,6 +62633,10 @@ "comment" : "A message displayed when a user successfully configures their primary channel for TAK. It instructs the user to share the QR code to invite TAK buddies.", "isCommentAutoGenerated" : true }, + "Your connected device is running firmware older than **%@**, which contains known security vulnerabilities. Updating your firmware is strongly recommended to protect your device and mesh network." : { + "comment" : "A body text that describes the security advisory.", + "isCommentAutoGenerated" : true + }, "Your current location will be set as the fixed position and broadcast over the mesh on the position interval." : { "localizations" : { "da" : { @@ -62889,88 +63110,6 @@ } } } - }, - ": %@" : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %@" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %@" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %@" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %@" - } - } - }, - "shouldTranslate" : false - }, - ": %d" : { - "localizations" : { - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : ": %d" - } - } - }, - "shouldTranslate" : false } }, "version" : "1.1" diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index e3504190..0d1bdb7e 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -83,6 +83,7 @@ 25F5D5C22C3F6E4B008036E3 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5C12C3F6E4B008036E3 /* AppState.swift */; }; 25F5D5D12C4375DF008036E3 /* RouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5D02C4375DF008036E3 /* RouterTests.swift */; }; AA0001012E2730EC00600001 /* ConnectViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00010022E2730EC0060000 /* ConnectViewTests.swift */; }; + DD9C70102E9F2A0000029299 /* DeviceOnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9C70112E9F2A0000029299 /* DeviceOnboardingTests.swift */; }; 2849A5E4CE9FDC1DB33DFA34 /* TAKConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01028778B8BFD81F7A039593 /* TAKConnection.swift */; }; 300424F80C4A445A0FBAE82D /* TAKMeshtasticBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D006C85B250291D5925F30 /* TAKMeshtasticBridge.swift */; }; 3D3417B42E2730EC006A988B /* GeoJSONOverlayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */; }; @@ -204,6 +205,7 @@ DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD798B062915928D005217CD /* ChannelMessageList.swift */; }; DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8169FE272476C700F4AB02 /* LogDocument.swift */; }; DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD836AE626F6B38600ABCC23 /* Connect.swift */; }; + DD4756AB2E9F1A0000029299 /* SecurityVersionNag.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4756AA2E9F1A0000029299 /* SecurityVersionNag.swift */; }; DD86D40A287F04F100BAEB7A /* InvalidVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD86D409287F04F100BAEB7A /* InvalidVersion.swift */; }; DD86D40C287F401000BAEB7A /* SaveChannelQRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD86D40B287F401000BAEB7A /* SaveChannelQRCode.swift */; }; DD86D40F2881BE4C00BAEB7A /* CsvDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD86D40E2881BE4C00BAEB7A /* CsvDocument.swift */; }; @@ -413,6 +415,7 @@ 25F5D5C72C4375A8008036E3 /* MeshtasticTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeshtasticTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 25F5D5D02C4375DF008036E3 /* RouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterTests.swift; sourceTree = ""; }; AA00010022E2730EC0060000 /* ConnectViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewTests.swift; sourceTree = ""; }; + DD9C70112E9F2A0000029299 /* DeviceOnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceOnboardingTests.swift; sourceTree = ""; }; 2B37CCEE8B44A4BA123ED118 /* TAKServerManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKServerManager.swift; sourceTree = ""; }; 3D0A8ABAEF1E587683970927 /* EXICodec.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EXICodec.swift; sourceTree = ""; }; 3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJSONOverlayManager.swift; sourceTree = ""; }; @@ -565,6 +568,7 @@ DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 43.xcdatamodel"; sourceTree = ""; }; DD8169FE272476C700F4AB02 /* LogDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDocument.swift; sourceTree = ""; }; DD836AE626F6B38600ABCC23 /* Connect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connect.swift; sourceTree = ""; }; + DD4756AA2E9F1A0000029299 /* SecurityVersionNag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityVersionNag.swift; sourceTree = ""; }; DD86D409287F04F100BAEB7A /* InvalidVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvalidVersion.swift; sourceTree = ""; }; DD86D40B287F401000BAEB7A /* SaveChannelQRCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveChannelQRCode.swift; sourceTree = ""; }; DD86D40E2881BE4C00BAEB7A /* CsvDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CsvDocument.swift; sourceTree = ""; }; @@ -903,6 +907,7 @@ isa = PBXGroup; children = ( AA00010022E2730EC0060000 /* ConnectViewTests.swift */, + DD9C70112E9F2A0000029299 /* DeviceOnboardingTests.swift */, 25F5D5D02C4375DF008036E3 /* RouterTests.swift */, ); path = MeshtasticTests; @@ -1002,7 +1007,8 @@ isa = PBXGroup; children = ( DD836AE626F6B38600ABCC23 /* Connect.swift */, - DD86D409287F04F100BAEB7A /* InvalidVersion.swift */, + DD4756AA2E9F1A0000029299 /* SecurityVersionNag.swift */, + DD86D409287F04F100BAEB7A /* InvalidVersion.swift */ ); path = Connect; sourceTree = ""; @@ -1661,6 +1667,7 @@ buildActionMask = 2147483647; files = ( AA0001012E2730EC00600001 /* ConnectViewTests.swift in Sources */, + DD9C70102E9F2A0000029299 /* DeviceOnboardingTests.swift in Sources */, 25F5D5D12C4375DF008036E3 /* RouterTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1684,6 +1691,7 @@ DDA9515C2BC6631200CEA535 /* TelemetryEnums.swift in Sources */, DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */, DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */, + DD4756AB2E9F1A0000029299 /* SecurityVersionNag.swift in Sources */, D93069082B81DF040066FBC8 /* SaveConfigButton.swift in Sources */, DD5E523F298F5A9E00D21B61 /* AirQualityIndex.swift in Sources */, DDD5BB182C2F9C36007E03CA /* OSLogEntryLog.swift in Sources */, diff --git a/Meshtastic/Extensions/View.swift b/Meshtastic/Extensions/View.swift index ec27882d..1b70fa9b 100644 --- a/Meshtastic/Extensions/View.swift +++ b/Meshtastic/Extensions/View.swift @@ -46,6 +46,26 @@ extension View { self } } + + /// Standard capsule-shaped prominent button styling. + /// On iOS 26+ the button also receives a glass background effect. + @ViewBuilder + func capsuleButtonStyle() -> some View { + if #available(iOS 26.0, macOS 26.0, *) { + self + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .buttonStyle(.borderedProminent) + .glassEffect(in: .capsule) + } else { + self + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .buttonStyle(.borderedProminent) + } + } } private struct FirstAppear: ViewModifier { diff --git a/Meshtastic/Meshtastic.entitlements b/Meshtastic/Meshtastic.entitlements index e8c10bea..62fa1bb6 100644 --- a/Meshtastic/Meshtastic.entitlements +++ b/Meshtastic/Meshtastic.entitlements @@ -9,6 +9,8 @@ com.apple.developer.carplay-communication + com.apple.developer.siri + com.apple.developer.usernotifications.critical-alerts com.apple.developer.weatherkit diff --git a/Meshtastic/Views/Connect/Connect.swift b/Meshtastic/Views/Connect/Connect.swift index 5227a52d..572ddc1a 100644 --- a/Meshtastic/Views/Connect/Connect.swift +++ b/Meshtastic/Views/Connect/Connect.swift @@ -581,18 +581,18 @@ struct DeviceConnectRow: View { } // Show transport type #if !targetEnvironment(macCatalyst) - HStack(alignment: .center){ + HStack(alignment: .center) { TransportIcon(transportType: device.transportType) if device.isManualConnection && (device.longName != nil || device.shortName != nil) { - VStack (alignment: .leading) { + VStack(alignment: .leading) { Text("Last seen device:") Text("\(String(describing: device))") } } }.padding(.top, 3.0) #else - //Different alignment for Mac - HStack(alignment: .firstTextBaseline){ + // Different alignment for Mac + HStack(alignment: .firstTextBaseline) { TransportIcon(transportType: device.transportType) if device.isManualConnection && (device.longName != nil || device.shortName != nil) { Text("Last seen device: \(String(describing: device))") @@ -625,4 +625,3 @@ struct DeviceConnectRow: View { } } } - diff --git a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift index d684d870..a668e8cc 100644 --- a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift +++ b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift @@ -25,25 +25,20 @@ struct DeviceOnboarding: View { /// The Title View var title: some View { VStack { - Text("Welcome to") - .font(.title2.bold()) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - Text("Meshtastic") - .font(.largeTitle.bold()) + Text("Welcome to Meshtastic") + .font(.title.bold()) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) } } var welcomeView: some View { - VStack { + VStack(spacing: 0) { ScrollView(.vertical) { VStack { // Title title .padding(.top) - // Onboarding VStack(alignment: .leading, spacing: 16) { makeRow( icon: "antenna.radiowaves.left.and.right", @@ -63,14 +58,34 @@ struct DeviceOnboarding: View { makeRow( icon: "person.2.shield", title: String(localized: "User Privacy"), - subtitle: String(localized: "Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. You can opt out under app settings.") + subtitle: String(localized: "Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app.") + ) + makeRow( + icon: "bell.badge", + title: String(localized: "Message Notifications"), + subtitle: String(localized: "Receive notifications for incoming messages and critical alerts even when the app is in the background.") + ) + makeRow( + icon: "custom.bluetooth", + title: String(localized: "Bluetooth Connectivity"), + subtitle: String(localized: "Connect to your Meshtastic node via Bluetooth Low Energy for the best messaging experience.") + ) + makeRow( + icon: "network", + title: String(localized: "Local Network Access"), + subtitle: String(localized: "Connect to nodes on your local Wi-Fi network.") + ) + makeRow( + icon: "car.fill", + title: String(localized: "Siri & CarPlay"), + subtitle: String(localized: "Send and receive Meshtastic messages hands-free using Siri and CarPlay.") ) } - .padding() + .padding(.horizontal) + .padding(.bottom) } - .interactiveDismissDisabled() } - Spacer() + .interactiveDismissDisabled() Button { Task { await goToNextStep(after: nil) @@ -79,10 +94,7 @@ struct DeviceOnboarding: View { Text("Get started") .frame(maxWidth: .infinity) } - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .buttonStyle(.borderedProminent) + .capsuleButtonStyle() } } @@ -137,10 +149,7 @@ struct DeviceOnboarding: View { Text("Configure notification permissions") .frame(maxWidth: .infinity) } - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .buttonStyle(.borderedProminent) + .capsuleButtonStyle() } } @@ -206,10 +215,7 @@ struct DeviceOnboarding: View { .frame(maxWidth: .infinity) } .padding() - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .buttonStyle(.borderedProminent) + .capsuleButtonStyle() } } @@ -266,10 +272,7 @@ struct DeviceOnboarding: View { .frame(maxWidth: .infinity) } .padding() - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .buttonStyle(.borderedProminent) + .capsuleButtonStyle() } } @@ -316,10 +319,7 @@ struct DeviceOnboarding: View { .frame(maxWidth: .infinity) } .padding() - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .buttonStyle(.borderedProminent) + .capsuleButtonStyle() } } @@ -361,10 +361,7 @@ struct DeviceOnboarding: View { .frame(maxWidth: .infinity) } .padding() - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .buttonStyle(.borderedProminent) + .capsuleButtonStyle() } } @@ -372,7 +369,7 @@ struct DeviceOnboarding: View { VStack { ScrollView(.vertical) { VStack { - Text("Siri & Shortcuts") + Text("Siri, Shortcuts & CarPlay") .font(.largeTitle.bold()) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) @@ -382,6 +379,11 @@ struct DeviceOnboarding: View { .font(.body.bold()) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) + makeRow( + icon: "car.fill", + title: String(localized: "CarPlay Messaging"), + subtitle: String(localized: "Read and reply to Meshtastic channel and direct messages directly from your car's display using CarPlay.") + ) makeRow( icon: "message", title: String(localized: "Send a Group Message"), @@ -416,10 +418,7 @@ struct DeviceOnboarding: View { .frame(maxWidth: .infinity) } .padding() - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .buttonStyle(.borderedProminent) + .capsuleButtonStyle() } } @@ -445,7 +444,37 @@ struct DeviceOnboarding: View { } .toolbar(.hidden) } - + + @ViewBuilder + func makeCompactRow(icon: String, title: String, subtitle: String) -> some View { + HStack(alignment: .center, spacing: 12) { + Group { + if icon.starts(with: "custom.") { + Image(icon) + .resizable() + .symbolRenderingMode(.multicolor) + } else { + Image(systemName: icon) + .resizable() + .symbolRenderingMode(.multicolor) + } + } + .aspectRatio(contentMode: .fit) + .frame(width: 28, height: 28) + .padding(.leading, 4) + VStack(alignment: .leading, spacing: 1) { + Text(title) + .font(.footnote.weight(.semibold)) + .foregroundColor(.primary) + Text(subtitle) + .font(.caption) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + } + .accessibilityElement(children: .combine) + } + @ViewBuilder func makeRow( icon: String, @@ -474,11 +503,11 @@ struct DeviceOnboarding: View { } VStack(alignment: .leading) { Text(title) - .font(.subheadline.weight(.semibold)) + .font(.footnote.weight(.semibold)) .foregroundColor(.primary) .fixedSize(horizontal: false, vertical: true) Text(subtitle) - .font(.subheadline) + .font(.footnote) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) }.multilineTextAlignment(.leading) @@ -557,7 +586,7 @@ struct DeviceOnboarding: View { } func createSiriString() -> AttributedString { - var fullText = AttributedString("Meshtastic supports Siri and Shortcuts so you can control your mesh hands-free. You can update Siri permissions at any time from settings.") + var fullText = AttributedString("Meshtastic supports Siri, Shortcuts, and CarPlay so you can send and receive messages hands-free. You can update Siri permissions at any time from settings.") if let range = fullText.range(of: "settings") { fullText[range].link = URL(string: UIApplication.openSettingsURLString)! fullText[range].foregroundColor = .blue diff --git a/MeshtasticTests/ConnectViewTests.swift b/MeshtasticTests/ConnectViewTests.swift index cbbcd331..f44099ce 100644 --- a/MeshtasticTests/ConnectViewTests.swift +++ b/MeshtasticTests/ConnectViewTests.swift @@ -364,7 +364,7 @@ struct InvalidVersionTests { } @Test func viewCreationWithEmptyVersions() { - let view = InvalidVersion() + let view = InvalidVersion(minimumVersion: "", version: "") #expect(view.minimumVersion == "") #expect(view.version == "") } diff --git a/MeshtasticTests/DeviceOnboardingTests.swift b/MeshtasticTests/DeviceOnboardingTests.swift new file mode 100644 index 00000000..4831c87c --- /dev/null +++ b/MeshtasticTests/DeviceOnboardingTests.swift @@ -0,0 +1,181 @@ +// +// DeviceOnboardingTests.swift +// MeshtasticTests +// +// Copyright(c) Garth Vander Houwen 2026. +// + +import Foundation +import Testing +@testable import Meshtastic + +// MARK: - SetupGuide Enum + +@Suite("DeviceOnboarding.SetupGuide") +struct SetupGuideTests { + + @Test func allCasesExist() { + let cases: [DeviceOnboarding.SetupGuide] = [ + .notifications, .location, .backgroundActivity, + .localNetwork, .bluetooth, .siri + ] + #expect(cases.count == 6) + } + + @Test func isHashable() { + var seen = Set() + seen.insert(.notifications) + seen.insert(.notifications) // duplicate should not grow set + seen.insert(.siri) + #expect(seen.count == 2) + } + + @Test func equality() { + #expect(DeviceOnboarding.SetupGuide.bluetooth == .bluetooth) + #expect(DeviceOnboarding.SetupGuide.notifications != .siri) + #expect(DeviceOnboarding.SetupGuide.location != .backgroundActivity) + } + + @Test func allCasesAreUnique() { + let cases: [DeviceOnboarding.SetupGuide] = [ + .notifications, .location, .backgroundActivity, + .localNetwork, .bluetooth, .siri + ] + let unique = Set(cases) + #expect(unique.count == cases.count) + } +} + +// MARK: - Attributed String Formatters + +@Suite("DeviceOnboarding string formatters") +struct OnboardingStringFormatterTests { + + let view = DeviceOnboarding() + + // Helpers + private func hasSettingsLink(_ string: AttributedString) -> Bool { + guard let range = string.range(of: "settings") else { return false } + return string[range].link != nil + } + + private func settingsLinkURL(_ string: AttributedString) -> URL? { + guard let range = string.range(of: "settings") else { return nil } + return string[range].link + } + + @Test func backgroundActivityStringContainsText() { + let string = view.createBackgroundActivityString() + #expect(string.description.contains("background")) + #expect(string.description.contains("settings")) + } + + @Test func backgroundActivityStringHasSettingsLink() { + let string = view.createBackgroundActivityString() + #expect(hasSettingsLink(string)) + } + + @Test func backgroundActivitySettingsLinkIsAppSettings() { + let string = view.createBackgroundActivityString() + let url = settingsLinkURL(string) + #expect(url?.scheme == "app-settings" || url?.absoluteString.contains("settings") == true) + } + + @Test func locationStringContainsText() { + let string = view.createLocationString() + #expect(string.description.contains("location")) + #expect(string.description.contains("settings")) + } + + @Test func locationStringHasSettingsLink() { + let string = view.createLocationString() + #expect(hasSettingsLink(string)) + } + + @Test func localNetworkStringContainsText() { + let string = view.createLocalNetworkString() + #expect(string.description.contains("local network") || string.description.contains("TCP")) + #expect(string.description.contains("settings")) + } + + @Test func localNetworkStringHasSettingsLink() { + let string = view.createLocalNetworkString() + #expect(hasSettingsLink(string)) + } + + @Test func bluetoothStringContainsText() { + let string = view.createBluetoothString() + #expect(string.description.contains("Bluetooth") || string.description.contains("BLE")) + #expect(string.description.contains("settings")) + } + + @Test func bluetoothStringHasSettingsLink() { + let string = view.createBluetoothString() + #expect(hasSettingsLink(string)) + } + + @Test func siriStringContainsCarPlay() { + let string = view.createSiriString() + #expect(string.description.contains("CarPlay")) + } + + @Test func siriStringContainsSiri() { + let string = view.createSiriString() + #expect(string.description.contains("Siri")) + } + + @Test func siriStringHasSettingsLink() { + let string = view.createSiriString() + #expect(hasSettingsLink(string)) + } + + @Test func allStringsHaveSettingsLinks() { + let strings = [ + view.createBackgroundActivityString(), + view.createLocationString(), + view.createLocalNetworkString(), + view.createBluetoothString(), + view.createSiriString() + ] + for string in strings { + #expect(hasSettingsLink(string), "Expected 'settings' link in: \(string)") + } + } +} + +// MARK: - Navigation Flow + +@Suite("DeviceOnboarding navigation") +struct OnboardingNavigationTests { + + @Test func backgroundActivityAlwaysGoesToLocalNetwork() async { + let view = DeviceOnboarding() + await view.goToNextStep(after: .backgroundActivity) + #expect(view.navigationPath == [.localNetwork]) + } + + @Test func localNetworkAlwaysGoesToBluetooth() async { + let view = DeviceOnboarding() + await view.goToNextStep(after: .localNetwork) + #expect(view.navigationPath == [.bluetooth]) + } + + @Test func bluetoothAlwaysGoesToSiri() async { + let view = DeviceOnboarding() + await view.goToNextStep(after: .bluetooth) + #expect(view.navigationPath == [.siri]) + } + + @Test func navigationPathStartsEmpty() { + let view = DeviceOnboarding() + #expect(view.navigationPath.isEmpty) + } + + @Test func deterministicStepsAppendInOrder() async { + let view = DeviceOnboarding() + await view.goToNextStep(after: .backgroundActivity) + await view.goToNextStep(after: .localNetwork) + await view.goToNextStep(after: .bluetooth) + #expect(view.navigationPath == [.localNetwork, .bluetooth, .siri]) + } +}