Additional onboarding cleanup

This commit is contained in:
Garth Vander Houwen 2026-04-18 10:34:09 -07:00
parent f7531f5b9a
commit f58b8376e6
8 changed files with 567 additions and 189 deletions

View file

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

View file

@ -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 = "<group>"; };
AA00010022E2730EC0060000 /* ConnectViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewTests.swift; sourceTree = "<group>"; };
DD9C70112E9F2A0000029299 /* DeviceOnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceOnboardingTests.swift; sourceTree = "<group>"; };
2B37CCEE8B44A4BA123ED118 /* TAKServerManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKServerManager.swift; sourceTree = "<group>"; };
3D0A8ABAEF1E587683970927 /* EXICodec.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EXICodec.swift; sourceTree = "<group>"; };
3D3417B32E2730EC006A988B /* GeoJSONOverlayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJSONOverlayManager.swift; sourceTree = "<group>"; };
@ -565,6 +568,7 @@
DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 43.xcdatamodel"; sourceTree = "<group>"; };
DD8169FE272476C700F4AB02 /* LogDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDocument.swift; sourceTree = "<group>"; };
DD836AE626F6B38600ABCC23 /* Connect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connect.swift; sourceTree = "<group>"; };
DD4756AA2E9F1A0000029299 /* SecurityVersionNag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityVersionNag.swift; sourceTree = "<group>"; };
DD86D409287F04F100BAEB7A /* InvalidVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvalidVersion.swift; sourceTree = "<group>"; };
DD86D40B287F401000BAEB7A /* SaveChannelQRCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveChannelQRCode.swift; sourceTree = "<group>"; };
DD86D40E2881BE4C00BAEB7A /* CsvDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CsvDocument.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 */,

View file

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

View file

@ -9,6 +9,8 @@
</array>
<key>com.apple.developer.carplay-communication</key>
<true/>
<key>com.apple.developer.siri</key>
<true/>
<key>com.apple.developer.usernotifications.critical-alerts</key>
<true/>
<key>com.apple.developer.weatherkit</key>

View file

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

View file

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

View file

@ -364,7 +364,7 @@ struct InvalidVersionTests {
}
@Test func viewCreationWithEmptyVersions() {
let view = InvalidVersion()
let view = InvalidVersion(minimumVersion: "", version: "")
#expect(view.minimumVersion == "")
#expect(view.version == "")
}

View file

@ -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<DeviceOnboarding.SetupGuide>()
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])
}
}