Skip to content

Add share contact via QR (and untested scanning of contact QR codes) #1225

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 77 additions & 46 deletions Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -7202,6 +7202,16 @@
}
}
},
"Contact URL" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Contact URL"
}
}
}
},
"Contacts (%@)" : {
"localizations" : {
"de" : {
Expand Down Expand Up @@ -9994,6 +10004,9 @@
}
}
}
},
"Done" : {

},
"Double Tap as Button" : {
"localizations" : {
Expand Down Expand Up @@ -15082,6 +15095,12 @@
}
}
}
},
"Add Contact" : {

},
"Add Meshtastic Node %@ as a contact" : {

},
"Import Route" : {
"localizations" : {
Expand Down Expand Up @@ -22193,6 +22212,52 @@
}
}
},
"Position config received: %@" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Positionskonfiguration empfangen: %@"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Configuration de la position reçue : %@"
}
},
"he" : {
"stringUnit" : {
"state" : "translated",
"value" : "הגדרות מיקום התקבלו: %@"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Configurazione della posizione ricevuta: %@"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Odebrano konfigurację pozycji: %@"
}
},
"se" : {
"stringUnit" : {
"state" : "translated",
"value" : "Positionskonfiguration mottagen: %@"
}
},
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Конфигурација позиције примљена: %@"
}
}
}
},
"Position Exchange Failed" : {
"localizations" : {
"it" : {
Expand Down Expand Up @@ -22467,52 +22532,6 @@
}
}
},
"Position config received: %@" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Positionskonfiguration empfangen: %@"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Configuration de la position reçue : %@"
}
},
"he" : {
"stringUnit" : {
"state" : "translated",
"value" : "הגדרות מיקום התקבלו: %@"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Configurazione della posizione ricevuta: %@"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Odebrano konfigurację pozycji: %@"
}
},
"se" : {
"stringUnit" : {
"state" : "translated",
"value" : "Positionskonfiguration mottagen: %@"
}
},
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Конфигурација позиције примљена: %@"
}
}
}
},
"Power" : {
"localizations" : {
"de" : {
Expand Down Expand Up @@ -26021,6 +26040,9 @@
}
}
}
},
"Scan this QR code to add %@ to another device." : {

},
"Screen on for" : {
"localizations" : {
Expand Down Expand Up @@ -28033,6 +28055,9 @@
}
}
}
},
"Share Contact QR" : {

},
"Share QR Code" : {
"localizations" : {
Expand Down Expand Up @@ -29711,6 +29736,9 @@
}
}
}
},
"Takes a Meshtastic contact URL and saves it to the nodes database" : {

},
"Tapback" : {
"localizations" : {
Expand Down Expand Up @@ -30891,6 +30919,9 @@
}
}
}
},
"The URL for the node to import" : {

},
"There has been no response to a request for device metadata over the admin channel for this node." : {
"localizations" : {
Expand Down
12 changes: 12 additions & 0 deletions Meshtastic.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
108FFECB2DD3F43C00BFAA81 /* ShareContactQRDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */; };
108FFECD2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */; };
231B3F212D087A4C0069A07D /* MetricTableColumn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F202D087A4C0069A07D /* MetricTableColumn.swift */; };
231B3F222D087A4C0069A07D /* MetricsColumnList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F1F2D087A4C0069A07D /* MetricsColumnList.swift */; };
231B3F252D087C3C0069A07D /* EnvironmentDefaultColumns.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231B3F242D087C3C0069A07D /* EnvironmentDefaultColumns.swift */; };
Expand Down Expand Up @@ -56,6 +58,7 @@
8D3F8A412D44C2A6009EAAA4 /* PowerMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D3F8A402D44C2A6009EAAA4 /* PowerMetricsLog.swift */; };
B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B399E8A32B6F486400E4488E /* RetryButton.swift */; };
B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; };
BC10380F2DD4334400B00BFA /* AddContactIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */; };
BC47C2EF2CE0017D008245CA /* MessageNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */; };
BC6B45FF2CB2F98900723CEB /* SaveChannelSettingsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */; };
BCB613812C67290800485544 /* SendWaypointIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613802C67290800485544 /* SendWaypointIntent.swift */; };
Expand Down Expand Up @@ -274,6 +277,8 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContactQRDialog.swift; sourceTree = "<group>"; };
108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityToNodeInfo.swift; sourceTree = "<group>"; };
231B3F1F2D087A4C0069A07D /* MetricsColumnList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsColumnList.swift; sourceTree = "<group>"; };
231B3F202D087A4C0069A07D /* MetricTableColumn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricTableColumn.swift; sourceTree = "<group>"; };
231B3F242D087C3C0069A07D /* EnvironmentDefaultColumns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentDefaultColumns.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -318,6 +323,7 @@
8D3F8A402D44C2A6009EAAA4 /* PowerMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerMetricsLog.swift; sourceTree = "<group>"; };
B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = "<group>"; };
B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = "<group>"; };
BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactIntent.swift; sourceTree = "<group>"; };
BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = "<group>"; };
BC5EBA3B2D002A2000C442FF /* MessageNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageNodeIntent.swift; sourceTree = "<group>"; };
BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveChannelSettingsIntent.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -672,6 +678,7 @@
BCB6137F2C6728E700485544 /* AppIntents */ = {
isa = PBXGroup;
children = (
BC10380E2DD4333C00B00BFA /* AddContactIntent.swift */,
BC5EBA3B2D002A2000C442FF /* MessageNodeIntent.swift */,
BCB613802C67290800485544 /* SendWaypointIntent.swift */,
BCB613822C672A2600485544 /* MessageChannelIntent.swift */,
Expand Down Expand Up @@ -701,6 +708,7 @@
DD007BB12AA59B9A00F5FA12 /* CoreData */ = {
isa = PBXGroup;
children = (
108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */,
2344A2AA2D66973D00170A77 /* ManagedAttributePropertyWrapper.swift */,
DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */,
6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */,
Expand Down Expand Up @@ -1101,6 +1109,7 @@
DDDB26402AABEF7B003AFCB7 /* Helpers */ = {
isa = PBXGroup;
children = (
108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */,
231B3F232D087C020069A07D /* Metrics Columns */,
DDAD49EB2AFAE82500B4425D /* Map */,
DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */,
Expand Down Expand Up @@ -1396,6 +1405,7 @@
DD4640202AFF10F4002A5ECB /* WaypointForm.swift in Sources */,
233E99C12D849D6000CC3A77 /* DistanceCompactWidget.swift in Sources */,
DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */,
108FFECD2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift in Sources */,
DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */,
DD15E4F32B8BA56E00654F61 /* PaxCounterConfig.swift in Sources */,
DDDB445229F8ACF900EE2349 /* Date.swift in Sources */,
Expand Down Expand Up @@ -1486,6 +1496,7 @@
DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */,
DDD5BB092C285DDC007E03CA /* AppLog.swift in Sources */,
DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */,
108FFECB2DD3F43C00BFAA81 /* ShareContactQRDialog.swift in Sources */,
233E99C52D84A0B600CC3A77 /* CompactWidget.swift in Sources */,
DDC1B81A2AB5377B00C71E39 /* MessagesTips.swift in Sources */,
DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */,
Expand Down Expand Up @@ -1543,6 +1554,7 @@
2344A2AB2D66974300170A77 /* ManagedAttributePropertyWrapper.swift in Sources */,
BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */,
D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */,
BC10380F2DD4334400B00BFA /* AddContactIntent.swift in Sources */,
DDA9515E2BC6F56F00CEA535 /* IndoorAirQuality.swift in Sources */,
DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */,
DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */,
Expand Down
51 changes: 51 additions & 0 deletions Meshtastic/AppIntents/AddContactIntent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// AddContactIntent.swift
// Meshtastic
//
// Created by Benjamin Faershtein on 5/13/25.
//

import AppIntents
import MeshtasticProtobufs

struct AddContactIntent: AppIntent {
static var title: LocalizedStringResource = "Add Contact"
static var description: IntentDescription = "Takes a Meshtastic contact URL and saves it to the nodes database"

@Parameter(title: "Contact URL", description: "The URL for the node to add")
var contactUrl: URL

// Define the function that performs the main logic
func perform() async throws -> some IntentResult {
// Ensure the BLE Manager is connected
if !BLEManager.shared.isConnected {
throw AppIntentErrors.AppIntentError.notConnected
}

if contactUrl.absoluteString.lowercased().contains("meshtastic.org/v/#") {

let components = self.contactUrl.absoluteString.components(separatedBy: "#")
// Extract contact information from the URL
if let contactData = components.last {

let decodedString = contactData.base64urlToBase64()
if let decodedData = Data(base64Encoded: decodedString) {
do {
let success = BLEManager.shared.addContactFromURL(base64UrlString: contactData)
if !success {
throw AppIntentErrors.AppIntentError.message("Failed to add contact")
}

} catch {
throw AppIntentErrors.AppIntentError.message("Failed to parse contact data: \(error.localizedDescription)")

}
}
}
// Return a success result
return .result()
} else {
throw AppIntentErrors.AppIntentError.message("The URL is not a valid Meshtastic contact link")
}
}
}
28 changes: 28 additions & 0 deletions Meshtastic/Extensions/CoreData/NodeInfoEntityToNodeInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// NodeInfoEntityToNodeInfo.swift
// Meshtastic
//
// Utility to convert NodeInfoEntity (Core Data) to NodeInfo (protobuf)

import Foundation
import MeshtasticProtobufs

extension NodeInfoEntity {
func toProto() -> NodeInfo {
var userProto = User()
if let user = self.user {
userProto.id = user.userId ?? ""
userProto.longName = user.longName ?? ""
userProto.shortName = user.shortName ?? ""
userProto.hwModel = HardwareModel(rawValue: Int(user.hwModelId)) ?? .unset
userProto.isLicensed = user.isLicensed
userProto.isUnmessagable = false
userProto.role = Config.DeviceConfig.Role(rawValue: Int(user.role)) ?? .client
userProto.publicKey = user.publicKey?.subdata(in: 0..<user.publicKey!.count) ?? Data()
}
var node = NodeInfo()
node.num = UInt32(self.num)
node.user = userProto
// Add more fields as needed
return node
}
}
Loading