diff --git a/Gemfile.lock b/Gemfile.lock index 69afd306a..e7d73be49 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,7 +3,7 @@ GEM specs: CFPropertyList (3.0.6) rexml - activesupport (7.0.5) + activesupport (7.0.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -16,20 +16,20 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.780.0) - aws-sdk-core (3.175.0) + aws-partitions (1.791.0) + aws-sdk-core (3.178.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.67.0) - aws-sdk-core (~> 3, >= 3.174.0) + aws-sdk-kms (1.71.0) + aws-sdk-core (~> 3, >= 3.177.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.125.0) - aws-sdk-core (~> 3, >= 3.174.0) + aws-sdk-s3 (1.131.0) + aws-sdk-core (~> 3, >= 3.177.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.4) - aws-sigv4 (1.5.2) + aws-sigv4 (~> 1.6) + aws-sigv4 (1.6.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) claide (1.1.0) @@ -76,7 +76,7 @@ GEM highline (~> 2.0.0) concurrent-ruby (1.2.2) declarative (0.0.20) - digest-crc (0.6.4) + digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) @@ -115,7 +115,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.7) - fastlane (2.213.0) + fastlane (2.214.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -159,9 +159,9 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.43.0) + google-apis-androidpublisher_v3 (0.46.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.0) + google-apis-core (0.11.1) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -190,7 +190,7 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.5.2) + googleauth (1.7.0) faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) memoist (~> 0.16) diff --git a/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/Discover.storyboard b/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/Discover.storyboard index 3106362b6..4eec0a4a6 100644 --- a/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/Discover.storyboard +++ b/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/Discover.storyboard @@ -1,9 +1,9 @@ - + - + @@ -142,7 +142,7 @@ - + @@ -168,7 +168,7 @@ - + diff --git a/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/de.lproj/RuuviDiscover.strings b/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/de.lproj/RuuviDiscover.strings index 3aabf4390..ecafd1f99 100644 --- a/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/de.lproj/RuuviDiscover.strings +++ b/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/de.lproj/RuuviDiscover.strings @@ -14,3 +14,17 @@ "WebTagLocationSource.current" = "Ihr Standort"; "WebTagLocationSource.manual" = "Wählen Sie aus der Karte"; "Ruuvi.BuySensors.URL.IOS" = "https://ruuvi.com/products?utm_campaign=app_ua&utm_medium=referral&utm_source=ios"; +"add_with_nfc" = "Mit NFC hinzufügen"; +"sensor_details" = "Sensordetails"; +"add_sensor" = "Sensor hinzufügen"; +"copy_mac_address" = "MAC-Adresse kopieren"; +"copy_unique_id" = "Eindeutige ID kopieren"; +"name" = "Name:"; +"mac_address" = "MAC-Adresse:"; +"go_to_sensor" = "Gehen Sie zur Sensorkarte"; +"unique_id" = "Eindeutige ID:"; +"firmware_version" = "Firmware Version:"; +"close" = "Schließen"; +"add_sensor_nfc_df3_error" = "Dieser Sensor kann aufgrund der alten Firmware nicht mit NFC hinzugefügt werden. Bitte fügen Sie den Sensor über Bluetooth hinzu und aktualisieren Sie die Firmware."; +"add_sensor_description" = "Auf dieser Seite werden Ruuvi-Sensoren in der Nähe angezeigt, die der App noch nicht hinzugefügt wurden. Tippen Sie auf einen Sensor, um ihn hinzuzufügen."; +"add_sensor_via_nfc" = "Alternatively, you can add a sensor using NFC by selecting Add with NFC and touching it with your phone."; diff --git a/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/en.lproj/RuuviDiscover.strings b/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/en.lproj/RuuviDiscover.strings index 57a531854..c6b4a9904 100644 --- a/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/en.lproj/RuuviDiscover.strings +++ b/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/en.lproj/RuuviDiscover.strings @@ -14,3 +14,17 @@ "WebTagLocationSource.current" = "Your location"; "WebTagLocationSource.manual" = "Pick from the map"; "Ruuvi.BuySensors.URL.IOS" = "https://ruuvi.com/products?utm_campaign=app_ua&utm_medium=referral&utm_source=ios"; +"add_with_nfc" = "Add with NFC"; +"sensor_details" = "Sensor Details"; +"add_sensor" = "Add Sensor"; +"copy_mac_address" = "Copy MAC Address"; +"copy_unique_id" = "Copy Unique ID"; +"name" = "Name:"; +"mac_address" = "Mac Address:"; +"go_to_sensor" = "Go to sensor card"; +"unique_id" = "Unique ID:"; +"firmware_version" = "Firmware Version:"; +"close" = "Close"; +"add_sensor_nfc_df3_error" = "This tag cannot be added with NFC due to old firmware. Please add the tag with Bluetooth and update firmware."; +"add_sensor_description" = "This page shows nearby Ruuvi sensors not yet added to the app. Tap a sensor to add it."; +"add_sensor_via_nfc" = "Alternatively, you can add a sensor using NFC by selecting Add with NFC and touching it with your phone."; diff --git a/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/fi.lproj/RuuviDiscover.strings b/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/fi.lproj/RuuviDiscover.strings index 2710fbf9d..9e63aa475 100644 --- a/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/fi.lproj/RuuviDiscover.strings +++ b/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/fi.lproj/RuuviDiscover.strings @@ -14,3 +14,17 @@ "WebTagLocationSource.current" = "Nykyinen sijaintisi"; "WebTagLocationSource.manual" = "Valitse kartalta"; "Ruuvi.BuySensors.URL.IOS" = "https://ruuvi.com/fi/tuotteet?utm_campaign=app_ua&utm_medium=referral&utm_source=ios"; +"add_with_nfc" = "Lisää NFC:llä"; +"sensor_details" = "Anturin tiedot"; +"add_sensor" = "Lisää anturi"; +"copy_mac_address" = "Kopioi MAC-osoite"; +"copy_unique_id" = "Kopioi yksilöivä tunniste"; +"name" = "Nimi:"; +"mac_address" = "MAC-osoite:"; +"go_to_sensor" = "Siirry anturikortille"; +"unique_id" = "Yksilöivä tunniste:"; +"firmware_version" = "Laiteohjelmistoversio:"; +"close" = "Sulje"; +"add_sensor_nfc_df3_error" = "Vanha laiteohjelmisto ei salli anturin lisäämistä NFC:llä. Lisää anturi Bluetooth-yhteydellä ja päivitä laiteohjelmisto."; +"add_sensor_description" = "Tällä sivulla näet lähelläsi olevat Ruuvi-anturit, joita ei ole vielä lisätty sovellukseen. Lisää listattu anturi napauttamalla."; +"add_sensor_via_nfc" = "Voit vaihtoehtoisesti lisätä anturin sovellukseen NFC:llä napauttamalla Lisää NFC:llä painiketta ja koskettamalla sitä."; diff --git a/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/fr.lproj/RuuviDiscover.strings b/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/fr.lproj/RuuviDiscover.strings index f5c02779e..eaa1e9af4 100644 --- a/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/fr.lproj/RuuviDiscover.strings +++ b/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/fr.lproj/RuuviDiscover.strings @@ -14,3 +14,17 @@ "WebTagLocationSource.current" = "Localisation actuelle"; "WebTagLocationSource.manual" = "Choisir sur la carte"; "Ruuvi.BuySensors.URL.IOS" = "https://ruuvi.com/products?utm_campaign=app_ua&utm_medium=referral&utm_source=ios"; +"add_with_nfc" = "Ajouter avec NFC"; +"sensor_details" = "Détails du capteur"; +"add_sensor" = "Ajouter un capteur"; +"copy_mac_address" = "Copier l'adresse MAC"; +"copy_unique_id" = "Copier l'identifiant unique"; +"name" = "Nom:"; +"mac_address" = "Adresse Mac:"; +"go_to_sensor" = "Aller à la carte du capteur"; +"unique_id" = "Identifiant unique:"; +"firmware_version" = "Version du firmware:"; +"close" = "Fermer"; +"add_sensor_nfc_df3_error" = "Ce capteur ne peut pas être ajouté avec NFC en raison d'un ancien firmware. Veuillez ajouter le capteur avec Bluetooth et mettre à jour le firmware."; +"add_sensor_description" = "Cette page montre les capteurs Ruuvi à proximité qui n'ont pas encore été ajoutés à l'application. Appuyez sur un capteur pour l'ajouter."; +"add_sensor_via_nfc" = "Alternatively, you can add a sensor using NFC by selecting Add with NFC and touching it with your phone."; diff --git a/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/ru.lproj/RuuviDiscover.strings b/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/ru.lproj/RuuviDiscover.strings index 026bfe81d..78c812c9a 100644 --- a/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/ru.lproj/RuuviDiscover.strings +++ b/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/ru.lproj/RuuviDiscover.strings @@ -14,3 +14,17 @@ "WebTagLocationSource.current" = "Ваше текущее месторасположение"; "WebTagLocationSource.manual" = "Месторасположение на карте"; "Ruuvi.BuySensors.URL.IOS" = "https://ruuvi.com/products?utm_campaign=app_ua&utm_medium=referral&utm_source=ios"; +"add_with_nfc" = "Add with NFC"; +"sensor_details" = "Sensor Details"; +"add_sensor" = "Add Sensor"; +"copy_mac_address" = "Копировать MAC-адрес"; +"copy_unique_id" = "Скопировать уникальный идентификатор"; +"name" = "Name:"; +"mac_address" = "Mac Address:"; +"go_to_sensor" = "Go to sensor card"; +"unique_id" = "Unique ID:"; +"firmware_version" = "Firmware Version:"; +"close" = "Close"; +"add_sensor_nfc_df3_error" = "This sensor cannot be added with NFC due to old firmware. Please add the sensor with Bluetooth and update firmware."; +"add_sensor_description" = "Здесь показаны датчики Ruuvi, которые еще не были добавлены в приложение. Нажмите на сенсор, чтобы добавить его."; +"add_sensor_via_nfc" = "Alternatively, you can add a sensor using NFC by selecting Add with NFC and touching it with your phone."; diff --git a/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/sv.lproj/RuuviDiscover.strings b/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/sv.lproj/RuuviDiscover.strings index 3dcf78747..e17c35df6 100644 --- a/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/sv.lproj/RuuviDiscover.strings +++ b/Modules/RuuviDiscover/Sources/RuuviDiscover/Resources/sv.lproj/RuuviDiscover.strings @@ -14,3 +14,17 @@ "WebTagLocationSource.current" = "Din plats"; "WebTagLocationSource.manual" = "Välj från karta"; "Ruuvi.BuySensors.URL.IOS" = "https://ruuvi.com/products?utm_campaign=app_ua&utm_medium=referral&utm_source=ios"; +"add_with_nfc" = "Lägg till med NFC"; +"sensor_details" = "Sensordetaljer"; +"add_sensor" = "Lägg till sensor"; +"copy_mac_address" = "Kopiera MAC-adress"; +"copy_unique_id" = "Kopiera unikt ID"; +"name" = "Namn:"; +"mac_address" = "MAC-adress:"; +"go_to_sensor" = "Gå till sensorkortet"; +"unique_id" = "Unikt ID:"; +"firmware_version" = "Firmwareversion:"; +"close" = "Stänga"; +"add_sensor_nfc_df3_error" = "Denna sensor kan inte läggas till med NFC på grund av gammal firmware. Lägg till sensorn med Bluetooth och uppdatera firmware."; +"add_sensor_description" = "Den här sidan visar närliggande Ruuvi-sensorer som ännu inte har lagts till i appen. Tryck på en sensor för att lägga till den."; +"add_sensor_via_nfc" = "Alternatively, you can add a sensor using NFC by selecting Add with NFC and touching it with your phone."; diff --git a/Modules/RuuviDiscover/Sources/RuuviDiscover/RuuviDiscover.swift b/Modules/RuuviDiscover/Sources/RuuviDiscover/RuuviDiscover.swift index 9d143e220..2b70c5c2b 100644 --- a/Modules/RuuviDiscover/Sources/RuuviDiscover/RuuviDiscover.swift +++ b/Modules/RuuviDiscover/Sources/RuuviDiscover/RuuviDiscover.swift @@ -12,6 +12,7 @@ public protocol RuuviDiscover: AnyObject { public protocol RuuviDiscoverOutput: AnyObject { func ruuviDiscoverWantsClose(_ ruuviDiscover: RuuviDiscover) func ruuvi(discover: RuuviDiscover, didAdd ruuviTag: AnyRuuviTagSensor) + func ruuvi(discover: RuuviDiscover, didSelectFromNFC ruuviTag: RuuviTagSensor) // Will be deprecated in near future. Currently retained to support already // added web tags. func ruuviDiscoverWantsPickLocation(_ ruuviDiscover: RuuviDiscover) diff --git a/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/Presenter/DiscoverPresenter.swift b/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/Presenter/DiscoverPresenter.swift index 10cd4f613..9575786e3 100644 --- a/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/Presenter/DiscoverPresenter.swift +++ b/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/Presenter/DiscoverPresenter.swift @@ -1,3 +1,4 @@ +// swiftlint:disable file_length import Foundation import BTKit import UIKit @@ -11,6 +12,7 @@ import RuuviVirtual import RuuviCore import RuuviPresenters import CoreBluetooth +import CoreNFC class DiscoverPresenter: NSObject, RuuviDiscover { var viewController: UIViewController { @@ -73,6 +75,7 @@ class DiscoverPresenter: NSObject, RuuviDiscover { } } } + private var nfcSensor: NFCSensor? private var reloadTimer: Timer? private var scanToken: ObservationToken? @@ -123,15 +126,11 @@ extension DiscoverPresenter: DiscoverViewOutput { func viewDidChoose(device: DiscoverRuuviTagViewModel, displayName: String) { if let ruuviTag = ruuviTags.first(where: { $0.luid?.any != nil && $0.luid?.any == device.luid?.any }) { - ruuviOwnershipService.add( - sensor: ruuviTag.with(name: displayName), - record: ruuviTag.with(source: .advertisement)) - .on(success: { [weak self] anyRuuviTagSensor in - guard let sSelf = self else { return } - sSelf.output?.ruuvi(discover: sSelf, didAdd: anyRuuviTagSensor) - }, failure: { [weak self] error in - self?.errorPresenter.present(error: error) - }) + addRuuviTagOwnership( + for: ruuviTag, + displayName: displayName, + firmwareVersion: nil + ) } } @@ -148,6 +147,121 @@ extension DiscoverPresenter: DiscoverViewOutput { else { return } UIApplication.shared.open(url, options: [:], completionHandler: nil) } + + func viewDidTapUseNFC() { + view?.startNFCSession() + } + + // swiftlint:disable:next cyclomatic_complexity function_body_length + func viewDidReceiveNFCMessages(messages: [NFCNDEFMessage]) { + // Stop NFC session + view?.stopNFCSession() + + nfcSensor = NFCSensor(id: "", macId: "", firmwareVersion: "") + // Parse the message + for message in messages { + for record in message.records { + if let (key, value) = parse(record: record) { + switch key { + case "idID": + self.nfcSensor?.id = trimNulls(from: value) + case "adMAC": + self.nfcSensor?.macId = trimNulls(from: value) + case "swSW": + self.nfcSensor?.firmwareVersion = trimNulls(from: value) + default: + break + } + } + } + } + + // Stop NFC session + view?.stopNFCSession() + + // If tag is already added show the name from RuuviStation alongside + // other info. + if let addedTag = persistedSensors.first(where: { ruuviTag in + ruuviTag.macId?.mac == nfcSensor?.macId + }) { + guard let message = self.message( + for: nfcSensor, + displayName: addedTag.name + ) else { return } + self.view?.showSensorDetailsDialog( + for: nfcSensor, + message: message, + showAddSensor: false, + showGoToSensor: true, + isDF3: false + ) + return + } + + // If tag is not added get the name from the mac and show other info. + if let addableTag = ruuviTags.first(where: { ruuviTag in + ruuviTag.mac == nfcSensor?.macId + }) { + guard let message = self.message( + for: nfcSensor, + displayName: self.displayName(for: nfcSensor) + ) else { return } + self.view?.showSensorDetailsDialog( + for: nfcSensor, + message: message, + showAddSensor: true, + showGoToSensor: false, + isDF3: false + ) + return + } + + // Got mac id from scan, but no match in the persisted tag or available tag. + // which means either its a DF3 tag where mac id is not present or NFC scan + // is done when sensor is not yet seen by BT. + // Show info for DF3 case to add the tag using BT and update FW. + // TODO: Discuss about the other case to handle it. + guard let message = self.message( + for: nfcSensor, + displayName: self.displayName(for: nfcSensor) + ) else { return } + self.view?.showSensorDetailsDialog( + for: nfcSensor, + message: message, + showAddSensor: false, + showGoToSensor: false, + isDF3: nfcSensor?.firmwareVersion == "2.5.9" + ) + } + + func viewDidAddDeviceWithNFC(with tag: NFCSensor?) { + guard let displayName = displayName(for: tag) else { + return + } + if let ruuviTag = ruuviTags.first(where: { $0.mac != nil && $0.mac == tag?.macId }) { + addRuuviTagOwnership( + for: ruuviTag, + displayName: displayName, + firmwareVersion: tag?.firmwareVersion + ) + } + } + + func viewDidGoToSensor(with sensor: NFCSensor?) { + if let ruuviTag = persistedSensors.first(where: { ruuviTag in + ruuviTag.macId?.mac == sensor?.macId + }) { + output?.ruuvi(discover: self, didSelectFromNFC: ruuviTag) + } + } + + func viewDidACopyMacAddress(of sensor: NFCSensor?) { + UIPasteboard.general.string = sensor?.macId + } + + func viewDidACopySecret(of sensor: NFCSensor?) { + UIPasteboard.general.string = sensor?.id + } } extension DiscoverPresenter { @@ -277,6 +391,73 @@ extension DiscoverPresenter { return filtered } + + /// Parse the NFC payload + private func parse(record: NFCNDEFPayload) -> (String, String)? { + let payload = record.payload + let prefix = payload.prefix(1) + let rest = payload.dropFirst(1) + + switch prefix { + case .init([0x02]): + guard let restString = String( + data: rest, encoding: .utf8 + ) else { return nil } + + let components = restString.components(separatedBy: ": ") + if components.count == 2 { + let key = components[0] + let value = components[1].trimmingCharacters(in: .whitespacesAndNewlines) + return (key, value) + } + default: + return nil + } + return nil + } + + private func displayName(for tag: NFCSensor?) -> String? { + guard let tag = tag else { + return nil + } + return "DiscoverTable.RuuviDevice.prefix".localized(for: Self.self) + + " " + tag.macId.replacingOccurrences(of: ":", with: "").suffix(4) + } + + private func message(for tag: NFCSensor?, displayName: String?) -> String? { + guard let tag = tag, + let displayName = displayName else { + return nil + } + + let nameString = "\("name".localized(for: Self.self))\n\(displayName)" + let macIdString = "\("mac_address".localized(for: Self.self))\n\(tag.macId)" + let uniqueIdString = "\("unique_id".localized(for: Self.self))\n\(tag.id)" + let fwString = "\("firmware_version".localized(for: Self.self))\n\(tag.firmwareVersion)" + + return "\n\(nameString)\n\n\(macIdString)\n\n\(uniqueIdString)\n\n\(fwString)\n".localized(for: Self.self) + } + + private func addRuuviTagOwnership( + for ruuviTag: RuuviTag, + displayName: String, + firmwareVersion: String? + ) { + ruuviOwnershipService.add( + sensor: ruuviTag.with(name: displayName) + .with(firmwareVersion: firmwareVersion ?? ""), + record: ruuviTag.with(source: .advertisement)) + .on(success: { [weak self] anyRuuviTagSensor in + guard let sSelf = self else { return } + sSelf.output?.ruuvi(discover: sSelf, didAdd: anyRuuviTagSensor) + }, failure: { [weak self] error in + self?.errorPresenter.present(error: error) + }) + } + + private func trimNulls(from string: String) -> String { + return string.replacingOccurrences(of: "\0", with: "") + } } extension DiscoverPresenter { @@ -295,3 +476,4 @@ extension DiscoverPresenter { }) } } +// swiftlint:enable file_length diff --git a/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/View/DiscoverViewInput.swift b/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/View/DiscoverViewInput.swift index 47ad3219c..0e963004c 100644 --- a/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/View/DiscoverViewInput.swift +++ b/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/View/DiscoverViewInput.swift @@ -10,4 +10,13 @@ protocol DiscoverViewInput: UIViewController, Localizable { var isCloseEnabled: Bool { get set } func showBluetoothDisabled(userDeclined: Bool) + func startNFCSession() + func stopNFCSession() + func showSensorDetailsDialog( + for tag: NFCSensor?, + message: String, + showAddSensor: Bool, + showGoToSensor: Bool, + isDF3: Bool + ) } diff --git a/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/View/DiscoverViewOutput.swift b/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/View/DiscoverViewOutput.swift index 148546a4b..4b94dfa19 100644 --- a/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/View/DiscoverViewOutput.swift +++ b/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/View/DiscoverViewOutput.swift @@ -1,4 +1,6 @@ import Foundation +import CoreNFC +import RuuviOntology protocol DiscoverViewOutput { func viewDidLoad() @@ -8,4 +10,10 @@ protocol DiscoverViewOutput { func viewDidTriggerClose() func viewDidTriggerDisabledBTRow() func viewDidTriggerBuySensors() + func viewDidTapUseNFC() + func viewDidReceiveNFCMessages(messages: [NFCNDEFMessage]) + func viewDidAddDeviceWithNFC(with sensor: NFCSensor?) + func viewDidGoToSensor(with sensor: NFCSensor?) + func viewDidACopyMacAddress(of sensor: NFCSensor?) + func viewDidACopySecret(of sensor: NFCSensor?) } diff --git a/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/View/Table/DiscoverTableHeaderView.swift b/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/View/Table/DiscoverTableHeaderView.swift new file mode 100644 index 000000000..068d60ba9 --- /dev/null +++ b/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/View/Table/DiscoverTableHeaderView.swift @@ -0,0 +1,197 @@ +import UIKit +import CoreBluetooth +import CoreNFC + +protocol DiscoverTableHeaderViewDelegate: NSObjectProtocol { + func didTapAddWithNFCButton(sender: DiscoverTableHeaderView) +} + +class DiscoverTableHeaderView: UIView { + + weak var delegate: DiscoverTableHeaderViewDelegate? + + // ----- Private + private var isNFCAvailable: Bool { + return NFCNDEFReaderSession.readingAvailable + } + + private var isBluetoothPermissionGranted: Bool { + if #available(iOS 13.1, *) { + return CBCentralManager.authorization == .allowedAlways + } else if #available(iOS 13.0, *) { + return CBCentralManager().authorization == .allowedAlways + } + // Before iOS 13, Bluetooth permissions are not required + return true + } + + private let addSensorDescriptionKey: String = "add_sensor_description" + private let addSensorViaNFCKey: String = "add_sensor_via_nfc" + + // UI + private lazy var descriptionLabel = createDescriptionLabel() + private lazy var nfcButton = createAddWithNFCButton() + + private var descriptionLabelBottomConstraint: NSLayoutConstraint! + private var nfcButtonTopConstraint: NSLayoutConstraint! + private var nfcButtonBottomConstraint: NSLayoutConstraint! + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + translatesAutoresizingMaskIntoConstraints = false + let headerView = createHeaderView() + addSubview(headerView) + + headerView.addSubview(descriptionLabel) + + if isBluetoothPermissionGranted && isNFCAvailable { + headerView.addSubview(nfcButton) + } + + setupLayout(headerView: headerView, + descriptionLabel: descriptionLabel, + button: nfcButton) + } + + private func createHeaderView() -> UIView { + let headerView = UIView() + headerView.backgroundColor = .clear + headerView.translatesAutoresizingMaskIntoConstraints = false + return headerView + } + + private func createDescriptionLabel() -> UILabel { + let descriptionLabel = UILabel() + descriptionLabel.translatesAutoresizingMaskIntoConstraints = false + descriptionLabel.numberOfLines = 0 + + let addSensorString: String = addSensorDescriptionKey.localized(for: Self.self) + let addSensorViaNFCString = addSensorViaNFCKey.localized(for: Self.self) + let descriptionString = + (isBluetoothPermissionGranted && isNFCAvailable) ? + (addSensorString + "\n\n" + addSensorViaNFCString) : addSensorString + + descriptionLabel.text = descriptionString + descriptionLabel.textColor = UIColor(named: "ruuvi_text_color") + if let font = UIFont(name: "Muli-Regular", size: 14) { + descriptionLabel.font = font + } + return descriptionLabel + } + + private func createAddWithNFCButton() -> UIButton { + let button = UIButton(type: .custom) + button.translatesAutoresizingMaskIntoConstraints = false + button.setImage(UIImage(systemName: "plus.circle.fill"), for: .normal) + button.setTitle("add_with_nfc".localized(for: Self.self), for: .normal) + button.setTitleColor(.label, for: .normal) + button.setInsets(forContentPadding: .zero, imageTitlePadding: 8) + if let font = UIFont(name: "Muli-Regular", size: 16) { + button.titleLabel?.font = font + } + button.tintColor = UIColor(named: "RuuviTintColor") + button.addTarget(self, + action: #selector(handleButtonTap), + for: .touchUpInside) + return button + } + + private func setupLayout( + headerView: UIView, descriptionLabel: UILabel, button: UIButton + ) { + let headerPadding = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16) + NSLayoutConstraint.activate(headerView.constraints(to: self, padding: headerPadding)) + + NSLayoutConstraint.activate([ + descriptionLabel.topAnchor.constraint(equalTo: headerView.topAnchor), + descriptionLabel.leadingAnchor.constraint(equalTo: headerView.leadingAnchor), + descriptionLabel.trailingAnchor.constraint(equalTo: headerView.trailingAnchor) + ]) + + // variable constraints + nfcButtonTopConstraint = button + .topAnchor + .constraint( + equalTo: descriptionLabel.bottomAnchor, constant: 12 + ) + nfcButtonBottomConstraint = button + .bottomAnchor + .constraint( + equalTo: headerView.bottomAnchor, constant: -12 + ) + descriptionLabelBottomConstraint = descriptionLabel + .bottomAnchor + .constraint(equalTo: headerView.bottomAnchor) + + if isBluetoothPermissionGranted && isNFCAvailable { + NSLayoutConstraint.activate([ + nfcButtonTopConstraint, + button.leadingAnchor.constraint(equalTo: headerView.leadingAnchor), + button.trailingAnchor.constraint(equalTo: headerView.trailingAnchor), + nfcButtonBottomConstraint + ]) + } else { + NSLayoutConstraint.activate([ + descriptionLabelBottomConstraint + ]) + } + } + + @objc private func handleButtonTap() { + delegate?.didTapAddWithNFCButton(sender: self) + } +} + +extension DiscoverTableHeaderView { + func handleNFCButtonViewVisibility(show: Bool) { + nfcButtonTopConstraint.isActive = show + nfcButtonBottomConstraint.isActive = show + descriptionLabelBottomConstraint.isActive = !show + nfcButton.isHidden = !show + let addSensorString: String = addSensorDescriptionKey.localized(for: Self.self) + let addSensorViaNFCString = addSensorViaNFCKey.localized(for: Self.self) + let descriptionString = + (show && isBluetoothPermissionGranted && isNFCAvailable) ? + (addSensorString + "\n\n" + addSensorViaNFCString) : addSensorString + descriptionLabel.text = descriptionString + } +} + +extension UIButton { + func setInsets( + forContentPadding contentPadding: UIEdgeInsets, + imageTitlePadding: CGFloat + ) { + self.contentEdgeInsets = UIEdgeInsets( + top: contentPadding.top, + left: contentPadding.left, + bottom: contentPadding.bottom, + right: contentPadding.right + imageTitlePadding + ) + self.titleEdgeInsets = UIEdgeInsets( + top: 0, + left: imageTitlePadding, + bottom: 0, + right: -imageTitlePadding + ) + } +} + +extension UIView { + func constraints(to view: UIView, padding: UIEdgeInsets = .zero) -> [NSLayoutConstraint] { + return [ + self.topAnchor.constraint(equalTo: view.topAnchor, constant: padding.top), + self.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding.left), + self.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -padding.bottom), + self.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -padding.right) + ] + } +} diff --git a/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/View/Table/DiscoverTableViewController.swift b/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/View/Table/DiscoverTableViewController.swift index e76cfeee9..711423604 100644 --- a/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/View/Table/DiscoverTableViewController.swift +++ b/Modules/RuuviDiscover/Sources/RuuviDiscover/VMP/View/Table/DiscoverTableViewController.swift @@ -4,6 +4,7 @@ import RuuviOntology import RuuviVirtual import RuuviLocalization import RuuviBundleUtils +import CoreNFC enum DiscoverTableSection { case device @@ -22,7 +23,8 @@ class DiscoverTableViewController: UIViewController { @IBOutlet weak var tableView: UITableView! @IBOutlet var closeBarButtonItem: UIBarButtonItem! - @IBOutlet weak var buyRuuviSensorsButton: UIButton! + @IBOutlet weak var actionButton: UIButton! + private var discoverTableHeaderView = DiscoverTableHeaderView() private var alertVC: UIAlertController? @@ -45,6 +47,7 @@ class DiscoverTableViewController: UIViewController { } private let hideAlreadyAddedWebProviders = false + private var session: NFCNDEFReaderSession? } // MARK: - DiscoverViewInput @@ -52,10 +55,6 @@ extension DiscoverTableViewController: DiscoverViewInput { func localize() { navigationItem.title = "DiscoverTable.NavigationItem.title".localized(for: Self.self) - buyRuuviSensorsButton.setTitle( - "DiscoverTable.GetMoreSensors.button.title".localized(for: Self.self).capitalized, - for: .normal - ) } func showBluetoothDisabled(userDeclined: Bool) { @@ -76,6 +75,84 @@ extension DiscoverTableViewController: DiscoverViewInput { alertVC.addAction(UIAlertAction(title: "OK".localized(for: Self.self), style: .cancel, handler: nil)) present(alertVC, animated: true) } + + func startNFCSession() { + session?.invalidate() + session = nil + + session = NFCNDEFReaderSession( + delegate: self, + queue: nil, + invalidateAfterFirstRead: false + ) + session?.begin() + } + + func stopNFCSession() { + session?.invalidate() + session = nil + } + + func showSensorDetailsDialog( + for tag: NFCSensor?, + message: String, + showAddSensor: Bool, + showGoToSensor: Bool, + isDF3: Bool + ) { + let title = "sensor_details".localized(for: Self.self) + + // Message + var messageString = message + // We show extra message for DF3 sensors since they can't be added with NFC. + if isDF3 { + let df3ErrorMessage = "add_sensor_nfc_df3_error".localized( + for: Self.self + ) + messageString = "\n\(df3ErrorMessage)\n" + message + } + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .left + let messageText = NSAttributedString( + string: messageString, + attributes: [ + NSAttributedString.Key.paragraphStyle: paragraphStyle, + NSAttributedString.Key.foregroundColor: UIColor.label, + NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .body) + ] + ) + + let alertVC = UIAlertController(title: title, message: nil, preferredStyle: .alert) + alertVC.setValue(messageText, forKey: "attributedMessage") + + if showAddSensor { + alertVC.addAction(UIAlertAction(title: "add_sensor".localized(for: Self.self), + style: .default, handler: { [weak self] _ in + self?.output.viewDidAddDeviceWithNFC(with: tag) + })) + } + + alertVC.addAction(UIAlertAction(title: "copy_mac_address".localized(for: Self.self), + style: .default, handler: { [weak self] _ in + self?.output.viewDidACopyMacAddress(of: tag) + })) + + alertVC.addAction(UIAlertAction(title: "copy_unique_id".localized(for: Self.self), + style: .default, handler: { [weak self] _ in + self?.output.viewDidACopySecret(of: tag) + })) + + if showGoToSensor { + alertVC.addAction(UIAlertAction(title: "go_to_sensor".localized(for: Self.self), + style: .default, handler: { [weak self] _ in + self?.output.viewDidGoToSensor(with: tag) + })) + } + + alertVC.addAction(UIAlertAction(title: "close".localized(for: Self.self), style: .cancel, handler: nil)) + present(alertVC, animated: true) + } } // MARK: - IBActions @@ -84,11 +161,18 @@ extension DiscoverTableViewController { output.viewDidTriggerClose() } - @IBAction func handleBuyRuuviSensorsButtonTap(_ sender: Any) { + @IBAction func handleActionButtonTap(_ sender: Any) { output.viewDidTriggerBuySensors() } } +// MARK: - DiscoverTableHeaderViewDelegate +extension DiscoverTableViewController: DiscoverTableHeaderViewDelegate { + func didTapAddWithNFCButton(sender: DiscoverTableHeaderView) { + output.viewDidTapUseNFC() + } +} + // MARK: - View lifecycle extension DiscoverTableViewController { @@ -110,6 +194,23 @@ extension DiscoverTableViewController { super.viewWillDisappear(animated) output.viewWillDisappear() } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + if let headerView = tableView.tableHeaderView { + + let height = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height + var headerFrame = headerView.frame + + if height != headerFrame.size.height { + headerFrame.size.height = height + headerView.frame = headerFrame + tableView.tableHeaderView = headerView + } + headerView.translatesAutoresizingMaskIntoConstraints = true + } + } } // MARK: - UITableViewDataSource @@ -163,34 +264,6 @@ extension DiscoverTableViewController: UITableViewDelegate { } } } - - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return 40 - } - - func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - let sectionType = DiscoverTableSection.section(for: ruuviTags.count) - switch sectionType { - case .device: - return ruuviTags.count > 0 ? "DiscoverTable.SectionTitle.Devices".localized(for: Self.self) : nil - case .noDevices: - return ruuviTags.count == 0 ? "DiscoverTable.SectionTitle.Devices".localized(for: Self.self) : nil - default: - return nil - } - } - - func tableView(_ tableView: UITableView, - willDisplayHeaderView view: UIView, - forSection: Int) { - if let headerView = view as? UITableViewHeaderFooterView { - headerView.textLabel?.textColor = UIColor(named: "ruuvi_text_color") - headerView.textLabel?.text = headerView.textLabel?.text?.capitalized - if let font = UIFont(name: "Muli-Regular", size: 13) { - headerView.textLabel?.font = font - } - } - } } // MARK: - Cell configuration @@ -224,6 +297,9 @@ extension DiscoverTableViewController { navigationController?.navigationBar.titleTextAttributes = [.font: muliBold] } + actionButton.setTitle("DiscoverTable.GetMoreSensors.button.title".localized( + for: Self.self + ).capitalized, for: .normal) configureTableView() } @@ -231,6 +307,9 @@ extension DiscoverTableViewController { tableView.rowHeight = 44 tableView.delegate = self tableView.dataSource = self + tableView.tableHeaderView = discoverTableHeaderView + discoverTableHeaderView.delegate = self + tableView.tableFooterView = UIView() } } @@ -253,6 +332,9 @@ extension DiscoverTableViewController { private func updateTableView() { if isViewLoaded { + discoverTableHeaderView.handleNFCButtonViewVisibility( + show: ruuviTags.count > 0 + ) tableView.reloadData() } } @@ -277,3 +359,20 @@ extension DiscoverTableViewController { UIApplication.shared.open(url) } } + +// MARK: - NFCNDEFReaderSessionDelegate +extension DiscoverTableViewController: NFCNDEFReaderSessionDelegate { + func readerSession(_ session: NFCNDEFReaderSession, + didInvalidateWithError error: Error) { + DispatchQueue.main.async { [weak self] in + self?.stopNFCSession() + } + } + + func readerSession(_ session: NFCNDEFReaderSession, + didDetectNDEFs messages: [NFCNDEFMessage]) { + DispatchQueue.main.async { [weak self] in + self?.output?.viewDidReceiveNFCMessages(messages: messages) + } + } +} diff --git a/Modules/RuuviOnboard/Sources/RuuviOnboard/Resources/RuuviOnboard.xcassets/Resources/onboarding_beaver_sign_in.imageset/beaver-sign.png b/Modules/RuuviOnboard/Sources/RuuviOnboard/Resources/RuuviOnboard.xcassets/Resources/onboarding_beaver_sign_in.imageset/beaver-sign.png index 7091cc65f..834810383 100644 Binary files a/Modules/RuuviOnboard/Sources/RuuviOnboard/Resources/RuuviOnboard.xcassets/Resources/onboarding_beaver_sign_in.imageset/beaver-sign.png and b/Modules/RuuviOnboard/Sources/RuuviOnboard/Resources/RuuviOnboard.xcassets/Resources/onboarding_beaver_sign_in.imageset/beaver-sign.png differ diff --git a/Packages/RuuviCloud/RuuviCloud.podspec b/Packages/RuuviCloud/RuuviCloud.podspec index 54cb4bd5a..a426e8225 100644 --- a/Packages/RuuviCloud/RuuviCloud.podspec +++ b/Packages/RuuviCloud/RuuviCloud.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'RuuviCloud' - s.version = '0.0.8' + s.version = '0.0.9' s.summary = 'Ruuvi Cloud' s.homepage = 'https://ruuvi.com' s.author = { 'Rinat Enikeev' => 'rinat@ruuvi.com' } diff --git a/Packages/RuuviCloud/Sources/RuuviCloud/RuuviCloud.swift b/Packages/RuuviCloud/Sources/RuuviCloud/RuuviCloud.swift index 71d6b9760..23ff2963c 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloud/RuuviCloud.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloud/RuuviCloud.swift @@ -16,7 +16,7 @@ public struct ValidateCodeResponse { public protocol RuuviCloud { @discardableResult - func requestCode(email: String) -> Future + func requestCode(email: String) -> Future @discardableResult func validateCode(code: String) -> Future @@ -60,13 +60,13 @@ public protocol RuuviCloud { func claim( name: String, macId: MACIdentifier - ) -> Future + ) -> Future @discardableResult func contest( macId: MACIdentifier, secret: String - ) -> Future + ) -> Future @discardableResult func unclaim(macId: MACIdentifier) -> Future @@ -75,7 +75,7 @@ public protocol RuuviCloud { func share( macId: MACIdentifier, with email: String - ) -> Future + ) -> Future @discardableResult func unshare( @@ -89,7 +89,7 @@ public protocol RuuviCloud { ) -> Future, RuuviCloudError> @discardableResult - func checkOwner(macId: MACIdentifier) -> Future + func checkOwner(macId: MACIdentifier) -> Future @discardableResult func update( @@ -111,7 +111,7 @@ public protocol RuuviCloud { ) -> Future @discardableResult - func getCloudSettings() -> Future + func getCloudSettings() -> Future @discardableResult func set(temperatureUnit: TemperatureUnit) -> Future diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudAPICheckOwnerResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudAPICheckOwnerResponse.swift index 1c4e5b351..f7805cb17 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudAPICheckOwnerResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudAPICheckOwnerResponse.swift @@ -1,5 +1,5 @@ import Foundation public struct RuuviCloudAPICheckOwnerResponse: Decodable { - public let email: String + public let email: String? } diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiAccountDeleteResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiAccountDeleteResponse.swift index 2660c6e07..8c73b899a 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiAccountDeleteResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiAccountDeleteResponse.swift @@ -1,5 +1,5 @@ import Foundation public struct RuuviCloudApiAccountDeleteResponse: Codable { - let email: String + let email: String? } diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiClaimResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiClaimResponse.swift index fc730a4d9..2b53de7e8 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiClaimResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiClaimResponse.swift @@ -1,10 +1,10 @@ import Foundation public struct RuuviCloudApiClaimResponse: Decodable { - public let sensor: String + public let sensor: String? } public struct RuuviCloudApiClaimError: Decodable { - public let error, code: String + public let error, code: String? } public struct RuuviCloudApiUnclaimResponse: Decodable { } diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiContestResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiContestResponse.swift index 584de6766..c308910d6 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiContestResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiContestResponse.swift @@ -1,5 +1,5 @@ import Foundation public struct RuuviCloudApiContestResponse: Decodable { - public let sensor: String + public let sensor: String? } diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetAlertsResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetAlertsResponse.swift index 984552c72..de5e40971 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetAlertsResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetAlertsResponse.swift @@ -2,14 +2,14 @@ import Foundation import RuuviOntology public struct RuuviCloudApiGetAlertsResponse: Decodable { - public var sensors: [RuuviCloudApiGetAlertSensor] + public var sensors: [RuuviCloudApiGetAlertSensor]? } public struct RuuviCloudApiGetAlertSensor: Decodable, RuuviCloudSensorAlerts { - public let sensor: String - public let apiAlerts: [RuuviCloudApiGetAlert] + public let sensor: String? + public let apiAlerts: [RuuviCloudApiGetAlert]? - public var alerts: [RuuviCloudAlert] { + public var alerts: [RuuviCloudAlert]? { return apiAlerts } @@ -20,10 +20,31 @@ public struct RuuviCloudApiGetAlertSensor: Decodable, RuuviCloudSensorAlerts { } public struct RuuviCloudApiGetAlert: Decodable, RuuviCloudAlert { - public let type: RuuviCloudAlertType - public let enabled: Bool - public let min: Double - public let max: Double - public let counter: Int - public let description: String + public let type: RuuviCloudAlertType? + public let enabled: Bool? + public let min: Double? + public let max: Double? + public let counter: Int? + public let description: String? + + enum CodingKeys: String, CodingKey { + case type, enabled, min, max, counter, description + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let typeString = try? container.decode(String.self, forKey: .type), + let type = RuuviCloudAlertType(rawValue: typeString) { + self.type = type + } else { + self.type = nil + } + + enabled = try container.decode(Bool.self, forKey: .enabled) + min = try container.decode(Double.self, forKey: .min) + max = try container.decode(Double.self, forKey: .max) + counter = try container.decode(Int.self, forKey: .counter) + description = try container.decode(String.self, forKey: .description) + } } diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSensorResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSensorResponse.swift index 9ff47b396..8fd560d56 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSensorResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSensorResponse.swift @@ -1,22 +1,25 @@ import Foundation public struct RuuviCloudApiGetSensorResponse: Decodable { - public let sensor: String - public let total: Int - public let name: String - public let measurements: [UserApiSensorRecord] + public let sensor: String? + public let total: Int? + public let name: String? + public let measurements: [UserApiSensorRecord]? } public struct UserApiSensorRecord: Decodable { - public let gwmac: String - public let coordinates: String - public let rssi: Int - public let timestamp: TimeInterval - public let data: String + public let gwmac: String? + public let coordinates: String? + public let rssi: Int? + public let timestamp: TimeInterval? + public let data: String? } extension UserApiSensorRecord { public var date: Date { + guard let timestamp = timestamp else { + return Date() + } return Date(timeIntervalSince1970: timestamp) } } diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSensorsDenseResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSensorsDenseResponse.swift index 955ce62d6..09b5e3f9a 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSensorsDenseResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSensorsDenseResponse.swift @@ -2,7 +2,7 @@ import Foundation import RuuviOntology public struct RuuviCloudApiGetSensorsDenseResponse: Decodable { - public let sensors: [CloudApiSensor] + public let sensors: [CloudApiSensor]? public struct CloudApiSensor: Decodable { public let sensor: String @@ -11,9 +11,9 @@ public struct RuuviCloudApiGetSensorsDenseResponse: Decodable { public let picture: String public let isPublic: Bool public let canShare: Bool - public let offsetTemperature: Double - public let offsetHumidity: Double - public let offsetPressure: Double + public let offsetTemperature: Double? + public let offsetHumidity: Double? + public let offsetPressure: Double? public let sharedTo: [String]? public let measurements: [UserApiSensorRecord]? public let apiAlerts: [RuuviCloudApiGetAlert]? diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSensorsResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSensorsResponse.swift index 120923046..081c505c5 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSensorsResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSensorsResponse.swift @@ -2,15 +2,15 @@ import Foundation import RuuviOntology public struct RuuviCloudApiGetSensorsResponse: Decodable { - public let sensors: [CloudApiShareableSensor] + public let sensors: [CloudApiShareableSensor]? public struct CloudApiShareableSensor: Decodable { public let sensor: String - public let name: String - public let picture: String - public let isPublic: Bool - public let canShare: Bool - public let sharedTo: [String] + public let name: String? + public let picture: String? + public let isPublic: Bool? + public let canShare: Bool? + public let sharedTo: [String]? enum CodingKeys: String, CodingKey { case sensor @@ -22,9 +22,11 @@ public struct RuuviCloudApiGetSensorsResponse: Decodable { } public var shareableSensor: ShareableSensor { - return ShareableSensorStruct(id: sensor, - canShare: canShare, - sharedTo: sharedTo) + return ShareableSensorStruct( + id: sensor, + canShare: canShare ?? false, + sharedTo: sharedTo ?? [] + ) } } } diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSettingsResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSettingsResponse.swift index 4c249bf7e..45c0b3ce3 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSettingsResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiGetSettingsResponse.swift @@ -3,7 +3,7 @@ import RuuviOntology import Humidity public struct RuuviCloudApiGetSettingsResponse: Decodable { - public let settings: RuuviCloudApiSettings + public let settings: RuuviCloudApiSettings? } public struct RuuviCloudApiSettings: Decodable, RuuviCloudSettings { diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiPostAlertResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiPostAlertResponse.swift index 8f9cae228..df24cae08 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiPostAlertResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiPostAlertResponse.swift @@ -1,5 +1,5 @@ import Foundation public struct RuuviCloudApiPostAlertResponse: Decodable { - public let action: String + public let action: String? } diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiPostSettingResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiPostSettingResponse.swift index 4325f5c5e..efc7d2b31 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiPostSettingResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiPostSettingResponse.swift @@ -1,5 +1,5 @@ import Foundation public struct RuuviCloudApiPostSettingResponse: Decodable { - public let action: String + public let action: String? } diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiRegisterResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiRegisterResponse.swift index 95a73bad5..b3ee4e3c2 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiRegisterResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiRegisterResponse.swift @@ -1,5 +1,5 @@ import Foundation public struct RuuviCloudApiRegisterResponse: Decodable { - public let email: String + public let email: String? } diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiSensorImageResetResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiSensorImageResetResponse.swift index 2ee6e4f38..1225ffd1e 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiSensorImageResetResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiSensorImageResetResponse.swift @@ -1,4 +1,3 @@ import Foundation -public struct RuuviCloudApiSensorImageResetResponse: Decodable { -} +public struct RuuviCloudApiSensorImageResetResponse: Decodable {} diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiSensorUpdateResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiSensorUpdateResponse.swift index 5818f2a09..d8190f506 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiSensorUpdateResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiSensorUpdateResponse.swift @@ -1,5 +1,5 @@ import Foundation public struct RuuviCloudApiSensorUpdateResponse: Decodable { - public let name: String + public let name: String? } diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiShareResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiShareResponse.swift index fbc84a20d..e51614609 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiShareResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiShareResponse.swift @@ -1,5 +1,5 @@ import Foundation public struct RuuviCloudApiShareResponse: Decodable { - public let sensor: String + public let sensor: String? } diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiUnshareResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiUnshareResponse.swift index d2b8142b7..17f118dbc 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiUnshareResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiUnshareResponse.swift @@ -1,4 +1,3 @@ import Foundation -public struct RuuviCloudApiUnshareResponse: Decodable { -} +public struct RuuviCloudApiUnshareResponse: Decodable {} diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiUserResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiUserResponse.swift index 2fc6e3eae..07b54a89f 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiUserResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiUserResponse.swift @@ -57,6 +57,11 @@ extension RuuviCloudApiSensor: CloudSensor { public var owner: String? { return sensorOwner } + + public var ownersPlan: String? { + return nil + } + /// Returns status of sensor whether it is already claimed public var isClaimed: Bool { return isOwner diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiVerifyResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiVerifyResponse.swift index 1cd8d3e8a..85b2b72b1 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiVerifyResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudApiVerifyResponse.swift @@ -1,9 +1,9 @@ import Foundation public struct RuuviCloudApiVerifyResponse: Decodable { - public let email: String - public let accessToken: String - public let isNewUser: Bool + public let email: String? + public let accessToken: String? + public let isNewUser: Bool? enum CodingKeys: String, CodingKey { case email diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudPNTokenListResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudPNTokenListResponse.swift index fec132dbc..d0d9c89ab 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudPNTokenListResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudPNTokenListResponse.swift @@ -2,7 +2,7 @@ import Foundation import RuuviOntology public struct RuuviCloudPNTokenListResponse: Decodable { - public let tokens: [CloudPNTokens] + public let tokens: [CloudPNTokens]? public struct CloudPNTokens: Decodable { public let id: Int @@ -11,8 +11,15 @@ public struct RuuviCloudPNTokenListResponse: Decodable { } public var anyTokens: [RuuviCloudPNToken] { - return tokens.map({ RuuviCloudPNTokenStruct(id: $0.id, - lastAccessed: $0.lastAccessed, - name: $0.name) }) + guard let tokens = tokens else { + return [] + } + return tokens.map({ + RuuviCloudPNTokenStruct( + id: $0.id, + lastAccessed: $0.lastAccessed, + name: $0.name + ) + }) } } diff --git a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudPNTokenUnregisterResponse.swift b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudPNTokenUnregisterResponse.swift index 72b501945..3f3dbac7f 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudPNTokenUnregisterResponse.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudApi/URLSession/Models/Response/RuuviCloudPNTokenUnregisterResponse.swift @@ -1,5 +1,3 @@ import Foundation -public struct RuuviCloudPNTokenUnregisterResponse: Decodable { - -} +public struct RuuviCloudPNTokenUnregisterResponse: Decodable {} diff --git a/Packages/RuuviCloud/Sources/RuuviCloudPure/RuuviCloudPure.swift b/Packages/RuuviCloud/Sources/RuuviCloudPure/RuuviCloudPure.swift index 8b5dd143d..bd5d983b5 100644 --- a/Packages/RuuviCloud/Sources/RuuviCloudPure/RuuviCloudPure.swift +++ b/Packages/RuuviCloud/Sources/RuuviCloudPure/RuuviCloudPure.swift @@ -34,7 +34,7 @@ public final class RuuviCloudPure: RuuviCloud { let request = RuuviCloudApiGetAlertsRequest() api.getAlerts(request, authorization: apiKey) .on(success: { response in - promise.succeed(value: response.sensors) + promise.succeed(value: response.sensors ?? []) }, failure: { error in promise.fail(error: .api(error)) }) @@ -476,8 +476,8 @@ public final class RuuviCloudPure: RuuviCloud { } @discardableResult - public func getCloudSettings() -> Future { - let promise = Promise() + public func getCloudSettings() -> Future { + let promise = Promise() guard let apiKey = user.apiKey else { promise.fail(error: .notAuthorized) return promise.future @@ -637,8 +637,8 @@ public final class RuuviCloudPure: RuuviCloud { let request = RuuviCloudApiGetSensorsRequest(sensor: sensor.id) api.sensors(request, authorization: apiKey) .on(success: { response in - let arrayOfAny = response.sensors.map({ $0.shareableSensor.any }) - let setOfAny = Set(arrayOfAny) + let arrayOfAny = response.sensors?.map({ $0.shareableSensor.any }) + let setOfAny = Set(arrayOfAny ?? []) promise.succeed(value: setOfAny) }, failure: { error in promise.fail(error: .api(error)) @@ -647,8 +647,8 @@ public final class RuuviCloudPure: RuuviCloud { } @discardableResult - public func checkOwner(macId: MACIdentifier) -> Future { - let promise = Promise() + public func checkOwner(macId: MACIdentifier) -> Future { + let promise = Promise() guard let apiKey = user.apiKey else { promise.fail(error: .notAuthorized) return promise.future @@ -684,7 +684,7 @@ public final class RuuviCloudPure: RuuviCloud { ) api.sensorsDense(request, authorization: apiKey) .on(success: { [weak self] response in - let arrayOfAny = response.sensors.compactMap({ sensor in + let arrayOfAny = response.sensors?.compactMap({ sensor in RuuviCloudSensorDense( sensor: CloudSensorStruct( id: sensor.sensor, @@ -692,12 +692,13 @@ public final class RuuviCloudPure: RuuviCloud { isClaimed: true, isOwner: sensor.owner == self?.user.email, owner: sensor.owner, + ownersPlan: sensor.subscription?.subscriptionName, picture: URL(string: sensor.picture), offsetTemperature: sensor.offsetTemperature, offsetHumidity: sensor.offsetHumidity, offsetPressure: sensor.offsetPressure, isCloudSensor: true, - canShare: sensor.canShare, + canShare: sensor.canShare ?? false, sharedTo: sensor.sharedTo ?? [] ), record: self?.decodeSensorRecord( @@ -708,15 +709,15 @@ public final class RuuviCloudPure: RuuviCloud { subscription: sensor.subscription ) }) - promise.succeed(value: arrayOfAny) + promise.succeed(value: arrayOfAny ?? []) }, failure: { error in promise.fail(error: .api(error)) }) return promise.future } - public func share(macId: MACIdentifier, with email: String) -> Future { - let promise = Promise() + public func share(macId: MACIdentifier, with email: String) -> Future { + let promise = Promise() guard let apiKey = user.apiKey else { promise.fail(error: .notAuthorized) return promise.future @@ -724,7 +725,7 @@ public final class RuuviCloudPure: RuuviCloud { let request = RuuviCloudApiShareRequest(user: email, sensor: macId.value) api.share(request, authorization: apiKey) .on(success: { response in - promise.succeed(value: response.sensor.mac) + promise.succeed(value: response.sensor?.mac) }, failure: { error in promise.fail(error: .api(error)) }) @@ -760,8 +761,8 @@ public final class RuuviCloudPure: RuuviCloud { public func claim( name: String, macId: MACIdentifier - ) -> Future { - let promise = Promise() + ) -> Future { + let promise = Promise() guard let apiKey = user.apiKey else { promise.fail(error: .notAuthorized) return promise.future @@ -769,7 +770,7 @@ public final class RuuviCloudPure: RuuviCloud { let request = RuuviCloudApiClaimRequest(name: name, sensor: macId.value) api.claim(request, authorization: apiKey) .on(success: { response in - promise.succeed(value: response.sensor.mac) + promise.succeed(value: response.sensor?.mac) }, failure: { error in promise.fail(error: .api(error)) }) @@ -780,8 +781,8 @@ public final class RuuviCloudPure: RuuviCloud { public func contest( macId: MACIdentifier, secret: String - ) -> Future { - let promise = Promise() + ) -> Future { + let promise = Promise() guard let apiKey = user.apiKey else { promise.fail(error: .notAuthorized) return promise.future @@ -789,7 +790,7 @@ public final class RuuviCloudPure: RuuviCloud { let request = RuuviCloudApiContestRequest(sensor: macId.value, secret: secret) api.contest(request, authorization: apiKey) .on(success: { response in - promise.succeed(value: response.sensor.mac) + promise.succeed(value: response.sensor?.mac) }, failure: { error in promise.fail(error: .api(error)) }) @@ -818,8 +819,8 @@ public final class RuuviCloudPure: RuuviCloud { return promise.future } - public func requestCode(email: String) -> Future { - let promise = Promise() + public func requestCode(email: String) -> Future { + let promise = Promise() let request = RuuviCloudApiRegisterRequest(email: email) api.register(request) .on(success: { response in @@ -835,9 +836,13 @@ public final class RuuviCloudPure: RuuviCloud { let request = RuuviCloudApiVerifyRequest(token: code) api.verify(request) .on(success: { response in + guard let email = response.email, + let accessToken = response.accessToken else { + return promise.fail(error: .api(.api(.erInternal))) + } let result = ValidateCodeResponse( - email: response.email, - apiKey: response.accessToken + email: email, + apiKey: accessToken ) promise.succeed(value: result) }, failure: { error in @@ -1138,22 +1143,27 @@ public final class RuuviCloudPure: RuuviCloud { response: RuuviCloudApiGetSensorResponse ) -> [AnyRuuviTagSensorRecord] { let decoder = Ruuvi.decoder - return response.measurements.compactMap({ - guard let device = decoder.decodeNetwork( + guard let measurements = response.measurements else { + return [] + } + return measurements.compactMap({ measurement in + guard let rssi = measurement.rssi, + let data = measurement.data, + let device = decoder.decodeNetwork( uuid: macId.value, - rssi: $0.rssi, + rssi: rssi, isConnectable: true, - payload: $0.data + payload: data ), let tag = device.ruuvi?.tag else { return nil } return RuuviTagSensorRecordStruct( luid: nil, - date: $0.date, + date: measurement.date, source: .ruuviNetwork, macId: macId, - rssi: $0.rssi, + rssi: rssi, temperature: tag.temperature, humidity: tag.humidity, pressure: tag.pressure, @@ -1175,12 +1185,14 @@ public final class RuuviCloudPure: RuuviCloud { ) -> AnyRuuviTagSensorRecord? { let decoder = Ruuvi.decoder guard let record = record, - let device = decoder.decodeNetwork( + let rssi = record.rssi, + let data = record.data, + let device = decoder.decodeNetwork( uuid: macId.value, - rssi: record.rssi, + rssi: rssi, isConnectable: true, - payload: record.data - ), + payload: data + ), let tag = device.ruuvi?.tag else { return nil } diff --git a/Packages/RuuviContext/Sources/RuuviContextSQLite/SQLiteContextGRDB.swift b/Packages/RuuviContext/Sources/RuuviContextSQLite/SQLiteContextGRDB.swift index 006b8b0a9..1dd403631 100644 --- a/Packages/RuuviContext/Sources/RuuviContextSQLite/SQLiteContextGRDB.swift +++ b/Packages/RuuviContext/Sources/RuuviContextSQLite/SQLiteContextGRDB.swift @@ -191,6 +191,16 @@ extension SQLiteGRDBDatabase { .defaults(to: "") }) } + // v11 + migrator.registerMigration("Create RuuviTagSQLite ownersPlan column") { db in + guard try db.columns(in: RuuviTagSQLite.databaseTableName) + .contains(where: {$0.name == RuuviTagSQLite.ownersPlan.name}) == false else { + return + } + try db.alter(table: RuuviTagSQLite.databaseTableName, body: { (t) in + t.add(column: RuuviTagSQLite.ownersPlan.name, .text) + }) + } try migrator.migrate(dbPool) } diff --git a/Packages/RuuviDaemon/Sources/RuuviDaemonRuuviTag/Advertisement/RuuviTagAdvertisementDaemonBTKit.swift b/Packages/RuuviDaemon/Sources/RuuviDaemonRuuviTag/Advertisement/RuuviTagAdvertisementDaemonBTKit.swift index cf1de7750..06844964f 100644 --- a/Packages/RuuviDaemon/Sources/RuuviDaemonRuuviTag/Advertisement/RuuviTagAdvertisementDaemonBTKit.swift +++ b/Packages/RuuviDaemon/Sources/RuuviDaemonRuuviTag/Advertisement/RuuviTagAdvertisementDaemonBTKit.swift @@ -215,15 +215,17 @@ public final class RuuviTagAdvertisementDaemonBTKit: RuuviDaemonWorker, RuuviTag } @objc private func persist(wrapper: RuuviTagWrapper) { + guard wrapper.device.luid != nil else { return } let uuid = wrapper.device.uuid // If the tag chart is on foreground store all advertisements // Otherwise respect the settings - guard wrapper.device.luid != nil else { return } if settings.appIsOnForeground { - if let previous = advertisementSequence[uuid], let previous = previous { - if let next = wrapper.device.measurementSequenceNumber, next > previous { - persist(wrapper.device, uuid) + let previous = advertisementSequence[uuid] + if previous != nil && previous!! != nil { + guard let next = wrapper.device.measurementSequenceNumber, next > previous!! else { + return } + persist(wrapper.device, uuid) } else { // Tags with data format 3 doesn't sent duplicates packets* if wrapper.device.version == 3 { diff --git a/Packages/RuuviDaemon/Sources/RuuviDaemonRuuviTag/Properties/RuuviTagPropertiesDaemonBTKit.swift b/Packages/RuuviDaemon/Sources/RuuviDaemonRuuviTag/Properties/RuuviTagPropertiesDaemonBTKit.swift index 9e690d48a..f91db2d27 100644 --- a/Packages/RuuviDaemon/Sources/RuuviDaemonRuuviTag/Properties/RuuviTagPropertiesDaemonBTKit.swift +++ b/Packages/RuuviDaemon/Sources/RuuviDaemonRuuviTag/Properties/RuuviTagPropertiesDaemonBTKit.swift @@ -143,7 +143,9 @@ public final class RuuviTagPropertiesDaemonBTKit: RuuviDaemonWorker, RuuviTagPro // either by pressing B or by upgrading firmware if let mac = idPersistence.mac(for: pair.device.uuid.luid) { // tag is already saved to SQLite - ruuviPool.update(pair.ruuviTag.with(macId: mac)) + ruuviPool.update(pair.ruuviTag + .with(macId: mac) + .with(version: pair.device.version)) .on(failure: { [weak self] error in self?.post(error: .ruuviPool(error)) }) @@ -199,14 +201,22 @@ public final class RuuviTagPropertiesDaemonBTKit: RuuviDaemonWorker, RuuviTagPro } else if pair.ruuviTag.macId?.value != nil, pair.device.mac == nil { // this is the case when 2.5.9 tag is returning to data format 3 mode // but we have it in sqlite database already - if let mac = idPersistence.mac(for: pair.device.uuid.luid) { - ruuviPool.update(pair.ruuviTag.with(macId: mac)) + if let mac = idPersistence.mac(for: pair.device.uuid.luid), + pair.device.version != pair.ruuviTag.version { + ruuviPool.update(pair.ruuviTag + .with(macId: mac) + .with(version: pair.device.version)) + .on(failure: { [weak self] error in + self?.post(error: .ruuviPool(error)) + }) + } + } else { + if pair.device.version != pair.ruuviTag.version { + ruuviPool.update(pair.ruuviTag + .with(version: pair.device.version)) .on(failure: { [weak self] error in self?.post(error: .ruuviPool(error)) }) - } else { - // Should never be there - return } } } @@ -235,6 +245,7 @@ public final class RuuviTagPropertiesDaemonBTKit: RuuviDaemonWorker, RuuviTagPro isClaimed: ruuviTag.isClaimed, isOwner: ruuviTag.isOwner, owner: ruuviTag.owner, + ownersPlan: ruuviTag.ownersPlan, isCloudSensor: ruuviTag.isCloudSensor, canShare: ruuviTag.canShare, sharedTo: ruuviTag.sharedTo) diff --git a/Packages/RuuviNotification/Sources/RuuviNotificationLocal/RuuviNotificationLocalImpl.swift b/Packages/RuuviNotification/Sources/RuuviNotificationLocal/RuuviNotificationLocalImpl.swift index 58728096e..04f64f192 100644 --- a/Packages/RuuviNotification/Sources/RuuviNotificationLocal/RuuviNotificationLocalImpl.swift +++ b/Packages/RuuviNotification/Sources/RuuviNotificationLocal/RuuviNotificationLocalImpl.swift @@ -484,6 +484,7 @@ extension RuuviNotificationLocalImpl: UNUserNotificationCenterDelegate { isClaimed: false, isOwner: false, owner: nil, + ownersPlan: nil, isCloudSensor: false, canShare: false, sharedTo: [] @@ -513,6 +514,7 @@ extension RuuviNotificationLocalImpl: UNUserNotificationCenterDelegate { isClaimed: false, isOwner: false, owner: nil, + ownersPlan: nil, isCloudSensor: false, canShare: false, sharedTo: [] @@ -562,6 +564,7 @@ extension RuuviNotificationLocalImpl: UNUserNotificationCenterDelegate { isClaimed: false, isOwner: false, owner: nil, + ownersPlan: nil, isCloudSensor: false, canShare: false, sharedTo: [] @@ -594,6 +597,7 @@ extension RuuviNotificationLocalImpl: UNUserNotificationCenterDelegate { isClaimed: false, isOwner: false, owner: nil, + ownersPlan: nil, isCloudSensor: false, canShare: false, sharedTo: [] diff --git a/Packages/RuuviOntology/Sources/RuuviOntology/Common/RuuviCloudAlert.swift b/Packages/RuuviOntology/Sources/RuuviOntology/Common/RuuviCloudAlert.swift index 8af68fe40..72a502fc7 100644 --- a/Packages/RuuviOntology/Sources/RuuviOntology/Common/RuuviCloudAlert.swift +++ b/Packages/RuuviOntology/Sources/RuuviOntology/Common/RuuviCloudAlert.swift @@ -17,15 +17,15 @@ public enum RuuviCloudAlertSettingType: String { } public protocol RuuviCloudSensorAlerts { - var sensor: String { get } - var alerts: [RuuviCloudAlert] { get } + var sensor: String? { get } + var alerts: [RuuviCloudAlert]? { get } } public protocol RuuviCloudAlert { - var type: RuuviCloudAlertType { get } - var enabled: Bool { get } - var min: Double { get } - var max: Double { get } - var counter: Int { get } - var description: String { get } + var type: RuuviCloudAlertType? { get } + var enabled: Bool? { get } + var min: Double? { get } + var max: Double? { get } + var counter: Int? { get } + var description: String? { get } } diff --git a/Packages/RuuviOntology/Sources/RuuviOntology/Identifier/Identifier.swift b/Packages/RuuviOntology/Sources/RuuviOntology/Identifier/Identifier.swift index d971337cf..7ef96e402 100644 --- a/Packages/RuuviOntology/Sources/RuuviOntology/Identifier/Identifier.swift +++ b/Packages/RuuviOntology/Sources/RuuviOntology/Identifier/Identifier.swift @@ -77,6 +77,22 @@ extension String { } } +extension Optional where Wrapped == String { + public var luid: LocalIdentifier? { + guard let self = self else { + return nil + } + return LocalIdentifierStruct(value: self).any + } + + public var mac: MACIdentifier? { + guard let self = self else { + return nil + } + return MACIdentifierStruct(value: self).any + } +} + extension LocalIdentifier { public var any: AnyLocalIdentifier { return AnyLocalIdentifier(object: self) diff --git a/Packages/RuuviOntology/Sources/RuuviOntology/Mappers/RuuviTag+RuuviTagSensor.swift b/Packages/RuuviOntology/Sources/RuuviOntology/Mappers/RuuviTag+RuuviTagSensor.swift index 8d7993d92..155648f62 100644 --- a/Packages/RuuviOntology/Sources/RuuviOntology/Mappers/RuuviTag+RuuviTagSensor.swift +++ b/Packages/RuuviOntology/Sources/RuuviOntology/Mappers/RuuviTag+RuuviTagSensor.swift @@ -13,6 +13,7 @@ extension RuuviTag { isClaimed: false, isOwner: true, owner: nil, + ownersPlan: nil, isCloudSensor: false, canShare: false, sharedTo: [] diff --git a/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/CloudSensor.swift b/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/CloudSensor.swift index 0cf3ffea3..d81bc01b9 100644 --- a/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/CloudSensor.swift +++ b/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/CloudSensor.swift @@ -12,6 +12,7 @@ extension CloudSensor { isClaimed: isOwner, isOwner: isOwner, owner: owner, + ownersPlan: ownersPlan, isCloudSensor: isCloudSensor, canShare: canShare, sharedTo: sharedTo @@ -25,6 +26,7 @@ extension CloudSensor { isClaimed: email == owner, isOwner: email == owner, owner: owner, + ownersPlan: ownersPlan, picture: picture, offsetTemperature: offsetTemperature, offsetHumidity: offsetHumidity, @@ -49,6 +51,7 @@ public struct CloudSensorStruct: CloudSensor { public var isCloudSensor: Bool? public var canShare: Bool public var sharedTo: [String] + public var ownersPlan: String? public init( id: String, @@ -56,6 +59,7 @@ public struct CloudSensorStruct: CloudSensor { isClaimed: Bool, isOwner: Bool, owner: String?, + ownersPlan: String?, picture: URL?, offsetTemperature: Double?, offsetHumidity: Double?, @@ -69,6 +73,7 @@ public struct CloudSensorStruct: CloudSensor { self.isClaimed = isClaimed self.isOwner = isOwner self.owner = owner + self.ownersPlan = ownersPlan self.picture = picture self.offsetTemperature = offsetTemperature self.offsetHumidity = offsetHumidity @@ -112,6 +117,10 @@ public struct AnyCloudSensor: CloudSensor, Equatable, Hashable, Reorderable { return object.owner } + public var ownersPlan: String? { + return object.ownersPlan + } + public var picture: URL? { return object.picture } diff --git a/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/CloudSensorDense.swift b/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/CloudSensorDense.swift index 96530f914..33bca26bc 100644 --- a/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/CloudSensorDense.swift +++ b/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/CloudSensorDense.swift @@ -20,11 +20,14 @@ public struct RuuviCloudSensorDense { public struct AnyCloudSensorDense: CloudSensor, Equatable, Hashable, Reorderable { private let sensor: CloudSensor private let record: RuuviTagSensorRecord + private let subscription: CloudSensorSubscription? public init(sensor: CloudSensor, - record: RuuviTagSensorRecord) { + record: RuuviTagSensorRecord, + subscription: CloudSensorSubscription?) { self.sensor = sensor self.record = record + self.subscription = subscription } public var id: String { @@ -47,6 +50,10 @@ public struct AnyCloudSensorDense: CloudSensor, Equatable, Hashable, Reorderable return sensor.owner } + public var ownersPlan: String? { + return subscription?.subscriptionName + } + public var picture: URL? { return sensor.picture } diff --git a/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/NFCSensor.swift b/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/NFCSensor.swift new file mode 100644 index 000000000..571e792f1 --- /dev/null +++ b/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/NFCSensor.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct NFCSensor: Sensor { + public var id: String + public var macId: String + public var firmwareVersion: String + + public init(id: String, macId: String, firmwareVersion: String) { + self.id = id + self.macId = macId + self.firmwareVersion = firmwareVersion + } +} diff --git a/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/RuuviTag/RuuviTagSensor.swift b/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/RuuviTag/RuuviTagSensor.swift index ea879eee6..0dc92ddcf 100644 --- a/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/RuuviTag/RuuviTagSensor.swift +++ b/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/RuuviTag/RuuviTagSensor.swift @@ -30,6 +30,7 @@ extension RuuviTagSensor { isClaimed: isClaimed, isOwner: isOwner, owner: owner, + ownersPlan: ownersPlan, isCloudSensor: isCloudSensor, canShare: canShare, sharedTo: sharedTo @@ -47,6 +48,7 @@ extension RuuviTagSensor { isClaimed: isClaimed, isOwner: isOwner, owner: owner, + ownersPlan: ownersPlan, isCloudSensor: isCloudSensor, canShare: canShare, sharedTo: sharedTo @@ -64,6 +66,7 @@ extension RuuviTagSensor { isClaimed: isClaimed, isOwner: isOwner, owner: owner, + ownersPlan: ownersPlan, isCloudSensor: isCloudSensor, canShare: canShare, sharedTo: sharedTo @@ -81,6 +84,7 @@ extension RuuviTagSensor { isClaimed: isClaimed, isOwner: isOwner, owner: owner, + ownersPlan: ownersPlan, isCloudSensor: isCloudSensor, canShare: canShare, sharedTo: sharedTo @@ -98,6 +102,7 @@ extension RuuviTagSensor { isClaimed: isClaimed, isOwner: isOwner, owner: owner, + ownersPlan: ownersPlan, isCloudSensor: isCloudSensor, canShare: canShare, sharedTo: sharedTo @@ -115,6 +120,7 @@ extension RuuviTagSensor { isClaimed: isClaimed, isOwner: isOwner, owner: owner, + ownersPlan: ownersPlan, isCloudSensor: isCloudSensor, canShare: canShare, sharedTo: sharedTo @@ -132,6 +138,25 @@ extension RuuviTagSensor { isClaimed: isClaimed, isOwner: isOwner, owner: owner, + ownersPlan: ownersPlan, + isCloudSensor: isCloudSensor, + canShare: canShare, + sharedTo: sharedTo + ) + } + + public func with(ownersPlan: String) -> RuuviTagSensor { + return RuuviTagSensorStruct( + version: version, + firmwareVersion: firmwareVersion, + luid: luid, + macId: macId, + isConnectable: isConnectable, + name: name, + isClaimed: isClaimed, + isOwner: isOwner, + owner: owner, + ownersPlan: ownersPlan, isCloudSensor: isCloudSensor, canShare: canShare, sharedTo: sharedTo @@ -149,6 +174,7 @@ extension RuuviTagSensor { isClaimed: isClaimed, isOwner: isOwner, owner: owner, + ownersPlan: ownersPlan, isCloudSensor: isCloudSensor, canShare: canShare, sharedTo: sharedTo @@ -166,6 +192,7 @@ extension RuuviTagSensor { isClaimed: isClaimed, isOwner: isOwner, owner: owner, + ownersPlan: ownersPlan, isCloudSensor: isCloudSensor, canShare: canShare, sharedTo: sharedTo @@ -183,6 +210,7 @@ extension RuuviTagSensor { isClaimed: isClaimed, isOwner: isOwner, owner: nil, + ownersPlan: nil, isCloudSensor: isCloudSensor, canShare: canShare, sharedTo: sharedTo @@ -200,6 +228,7 @@ extension RuuviTagSensor { isClaimed: isClaimed, isOwner: isOwner, owner: owner, + ownersPlan: ownersPlan, isCloudSensor: isCloudSensor, canShare: canShare, sharedTo: sharedTo @@ -217,6 +246,7 @@ extension RuuviTagSensor { isClaimed: cloudSensor.isOwner, isOwner: cloudSensor.isOwner, owner: cloudSensor.owner, + ownersPlan: cloudSensor.ownersPlan, isCloudSensor: cloudSensor.isCloudSensor ?? true, canShare: cloudSensor.canShare, sharedTo: cloudSensor.sharedTo @@ -235,6 +265,7 @@ extension RuuviTagSensor { isClaimed: isClaimed, isOwner: isOwner, owner: owner, + ownersPlan: ownersPlan, isCloudSensor: isCloudSensor, canShare: canShare, sharedTo: sharedTo @@ -252,6 +283,7 @@ extension RuuviTagSensor { isClaimed: false, isOwner: true, owner: owner, + ownersPlan: ownersPlan, isCloudSensor: false, canShare: canShare, sharedTo: sharedTo @@ -269,6 +301,7 @@ extension RuuviTagSensor { isClaimed: isClaimed, isOwner: isOwner, owner: owner, + ownersPlan: ownersPlan, isCloudSensor: isCloudSensor, canShare: canShare, sharedTo: sharedTo @@ -286,6 +319,7 @@ extension RuuviTagSensor { isClaimed: isClaimed, isOwner: isOwner, owner: owner, + ownersPlan: ownersPlan, isCloudSensor: isCloudSensor, canShare: canShare, sharedTo: sharedTo @@ -309,6 +343,7 @@ public struct RuuviTagSensorStruct: RuuviTagSensor { public var isClaimed: Bool public var isOwner: Bool public var owner: String? + public var ownersPlan: String? public var isCloudSensor: Bool? public var canShare: Bool public var sharedTo: [String] @@ -323,6 +358,7 @@ public struct RuuviTagSensorStruct: RuuviTagSensor { isClaimed: Bool, isOwner: Bool, owner: String?, + ownersPlan: String?, isCloudSensor: Bool?, canShare: Bool, sharedTo: [String] @@ -336,6 +372,7 @@ public struct RuuviTagSensorStruct: RuuviTagSensor { self.isClaimed = isClaimed self.isOwner = isOwner self.owner = owner + self.ownersPlan = ownersPlan self.isCloudSensor = isCloudSensor self.canShare = canShare self.sharedTo = sharedTo @@ -379,6 +416,9 @@ public struct AnyRuuviTagSensor: RuuviTagSensor, Equatable, Hashable, Reorderabl public var owner: String? { return object.owner } + public var ownersPlan: String? { + return object.ownersPlan + } public var isCloudSensor: Bool? { return object.isCloudSensor } diff --git a/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/Sensor.swift b/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/Sensor.swift index 20e3f2fc5..f0eb46d7e 100644 --- a/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/Sensor.swift +++ b/Packages/RuuviOntology/Sources/RuuviOntology/Sensor/Sensor.swift @@ -26,6 +26,7 @@ public protocol Claimable { var isOwner: Bool { get } var owner: String? { get } var isCloudSensor: Bool? { get } + var ownersPlan: String? { get } } public protocol Sensor: StringIdentifieable {} diff --git a/Packages/RuuviOntology/Sources/RuuviOntologyRealm/RuuviTagRealm+RuuviTagSensor.swift b/Packages/RuuviOntology/Sources/RuuviOntologyRealm/RuuviTagRealm+RuuviTagSensor.swift index c18071be5..df48f6c57 100644 --- a/Packages/RuuviOntology/Sources/RuuviOntologyRealm/RuuviTagRealm+RuuviTagSensor.swift +++ b/Packages/RuuviOntology/Sources/RuuviOntologyRealm/RuuviTagRealm+RuuviTagSensor.swift @@ -23,6 +23,7 @@ extension RuuviTagRealm: RuuviTagSensor { isClaimed: isClaimed, isOwner: isOwner, owner: owner, + ownersPlan: ownersPlan, isCloudSensor: isCloudSensor, canShare: canShare, sharedTo: sharedTo @@ -36,6 +37,9 @@ extension RuuviTagRealm: RuuviTagSensor { public var owner: String? { return nil } + public var ownersPlan: String? { + return nil + } public var isCloudSensor: Bool? { return false } diff --git a/Packages/RuuviOntology/Sources/RuuviOntologySQLite/RuuviTagSQLite.swift b/Packages/RuuviOntology/Sources/RuuviOntologySQLite/RuuviTagSQLite.swift index 5862ca4fd..fed6d3e59 100644 --- a/Packages/RuuviOntology/Sources/RuuviOntologySQLite/RuuviTagSQLite.swift +++ b/Packages/RuuviOntology/Sources/RuuviOntologySQLite/RuuviTagSQLite.swift @@ -13,6 +13,7 @@ public struct RuuviTagSQLite: RuuviTagSensor { public var isClaimed: Bool public var isOwner: Bool public var owner: String? + public var ownersPlan: String? public var isCloudSensor: Bool? public var canShare: Bool public var sharedTo: [String] @@ -28,6 +29,7 @@ public struct RuuviTagSQLite: RuuviTagSensor { isClaimed: Bool, isOwner: Bool, owner: String?, + ownersPlan: String?, isCloudSensor: Bool?, canShare: Bool, sharedTo: [String] @@ -42,6 +44,7 @@ public struct RuuviTagSQLite: RuuviTagSensor { self.isClaimed = isClaimed self.isOwner = isOwner self.owner = owner + self.ownersPlan = ownersPlan self.isCloudSensor = isCloudSensor self.canShare = canShare self.sharedTo = sharedTo @@ -60,6 +63,7 @@ extension RuuviTagSQLite { public static let isClaimedColumn = Column("isClaimed") public static let isOwnerColumn = Column("isOwner") public static let owner = Column("owner") + public static let ownersPlan = Column("ownersPlan") public static let isCloudSensor = Column("isCloudSensor") public static let canShareColumn = Column("canShare") public static let sharedToColumn = Column("sharedTo") @@ -81,6 +85,7 @@ extension RuuviTagSQLite: FetchableRecord { isClaimed = row[RuuviTagSQLite.isClaimedColumn] isOwner = row[RuuviTagSQLite.isOwnerColumn] owner = row[RuuviTagSQLite.owner] + ownersPlan = row[RuuviTagSQLite.ownersPlan] isCloudSensor = row[RuuviTagSQLite.isCloudSensor] canShare = row[RuuviTagSQLite.canShareColumn] if let sharedToColumn = row[RuuviTagSQLite.sharedToColumn] as? String { @@ -107,6 +112,7 @@ extension RuuviTagSQLite: PersistableRecord { container[RuuviTagSQLite.isClaimedColumn] = isClaimed container[RuuviTagSQLite.isOwnerColumn] = isOwner container[RuuviTagSQLite.owner] = owner + container[RuuviTagSQLite.ownersPlan] = ownersPlan container[RuuviTagSQLite.isCloudSensor] = isCloudSensor container[RuuviTagSQLite.canShareColumn] = canShare container[RuuviTagSQLite.sharedToColumn] = sharedTo.joined(separator: ",") @@ -130,6 +136,7 @@ extension RuuviTagSQLite { .notNull() .defaults(to: true) table.column(RuuviTagSQLite.owner.name, .text) + table.column(RuuviTagSQLite.ownersPlan.name, .text) table.column(RuuviTagSQLite.isCloudSensor.name, .boolean) table.column(RuuviTagSQLite.canShareColumn.name, .boolean) table.column(RuuviTagSQLite.sharedToColumn.name, .text) diff --git a/Packages/RuuviPersistence/Sources/RuuviPersistenceRealm/RuuviPersistenceRealm.swift b/Packages/RuuviPersistence/Sources/RuuviPersistenceRealm/RuuviPersistenceRealm.swift index c735eecec..bf7a4429f 100644 --- a/Packages/RuuviPersistence/Sources/RuuviPersistenceRealm/RuuviPersistenceRealm.swift +++ b/Packages/RuuviPersistence/Sources/RuuviPersistenceRealm/RuuviPersistenceRealm.swift @@ -671,6 +671,7 @@ extension RuuviPersistenceRealm { isClaimed: false, isOwner: ruuviTagRealm.isOwner, owner: ruuviTagRealm.owner, + ownersPlan: ruuviTagRealm.ownersPlan, isCloudSensor: ruuviTagRealm.isCloudSensor, canShare: ruuviTagRealm.canShare, sharedTo: ruuviTagRealm.sharedTo diff --git a/Packages/RuuviPersistence/Sources/RuuviPersistenceSQLite/RuuviPersistenceSQLite.swift b/Packages/RuuviPersistence/Sources/RuuviPersistenceSQLite/RuuviPersistenceSQLite.swift index cf89fdbf7..4920f3b66 100644 --- a/Packages/RuuviPersistence/Sources/RuuviPersistenceSQLite/RuuviPersistenceSQLite.swift +++ b/Packages/RuuviPersistence/Sources/RuuviPersistenceSQLite/RuuviPersistenceSQLite.swift @@ -49,6 +49,7 @@ public class RuuviPersistenceSQLite: RuuviPersistence, DatabaseService { isClaimed: ruuviTag.isClaimed, isOwner: ruuviTag.isOwner, owner: ruuviTag.owner, + ownersPlan: ruuviTag.ownersPlan, isCloudSensor: ruuviTag.isCloudSensor, canShare: ruuviTag.canShare, sharedTo: ruuviTag.sharedTo @@ -419,6 +420,7 @@ public class RuuviPersistenceSQLite: RuuviPersistence, DatabaseService { isClaimed: ruuviTag.isClaimed, isOwner: ruuviTag.isOwner, owner: ruuviTag.owner, + ownersPlan: ruuviTag.ownersPlan, isCloudSensor: ruuviTag.isCloudSensor, canShare: ruuviTag.canShare, sharedTo: ruuviTag.sharedTo @@ -450,6 +452,7 @@ public class RuuviPersistenceSQLite: RuuviPersistence, DatabaseService { isClaimed: ruuviTag.isClaimed, isOwner: ruuviTag.isOwner, owner: ruuviTag.owner, + ownersPlan: ruuviTag.ownersPlan, isCloudSensor: ruuviTag.isCloudSensor, canShare: ruuviTag.canShare, sharedTo: ruuviTag.sharedTo diff --git a/Packages/RuuviService/Sources/RuuviService/RuuviServiceOwnership.swift b/Packages/RuuviService/Sources/RuuviService/RuuviServiceOwnership.swift index 393881c1b..bf662c768 100644 --- a/Packages/RuuviService/Sources/RuuviService/RuuviServiceOwnership.swift +++ b/Packages/RuuviService/Sources/RuuviService/RuuviServiceOwnership.swift @@ -22,7 +22,7 @@ public protocol RuuviServiceOwnership { func unclaim(sensor: RuuviTagSensor) -> Future @discardableResult - func share(macId: MACIdentifier, with email: String) -> Future + func share(macId: MACIdentifier, with email: String) -> Future @discardableResult func unshare(macId: MACIdentifier, with email: String?) -> Future @@ -31,7 +31,7 @@ public protocol RuuviServiceOwnership { func loadShared(for sensor: RuuviTagSensor) -> Future, RuuviServiceError> @discardableResult - func checkOwner(macId: MACIdentifier) -> Future + func checkOwner(macId: MACIdentifier) -> Future @discardableResult func updateShareable(for sensor: RuuviTagSensor) -> Future diff --git a/Packages/RuuviService/Sources/RuuviService/RuuviServiceSensorProperties.swift b/Packages/RuuviService/Sources/RuuviService/RuuviServiceSensorProperties.swift index a219c7672..14e658156 100644 --- a/Packages/RuuviService/Sources/RuuviService/RuuviServiceSensorProperties.swift +++ b/Packages/RuuviService/Sources/RuuviService/RuuviServiceSensorProperties.swift @@ -48,7 +48,7 @@ extension RuuviServiceSensorProperties { image: image, for: sensor, progress: nil, - maxSize: CGSize(width: 1080, height: 1920) + maxSize: CGSize(width: 3000, height: 3000) ) } } diff --git a/Packages/RuuviService/Sources/RuuviServiceAlert/RuuviServiceAlertImpl.swift b/Packages/RuuviService/Sources/RuuviServiceAlert/RuuviServiceAlertImpl.swift index a17179540..c88dc4caa 100644 --- a/Packages/RuuviService/Sources/RuuviServiceAlert/RuuviServiceAlertImpl.swift +++ b/Packages/RuuviService/Sources/RuuviServiceAlert/RuuviServiceAlertImpl.swift @@ -371,44 +371,52 @@ public final class RuuviServiceAlertImpl: RuuviServiceAlert { } // RuuviCloudAlert + // swiftlint:disable:next cyclomatic_complexity public func sync(cloudAlerts: [RuuviCloudSensorAlerts]) { cloudAlerts.forEach { cloudSensorAlert in - let macId = cloudSensorAlert.sensor.mac + guard let macId = cloudSensorAlert.sensor?.mac else { return } let luid = localIDs.luid(for: macId) let physicalSensor = PhysicalSensorStruct(luid: luid, macId: macId) - cloudSensorAlert.alerts.forEach { cloudAlert in + cloudSensorAlert.alerts?.forEach { cloudAlert in var type: AlertType? switch cloudAlert.type { case .temperature: - type = .temperature(lower: cloudAlert.min, upper: cloudAlert.max) + guard let min = cloudAlert.min, let max = cloudAlert.max else { return } + type = .temperature(lower: min, upper: max) setTemperature(description: cloudAlert.description, for: physicalSensor) case .humidity: + guard let min = cloudAlert.min, let max = cloudAlert.max else { return } // in percent on cloud, in fraction locally type = .relativeHumidity( - lower: cloudAlert.min / 100.0, - upper: cloudAlert.max / 100.0 + lower: min / 100.0, + upper: max / 100.0 ) setRelativeHumidity(description: cloudAlert.description, for: physicalSensor) case .pressure: + guard let min = cloudAlert.min, let max = cloudAlert.max else { return } // in Pa on cloud, in hPa locally type = .pressure( - lower: cloudAlert.min / 100.0, - upper: cloudAlert.max / 100.0 + lower: min / 100.0, + upper: max / 100.0 ) setPressure(description: cloudAlert.description, for: physicalSensor) case .movement: - type = .movement(last: cloudAlert.counter) + guard let counter = cloudAlert.counter else { return } + type = .movement(last: counter) setMovement(description: cloudAlert.description, for: physicalSensor) case .signal: - type = .signal(lower: cloudAlert.min, - upper: cloudAlert.max) + guard let min = cloudAlert.min, let max = cloudAlert.max else { return } + type = .signal(lower: min, + upper: max) setSignal(description: cloudAlert.description, for: physicalSensor) case .offline: // Not supported on app yet. break + default: + break } if let type = type { - if cloudAlert.enabled { + if let enabled = cloudAlert.enabled, enabled { register(type: type, for: physicalSensor) } else { unregister(type: type, for: physicalSensor) diff --git a/Packages/RuuviService/Sources/RuuviServiceCloudSync/RuuviServiceCloudSyncImpl.swift b/Packages/RuuviService/Sources/RuuviServiceCloudSync/RuuviServiceCloudSyncImpl.swift index bb0d51514..9ad6fc30f 100644 --- a/Packages/RuuviService/Sources/RuuviServiceCloudSync/RuuviServiceCloudSyncImpl.swift +++ b/Packages/RuuviService/Sources/RuuviServiceCloudSync/RuuviServiceCloudSyncImpl.swift @@ -49,7 +49,7 @@ public final class RuuviServiceCloudSyncImpl: RuuviServiceCloudSync { let promise = Promise() ruuviCloud.getCloudSettings() .on(success: { [weak self] cloudSettings in - guard let sSelf = self else { return } + guard let cloudSettings = cloudSettings, let sSelf = self else { return } if let unitTemperature = cloudSettings.unitTemperature, unitTemperature != sSelf.ruuviLocalSettings.temperatureUnit { sSelf.ruuviLocalSettings.temperatureUnit = unitTemperature diff --git a/Packages/RuuviService/Sources/RuuviServiceOwnership/RuuviServiceOwnershipImpl.swift b/Packages/RuuviService/Sources/RuuviServiceOwnership/RuuviServiceOwnershipImpl.swift index 13b16377b..fc7713ffa 100644 --- a/Packages/RuuviService/Sources/RuuviServiceOwnership/RuuviServiceOwnershipImpl.swift +++ b/Packages/RuuviService/Sources/RuuviServiceOwnership/RuuviServiceOwnershipImpl.swift @@ -8,6 +8,14 @@ import RuuviLocal import RuuviService import RuuviUser +extension Notification.Name { + public static let RuuviTagOwnershipCheckDidEnd = Notification.Name("RuuviTagOwnershipCheckDidEnd") +} + +public enum RuuviTagOwnershipCheckResultKey: String { + case hasOwner = "hasTagOwner" +} + public final class RuuviServiceOwnershipImpl: RuuviServiceOwnership { private let cloud: RuuviCloud private let pool: RuuviPool @@ -51,8 +59,8 @@ public final class RuuviServiceOwnershipImpl: RuuviServiceOwnership { } @discardableResult - public func share(macId: MACIdentifier, with email: String) -> Future { - let promise = Promise() + public func share(macId: MACIdentifier, with email: String) -> Future { + let promise = Promise() cloud.share(macId: macId, with: email) .on(success: { macId in promise.succeed(value: macId) @@ -140,6 +148,9 @@ public final class RuuviServiceOwnershipImpl: RuuviServiceOwnership { guard let sSelf = self else { return } let unclaimedSensor = sensor .with(isClaimed: false) + .with(canShare: false) + .with(sharedTo: []) + .with(isCloudSensor: false) .withoutOwner() sSelf.pool .update(unclaimedSensor) @@ -212,8 +223,8 @@ public final class RuuviServiceOwnershipImpl: RuuviServiceOwnership { } @discardableResult - public func checkOwner(macId: MACIdentifier) -> Future { - let promise = Promise() + public func checkOwner(macId: MACIdentifier) -> Future { + let promise = Promise() cloud.checkOwner(macId: macId) .on(success: { owner in promise.succeed(value: owner) diff --git a/Packages/RuuviService/Sources/RuuviServiceSensorProperties/RuuviServiceSensorPropertiesImpl.swift b/Packages/RuuviService/Sources/RuuviServiceSensorProperties/RuuviServiceSensorPropertiesImpl.swift index 830a883ac..24771ff6a 100644 --- a/Packages/RuuviService/Sources/RuuviServiceSensorProperties/RuuviServiceSensorPropertiesImpl.swift +++ b/Packages/RuuviService/Sources/RuuviServiceSensorProperties/RuuviServiceSensorPropertiesImpl.swift @@ -111,7 +111,7 @@ public final class RuuviServiceSensorPropertiesImpl: RuuviServiceSensorPropertie ) -> Future { let promise = Promise() let croppedImage = coreImage.cropped(image: image, to: maxSize) - guard let jpegData = croppedImage.jpegData(compressionQuality: 1.0) else { + guard let jpegData = croppedImage.jpegData(compressionQuality: 0.6) else { promise.fail(error: .failedToGetJpegRepresentation) return promise.future } diff --git a/Podfile.lock b/Podfile.lock index 20f3ec2a4..d58ce8b49 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -178,9 +178,9 @@ PODS: - RuuviBundleUtils/RuuviBundleUtils (= 0.0.1) - RuuviBundleUtils/RuuviBundleUtils (0.0.1) - RuuviBundleUtils/Tests (0.0.1) - - RuuviCloud (0.0.8): - - RuuviCloud/Contract (= 0.0.8) - - RuuviCloud/Api (0.0.8): + - RuuviCloud (0.0.9): + - RuuviCloud/Contract (= 0.0.9) + - RuuviCloud/Api (0.0.9): - BTKit - FutureX - RuuviCloud/Contract @@ -188,13 +188,13 @@ PODS: - RuuviOntology/Mappers - RuuviPersistence - RuuviPool - - RuuviCloud/Contract (0.0.8): + - RuuviCloud/Contract (0.0.9): - FutureX - RuuviOntology - RuuviPersistence - RuuviPool - RuuviUser - - RuuviCloud/Pure (0.0.8): + - RuuviCloud/Pure (0.0.9): - FutureX - RuuviCloud/Api - RuuviCloud/Contract @@ -202,7 +202,7 @@ PODS: - RuuviPersistence - RuuviPool - RuuviUser - - RuuviCloud/Tests (0.0.8) + - RuuviCloud/Tests (0.0.9) - RuuviContext (0.0.1): - RuuviContext/SQLite (= 0.0.1) - RuuviContext/Contract (0.0.1) @@ -856,7 +856,7 @@ SPEC CHECKSUMS: RealmSwift: cef9946f09f2333a8f2ac8bac4f8de52fb9f5ac3 RuuviAnalytics: 1442f702d42ca8a7c4000394a534cd54bf5fd67a RuuviBundleUtils: b143f4bb7bbb9b173527bec8836b3a507b6ca8f2 - RuuviCloud: 8bbfbfe12801da753dc60100cfdff579e5588361 + RuuviCloud: 9f6f077f279f27df18fc2b1af674ece7bd3984f3 RuuviContext: 3c3a03e1791189e57d35252cac2448b30cb5c8c4 RuuviCore: c42d46fd24adec33663aa61a7b73430b448a56e4 RuuviDaemon: 029c2bcced7d9fdfc3aa32d452701a7d1306f293 diff --git a/ruuvi-widgets/View/EmptyWidgetView.swift b/ruuvi-widgets/View/EmptyWidgetView.swift index 5b4875513..836613b51 100644 --- a/ruuvi-widgets/View/EmptyWidgetView.swift +++ b/ruuvi-widgets/View/EmptyWidgetView.swift @@ -61,7 +61,7 @@ struct EmptyWidgetView: View { var body: some View { ZStack { - Color.backgroundColor.edgesIgnoringSafeArea(.all) + Color.backgroundColor.edgesIgnoringSafeArea(.all).clipShape(Circle()) } VStack { diff --git a/ruuvi-widgets/View/SimpleWidgetViewCircular.swift b/ruuvi-widgets/View/SimpleWidgetViewCircular.swift index aae3cadc1..656377cc1 100644 --- a/ruuvi-widgets/View/SimpleWidgetViewCircular.swift +++ b/ruuvi-widgets/View/SimpleWidgetViewCircular.swift @@ -5,8 +5,7 @@ struct SimpleWidgetViewCircular: View { var entry: WidgetProvider.Entry var body: some View { ZStack { - Color.backgroundColor - .ignoresSafeArea() + Color.backgroundColor.edgesIgnoringSafeArea(.all).clipShape(Circle()) } VStack(spacing: 0) { @@ -35,9 +34,9 @@ struct SimpleWidgetViewCircular: View { size: 10, relativeTo: .body)) .minimumScaleFactor(0.5) - .padding(.top, -4) + .padding(.top, -2) - }.padding(EdgeInsets(top: 4, leading: 10, bottom: 0, trailing: 10)) + }.padding(EdgeInsets(top: 4, leading: 10, bottom: 4, trailing: 10)) .widgetURL(URL(string: "\(entry.tag.identifier.unwrapped)")) } } diff --git a/ruuvi-widgets/View/UnauthorizedView.swift b/ruuvi-widgets/View/UnauthorizedView.swift index 16df4b47c..9633a2894 100644 --- a/ruuvi-widgets/View/UnauthorizedView.swift +++ b/ruuvi-widgets/View/UnauthorizedView.swift @@ -52,7 +52,7 @@ struct UnauthorizedView: View { var body: some View { ZStack { - Color.backgroundColor.edgesIgnoringSafeArea(.all) + Color.backgroundColor.edgesIgnoringSafeArea(.all).clipShape(Circle()) } VStack { diff --git a/station.localization b/station.localization index 64b677de8..14e51876b 160000 --- a/station.localization +++ b/station.localization @@ -1 +1 @@ -Subproject commit 64b677de87a06f19ed661abd0fa5550741dc955a +Subproject commit 14e51876becd7974580071fd7065147f9ea85def diff --git a/station.xcodeproj/project.pbxproj b/station.xcodeproj/project.pbxproj index d2d0f36fc..942d54fcb 100644 --- a/station.xcodeproj/project.pbxproj +++ b/station.xcodeproj/project.pbxproj @@ -1012,6 +1012,8 @@ E1B5800C29859EEB00B441FB /* DevicesInteractorOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B5800A29859EEB00B441FB /* DevicesInteractorOutput.swift */; }; E1B5800E2986AE0800B441FB /* RuuviContextMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B5800D2986AE0800B441FB /* RuuviContextMenuButton.swift */; }; E1B5800F2986AE0800B441FB /* RuuviContextMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B5800D2986AE0800B441FB /* RuuviContextMenuButton.swift */; }; + E1BAC13A2A7598F000EA820E /* Muli-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E1BAC1392A7598F000EA820E /* Muli-ExtraBold.ttf */; }; + E1BAC13B2A7598F000EA820E /* Muli-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E1BAC1392A7598F000EA820E /* Muli-ExtraBold.ttf */; }; E1C395D329B26044009301D3 /* UICollectionView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C395D229B26044009301D3 /* UICollectionView+Extension.swift */; }; E1C395D429B26044009301D3 /* UICollectionView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C395D229B26044009301D3 /* UICollectionView+Extension.swift */; }; E1CA28AE29201F34009E4423 /* RUAlertExpandButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CA28AD29201F34009E4423 /* RUAlertExpandButton.swift */; }; @@ -1741,6 +1743,7 @@ E1B5800729859EDE00B441FB /* DevicesInteractorInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesInteractorInput.swift; sourceTree = ""; }; E1B5800A29859EEB00B441FB /* DevicesInteractorOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesInteractorOutput.swift; sourceTree = ""; }; E1B5800D2986AE0800B441FB /* RuuviContextMenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuuviContextMenuButton.swift; sourceTree = ""; }; + E1BAC1392A7598F000EA820E /* Muli-ExtraBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Muli-ExtraBold.ttf"; sourceTree = ""; }; E1C395D229B26044009301D3 /* UICollectionView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Extension.swift"; sourceTree = ""; }; E1CA28AD29201F34009E4423 /* RUAlertExpandButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUAlertExpandButton.swift; sourceTree = ""; }; E1CA28B7292037E4009E4423 /* RUAlertDetailsCellChildView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUAlertDetailsCellChildView.swift; sourceTree = ""; }; @@ -3259,6 +3262,7 @@ E19EAF512996C828005827E4 /* Montserrat-ExtraBold.ttf */, 64678190225D02CE0072856A /* Muli-Bold.ttf */, E19EAF5A2996CF38005827E4 /* Muli-SemiBoldItalic.ttf */, + E1BAC1392A7598F000EA820E /* Muli-ExtraBold.ttf */, 6467818F225CFB170072856A /* Muli-Regular.ttf */, 643C651E21C38F490037BE5B /* Montserrat-Bold.ttf */, 6486971120E0439200CCD7C1 /* Montserrat-Regular.ttf */, @@ -4856,6 +4860,7 @@ 0E197C8223C5CDBE0074015B /* iOSDeviceModelMapping.plist in Resources */, 0E8BD3FD238566AB008B31EF /* Montserrat-Bold.ttf in Resources */, 0E8BD3FE238566AB008B31EF /* WebTagSettings.storyboard in Resources */, + E1BAC13B2A7598F000EA820E /* Muli-ExtraBold.ttf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4921,6 +4926,7 @@ 0E197C8123C5CDBE0074015B /* iOSDeviceModelMapping.plist in Resources */, 643C651F21C38F490037BE5B /* Montserrat-Bold.ttf in Resources */, 0E046F2722F04A0300BD4E9C /* WebTagSettings.storyboard in Resources */, + E1BAC13A2A7598F000EA820E /* Muli-ExtraBold.ttf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6363,7 +6369,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 391; + CURRENT_PROJECT_VERSION = 398; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 4MUYJ4YYH4; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; @@ -6373,7 +6379,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.3.0; OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS -D DEVELOPMENT"; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6395,7 +6401,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 391; + CURRENT_PROJECT_VERSION = 398; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 4MUYJ4YYH4; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; @@ -6405,7 +6411,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.3.0; OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS"; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6427,7 +6433,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 391; + CURRENT_PROJECT_VERSION = 398; DEVELOPMENT_TEAM = 4MUYJ4YYH4; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; GENERATE_INFOPLIST_FILE = YES; @@ -6440,7 +6446,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.3.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station.widgets; @@ -6464,7 +6470,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 391; + CURRENT_PROJECT_VERSION = 398; DEVELOPMENT_TEAM = 4MUYJ4YYH4; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; GENERATE_INFOPLIST_FILE = YES; @@ -6477,7 +6483,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.3.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station.widgets; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6499,7 +6505,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 391; + CURRENT_PROJECT_VERSION = 398; DEVELOPMENT_TEAM = 4MUYJ4YYH4; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; GENERATE_INFOPLIST_FILE = YES; @@ -6512,7 +6518,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.3.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station.intents; @@ -6535,7 +6541,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 391; + CURRENT_PROJECT_VERSION = 398; DEVELOPMENT_TEAM = 4MUYJ4YYH4; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; GENERATE_INFOPLIST_FILE = YES; @@ -6548,7 +6554,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.3.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station.intents; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6687,7 +6693,7 @@ CODE_SIGN_ENTITLEMENTS = station/station.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 391; + CURRENT_PROJECT_VERSION = 398; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 4MUYJ4YYH4; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; @@ -6729,7 +6735,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.3.0; OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS"; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6749,7 +6755,7 @@ CODE_SIGN_ENTITLEMENTS = station/station.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 391; + CURRENT_PROJECT_VERSION = 398; DEVELOPMENT_TEAM = 4MUYJ4YYH4; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; INFOPLIST_FILE = "$(SRCROOT)/station/Resources/Plists/Info.plist"; @@ -6758,7 +6764,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.3.0; OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS"; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6863,7 +6869,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 391; + CURRENT_PROJECT_VERSION = 398; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; GENERATE_INFOPLIST_FILE = YES; @@ -6876,7 +6882,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.3.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station.pnservice; @@ -6898,7 +6904,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 391; + CURRENT_PROJECT_VERSION = 398; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 4MUYJ4YYH4; GENERATE_INFOPLIST_FILE = YES; @@ -6911,7 +6917,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.3.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.ruuvi.station.pnservice; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/station/Classes/Presentation/Modules/Dashboard/Cards/Interactor/CardsInteractor.swift b/station/Classes/Presentation/Modules/Dashboard/Cards/Interactor/CardsInteractor.swift index 7cd07a183..0f1cf31f1 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Cards/Interactor/CardsInteractor.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Cards/Interactor/CardsInteractor.swift @@ -15,7 +15,8 @@ extension CardsInteractor: CardsInteractorInput { func checkAndUpdateFirmwareVersion(for ruuviTag: RuuviTagSensor, settings: RuuviLocalSettings) { guard let luid = ruuviTag.luid, - ruuviTag.firmwareVersion == nil && + ruuviTag.firmwareVersion == nil || + !ruuviTag.firmwareVersion.hasText() && settings.firmwareVersion(for: luid) == nil else { return } @@ -27,9 +28,7 @@ extension CardsInteractor: CardsInteractorInput { ) { [weak self] _, result in switch result { case .success(let version): - // TODO: - @priyonto - Handle this prefix properly. - let currentVersion = version.replace("Ruuvi FW ", with: "") - let tagWithVersion = ruuviTag.with(firmwareVersion: currentVersion) + let tagWithVersion = ruuviTag.with(firmwareVersion: version) self?.ruuviPool.update(tagWithVersion) default: break diff --git a/station/Classes/Presentation/Modules/Dashboard/Cards/Router/CardsRouter.swift b/station/Classes/Presentation/Modules/Dashboard/Cards/Router/CardsRouter.swift index ab3eaa0e2..916d3b4a5 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Cards/Router/CardsRouter.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Cards/Router/CardsRouter.swift @@ -64,4 +64,11 @@ extension CardsRouter: DiscoverRouterDelegate { func discoverRouterWantsClose(_ router: DiscoverRouter) { router.viewController.dismiss(animated: true) } + + func discoverRouterWantsCloseWithRuuviTagNavigation( + _ router: DiscoverRouter, + ruuviTag: RuuviTagSensor + ) { + router.viewController.dismiss(animated: true) + } } diff --git a/station/Classes/Presentation/Modules/Dashboard/Cards/View/CardsViewModel.swift b/station/Classes/Presentation/Modules/Dashboard/Cards/View/CardsViewModel.swift index 37c3b2ba8..2fec2fb80 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Cards/View/CardsViewModel.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Cards/View/CardsViewModel.swift @@ -35,6 +35,7 @@ struct CardsViewModel { var isConnected: Observable = Observable() var isCloud: Observable = Observable() var isOwner: Observable = Observable() + var canShareTag: Observable = Observable(false) var alertState: Observable = Observable() var rhAlertLowerBound: Observable = Observable() var rhAlertUpperBound: Observable = Observable() @@ -127,6 +128,8 @@ struct CardsViewModel { isAlertAvailable.value = ruuviTag.isCloud || isConnected.value ?? false isCloud.value = ruuviTag.isCloud isOwner.value = ruuviTag.isOwner + canShareTag.value = + (ruuviTag.isOwner && ruuviTag.isClaimed) || ruuviTag.canShare } func update(_ record: RuuviTagSensorRecord) { diff --git a/station/Classes/Presentation/Modules/Dashboard/Cards/View/UI/CardsIndicatorView.swift b/station/Classes/Presentation/Modules/Dashboard/Cards/View/UI/CardsIndicatorView.swift index 35453f4c9..01b4790cd 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Cards/View/UI/CardsIndicatorView.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Cards/View/UI/CardsIndicatorView.swift @@ -14,9 +14,17 @@ class CardsIndicatorView: UIView { label.textColor = .white label.textAlignment = .left label.numberOfLines = 0 - let font = UIFont(name: "Montserrat-Bold", size: 18) - label.font = font ?? UIFont.systemFont(ofSize: 16, weight: .bold) - label.text = "Indicator" + label.font = UIFont.Muli(.bold, size: 18) + return label + }() + + private lazy var indicatorUnitLabel: UILabel = { + let label = UILabel() + label.textColor = .white.withAlphaComponent(0.8) + label.textAlignment = .left + label.numberOfLines = 0 + label.font = UIFont.Muli(.regular, size: 14) + label.sizeToFit() return label }() @@ -49,21 +57,57 @@ class CardsIndicatorView: UIView { indicatorIconView.centerYInSuperview() addSubview(indicatorValueLabel) - indicatorValueLabel.anchor(top: nil, - leading: indicatorIconView.trailingAnchor, - bottom: nil, - trailing: trailingAnchor, - padding: .init(top: 0, left: 16, bottom: 0, right: 0)) + indicatorValueLabel.anchor( + top: nil, + leading: indicatorIconView.trailingAnchor, + bottom: nil, + trailing: nil, + padding: .init(top: 0, left: 16, bottom: 0, right: 0) + ) indicatorValueLabel.centerYInSuperview() + + addSubview(indicatorUnitLabel) + indicatorUnitLabel.anchor( + top: nil, + leading: indicatorValueLabel.trailingAnchor, + bottom: indicatorValueLabel.bottomAnchor, + trailing: nil, + padding: .init( + top: 0, + left: 4, + bottom: 0, + right: 0 + ) + ) + indicatorUnitLabel + .topAnchor + .constraint( + lessThanOrEqualTo: indicatorValueLabel.topAnchor, + constant: 3 + ).isActive = true + + indicatorUnitLabel.trailingAnchor + .constraint(greaterThanOrEqualTo: trailingAnchor) + .isActive = true } } extension CardsIndicatorView { - func setValue(with value: String?) { + func setValue(with value: String?, unit: String? = nil) { indicatorValueLabel.text = value + indicatorUnitLabel.text = unit + + indicatorValueLabel.sizeToFit() + indicatorUnitLabel.sizeToFit() + layoutIfNeeded() } func setIcon(with image: UIImage?) { indicatorIconView.image = image } + + func clearValues() { + indicatorValueLabel.text = nil + indicatorUnitLabel.text = nil + } } diff --git a/station/Classes/Presentation/Modules/Dashboard/Cards/View/UI/CardsLargeImageCell.swift b/station/Classes/Presentation/Modules/Dashboard/Cards/View/UI/CardsLargeImageCell.swift index 6423ffb30..47e74e9d2 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Cards/View/UI/CardsLargeImageCell.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Cards/View/UI/CardsLargeImageCell.swift @@ -11,7 +11,7 @@ class CardsLargeImageCell: UICollectionViewCell { label.textColor = .white label.textAlignment = .center label.numberOfLines = 0 - label.font = UIFont.Montserrat(.bold, size: 20) + label.font = UIFont.Muli(.extraBold, size: 20) return label }() @@ -37,7 +37,7 @@ class CardsLargeImageCell: UICollectionViewCell { private lazy var pressureView = CardsIndicatorView(icon: RuuviAssets.pressureImage) private lazy var movementView = CardsIndicatorView(icon: RuuviAssets.movementCounterImage) - private lazy var batteryLevelView = BatteryLevelView() + private lazy var batteryLevelView = BatteryLevelView(fontSize: 12) private lazy var syncStateLabel: UILabel = { let label = UILabel() @@ -207,7 +207,7 @@ class CardsLargeImageCell: UICollectionViewCell { left: 6, bottom: 0, right: 0), - size: .init(width: 20, height: 20)) + size: .init(width: 22, height: 22)) dataSourceIconView.centerYInSuperview() } } @@ -218,9 +218,9 @@ extension CardsLargeImageCell { ruuviTagNameLabel.text = nil temperatureLabel.text = nil temperatureUnitLabel.text = nil - humidityView.setValue(with: nil) - pressureView.setValue(with: nil) - movementView.setValue(with: nil) + humidityView.clearValues() + pressureView.clearValues() + movementView.clearValues() updatedAtLabel.text = nil dataSourceIconView.image = nil syncStateLabel.text = nil @@ -254,12 +254,20 @@ extension CardsLargeImageCell { } // Humidity - if let humidity = viewModel.humidity.value { + if let humidity = viewModel.humidity.value, + let measurementService = measurementService { hideHumidityView(hide: false) - let humidityValue = measurementService?.string(for: humidity, - temperature: viewModel.temperature.value, - allowSettings: true) - humidityView.setValue(with: humidityValue) + let humidityValue = measurementService.stringWithoutSign( + for: humidity, + temperature: viewModel.temperature.value + ) + let humidityUnit = measurementService.units.humidityUnit + let humidityUnitSymbol = humidityUnit.symbol + let temperatureUnitSymbol = measurementService.units.temperatureUnit.symbol + let unit = humidityUnit == .dew ? temperatureUnitSymbol + : humidityUnitSymbol + humidityView.setValue(with: humidityValue, + unit: unit) } else { hideHumidityView(hide: true) } @@ -267,9 +275,9 @@ extension CardsLargeImageCell { // Pressure if let pressure = viewModel.pressure.value { hidePressureView(hide: false) - let pressureValue = measurementService?.string(for: pressure, - allowSettings: true) - pressureView.setValue(with: pressureValue) + let pressureValue = measurementService?.stringWithoutSign(for: pressure) + pressureView.setValue(with: pressureValue, + unit: measurementService?.units.pressureUnit.symbol) } else { hidePressureView(hide: true) } @@ -279,12 +287,13 @@ extension CardsLargeImageCell { case .ruuvi: if let movement = viewModel.movementCounter.value { hideMovementView(hide: false) - let movementValue = "\(movement) " + "Cards.Movements.title".localized() - movementView.setValue(with: movementValue) + movementView.setValue( + with: "\(movement)", + unit: "Cards.Movements.title".localized() + ) } else { hideMovementView(hide: true) } - movementView.setIcon(with: RuuviAssets.movementCounterImage) case .web: let location = viewModel.location if let location = location.value { diff --git a/station/Classes/Presentation/Modules/Dashboard/Charts/Interactor/TagChartsViewInteractor.swift b/station/Classes/Presentation/Modules/Dashboard/Charts/Interactor/TagChartsViewInteractor.swift index 67d85ff15..c30fd25dc 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Charts/Interactor/TagChartsViewInteractor.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Charts/Interactor/TagChartsViewInteractor.swift @@ -35,6 +35,8 @@ class TagChartsViewInteractor { private let maximumPointsCount: Double = 3000.0 private let minimumDownsampleThreshold: Int = 1000 + private var gattSyncInterruptedByUser: Bool = false + deinit { ruuviTagSensorObservationToken?.invalidate() ruuviTagSensorObservationToken = nil @@ -163,7 +165,10 @@ extension TagChartsViewInteractor: TagChartsViewInteractorInput { connectionTimeout: connectionTimeout, serviceTimeout: serviceTimeout) op.on(success: { [weak self] _ in - self?.localSyncState.setGattSyncDate(Date(), for: self?.ruuviTagSensor.macId) + if let isInterrupted = self?.gattSyncInterruptedByUser, !isInterrupted { + self?.localSyncState.setGattSyncDate(Date(), for: self?.ruuviTagSensor.macId) + } + self?.gattSyncInterruptedByUser = false promise.succeed(value: ()) }, failure: {error in promise.fail(error: .ruuviService(error)) @@ -178,7 +183,8 @@ extension TagChartsViewInteractor: TagChartsViewInteractorInput { return promise.future } let op = gattService.stopGattSync(for: luid.value) - op.on(success: { response in + op.on(success: { [weak self] response in + self?.gattSyncInterruptedByUser = true promise.succeed(value: (response)) }, failure: {error in promise.fail(error: .ruuviService(error)) diff --git a/station/Classes/Presentation/Modules/Dashboard/Charts/View/UI/TagChartsViewController.swift b/station/Classes/Presentation/Modules/Dashboard/Charts/View/UI/TagChartsViewController.swift index bfaa9d1b9..c488933c7 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Charts/View/UI/TagChartsViewController.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Charts/View/UI/TagChartsViewController.swift @@ -42,8 +42,7 @@ class TagChartsViewController: UIViewController { label.textColor = .white label.textAlignment = .center label.numberOfLines = 0 - let font = UIFont(name: "Montserrat-Bold", size: 20) - label.font = font ?? UIFont.systemFont(ofSize: 16, weight: .bold) + label.font = UIFont.Muli(.extraBold, size: 20) return label }() @@ -53,8 +52,7 @@ class TagChartsViewController: UIViewController { label.textColor = .white label.textAlignment = .center label.numberOfLines = 0 - let font = UIFont(name: "Montserrat-Bold", size: 14) - label.font = font ?? UIFont.systemFont(ofSize: 14, weight: .bold) + label.font = UIFont.Montserrat(.bold, size: 14) return label }() @@ -118,6 +116,7 @@ class TagChartsViewController: UIViewController { title: "TagCharts.Sync.title".localized(), icon: UIImage(named: "icon_sync_bt"), iconTintColor: .white, + iconSize: .init(width: 22, height: 22), preccedingIcon: true ) button.button.showsMenuAsPrimaryAction = false @@ -344,7 +343,7 @@ class TagChartsViewController: UIViewController { left: 16, bottom: 8, right: 16), - size: .init(width: 0, height: 24)) + size: .init(width: 0, height: 26)) footerView.addSubview(updatedAtLabel) updatedAtLabel.anchor(top: footerView.topAnchor, @@ -365,7 +364,7 @@ class TagChartsViewController: UIViewController { left: 6, bottom: 0, right: 0), - size: .init(width: 20, height: 20)) + size: .init(width: 22, height: 22)) dataSourceIconView.centerYInSuperview() } @@ -673,7 +672,7 @@ extension TagChartsViewController: TagChartsViewInput { let title = "synchronisation".localized() let message = "gatt_sync_description".localized() let alertVC = UIAlertController(title: title, message: message, preferredStyle: .alert) - alertVC.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil)) + alertVC.addAction(UIAlertAction(title: "Close".localized(), style: .cancel, handler: nil)) let actionTitle = "do_not_show_again".localized() alertVC.addAction(UIAlertAction(title: actionTitle, style: .default, diff --git a/station/Classes/Presentation/Modules/Dashboard/Home/Interactor/DashboardInteractor.swift b/station/Classes/Presentation/Modules/Dashboard/Home/Interactor/DashboardInteractor.swift index a42d0cb88..57ab2b804 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Home/Interactor/DashboardInteractor.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Home/Interactor/DashboardInteractor.swift @@ -19,8 +19,15 @@ class DashboardInteractor { extension DashboardInteractor: DashboardInteractorInput { func checkAndUpdateFirmwareVersion(for ruuviTag: RuuviTagSensor) { guard let luid = ruuviTag.luid, - ruuviTag.firmwareVersion == nil && + ruuviTag.firmwareVersion == nil || + !ruuviTag.firmwareVersion.hasText() && settings.firmwareVersion(for: luid) == nil else { + // Trigger the method after 2 seconds so that sensor settings page can + // be set and start observing for owner check notification. + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2), + execute: { [weak self] in + self?.checkOwner(for: ruuviTag) + }) return } @@ -31,9 +38,7 @@ extension DashboardInteractor: DashboardInteractorInput { ) { [weak self] _, result in switch result { case .success(let version): - // TODO: - @priyonto - Handle this prefix properly. - let currentVersion = version.replace("Ruuvi FW ", with: "") - let tagWithVersion = ruuviTag.with(firmwareVersion: currentVersion) + let tagWithVersion = ruuviTag.with(firmwareVersion: version) self?.ruuviPool.update(tagWithVersion) self?.checkOwner(for: tagWithVersion) default: @@ -56,13 +61,21 @@ extension DashboardInteractor: DashboardInteractorInput { ruuviOwnershipService.checkOwner(macId: macId) .on(success: { [weak self] owner in - guard let self = self, !owner.isEmpty else { - self?.settings.setOwnerCheckDate(for: macId, value: Date()) + guard let sSelf = self else { + return + } + guard let owner = owner, !owner.isEmpty else { + NotificationCenter.default.post( + name: .RuuviTagOwnershipCheckDidEnd, + object: nil, + userInfo: [RuuviTagOwnershipCheckResultKey.hasOwner: false] + ) + sSelf.settings.setOwnerCheckDate(for: macId, value: Date()) return } - self.ruuviPool.update(ruuviTag + sSelf.ruuviPool.update(ruuviTag .with(owner: owner) - .with(isOwner: owner == self.ruuviUser.email)) + .with(isOwner: owner == sSelf.ruuviUser.email)) }) } } diff --git a/station/Classes/Presentation/Modules/Dashboard/Home/Presenter/DashboardPresenter.swift b/station/Classes/Presentation/Modules/Dashboard/Home/Presenter/DashboardPresenter.swift index 8ce736d69..8c1fd1e62 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Home/Presenter/DashboardPresenter.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Home/Presenter/DashboardPresenter.swift @@ -185,7 +185,7 @@ extension DashboardPresenter: DashboardViewOutput { } func viewDidTriggerAddSensors() { - router.openDiscover() + router.openDiscover(delegate: self) } func viewDidTriggerBuySensors() { @@ -261,7 +261,17 @@ extension DashboardPresenter: DashboardViewOutput { } } - + + func viewDidTriggerRename(for viewModel: CardsViewModel) { + view?.showSensorNameRenameDialog(for: viewModel) + } + + func viewDidTriggerShare(for viewModel: CardsViewModel) { + if let ruuviTag = ruuviTags.first(where: { $0.id == viewModel.id.value }) { + router.openShare(for: ruuviTag) + } + } + func viewDidDismissKeepConnectionDialogChart(for viewModel: CardsViewModel) { if let luid = viewModel.luid.value { settings.setKeepConnectionDialogWasShown(for: luid) @@ -315,13 +325,31 @@ extension DashboardPresenter: DashboardViewOutput { view?.dashboardTapActionType = type ruuviAppSettingsService.set(dashboardTapActionType: type) } + + func viewDidTriggerPullToRefresh() { + cloudSyncDaemon.refreshImmediately() + } + + func viewDidRenameTag(to name: String, viewModel: CardsViewModel) { + guard let ruuviTag = ruuviTags.first(where: { + $0.id == viewModel.id.value + }) else { + return + } + + let finalName = name.isEmpty ? (ruuviTag.macId?.value ?? ruuviTag.id) : name + ruuviSensorPropertiesService.set(name: finalName, for: ruuviTag) + .on(failure: { [weak self] error in + self?.errorPresenter.present(error: error) + }) + } } // MARK: - MenuModuleOutput extension DashboardPresenter: MenuModuleOutput { func menu(module: MenuModuleInput, didSelectAddRuuviTag sender: Any?) { module.dismiss() - router.openDiscover() + router.openDiscover(delegate: self) } func menu(module: MenuModuleInput, didSelectSettings sender: Any?) { @@ -395,6 +423,30 @@ extension DashboardPresenter: SignInBenefitsModuleOutput { } } +extension DashboardPresenter: DiscoverRouterDelegate { + func discoverRouterWantsClose(_ router: DiscoverRouter) { + router.viewController.dismiss(animated: true) + } + + func discoverRouterWantsCloseWithRuuviTagNavigation( + _ router: DiscoverRouter, + ruuviTag: RuuviTagSensor + ) { + router.viewController.dismiss(animated: true) + if let viewModel = viewModels.first(where: { + $0.id.value == ruuviTag.id + }) { + self.router.openCardImageView(with: viewModels, + ruuviTagSensors: ruuviTags, + virtualSensors: virtualSensors, + sensorSettings: sensorSettingsList, + scrollTo: viewModel, + showCharts: false, + output: self) + } + } +} + // MARK: - DashboardRouterDelegate extension DashboardPresenter: DashboardRouterDelegate { func shouldDismissDiscover() -> Bool { diff --git a/station/Classes/Presentation/Modules/Dashboard/Home/Router/DashboardRouter.swift b/station/Classes/Presentation/Modules/Dashboard/Home/Router/DashboardRouter.swift index 5ac142de9..52e8cee21 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Home/Router/DashboardRouter.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Home/Router/DashboardRouter.swift @@ -34,11 +34,10 @@ class DashboardRouter: NSObject, DashboardRouterInput { }) } - func openDiscover() { + func openDiscover(delegate: DiscoverRouterDelegate) { let discoverRouter = DiscoverRouter() - discoverRouter.delegate = self + discoverRouter.delegate = delegate let viewController = discoverRouter.viewController - viewController.presentationController?.delegate = self let navigationController = UINavigationController(rootViewController: viewController) transitionHandler.present(navigationController, animated: true) } @@ -255,12 +254,18 @@ class DashboardRouter: NSObject, DashboardRouterInput { ) } -} - -extension DashboardRouter: DiscoverRouterDelegate { - func discoverRouterWantsClose(_ router: DiscoverRouter) { - router.viewController.dismiss(animated: true) + func openShare(for sensor: RuuviTagSensor) { + let restorationId = "ShareViewController" + let factory = StoryboardFactory(storyboardName: "Share", bundle: .main, restorationId: restorationId) + try! transitionHandler + .forStoryboard(factory: factory, + to: ShareModuleInput.self) + .to(preferred: .navigation(style: .push)) + .then({ (module) -> Any? in + module.configure(sensor: sensor) + }) } + } extension DashboardRouter: UIAdaptivePresentationControllerDelegate { diff --git a/station/Classes/Presentation/Modules/Dashboard/Home/Router/DashboardRouterInput.swift b/station/Classes/Presentation/Modules/Dashboard/Home/Router/DashboardRouterInput.swift index 7b86cfcfb..61c22ad5c 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Home/Router/DashboardRouterInput.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Home/Router/DashboardRouterInput.swift @@ -4,7 +4,7 @@ import RuuviVirtual protocol DashboardRouterInput { func openMenu(output: MenuModuleOutput) - func openDiscover() + func openDiscover(delegate: DiscoverRouterDelegate) func openSettings() func openAbout() func openWhatToMeasurePage() @@ -46,4 +46,5 @@ protocol DashboardRouterInput { func openBackgroundSelectionView(ruuviTag: RuuviTagSensor) func openBackgroundSelectionView(virtualSensor: VirtualTagSensor) func openMyRuuviAccount() + func openShare(for sensor: RuuviTagSensor) } diff --git a/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardImageCell.swift b/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardImageCell.swift index c0358d45b..84c793a43 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardImageCell.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardImageCell.swift @@ -252,14 +252,14 @@ class DashboardImageCell: UICollectionViewCell { sourceAndUpdateStack.axis = .horizontal sourceAndUpdateStack.spacing = 6 sourceAndUpdateStack.distribution = .fill - dataSourceIconView.size(width: 12, height: 12) + dataSourceIconView.size(width: 22, height: 22) let footerStack = UIStackView(arrangedSubviews: [ sourceAndUpdateStack, batteryLevelView ]) footerStack.spacing = 4 footerStack.axis = .horizontal - footerStack.distribution = .fillEqually + footerStack.distribution = .fillProportionally container.addSubview(footerStack) footerStack.anchor(top: leftContainerView.bottomAnchor, diff --git a/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardPlainCell.swift b/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardPlainCell.swift index f9363ca37..a167890c5 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardPlainCell.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardPlainCell.swift @@ -218,14 +218,14 @@ class DashboardPlainCell: UICollectionViewCell { sourceAndUpdateStack.axis = .horizontal sourceAndUpdateStack.spacing = 6 sourceAndUpdateStack.distribution = .fill - dataSourceIconView.size(width: 12, height: 12) + dataSourceIconView.size(width: 22, height: 22) let footerStack = UIStackView(arrangedSubviews: [ sourceAndUpdateStack, batteryLevelView ]) footerStack.spacing = 4 footerStack.axis = .horizontal - footerStack.distribution = .fillEqually + footerStack.distribution = .fillProportionally container.addSubview(footerStack) footerStack.anchor(top: leftContainerView.bottomAnchor, diff --git a/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardViewController.swift b/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardViewController.swift index 5c3dd8d3b..7fa8fee9f 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardViewController.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardViewController.swift @@ -117,12 +117,25 @@ class DashboardViewController: UIViewController { cv.showsVerticalScrollIndicator = false cv.delegate = self cv.dataSource = self + cv.alwaysBounceVertical = true + cv.refreshControl = refresher return cv }() + private lazy var refresher: UIRefreshControl = { + let rc = UIRefreshControl() + rc.tintColor = RuuviColor.ruuviTintColor + rc.layer.zPosition = -1 + rc.addTarget(self, action: #selector(didPullToRefresh), for: .valueChanged) + return rc + }() + private var tagNameTextField = UITextField() + private let tagNameCharaterLimit: Int = 32 + private var appDidBecomeActiveToken: NSObjectProtocol? private var isListRefreshable: Bool = true + private var isRefreshing: Bool = false /// The view model when context menu is presented after a card tap. private var highlightedViewModel: CardsViewModel? @@ -193,6 +206,19 @@ extension DashboardViewController { self?.collectionView.reloadWithoutAnimation() } } + + @objc fileprivate func didPullToRefresh() { + guard !isRefreshing else { + refresher.endRefreshing() + return + } + isRefreshing = true + output.viewDidTriggerPullToRefresh() + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: { [weak self] in + self?.refresher.endRefreshing() + self?.isRefreshing = false + }) + } } extension DashboardViewController { @@ -280,11 +306,35 @@ extension DashboardViewController { } } - return UIMenu(title: "", - children: [fullImageViewAction, - historyViewAction, - settingsAction, - changeBackgroundAction]) + let renameAction = UIAction(title: "rename".localized()) { + [weak self] _ in + if let viewModel = self?.viewModels[index] { + self?.output.viewDidTriggerRename(for: viewModel) + } + } + + let shareSensorAction = UIAction(title: "TagSettings.ShareButton".localized()) { + [weak self] _ in + if let viewModel = self?.viewModels[index] { + self?.output.viewDidTriggerShare(for: viewModel) + } + } + + var contextMenuActions: [UIAction] = [ + fullImageViewAction, + historyViewAction, + settingsAction, + changeBackgroundAction, + renameAction + ] + + let viewModel = viewModels[index] + if let canShare = viewModel.canShareTag.value, + canShare { + contextMenuActions.append(shareSensorAction) + } + + return UIMenu(title: "", children: contextMenuActions) } } @@ -358,7 +408,7 @@ extension DashboardViewController { leading: view.safeLeftAnchor, bottom: view.bottomAnchor, trailing: view.safeRightAnchor, - padding: .init(top: 0, + padding: .init(top: 12, left: 12, bottom: 0, right: 12)) @@ -366,6 +416,8 @@ extension DashboardViewController { collectionView.showsVerticalScrollIndicator = false collectionView.register(DashboardImageCell.self, forCellWithReuseIdentifier: "cellId") collectionView.register(DashboardPlainCell.self, forCellWithReuseIdentifier: "cellIdPlain") + + collectionView.addSubview(refresher) } // swiftlint:disable:next function_body_length @@ -411,7 +463,7 @@ extension DashboardViewController { let section = NSCollectionLayoutSection(group: group) section.interGroupSpacing = GlobalHelpers.isDeviceTablet() ? 12 : 8 section.contentInsets = NSDirectionalEdgeInsets( - top: 12, + top: 0, leading: 0, bottom: 12, trailing: 0 @@ -631,6 +683,27 @@ extension DashboardViewController: DashboardViewInput { alert.addAction(UIAlertAction(title: "OK".localized(), style: .cancel, handler: nil)) present(alert, animated: true) } + + func showSensorNameRenameDialog(for viewModel: CardsViewModel) { + let alert = UIAlertController(title: "TagSettings.tagNameTitleLabel.text".localized(), + message: "TagSettings.tagNameTitleLabel.rename.text".localized(), + preferredStyle: .alert) + alert.addTextField { [weak self] alertTextField in + guard let self = self else { return } + alertTextField.delegate = self + alertTextField.text = viewModel.name.value + self.tagNameTextField = alertTextField + } + let action = UIAlertAction(title: "OK".localized(), style: .default) { [weak self] _ in + guard let self = self else { return } + guard let name = self.tagNameTextField.text, !name.isEmpty else { return } + self.output.viewDidRenameTag(to: name, viewModel: viewModel) + } + let cancelAction = UIAlertAction(title: "Cancel".localized(), style: .cancel) + alert.addAction(action) + alert.addAction(cancelAction) + present(alert, animated: true, completion: nil) + } } extension DashboardViewController: RuuviServiceMeasurementDelegate { @@ -664,3 +737,24 @@ extension DashboardViewController { collectionView.reloadWithoutAnimation() } } + + // MARK: - UITextFieldDelegate +extension DashboardViewController: UITextFieldDelegate { + func textField(_ textField: UITextField, shouldChangeCharactersIn + range: NSRange, + replacementString string: String) -> Bool { + guard let text = textField.text else { + return true + } + let limit = text.utf16.count + string.utf16.count - range.length + if textField == tagNameTextField { + if limit <= tagNameCharaterLimit { + return true + } else { + return false + } + } else { + return false + } + } +} diff --git a/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardViewInput.swift b/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardViewInput.swift index 58dd034b7..341a16fb9 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardViewInput.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardViewInput.swift @@ -14,4 +14,5 @@ protocol DashboardViewInput: ViewInput { func showKeepConnectionDialogSettings(for viewModel: CardsViewModel) func showReverseGeocodingFailed() func showAlreadyLoggedInAlert(with email: String) + func showSensorNameRenameDialog(for viewModel: CardsViewModel) } diff --git a/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardViewOutput.swift b/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardViewOutput.swift index b0057275d..f1a03cc77 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardViewOutput.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Home/View/DashboardViewOutput.swift @@ -12,6 +12,8 @@ protocol DashboardViewOutput { func viewDidTriggerSettings(for viewModel: CardsViewModel) func viewDidTriggerChart(for viewModel: CardsViewModel) func viewDidTriggerChangeBackground(for viewModel: CardsViewModel) + func viewDidTriggerRename(for viewModel: CardsViewModel) + func viewDidTriggerShare(for viewModel: CardsViewModel) func viewDidTriggerDashboardCard(for viewModel: CardsViewModel) func viewDidConfirmToKeepConnectionChart(to viewModel: CardsViewModel) func viewDidDismissKeepConnectionDialogChart(for viewModel: CardsViewModel) @@ -19,4 +21,6 @@ protocol DashboardViewOutput { func viewDidDismissKeepConnectionDialogSettings(for viewModel: CardsViewModel) func viewDidChangeDashboardType(dashboardType: DashboardType) func viewDidChangeDashboardTapAction(type: DashboardTapActionType) + func viewDidTriggerPullToRefresh() + func viewDidRenameTag(to name: String, viewModel: CardsViewModel) } diff --git a/station/Classes/Presentation/Modules/Dashboard/Home/View/LowBatteryView.swift b/station/Classes/Presentation/Modules/Dashboard/Home/View/LowBatteryView.swift index 6c7503b43..ece4295af 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Home/View/LowBatteryView.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Home/View/LowBatteryView.swift @@ -23,6 +23,12 @@ class BatteryLevelView: UIView { return iv }() + convenience init(fontSize: CGFloat = 10) { + self.init() + setUpUI() + batteryLevelLabel.font = UIFont.Muli(.regular, size: fontSize) + } + override init(frame: CGRect) { super.init(frame: frame) setUpUI() @@ -42,10 +48,10 @@ class BatteryLevelView: UIView { stack.axis = .horizontal stack.distribution = .fill batteryLevelIcon.heightAnchor.constraint( - lessThanOrEqualToConstant: 16 + lessThanOrEqualToConstant: 22 ).isActive = true batteryLevelIcon.widthAnchor.constraint( - lessThanOrEqualToConstant: 16 + lessThanOrEqualToConstant: 22 ).isActive = true addSubview(stack) diff --git a/station/Classes/Presentation/Modules/Dashboard/Home/View/RuuviContextMenuButton.swift b/station/Classes/Presentation/Modules/Dashboard/Home/View/RuuviContextMenuButton.swift index 60a856f23..5fa29cbb6 100644 --- a/station/Classes/Presentation/Modules/Dashboard/Home/View/RuuviContextMenuButton.swift +++ b/station/Classes/Presentation/Modules/Dashboard/Home/View/RuuviContextMenuButton.swift @@ -23,6 +23,7 @@ class RuuviContextMenuButton: UIView { }() private var preccedingIcon: Bool = false + private var iconSize: CGSize = .init(width: 16, height: 16) override init(frame: CGRect) { super.init(frame: frame) @@ -37,6 +38,7 @@ class RuuviContextMenuButton: UIView { title: String?, icon: UIImage?, iconTintColor: UIColor?, + iconSize: CGSize = .init(width: 16, height: 16), preccedingIcon: Bool = false) { self.init() self.preccedingIcon = preccedingIcon @@ -45,6 +47,7 @@ class RuuviContextMenuButton: UIView { self.buttonTitleLabel.textColor = titleColor self.buttonIconView.tintColor = iconTintColor self.buttonIconView.image = icon + self.iconSize = iconSize self.setUpUI() } } @@ -65,10 +68,10 @@ extension RuuviContextMenuButton { ]) } buttonIconView.heightAnchor.constraint( - lessThanOrEqualToConstant: 16 + lessThanOrEqualToConstant: iconSize.height ).isActive = true buttonIconView.widthAnchor.constraint( - lessThanOrEqualToConstant: 16 + lessThanOrEqualToConstant: iconSize.width ).isActive = true stackView.axis = .horizontal stackView.spacing = 6 diff --git a/station/Classes/Presentation/Modules/My Ruuvi/Presenter/MyRuuviAccountPresenter.swift b/station/Classes/Presentation/Modules/My Ruuvi/Presenter/MyRuuviAccountPresenter.swift index e10dc4f17..f190968a3 100644 --- a/station/Classes/Presentation/Modules/My Ruuvi/Presenter/MyRuuviAccountPresenter.swift +++ b/station/Classes/Presentation/Modules/My Ruuvi/Presenter/MyRuuviAccountPresenter.swift @@ -58,7 +58,7 @@ extension MyRuuviAccountPresenter { private func syncViewModel() { let viewModel = MyRuuviAccountViewModel() if ruuviUser.isAuthorized { - viewModel.username.value = ruuviUser.email?.lowercased() + viewModel.username.value = ruuviUser.email } view.viewModel = viewModel } diff --git a/station/Classes/Presentation/Modules/SignIn/Presenter/SignInPresenter.swift b/station/Classes/Presentation/Modules/SignIn/Presenter/SignInPresenter.swift index fc50ea3a9..2504356ef 100644 --- a/station/Classes/Presentation/Modules/SignIn/Presenter/SignInPresenter.swift +++ b/station/Classes/Presentation/Modules/SignIn/Presenter/SignInPresenter.swift @@ -129,7 +129,7 @@ extension SignInPresenter { ruuviCloud.requestCode(email: email) .on(success: { [weak self] email in guard let sSelf = self else { return } - sSelf.ruuviUser.email = email.lowercased() + sSelf.ruuviUser.email = email sSelf.viewModel.showVerficationScreen.value = true sSelf.state = .enterVerificationCode(nil) }, failure: { [weak self] (error) in diff --git a/station/Classes/Presentation/Modules/TagSettings/Presenter/TagSettingsPresenter.swift b/station/Classes/Presentation/Modules/TagSettings/Presenter/TagSettingsPresenter.swift index b5b52e43d..abea6da08 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Presenter/TagSettingsPresenter.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Presenter/TagSettingsPresenter.swift @@ -62,6 +62,7 @@ class TagSettingsPresenter: NSObject, TagSettingsModuleInput { private var ruuviTagToken: RuuviReactorToken? private var ruuviTagSensorRecordToken: RuuviReactorToken? + private var ruuviTagSensorOwnerCheckToken: NSObjectProtocol? private var advertisementToken: ObservationToken? private var heartbeatToken: ObservationToken? private var sensorSettingsToken: RuuviReactorToken? @@ -91,6 +92,7 @@ class TagSettingsPresenter: NSObject, TagSettingsModuleInput { } } } + private var firmwareUpdateDialogShown: Bool = false private var timer: Timer? @@ -99,6 +101,7 @@ class TagSettingsPresenter: NSObject, TagSettingsModuleInput { mutedTillTimer?.invalidate() ruuviTagToken?.invalidate() ruuviTagSensorRecordToken?.invalidate() + ruuviTagSensorOwnerCheckToken?.invalidate() advertisementToken?.invalidate() heartbeatToken?.invalidate() sensorSettingsToken?.invalidate() @@ -167,6 +170,8 @@ extension TagSettingsPresenter: TagSettingsViewOutput { func viewWillAppear() { checkPushNotificationsStatus() checkLastSensorSettings() + checkAndUpdateFirmwareVersion() + startObservingRuuviTagOwnerCheckResponse() } private func startObservingAppState() { @@ -207,6 +212,10 @@ extension TagSettingsPresenter: TagSettingsViewOutput { output?.tagSettingsDidDismiss(module: self) } + func viewDidConfirmClaimTag() { + router.openOwner(ruuviTag: ruuviTag, mode: .claim) + } + func viewDidTriggerChangeBackground() { router.openBackgroundSelectionView(ruuviTag: ruuviTag) } @@ -415,10 +424,15 @@ extension TagSettingsPresenter: TagSettingsViewOutput { func viewDidTapOnOwner() { if viewModel.isClaimedTag.value == false { - router.openOwner(ruuviTag: ruuviTag) + ruuviTagSensorOwnerCheckToken?.invalidate() + ruuviTagSensorOwnerCheckToken = nil + router.openOwner(ruuviTag: ruuviTag, mode: .claim) } else { - guard let isOwner = viewModel.isOwner.value, !isOwner else { return } - router.openContest(ruuviTag: ruuviTag) + if let isOwner = viewModel.isOwner.value, isOwner { + router.openOwner(ruuviTag: ruuviTag, mode: .unclaim) + } else { + router.openContest(ruuviTag: ruuviTag) + } } } } @@ -529,6 +543,7 @@ extension TagSettingsPresenter { } // Set isOwner value viewModel.isOwner.value = ruuviTag.isOwner + viewModel.ownersPlan.value = ruuviTag.ownersPlan if (ruuviTag.name == ruuviTag.luid?.value || ruuviTag.name == ruuviTag.macId?.value) @@ -774,6 +789,27 @@ extension TagSettingsPresenter { } }) } + + private func startObservingRuuviTagOwnerCheckResponse() { + ruuviTagSensorOwnerCheckToken?.invalidate() + ruuviTagSensorOwnerCheckToken = nil + + ruuviTagSensorOwnerCheckToken = NotificationCenter + .default + .addObserver(forName: .RuuviTagOwnershipCheckDidEnd, + object: nil, + queue: .main, + using: { [weak self] (notification) in + guard let sSelf = self, + let userInfo = notification.userInfo, + let hasOwner = userInfo[RuuviTagOwnershipCheckResultKey.hasOwner] as? Bool, + !hasOwner else { + return + } + sSelf.view.showTagClaimDialog() + }) + } + private func startScanningRuuviTag() { advertisementToken?.invalidate() heartbeatToken?.invalidate() @@ -800,6 +836,13 @@ extension TagSettingsPresenter { private func handleMeasurementPoint(tag: RuuviTag, luid: LocalIdentifier, source: RuuviTagSensorRecordSource) { + + // Trigger firmware aler dialog for DF3 tags. + if !firmwareUpdateDialogShown && tag.version < 5 { + view.showFirmwareUpdateDialog() + firmwareUpdateDialogShown = true + } + // RuuviTag with data format 5 or above has the measurements sequence number if tag.version >= 5 { if previousAdvertisementSequence != nil { @@ -1543,6 +1586,30 @@ extension TagSettingsPresenter { object: nil, userInfo: nil) } + + func checkAndUpdateFirmwareVersion() { + guard let luid = ruuviTag.luid, + ruuviTag.firmwareVersion == nil || + !ruuviTag.firmwareVersion.hasText() && + settings.firmwareVersion(for: luid) == nil else { + return + } + + background.services.gatt.firmwareRevision( + for: self, + uuid: luid.value, + options: [.connectionTimeout(15)] + ) { [weak self] _, result in + guard let sSelf = self else { return } + switch result { + case .success(let version): + let tagWithVersion = sSelf.ruuviTag.with(firmwareVersion: version) + self?.ruuviPool.update(tagWithVersion) + default: + break + } + } + } } extension TagSettingsPresenter { diff --git a/station/Classes/Presentation/Modules/TagSettings/Router/TagSettingsRouter.swift b/station/Classes/Presentation/Modules/TagSettings/Router/TagSettingsRouter.swift index f7e7cf5d9..d8ebc0dcd 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Router/TagSettingsRouter.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Router/TagSettingsRouter.swift @@ -67,13 +67,13 @@ class TagSettingsRouter: NSObject, TagSettingsRouterInput { .delegate = self } - func openOwner(ruuviTag: RuuviTagSensor) { + func openOwner(ruuviTag: RuuviTagSensor, mode: OwnershipMode) { let factory = StoryboardFactory(storyboardName: "Owner") try! transitionHandler .forStoryboard(factory: factory, to: OwnerModuleInput.self) .to(preferred: .navigation(style: .push)) .then({ module in - module.configure(ruuviTag: ruuviTag) + module.configure(ruuviTag: ruuviTag, mode: mode) }) } diff --git a/station/Classes/Presentation/Modules/TagSettings/Router/TagSettingsRouterInput.swift b/station/Classes/Presentation/Modules/TagSettings/Router/TagSettingsRouterInput.swift index 2142530df..48fa81dd0 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Router/TagSettingsRouterInput.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Router/TagSettingsRouterInput.swift @@ -10,7 +10,7 @@ protocol TagSettingsRouterInput { ruuviTag: RuuviTagSensor, sensorSettings: SensorSettings?) func openUpdateFirmware(ruuviTag: RuuviTagSensor) - func openOwner(ruuviTag: RuuviTagSensor) + func openOwner(ruuviTag: RuuviTagSensor, mode: OwnershipMode) func openContest(ruuviTag: RuuviTagSensor) } diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/DFU/View/DFUViewModel.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/DFU/View/DFUViewModel.swift index 79a3e50df..dab7709c2 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Submodules/DFU/View/DFUViewModel.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/DFU/View/DFUViewModel.swift @@ -116,11 +116,10 @@ final class DFUViewModel: ObservableObject { guard let currentRelease = currentRelease else { return } - let firmwareVersion = currentRelease.version.replace("Ruuvi FW ", with: "") isLoading = true ruuviPool.update(ruuviTag .with(isConnectable: true) - .with(firmwareVersion: firmwareVersion)) + .with(firmwareVersion: currentRelease.version)) .on(success: { [weak self] _ in self?.isLoading = false }, failure: { [weak self] _ in @@ -133,6 +132,17 @@ final class DFUViewModel: ObservableObject { } } + func storeCurrentFirmwareVersion(from currentRelease: CurrentRelease?) { + guard ruuviTag.firmwareVersion == nil || + !ruuviTag.firmwareVersion.hasText(), + let currentRelease = currentRelease else { + return + } + ruuviPool.update(ruuviTag + .with(isConnectable: true) + .with(firmwareVersion: currentRelease.version)) + } + private func startObserving() { guard let luid = ruuviTag.luid else { isLoading = false diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/DFU/View/SwiftUI/DFUUIView.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/DFU/View/SwiftUI/DFUUIView.swift index 66e9d8be2..5590c934b 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Submodules/DFU/View/SwiftUI/DFUUIView.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/DFU/View/SwiftUI/DFUUIView.swift @@ -157,7 +157,7 @@ struct DFUUIView: View { .padding() .onAppear { self.viewModel.send(event: .onLoadedAndServed(latestRelease, currentRelease)) } .eraseToAnyView() - case .noNeedToUpgrade: + case .noNeedToUpgrade(_, let currentRelease): return Text(texts.alreadyOnLatest) .font(muliRegular16) .foregroundColor(RuuviColor.ruuviTextColorSUI) @@ -167,6 +167,7 @@ struct DFUUIView: View { alignment: .topLeading ) .padding() + .onAppear { self.viewModel.storeCurrentFirmwareVersion(from: currentRelease) } .eraseToAnyView() case let .isAbleToUpgrade(latestRelease, currentRelease): return VStack { diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/Presenter/OwnerModuleInput.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/Presenter/OwnerModuleInput.swift index abf8c08c1..7112b0f0a 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/Presenter/OwnerModuleInput.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/Presenter/OwnerModuleInput.swift @@ -2,5 +2,5 @@ import Foundation import RuuviOntology protocol OwnerModuleInput: AnyObject { - func configure(ruuviTag: RuuviTagSensor) + func configure(ruuviTag: RuuviTagSensor, mode: OwnershipMode) } diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/Presenter/OwnerPresenter.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/Presenter/OwnerPresenter.swift index 5b1bfb76b..816535213 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/Presenter/OwnerPresenter.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/Presenter/OwnerPresenter.swift @@ -6,6 +6,11 @@ import RuuviStorage import RuuviPresenters import RuuviLocal +enum OwnershipMode { + case claim + case unclaim +} + final class OwnerPresenter: OwnerModuleInput { weak var view: OwnerViewInput! var router: OwnerRouterInput! @@ -19,6 +24,11 @@ final class OwnerPresenter: OwnerModuleInput { var settings: RuuviLocalSettings! private var ruuviTag: RuuviTagSensor! + private var ownershipMode: OwnershipMode = .claim { + didSet { + view.mode = ownershipMode + } + } private var isLoading: Bool = false { didSet { if isLoading { @@ -29,33 +39,22 @@ final class OwnerPresenter: OwnerModuleInput { } } - func configure(ruuviTag: RuuviTagSensor) { + func configure(ruuviTag: RuuviTagSensor, mode: OwnershipMode) { self.ruuviTag = ruuviTag + self.ownershipMode = mode } } extension OwnerPresenter: OwnerViewOutput { - /// This method is responsible for claiming the sensor - func viewDidTapOnClaim() { + /// This method is responsible for claiming/unclaiming the sensor + func viewDidTapOnClaim(mode: OwnershipMode) { isLoading = true - ruuviOwnershipService - .claim(sensor: ruuviTag) - .on(success: { [weak self] _ in - self?.router.dismiss() - self?.removeConnection() - }, failure: { [weak self] error in - switch error { - case .ruuviCloud(.api(.api(.erSensorAlreadyClaimed))): - if let luid = self?.ruuviTag.luid { - self?.connectionPersistence.setKeepConnection(false, for: luid) - } - self?.view.showSensorAlreadyClaimedDialog() - default: - self?.errorPresenter.present(error: error) - } - }, completion: { [weak self] in - self?.isLoading = false - }) + switch mode { + case .claim: + claimSensor() + case .unclaim: + unclaimSensor() + } } /// Update the tag with owner information @@ -98,4 +97,37 @@ extension OwnerPresenter { connectionPersistence.setKeepConnection(false, for: luid) } } + + private func claimSensor() { + ruuviOwnershipService + .claim(sensor: ruuviTag) + .on(success: { [weak self] _ in + self?.router.dismiss() + self?.removeConnection() + }, failure: { [weak self] error in + switch error { + case .ruuviCloud(.api(.api(.erSensorAlreadyClaimed))): + if let luid = self?.ruuviTag.luid { + self?.connectionPersistence.setKeepConnection(false, for: luid) + } + self?.view.showSensorAlreadyClaimedDialog() + default: + self?.errorPresenter.present(error: error) + } + }, completion: { [weak self] in + self?.isLoading = false + }) + } + + private func unclaimSensor() { + ruuviOwnershipService + .unclaim(sensor: ruuviTag) + .on(success: { [weak self] _ in + self?.router.dismiss() + }, failure: { [weak self] error in + self?.errorPresenter.present(error: error) + }, completion: { [weak self] in + self?.isLoading = false + }) + } } diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewController.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewController.swift index 257940d8c..f8699fd2b 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewController.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewController.swift @@ -3,6 +3,8 @@ import UIKit final class OwnerViewController: UIViewController { var output: OwnerViewOutput! + var mode: OwnershipMode = .claim + @IBOutlet weak var claimOwnershipDescriptionLabel: UILabel! @IBOutlet weak var claimOwnershipButton: UIButton! @@ -19,7 +21,7 @@ final class OwnerViewController: UIViewController { }() @IBAction func claimOwnershipButtonTouchUpInside(_ sender: Any) { - output.viewDidTapOnClaim() + output.viewDidTapOnClaim(mode: mode) } override func viewDidLoad() { @@ -44,11 +46,21 @@ extension OwnerViewController: OwnerViewInput { })) present(alertVC, animated: true) } + func localize() { - title = "Owner.title".localized() - claimOwnershipDescriptionLabel.text = "Owner.Claim.description".localized() - claimOwnershipButton.setTitle("Owner.ClaimOwnership.button".localized().capitalized, for: .normal) + // No op. + switch mode { + case .claim: + title = "Owner.title".localized() + claimOwnershipDescriptionLabel.text = "Owner.Claim.description".localized() + claimOwnershipButton.setTitle("Owner.ClaimOwnership.button".localized().capitalized, for: .normal) + case .unclaim: + title = "unclaim_sensor".localized() + claimOwnershipDescriptionLabel.text = "unclaim_sensor_description".localized() + claimOwnershipButton.setTitle("unclaim".localized().capitalized, for: .normal) + } } + func showFirmwareUpdateDialog() { let message = "Cards.LegacyFirmwareUpdateDialog.message".localized() let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert) diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewInput.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewInput.swift index 1b78079b6..f2b3a19c2 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewInput.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewInput.swift @@ -1,6 +1,7 @@ import Foundation protocol OwnerViewInput: ViewInput { + var mode: OwnershipMode { get set } func showSensorAlreadyClaimedDialog() func showFirmwareUpdateDialog() func showFirmwareDismissConfirmationUpdateDialog() diff --git a/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewOutput.swift b/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewOutput.swift index 08ae1708d..7fdf60272 100644 --- a/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewOutput.swift +++ b/station/Classes/Presentation/Modules/TagSettings/Submodules/Owner/View/OwnerViewOutput.swift @@ -2,7 +2,7 @@ import Foundation import RuuviOntology protocol OwnerViewOutput: AnyObject { - func viewDidTapOnClaim() + func viewDidTapOnClaim(mode: OwnershipMode) func updateOwnerInfo(with email: String) func viewDidTriggerFirmwareUpdateDialog() func viewDidConfirmFirmwareUpdate() diff --git a/station/Classes/Presentation/Modules/TagSettings/View/TagSettingsViewInput.swift b/station/Classes/Presentation/Modules/TagSettings/View/TagSettingsViewInput.swift index 0e2681c00..f906ff590 100644 --- a/station/Classes/Presentation/Modules/TagSettings/View/TagSettingsViewInput.swift +++ b/station/Classes/Presentation/Modules/TagSettings/View/TagSettingsViewInput.swift @@ -4,6 +4,7 @@ protocol TagSettingsViewInput: ViewInput { var viewModel: TagSettingsViewModel? { get set } func showTagRemovalConfirmationDialog(isOwner: Bool) + func showTagClaimDialog() func showUnclaimAndRemoveConfirmationDialog() func showMacAddressDetail() func showFirmwareUpdateDialog() diff --git a/station/Classes/Presentation/Modules/TagSettings/View/TagSettingsViewModel.swift b/station/Classes/Presentation/Modules/TagSettings/View/TagSettingsViewModel.swift index 1d99c2809..8865e6abe 100644 --- a/station/Classes/Presentation/Modules/TagSettings/View/TagSettingsViewModel.swift +++ b/station/Classes/Presentation/Modules/TagSettings/View/TagSettingsViewModel.swift @@ -75,6 +75,7 @@ struct TagSettingsViewModel { let isClaimedTag: Observable = Observable(false) let owner: Observable = Observable() let isOwner: Observable = Observable(false) + let ownersPlan: Observable = Observable() let temperatureOffsetCorrection: Observable = Observable() let humidityOffsetCorrection: Observable = Observable() diff --git a/station/Classes/Presentation/Modules/TagSettings/View/TagSettingsViewOutput.swift b/station/Classes/Presentation/Modules/TagSettings/View/TagSettingsViewOutput.swift index 8b0cf14c7..cb395c096 100644 --- a/station/Classes/Presentation/Modules/TagSettings/View/TagSettingsViewOutput.swift +++ b/station/Classes/Presentation/Modules/TagSettings/View/TagSettingsViewOutput.swift @@ -5,6 +5,7 @@ protocol TagSettingsViewOutput { func viewDidLoad() func viewWillAppear() func viewDidAskToDismiss() + func viewDidConfirmClaimTag() func viewDidTriggerChangeBackground() func viewDidAskToRemoveRuuviTag() func viewDidConfirmTagRemoval() diff --git a/station/Classes/Presentation/Modules/TagSettings/View/UI/RUAlertDetailsCellChildView/RUAlertDetailsCellChildView.swift b/station/Classes/Presentation/Modules/TagSettings/View/UI/RUAlertDetailsCellChildView/RUAlertDetailsCellChildView.swift index 21830eef8..806f3b251 100644 --- a/station/Classes/Presentation/Modules/TagSettings/View/UI/RUAlertDetailsCellChildView/RUAlertDetailsCellChildView.swift +++ b/station/Classes/Presentation/Modules/TagSettings/View/UI/RUAlertDetailsCellChildView/RUAlertDetailsCellChildView.swift @@ -12,7 +12,7 @@ class RUAlertDetailsCellChildView: UIView { // Private lazy var titleLabel: UILabel = { let label = UILabel() - label.textAlignment = .right + label.textAlignment = .left label.numberOfLines = 0 label.textColor = .label label.font = UIFont.Muli(.regular, size: 14) @@ -79,4 +79,8 @@ extension RUAlertDetailsCellChildView { func configure(with message: String?) { titleLabel.text = message } + + func configure(with message: NSMutableAttributedString?) { + titleLabel.attributedText = message + } } diff --git a/station/Classes/Presentation/Modules/TagSettings/View/UI/TagSettingsAlertConfigCell.swift b/station/Classes/Presentation/Modules/TagSettings/View/UI/TagSettingsAlertConfigCell.swift index e9809e1b4..fbed708be 100644 --- a/station/Classes/Presentation/Modules/TagSettings/View/UI/TagSettingsAlertConfigCell.swift +++ b/station/Classes/Presentation/Modules/TagSettings/View/UI/TagSettingsAlertConfigCell.swift @@ -83,7 +83,7 @@ class TagSettingsAlertConfigCell: UITableViewCell { private lazy var additionalTextLabel: UILabel = { let label = UILabel() - label.textAlignment = .right + label.textAlignment = .left label.numberOfLines = 0 label.textColor = .label label.font = .systemFont(ofSize: 14) @@ -260,6 +260,10 @@ extension TagSettingsAlertConfigCell { alertLimitDescriptionView.configure(with: description) } + func setAlertLimitDescription(description: NSMutableAttributedString?) { + alertLimitDescriptionView.configure(with: description) + } + func setAlertRange(minValue: CGFloat? = nil, selectedMinValue: CGFloat? = nil, maxValue: CGFloat? = nil, diff --git a/station/Classes/Presentation/Modules/TagSettings/View/UI/TagSettingsBasicCell.swift b/station/Classes/Presentation/Modules/TagSettings/View/UI/TagSettingsBasicCell.swift index 1eece9fbd..aadac31e4 100644 --- a/station/Classes/Presentation/Modules/TagSettings/View/UI/TagSettingsBasicCell.swift +++ b/station/Classes/Presentation/Modules/TagSettings/View/UI/TagSettingsBasicCell.swift @@ -73,7 +73,7 @@ class TagSettingsBasicCell: UITableViewCell { iconView.centerYInSuperview() iconHiddenWidthConstraints = [ iconView.widthAnchor.constraint(equalToConstant: 0), - stack.trailingAnchor.constraint(equalTo: safeRightAnchor, constant: -8) + stack.trailingAnchor.constraint(equalTo: safeRightAnchor, constant: -12) ] addSubview(separator) diff --git a/station/Classes/Presentation/Modules/TagSettings/View/UI/TagSettingsViewController.swift b/station/Classes/Presentation/Modules/TagSettings/View/UI/TagSettingsViewController.swift index 94fc2d31f..8e45f6d17 100644 --- a/station/Classes/Presentation/Modules/TagSettings/View/UI/TagSettingsViewController.swift +++ b/station/Classes/Presentation/Modules/TagSettings/View/UI/TagSettingsViewController.swift @@ -27,10 +27,11 @@ enum TagSettingsSectionIdentifier { enum TagSettingsItemCellIdentifier: Int { case generalName = 0 case generalOwner = 1 - case generalShare = 2 - case offsetTemperature = 3 - case offsetHumidity = 4 - case offsetPressure = 5 + case generalOwnersPlan = 2 + case generalShare = 3 + case offsetTemperature = 4 + case offsetHumidity = 5 + case offsetPressure = 6 } class TagSettingsSection { @@ -146,6 +147,10 @@ class TagSettingsViewController: UIViewController { return TagSettingsBasicCell(style: .value1, reuseIdentifier: Self.ReuseIdentifier) }() + private lazy var tagOwnersPlanCell: TagSettingsBasicCell? = { + return TagSettingsBasicCell(style: .value1, + reuseIdentifier: Self.ReuseIdentifier) + }() private lazy var tagShareCell: TagSettingsBasicCell? = { return TagSettingsBasicCell(style: .value1, reuseIdentifier: Self.ReuseIdentifier) @@ -332,6 +337,7 @@ class TagSettingsViewController: UIViewController { deinit { tagNameCell = nil tagOwnerCell = nil + tagOwnersPlanCell = nil tagShareCell = nil btPairCell = nil temperatureAlertSection = nil @@ -494,40 +500,27 @@ extension TagSettingsViewController { if let currentSection = tableViewSections.first(where: { $0.identifier == section }) { - if currentSection.cells.first(where: { - $0.identifier == .generalShare - }) == nil { - if showShare() { - let rowIndex = currentSection.cells.count - let sectionIndex = indexOfSection(section: section) - let indexPath = IndexPath(row: rowIndex, section: sectionIndex) - tableView.performBatchUpdates({ - currentSection.cells.insert( - tagShareSettingItem(), - at: rowIndex - ) - tableView.insertRows(at: [indexPath], with: .none) - }) - - if let tagOwnerCell = tagOwnerCell { - tagOwnerCell.hideSeparator(hide: false) - } - } - } else { - if !showShare() && currentSection.cells.count > 1 { - let rowIndex = currentSection.cells.count - 1 - let sectionIndex = indexOfSection(section: section) - let indexPath = IndexPath(row: rowIndex, section: sectionIndex) - tableView.performBatchUpdates({ - currentSection.cells.remove(at: rowIndex) - tableView.deleteRows(at: [indexPath], with: .none) - }) - - if let tagOwnerCell = tagOwnerCell { - tagOwnerCell.hideSeparator(hide: true) - } - } + let availableItems = itemsForGeneralSection(showPlan: true) + + let sectionIndex = indexOfSection(section: section) + var oldIndexPaths: [IndexPath] = [] + for rowIndex in 0.. TagSettingsSection { + private func itemsForGeneralSection(showPlan: Bool = false) -> [TagSettingsItem] { var availableItems: [TagSettingsItem] = [ tagNameSettingItem() ] if showOwner() { availableItems.append(tagOwnerSettingItem()) + if let isOwner = viewModel?.isOwner.value, !isOwner, + let isCloudTag = viewModel?.isNetworkConnected.value, + isCloudTag, showPlan { + availableItems.append(tagOwnersPlanSettingItem()) + } } if showShare() { availableItems.append(tagShareSettingItem()) } + return availableItems + } + private func configureGeneralSection() -> TagSettingsSection { + let availableItems = itemsForGeneralSection() let section = TagSettingsSection( identifier: .general, title: "TagSettings.SectionHeader.General.title".localized().capitalized, @@ -639,7 +651,7 @@ extension TagSettingsViewController { self?.tagOwnerCell?.configure(title: "TagSettings.NetworkInfo.Owner".localized(), value: self?.viewModel?.owner.value) self?.tagOwnerCell?.setAccessory(type: (isClaimed && isOwner) ? .none : .chevron ) - self?.tagOwnerCell?.hideSeparator(hide: !GlobalHelpers.getBool(from: self?.showShare())) + self?.tagOwnerCell?.hideSeparator(hide: false) return self?.tagOwnerCell ?? UITableViewCell() }, action: { [weak self] _ in @@ -649,6 +661,21 @@ extension TagSettingsViewController { return settingItem } + private func tagOwnersPlanSettingItem() -> TagSettingsItem { + let settingItem = TagSettingsItem( + identifier: .generalOwnersPlan, + createdCell: { [weak self] in + self?.tagOwnersPlanCell?.configure(title: "owners_plan".localized(), + value: self?.viewModel?.ownersPlan.value) + self?.tagOwnersPlanCell?.setAccessory(type: .none) + self?.tagOwnersPlanCell?.hideSeparator(hide: !GlobalHelpers.getBool(from: self?.showShare())) + return self?.tagOwnersPlanCell ?? UITableViewCell() + }, + action: nil + ) + return settingItem + } + private func tagShareSettingItem() -> TagSettingsItem { let settingItem = TagSettingsItem( identifier: .generalShare, @@ -674,6 +701,10 @@ extension TagSettingsViewController { return viewModel?.isAuthorized.value == true } + private func isOwner() -> Bool { + return viewModel?.isOwner.value == true + } + private func showShare() -> Bool { return viewModel?.canShareTag.value == true } @@ -1589,11 +1620,11 @@ extension TagSettingsViewController { } private func temperatureAlertRangeDescription(from min: CGFloat? = nil, - max: CGFloat? = nil) -> String? { + max: CGFloat? = nil) -> NSMutableAttributedString? { guard isViewLoaded else { return nil } var format = "TagSettings.Alerts.Temperature.description".localized() if let min = min, let max = max { - return String(format: format, min, max) + return attributedString(from: String(format: format, min, max)) } if let tu = viewModel?.temperatureUnit.value?.unitTemperature, @@ -1612,7 +1643,8 @@ extension TagSettingsViewController { } let message = String(format: format, l.value.round(to: 2), u.value.round(to: 2)) - return message + return attributedString(from: message) + } else { return nil } @@ -1652,11 +1684,11 @@ extension TagSettingsViewController { // Humidity private func humidityAlertRangeDescription(from min: CGFloat? = nil, - max: CGFloat? = nil) -> String? { + max: CGFloat? = nil) -> NSMutableAttributedString? { guard isViewLoaded else { return nil } var format = "TagSettings.Alerts.Temperature.description".localized() if let min = min, let max = max { - return String(format: format, min, max) + return attributedString(from: String(format: format, min, max)) } if let l = viewModel?.relativeHumidityLowerBound.value, let u = viewModel?.relativeHumidityUpperBound.value { @@ -1670,7 +1702,7 @@ extension TagSettingsViewController { format = format.replacingLastOccurrence(of: "%0.f", with: "%0.\(decimalPointToConsider)f") } let message = String(format: format, l.round(to: 2), u.round(to: 2)) - return message + return attributedString(from: message) } else { return nil } @@ -1704,12 +1736,12 @@ extension TagSettingsViewController { // Pressure private func pressureAlertRangeDescription(from minValue: CGFloat? = nil, - maxValue: CGFloat? = nil) -> String? { + maxValue: CGFloat? = nil) -> NSMutableAttributedString? { guard isViewLoaded else { return nil } var format = "TagSettings.Alerts.Temperature.description".localized() if let minValue = minValue, let maxValue = maxValue { - return String(format: format, minValue, maxValue) + return attributedString(from: String(format: format, minValue, maxValue)) } if let pu = viewModel?.pressureUnit.value, @@ -1733,7 +1765,7 @@ extension TagSettingsViewController { format = format.replacingLastOccurrence(of: "%0.f", with: "%0.\(decimalPointToConsider)f") } let message = String(format: format, l.round(to: 2), u.round(to: 2)) - return message + return attributedString(from: message) } else { return nil } @@ -1781,18 +1813,18 @@ extension TagSettingsViewController { // RSSI private func rssiAlertRangeDescription(from min: CGFloat? = nil, - max: CGFloat? = nil) -> String? { + max: CGFloat? = nil) -> NSMutableAttributedString? { guard isViewLoaded else { return nil } let format = "TagSettings.Alerts.Temperature.description".localized() if let min = min, let max = max { - return String(format: format, min, max) + return attributedString(from: String(format: format, min, max)) } if let lower = viewModel?.signalLowerBound.value, let upper = viewModel?.signalUpperBound.value { let message = String(format: format, lower, upper) - return message + return attributedString(from: message) } else { return nil } @@ -1823,6 +1855,23 @@ extension TagSettingsViewController { return (minimum: CGFloat(-105), maximum: CGFloat(0)) } + + private func attributedString(from message: String?) -> NSMutableAttributedString? { + if let message = message { + let attributedString = NSMutableAttributedString(string: message) + let boldFont = UIFont.Muli(.bold, size: 14) + let numberRegex = try? NSRegularExpression(pattern: "\\d+(\\.\\d+)?") + let range = NSRange(location: 0, length: message.utf16.count) + if let matches = numberRegex?.matches(in: message, options: [], range: range) { + for match in matches { + attributedString.addAttribute(.font, value: boldFont, range: match.range) + } + } + return attributedString + } else { + return nil + } + } } extension TagSettingsViewController: TagSettingsAlertConfigCellDelegate { @@ -1910,50 +1959,71 @@ extension TagSettingsViewController: TagSettingsAlertConfigCellDelegate { } } + // swiftlint:disable:next cyclomatic_complexity function_body_length func didSetAlertRange(sender: TagSettingsAlertConfigCell, minValue: CGFloat, maxValue: CGFloat) { guard minValue < maxValue else { return } switch sender { case temperatureAlertCell: - output.viewDidChangeAlertLowerBound( - for: .temperature(lower: 0, upper: 0), - lower: minValue - ) - output.viewDidChangeAlertUpperBound( - for: .temperature(lower: 0, upper: 0), - upper: maxValue - ) + if minValue != viewModel?.temperatureLowerBound.value?.value { + output.viewDidChangeAlertLowerBound( + for: .temperature(lower: 0, upper: 0), + lower: minValue + ) + } + + if maxValue != viewModel?.temperatureUpperBound.value?.value { + output.viewDidChangeAlertUpperBound( + for: .temperature(lower: 0, upper: 0), + upper: maxValue + ) + } case humidityAlertCell: - output.viewDidChangeAlertLowerBound( - for: .relativeHumidity(lower: 0, upper: 0), - lower: minValue - ) - output.viewDidChangeAlertUpperBound( - for: .relativeHumidity(lower: 0, upper: 0), - upper: maxValue - ) + if minValue != viewModel?.relativeHumidityLowerBound.value { + output.viewDidChangeAlertLowerBound( + for: .relativeHumidity(lower: 0, upper: 0), + lower: minValue + ) + } + + if maxValue != viewModel?.relativeHumidityUpperBound.value { + output.viewDidChangeAlertUpperBound( + for: .relativeHumidity(lower: 0, upper: 0), + upper: maxValue + ) + } case pressureAlertCell: - output.viewDidChangeAlertLowerBound( - for: .pressure(lower: 0, upper: 0), - lower: minValue - ) - output.viewDidChangeAlertUpperBound( - for: .pressure(lower: 0, upper: 0), - upper: maxValue - ) + if minValue != viewModel?.pressureLowerBound.value?.value { + output.viewDidChangeAlertLowerBound( + for: .pressure(lower: 0, upper: 0), + lower: minValue + ) + } + + if maxValue != viewModel?.pressureUpperBound.value?.value { + output.viewDidChangeAlertUpperBound( + for: .pressure(lower: 0, upper: 0), + upper: maxValue + ) + } case rssiAlertCell: - output.viewDidChangeAlertLowerBound( - for: .signal(lower: 0, upper: 0), - lower: minValue - ) - output.viewDidChangeAlertUpperBound( - for: .signal(lower: 0, upper: 0), - upper: maxValue - ) + if minValue != viewModel?.signalLowerBound.value { + output.viewDidChangeAlertLowerBound( + for: .signal(lower: 0, upper: 0), + lower: minValue + ) + } + + if maxValue != viewModel?.signalUpperBound.value { + output.viewDidChangeAlertUpperBound( + for: .signal(lower: 0, upper: 0), + upper: maxValue + ) + } default: break @@ -3310,6 +3380,19 @@ extension TagSettingsViewController: TagSettingsViewInput { present(controller, animated: true) } + func showTagClaimDialog() { + let title = "claim_sensor_ownership".localized() + let message = "do_you_own_sensor".localized() + let controller = UIAlertController(title: title, message: message, preferredStyle: .alert) + controller.addAction(UIAlertAction(title: "Yes".localized(), + style: .default, + handler: { [weak self] _ in + self?.output.viewDidConfirmClaimTag() + })) + controller.addAction(UIAlertAction(title: "No".localized(), style: .cancel, handler: nil)) + present(controller, animated: true) + } + func showUnclaimAndRemoveConfirmationDialog() { let title = "TagSettings.confirmTagRemovalDialog.title".localized() let message = "TagSettings.confirmTagUnclaimAndRemoveDialog.message".localized() diff --git a/station/Classes/Routers/AppRouter.swift b/station/Classes/Routers/AppRouter.swift index aa789c8e3..0f685c7d4 100644 --- a/station/Classes/Routers/AppRouter.swift +++ b/station/Classes/Routers/AppRouter.swift @@ -2,6 +2,7 @@ import UIKit import RuuviLocal import LightRoute import RuuviUser +import RuuviOntology final class AppRouter { var viewController: UIViewController { @@ -133,4 +134,11 @@ extension AppRouter: DiscoverRouterDelegate { navigationController.pushViewController(controller, animated: true) } } + + func discoverRouterWantsCloseWithRuuviTagNavigation( + _ router: DiscoverRouter, + ruuviTag: RuuviTagSensor + ) { + // No op. + } } diff --git a/station/Classes/Routers/DiscoverRouter.swift b/station/Classes/Routers/DiscoverRouter.swift index 6783af13a..6cb5ba781 100644 --- a/station/Classes/Routers/DiscoverRouter.swift +++ b/station/Classes/Routers/DiscoverRouter.swift @@ -6,6 +6,10 @@ import RuuviLocationPicker protocol DiscoverRouterDelegate: AnyObject { func discoverRouterWantsClose(_ router: DiscoverRouter) + func discoverRouterWantsCloseWithRuuviTagNavigation( + _ router: DiscoverRouter, + ruuviTag: RuuviTagSensor + ) } final class DiscoverRouter { @@ -64,6 +68,10 @@ extension DiscoverRouter: RuuviDiscoverOutput { func ruuvi(discover: RuuviDiscover, didAdd ruuviTag: AnyRuuviTagSensor) { delegate?.discoverRouterWantsClose(self) } + + func ruuvi(discover: RuuviDiscover, didSelectFromNFC ruuviTag: RuuviTagSensor) { + delegate?.discoverRouterWantsCloseWithRuuviTagNavigation(self, ruuviTag: ruuviTag) + } } extension DiscoverRouter { diff --git a/station/Extensions/Errors/RuuviCloudApiError+LocalizedError.swift b/station/Extensions/Errors/RuuviCloudApiError+LocalizedError.swift index 581fdd227..2fc08b7a0 100644 --- a/station/Extensions/Errors/RuuviCloudApiError+LocalizedError.swift +++ b/station/Extensions/Errors/RuuviCloudApiError+LocalizedError.swift @@ -15,7 +15,7 @@ extension RuuviCloudApiError: LocalizedError { case .api(let code): return "UserApiError.\(code.rawValue)".localized() case .claim(let claimError): - return claimError.error.localized() + return claimError.error?.localized() case .networking(let error): return error.localizedDescription case .parsing(let error): diff --git a/station/Extensions/UIFont+Extension.swift b/station/Extensions/UIFont+Extension.swift index afb922072..66acd5a1e 100644 --- a/station/Extensions/UIFont+Extension.swift +++ b/station/Extensions/UIFont+Extension.swift @@ -11,6 +11,7 @@ public extension UIFont { case bold = "Bold" case regular = "Regular" case semiBoldItalic = "SemiBoldItalic" + case extraBold = "ExtraBold" } enum OswaldStyles: String { @@ -28,7 +29,7 @@ public extension UIFont { static func Muli(_ type: MuliStyles = .regular, size: CGFloat = UIFont.systemFontSize) -> UIFont { - let prefix = type == .semiBoldItalic ? "Mulish" : "Muli" + let prefix = (type == .semiBoldItalic || type == .extraBold) ? "Mulish" : "Muli" return UIFont(name: "\(prefix)-\(type.rawValue)", size: size.adjustedSize()) ?? UIFont.systemFont(ofSize: size.adjustedSize()) diff --git a/station/Resources/Fonts/Muli-ExtraBold.ttf b/station/Resources/Fonts/Muli-ExtraBold.ttf new file mode 100644 index 000000000..971549d1b Binary files /dev/null and b/station/Resources/Fonts/Muli-ExtraBold.ttf differ diff --git a/station/Resources/Images/Assets.xcassets/beaver-mail.imageset/beaver-mail.png b/station/Resources/Images/Assets.xcassets/beaver-mail.imageset/beaver-mail.png index c830034a4..44b993cef 100644 Binary files a/station/Resources/Images/Assets.xcassets/beaver-mail.imageset/beaver-mail.png and b/station/Resources/Images/Assets.xcassets/beaver-mail.imageset/beaver-mail.png differ diff --git a/station/Resources/Images/Assets.xcassets/icon-bluetooth-connected.imageset/Contents.json b/station/Resources/Images/Assets.xcassets/icon-bluetooth-connected.imageset/Contents.json index 04d529aeb..95863a35b 100644 --- a/station/Resources/Images/Assets.xcassets/icon-bluetooth-connected.imageset/Contents.json +++ b/station/Resources/Images/Assets.xcassets/icon-bluetooth-connected.imageset/Contents.json @@ -10,6 +10,7 @@ "version" : 1 }, "properties" : { + "compression-type" : "lossless", "preserves-vector-representation" : true } } diff --git a/station/Resources/Images/Assets.xcassets/icon-bluetooth-connected.imageset/icon-bluetooth-connected.pdf b/station/Resources/Images/Assets.xcassets/icon-bluetooth-connected.imageset/icon-bluetooth-connected.pdf index 92b05135e..5f157713a 100644 Binary files a/station/Resources/Images/Assets.xcassets/icon-bluetooth-connected.imageset/icon-bluetooth-connected.pdf and b/station/Resources/Images/Assets.xcassets/icon-bluetooth-connected.imageset/icon-bluetooth-connected.pdf differ diff --git a/station/Resources/Images/Assets.xcassets/icon-bluetooth.imageset/Contents.json b/station/Resources/Images/Assets.xcassets/icon-bluetooth.imageset/Contents.json index 12895f8e6..fbc00f31c 100644 --- a/station/Resources/Images/Assets.xcassets/icon-bluetooth.imageset/Contents.json +++ b/station/Resources/Images/Assets.xcassets/icon-bluetooth.imageset/Contents.json @@ -10,6 +10,7 @@ "version" : 1 }, "properties" : { + "compression-type" : "lossless", "preserves-vector-representation" : true } } diff --git a/station/Resources/Images/Assets.xcassets/icon-bluetooth.imageset/icon-bluetooth.pdf b/station/Resources/Images/Assets.xcassets/icon-bluetooth.imageset/icon-bluetooth.pdf index 6f63157a5..69be54f75 100644 Binary files a/station/Resources/Images/Assets.xcassets/icon-bluetooth.imageset/icon-bluetooth.pdf and b/station/Resources/Images/Assets.xcassets/icon-bluetooth.imageset/icon-bluetooth.pdf differ diff --git a/station/Resources/Plists/DevInfo.plist b/station/Resources/Plists/DevInfo.plist index 9f80e8939..92b2609c8 100644 --- a/station/Resources/Plists/DevInfo.plist +++ b/station/Resources/Plists/DevInfo.plist @@ -24,7 +24,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 386 + 398 FirebaseMessagingAutoInitEnabled LSRequiresIPhoneOS @@ -58,6 +58,7 @@ Muli-Regular.ttf Muli-Bold.ttf Muli-SemiBoldItalic.ttf + Muli-ExtraBold.ttf Montserrat-Bold.ttf Montserrat-Regular.ttf Montserrat-ExtraBold.ttf diff --git a/station/Resources/Plists/Info.plist b/station/Resources/Plists/Info.plist index 86b512fc6..d555540eb 100644 --- a/station/Resources/Plists/Info.plist +++ b/station/Resources/Plists/Info.plist @@ -64,6 +64,7 @@ Muli-Regular.ttf Muli-Bold.ttf Muli-SemiBoldItalic.ttf + Muli-ExtraBold.ttf Montserrat-Bold.ttf Montserrat-Regular.ttf Montserrat-ExtraBold.ttf diff --git a/station/Resources/Strings/de.lproj/Localizable.strings b/station/Resources/Strings/de.lproj/Localizable.strings index f3f1260e5..22ba79752 100644 --- a/station/Resources/Strings/de.lproj/Localizable.strings +++ b/station/Resources/Strings/de.lproj/Localizable.strings @@ -294,6 +294,7 @@ "Menu.Label.Feedback.text" = "Feedback schicken"; "Menu.Label.MyRuuviAccount.text" = "Mein Ruuvi Konto"; "min" = "min"; +"minutes" = "Minuten"; "TagSettings.Label.moreInfo.text" = "Mehr info"; "TagSettings.SectionHeader.Name.title" = "NAME"; "No" = "Nein"; @@ -676,6 +677,9 @@ Wenn die Beanspruchung nicht erfolgreich war oder NFC auf Ihrem Gerät nicht ver "add_your_first_sensor" = "Fügen Sie Ihren ersten Sensor hinzu"; "changelog" = "(änderungsprotokoll)"; "changelog_ios_url" = "https://f.ruuvi.com/t/3192"; +"chart_stat_min" = "Min"; +"chart_stat_max" = "Max"; +"chart_stat_avg" = "Durchschnitt"; "shared_to_x" = "Freigegebene %d/%d"; "settings_alert_notifications" = "Alarmbenachrichtigungen"; "settings_alert_sound" = "Alarmton"; @@ -706,7 +710,8 @@ Wenn die Beanspruchung nicht erfolgreich war oder NFC auf Ihrem Gerät nicht ver "add_with_nfc" = "Mit NFC hinzufügen"; "sensor_details" = "Sensordetails"; "add_sensor" = "Sensor hinzufügen"; -"copy_details" = "Details kopieren"; +"copy_mac_address" = "MAC-Adresse kopieren"; +"copy_unique_id" = "Eindeutige ID kopieren"; "name" = "Name:"; "mac_address" = "MAC-Adresse:"; "go_to_sensor" = "Gehen Sie zur Sensorkarte"; @@ -722,4 +727,12 @@ Wenn die Beanspruchung nicht erfolgreich war oder NFC auf Ihrem Gerät nicht ver "claim_sensor_ownership" = "Beanspruchen Sie den Besitz des Sensors"; "do_you_own_sensor" = "Besitzen Sie diesen Sensor?"; "owners_plan" = "Ruuvi-Plan des Eigentümers"; -"rename" = "Rename"; +"alert_cloud_connection_title" = "Cloud-Verbindung"; +"alert_cloud_connection_description" = "Warnung, wenn Sensordaten länger als %d Minuten nicht in der Cloud aktualisiert wurden."; +"alert_cloud_connection_dialog_description" = "Geben Sie die gewünschte Verzögerung in Minuten ein, bevor die Warnung ausgelöst wird. Der Mindestwert beträgt 2 Minuten."; +"alert_cloud_connection_dialog_title" = "Cloud-Verbindungsalarm einstellen"; +"rename" = "Umbenennen"; +"chart_stat_show" = "Show min/max/avg"; +"chart_stat_hide" = "Hide min/max/avg"; +"settings_alert_limit_notification" = "Limit alert notifications"; +"settings_alert_limit_notification_description" = "Trigger Bluetooth alert notification only once per hour even if alert was retriggered."; diff --git a/station/Resources/Strings/en.lproj/Localizable.strings b/station/Resources/Strings/en.lproj/Localizable.strings index 153523ebf..237040ebc 100644 --- a/station/Resources/Strings/en.lproj/Localizable.strings +++ b/station/Resources/Strings/en.lproj/Localizable.strings @@ -295,6 +295,7 @@ If you cannot see the Language option in the settings, make sure that you have a "Menu.Label.Feedback.text" = "Send Feedback"; "Menu.Label.MyRuuviAccount.text" = "My Ruuvi Account"; "min" = "min"; +"minutes" = "Minutes"; "TagSettings.Label.moreInfo.text" = "More info"; "TagSettings.SectionHeader.Name.title" = "NAME"; "No" = "No"; @@ -671,6 +672,9 @@ Your RuuviTag sensor is ready for use!"; "add_your_first_sensor" = "Add Your First Sensor"; "changelog" = "(changelog)"; "changelog_ios_url" = "https://f.ruuvi.com/t/3192"; +"chart_stat_min" = "Min"; +"chart_stat_max" = "Max"; +"chart_stat_avg" = "Average"; "shared_to_x" = "Shared to %d/%d"; "settings_alert_notifications" = "Alert Notifications"; "settings_alert_sound" = "Alert Sound"; @@ -701,7 +705,8 @@ Your RuuviTag sensor is ready for use!"; "add_with_nfc" = "Add with NFC"; "sensor_details" = "Sensor Details"; "add_sensor" = "Add Sensor"; -"copy_details" = "Copy Details"; +"copy_mac_address" = "Copy MAC Address"; +"copy_unique_id" = "Copy Unique ID"; "name" = "Name:"; "mac_address" = "Mac Address:"; "go_to_sensor" = "Go to sensor card"; @@ -717,4 +722,12 @@ Your RuuviTag sensor is ready for use!"; "claim_sensor_ownership" = "Claim sensor ownership"; "do_you_own_sensor" = "Do you own this sensor?"; "owners_plan" = "Owner's Ruuvi Plan"; +"alert_cloud_connection_title" = "Cloud Connection"; +"alert_cloud_connection_description" = "Alert if sensor data hasn't been updated to the cloud for longer than %d minutes."; +"alert_cloud_connection_dialog_description" = "Enter the desired delay to be used in minutes before alert is triggered. Minimum value is 2 minutes."; +"alert_cloud_connection_dialog_title" = "Set cloud connection alert"; "rename" = "Rename"; +"chart_stat_show" = "Show min/max/avg"; +"chart_stat_hide" = "Hide min/max/avg"; +"settings_alert_limit_notification" = "Limit alert notifications"; +"settings_alert_limit_notification_description" = "Trigger Bluetooth alert notification only once per hour even if alert was retriggered."; diff --git a/station/Resources/Strings/fi.lproj/Localizable.strings b/station/Resources/Strings/fi.lproj/Localizable.strings index 9b904985e..e4f4d0d47 100644 --- a/station/Resources/Strings/fi.lproj/Localizable.strings +++ b/station/Resources/Strings/fi.lproj/Localizable.strings @@ -295,6 +295,7 @@ Mikäli et näe Kieli-valintaa asetuksissa, varmista, että sinulla on vähintä "Menu.Label.Feedback.text" = "Lähetä palautetta"; "Menu.Label.MyRuuviAccount.text" = "Minun Ruuvi-tilini"; "min" = "min"; +"minutes" = "Minuuttia"; "TagSettings.Label.moreInfo.text" = "Lisätietoa"; "TagSettings.SectionHeader.Name.title" = "NIMI"; "No" = "Ei"; @@ -671,6 +672,9 @@ RuuviTag on valmis käyttöön!"; "add_your_first_sensor" = "Lisää ensimmäinen anturi"; "changelog" = "(muutosloki)"; "changelog_ios_url" = "https://f.ruuvi.com/t/3192"; +"chart_stat_min" = "Min"; +"chart_stat_max" = "Max"; +"chart_stat_avg" = "Keskiarvo"; "shared_to_x" = "Jaettu %d/%d"; "settings_alert_notifications" = "Hälytysilmoitukset"; "settings_alert_sound" = "Hälytysääni"; @@ -701,7 +705,8 @@ RuuviTag on valmis käyttöön!"; "add_with_nfc" = "Lisää NFC:llä"; "sensor_details" = "Anturin tiedot"; "add_sensor" = "Lisää anturi"; -"copy_details" = "Kopioi tiedot"; +"copy_mac_address" = "Kopioi MAC-osoite"; +"copy_unique_id" = "Kopioi yksilöivä tunniste"; "name" = "Nimi:"; "mac_address" = "MAC-osoite:"; "go_to_sensor" = "Siirry anturikortille"; @@ -717,4 +722,12 @@ RuuviTag on valmis käyttöön!"; "claim_sensor_ownership" = "Vaadi anturin omistajuutta"; "do_you_own_sensor" = "Omistatko tämän anturin?"; "owners_plan" = "Omistajan Ruuvi-tilaus"; -"rename" = "Rename"; +"alert_cloud_connection_title" = "Yhteys pilveen"; +"alert_cloud_connection_description" = "Hälytä, jos anturitietoja ei ole päivitetty pilveen yli %d minuuttiin."; +"alert_cloud_connection_dialog_description" = "Syötä haluttu viive minuutteina, jonka jälkeen hälytys laukaistaan. Lyhyin valittava arvo on 2 minuuttia."; +"alert_cloud_connection_dialog_title" = "Aseta pilviyhteyden hälytys"; +"rename" = "Nimeä uudelleen"; +"chart_stat_show" = "Show min/max/avg"; +"chart_stat_hide" = "Hide min/max/avg"; +"settings_alert_limit_notification" = "Limit alert notifications"; +"settings_alert_limit_notification_description" = "Trigger Bluetooth alert notification only once per hour even if alert was retriggered."; diff --git a/station/Resources/Strings/fr.lproj/Localizable.strings b/station/Resources/Strings/fr.lproj/Localizable.strings index ae38dd723..6b2d2191b 100644 --- a/station/Resources/Strings/fr.lproj/Localizable.strings +++ b/station/Resources/Strings/fr.lproj/Localizable.strings @@ -294,6 +294,7 @@ "Menu.Label.Feedback.text" = "Donnez-nous votre avis!"; "Menu.Label.MyRuuviAccount.text" = "Mon compte Ruuvi"; "min" = "min"; +"minutes" = "Minutes"; "TagSettings.Label.moreInfo.text" = "Plus d'infos"; "TagSettings.SectionHeader.Name.title" = "NOM"; "No" = "Non"; @@ -672,6 +673,9 @@ RuuviTag est prêt à être utilisé !"; "add_your_first_sensor" = "Ajoutez votre premier capteur"; "changelog" = "(journal des modifications)"; "changelog_ios_url" = "https://f.ruuvi.com/t/3192"; +"chart_stat_min" = "Min"; +"chart_stat_max" = "Max"; +"chart_stat_avg" = "Moyenne"; "shared_to_x" = "Partagé %d/%d"; "settings_alert_notifications" = "Notifications d'alerte"; "settings_alert_sound" = "Son d'alerte"; @@ -702,7 +706,8 @@ RuuviTag est prêt à être utilisé !"; "add_with_nfc" = "Ajouter avec NFC"; "sensor_details" = "Détails du capteur"; "add_sensor" = "Ajouter un capteur"; -"copy_details" = "Copier les détails"; +"copy_mac_address" = "Copier l'adresse MAC"; +"copy_unique_id" = "Copier l'identifiant unique"; "name" = "Nom:"; "mac_address" = "Adresse Mac:"; "go_to_sensor" = "Aller à la carte du capteur"; @@ -718,5 +723,13 @@ RuuviTag est prêt à être utilisé !"; "claim_sensor_ownership" = "Revendiquer la propriété du capteur"; "do_you_own_sensor" = "Possédez-vous ce capteur ?"; "owners_plan" = "Plan Ruuvi du propriétaire"; -"rename" = "Rename"; +"alert_cloud_connection_title" = "Connexion infonuagique"; +"alert_cloud_connection_description" = "Alerte si les données du capteur n'ont pas été mises à jour dans le cloud depuis plus de %d minutes."; +"alert_cloud_connection_dialog_description" = "Entrez le délai souhaité à utiliser en minutes avant le déclenchement de l'alerte. La valeur minimale est de 2 minutes."; +"alert_cloud_connection_dialog_title" = "Définir une alerte de connexion au cloud"; +"rename" = "Renommer"; +"chart_stat_show" = "Show min/max/avg"; +"chart_stat_hide" = "Hide min/max/avg"; +"settings_alert_limit_notification" = "Limit alert notifications"; +"settings_alert_limit_notification_description" = "Trigger Bluetooth alert notification only once per hour even if alert was retriggered."; diff --git a/station/Resources/Strings/ru.lproj/Localizable.strings b/station/Resources/Strings/ru.lproj/Localizable.strings index af73b3505..d4e4f6053 100644 --- a/station/Resources/Strings/ru.lproj/Localizable.strings +++ b/station/Resources/Strings/ru.lproj/Localizable.strings @@ -295,6 +295,7 @@ If you cannot see the Language option in the settings, make sure that you have a "Menu.Label.Feedback.text" = "Отправить Отзыв"; "Menu.Label.MyRuuviAccount.text" = "My Ruuvi Account"; "min" = "мин"; +"minutes" = "Минут"; "TagSettings.Label.moreInfo.text" = "Дополнительно"; "TagSettings.SectionHeader.Name.title" = "ИМЯ"; "No" = "Нет"; @@ -671,6 +672,9 @@ If you cannot see the Language option in the settings, make sure that you have a "add_your_first_sensor" = "Add Your First Sensor"; "changelog" = "(changelog)"; "changelog_ios_url" = "https://f.ruuvi.com/t/3192"; +"chart_stat_min" = "Min"; +"chart_stat_max" = "Max"; +"chart_stat_avg" = "Average"; "shared_to_x" = "Shared to %d/%d"; "settings_alert_notifications" = "Уведомления о тревогах"; "settings_alert_sound" = "Alert Sound"; @@ -701,7 +705,8 @@ If you cannot see the Language option in the settings, make sure that you have a "add_with_nfc" = "Add with NFC"; "sensor_details" = "Sensor Details"; "add_sensor" = "Add Sensor"; -"copy_details" = "Copy Details"; +"copy_mac_address" = "Копировать MAC-адрес"; +"copy_unique_id" = "Скопировать уникальный идентификатор"; "name" = "Name:"; "mac_address" = "Mac Address:"; "go_to_sensor" = "Go to sensor card"; @@ -717,4 +722,12 @@ If you cannot see the Language option in the settings, make sure that you have a "claim_sensor_ownership" = "Заявить о собственности на датчик"; "do_you_own_sensor" = "Это ваш датчик?"; "owners_plan" = "Owner's Ruuvi Plan"; -"rename" = "Rename"; +"alert_cloud_connection_title" = "Cloud Connection"; +"alert_cloud_connection_description" = "Alert if sensor data hasn't been updated to the cloud for longer than %d minutes."; +"alert_cloud_connection_dialog_description" = "Enter the desired delay to be used in minutes before alert is triggered. Minimum value is 2 minutes."; +"alert_cloud_connection_dialog_title" = "Set cloud connection alert"; +"rename" = "Переименовать"; +"chart_stat_show" = "Show min/max/avg"; +"chart_stat_hide" = "Hide min/max/avg"; +"settings_alert_limit_notification" = "Limit alert notifications"; +"settings_alert_limit_notification_description" = "Trigger Bluetooth alert notification only once per hour even if alert was retriggered."; diff --git a/station/Resources/Strings/sv.lproj/Localizable.strings b/station/Resources/Strings/sv.lproj/Localizable.strings index f557038f3..5ccbc2211 100644 --- a/station/Resources/Strings/sv.lproj/Localizable.strings +++ b/station/Resources/Strings/sv.lproj/Localizable.strings @@ -295,6 +295,7 @@ Om du inte kan se språkalternativet i inställningarna, se till att du har lagt "Menu.Label.Feedback.text" = "Skicka Feedback"; "Menu.Label.MyRuuviAccount.text" = "Mitt Ruuvi-konto"; "min" = "min"; +"minutes" = "Minuter"; "TagSettings.Label.moreInfo.text" = "Mer info"; "TagSettings.SectionHeader.Name.title" = "NAMN"; "No" = "Nej"; @@ -671,6 +672,9 @@ RuuviTag-sensorn är redo att användas!"; "add_your_first_sensor" = "Lägg till din första sensor."; "changelog" = "(ändringslogg)"; "changelog_ios_url" = "https://f.ruuvi.com/t/3192"; +"chart_stat_min" = "Min"; +"chart_stat_max" = "Max"; +"chart_stat_avg" = "Medelvärde"; "shared_to_x" = "Delad %d/%d"; "settings_alert_notifications" = "Varningsmeddelanden"; "settings_alert_sound" = "Varningsljud"; @@ -701,7 +705,8 @@ RuuviTag-sensorn är redo att användas!"; "add_with_nfc" = "Lägg till med NFC"; "sensor_details" = "Sensordetaljer"; "add_sensor" = "Lägg till sensor"; -"copy_details" = "Kopiera detaljer"; +"copy_mac_address" = "Kopiera MAC-adress"; +"copy_unique_id" = "Kopiera unikt ID"; "name" = "Namn:"; "mac_address" = "MAC-adress:"; "go_to_sensor" = "Gå till sensorkortet"; @@ -717,4 +722,12 @@ RuuviTag-sensorn är redo att användas!"; "claim_sensor_ownership" = "Gör anspråk på sensorägande"; "do_you_own_sensor" = "Äger du denna sensor?"; "owners_plan" = "Ägarens Ruuvi-plan"; -"rename" = "Rename"; +"alert_cloud_connection_title" = "Molnuppkoppling"; +"alert_cloud_connection_description" = "Varning om sensordata inte har uppdaterats till molnet på mer än %d minuter."; +"alert_cloud_connection_dialog_description" = "Ange önskad fördröjning som ska användas i minuter innan larmet utlöses. Minsta värde är 2 minuter."; +"alert_cloud_connection_dialog_title" = "Ställ in molnanslutningsvarning"; +"rename" = "Döp om"; +"chart_stat_show" = "Show min/max/avg"; +"chart_stat_hide" = "Hide min/max/avg"; +"settings_alert_limit_notification" = "Limit alert notifications"; +"settings_alert_limit_notification_description" = "Trigger Bluetooth alert notification only once per hour even if alert was retriggered.";