diff --git a/RFR-App/RFR.xcodeproj/project.pbxproj b/RFR-App/RFR.xcodeproj/project.pbxproj
index e0c823f2..d8f79be1 100644
--- a/RFR-App/RFR.xcodeproj/project.pbxproj
+++ b/RFR-App/RFR.xcodeproj/project.pbxproj
@@ -560,7 +560,6 @@
};
AA2B0B6E2B7530B100756DBD /* Highlight TODO and FIXME as warnings */ = {
isa = PBXShellScriptBuildPhase;
- alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -755,7 +754,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 0;
+ CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"RFR/Resources/Preview Content\"";
DEVELOPMENT_TEAM = 45GZ5C9N64;
@@ -774,7 +773,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 3.2.1;
+ MARKETING_VERSION = 3.2.2;
PRODUCT_BUNDLE_IDENTIFIER = de.cyface.RFR;
PRODUCT_NAME = "Ready for Robots";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -880,7 +879,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 0;
+ CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"RFR/Resources/Preview Content\"";
DEVELOPMENT_TEAM = 45GZ5C9N64;
@@ -899,7 +898,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 3.2.1;
+ MARKETING_VERSION = 3.2.2;
PRODUCT_BUNDLE_IDENTIFIER = de.cyface.RFR.staging;
PRODUCT_NAME = "Ready for Robots Staging";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -999,7 +998,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 0;
+ CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"RFR/Resources/Preview Content\"";
DEVELOPMENT_TEAM = 45GZ5C9N64;
@@ -1018,7 +1017,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 3.2.1;
+ MARKETING_VERSION = 3.2.2;
PRODUCT_BUNDLE_IDENTIFIER = de.cyface.RFR.staging;
PRODUCT_NAME = "Ready for Robots Staging";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -1117,7 +1116,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 0;
+ CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"RFR/Resources/Preview Content\"";
DEVELOPMENT_TEAM = 45GZ5C9N64;
@@ -1136,7 +1135,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 3.2.1;
+ MARKETING_VERSION = 3.2.2;
PRODUCT_BUNDLE_IDENTIFIER = de.cyface.RFR;
PRODUCT_NAME = "Ready for Robots";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -1743,7 +1742,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 0;
+ CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"RFR/Resources/Preview Content\"";
DEVELOPMENT_TEAM = 45GZ5C9N64;
@@ -1762,7 +1761,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 3.2.1;
+ MARKETING_VERSION = 3.2.2;
PRODUCT_BUNDLE_IDENTIFIER = de.cyface.RFR.dev;
PRODUCT_NAME = "Ready for Robots Development";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -1781,7 +1780,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 0;
+ CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"RFR/Resources/Preview Content\"";
DEVELOPMENT_TEAM = 45GZ5C9N64;
@@ -1800,7 +1799,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 3.2.1;
+ MARKETING_VERSION = 3.2.2;
PRODUCT_BUNDLE_IDENTIFIER = de.cyface.RFR.dev;
PRODUCT_NAME = "Ready for Robots Development";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
diff --git a/RFR-App/RFR/InitializationView.swift b/RFR-App/RFR/InitializationView.swift
index f69cea22..6315d875 100644
--- a/RFR-App/RFR/InitializationView.swift
+++ b/RFR-App/RFR/InitializationView.swift
@@ -26,7 +26,7 @@ import AppAuthCore
The first view shown after starting the application. This should usually be the login link or some error message if startup failed.
- Author: Klemens Muthmann
- - Version: 1.0.1
+ - Version: 1.0.2
- Since: 3.1.2
*/
struct InitializationView: View {
diff --git a/RFR-App/RFR/Measurements/MeasurementsView.swift b/RFR-App/RFR/Measurements/MeasurementsView.swift
index ce63326d..b1167273 100644
--- a/RFR-App/RFR/Measurements/MeasurementsView.swift
+++ b/RFR-App/RFR/Measurements/MeasurementsView.swift
@@ -25,7 +25,7 @@ import MapKit
A view showing the lists of measurements capture by this device.
- Author: Klemens Muthmann
- - Version: 1.0.0
+ - Version: 1.0.1
- Since: 3.1.2
*/
struct MeasurementsView: View {
diff --git a/RFR-App/RFR/Preview_Mocks.swift b/RFR-App/RFR/Preview_Mocks.swift
index 6dd6c9b8..ea0efac4 100644
--- a/RFR-App/RFR/Preview_Mocks.swift
+++ b/RFR-App/RFR/Preview_Mocks.swift
@@ -27,6 +27,7 @@ import OSLog
- Author: Klemens Muthmann
- Version: 1.0.0
+ - Since: 3.2.2
*/
class MockAuthenticator: Authenticator {
func authenticate(onSuccess: @escaping (String) -> Void, onFailure: @escaping (Error) -> Void) {
@@ -55,6 +56,7 @@ class MockAuthenticator: Authenticator {
- Author: Klemens Muthmann
- Version: 1.0.0
+ - Since: 3.2.2
*/
class MockDataStoreStack: DataStoreStack {
@@ -82,10 +84,22 @@ class MockDataStoreStack: DataStoreStack {
}
}
+/**
+ A mock for the vouchers interface avoiding actual network communication.
+
+ This should be used during testing and for previews.
+
+ - Author: Klemens Muthmann
+ - Version: 1.0.0
+ - Since: 3.2.2
+ */
struct MockVouchers: Vouchers {
+ /// The amount of simulated vouchers available.
var count: Int
+ /// The voucher currently enabled for the active user.
let voucher: Voucher
+ /// Simulate requesting a voucher from the server. This will always return the hard coded voucher provided on initialization.
func requestVoucher() async throws -> Voucher {
return voucher
}
diff --git a/RFR-App/RFR/RFRApp.swift b/RFR-App/RFR/RFRApp.swift
index 9c64e8c4..7a0e87b9 100644
--- a/RFR-App/RFR/RFRApp.swift
+++ b/RFR-App/RFR/RFRApp.swift
@@ -76,7 +76,7 @@ struct RFRApp: App {
Those errors are published via the ``error`` property of this class.
- Author: Klemens Muthmann
- - Version: 1.0.1
+ - Version: 1.0.2
- Since: 3.1.2
*/
class AppModel: ObservableObject {
diff --git a/RFR-App/RFR/Resources/de.lproj/Localizable.strings b/RFR-App/RFR/Resources/de.lproj/Localizable.strings
index cba3ab96..35b2bd32 100644
--- a/RFR-App/RFR/Resources/de.lproj/Localizable.strings
+++ b/RFR-App/RFR/Resources/de.lproj/Localizable.strings
@@ -46,33 +46,6 @@
/* No comment provided by engineer. */
"Datenschutzbestimmungen" = "Datenschutzbestimmungen";
-/* Tell the user, that the OAuth discovery failed for some reason.
-The actual reason is provided as a String message, as the first argument. */
-"de.cyface.error.oauthauthenticationerror.discoveryFailed" = "Es war nicht möglich die Einstellungen für die Authentifizierung vom Identitätsanbieter zu laden. Dies wurde durch %s verursacht. Dies kann auftreten, wenn der Identitätsanbieter gewartet wird oder derzeit nicht erreichbar ist, weil Ihr Telefon zum Beispiel keine funktionsfähige Internetverbindung hat. Bitte stellen Sie sicher, dass sie mit dem Internet verbunden sind und wiederholen Sie den Vorgang später. Falls das Problem auch nach einiger Zeit noch auftritt, wenden Sie sich an den Anbieter dieser Anwendung.";
-
-/* Tell the user, that the wrong HTTP status code was recieved. It should be 200. The actual value is provided as the first argument. */
-"de.cyface.error.oauthauthenticationerror.errorResponse" = "HTTP Status Code %d empfangen. Erwartet war 200 OK. Dieser Fehler kann zum Beispiel durch eine Serverwartung auftreten. Bitte wiederholen Sie den Vorgang zu einem späteren Zeitpunkt. Falls der Fehler über längere Zeit auftritt, wenden Sie sich bitte an den Anbieter dieser Anwendung.";
-
-/* Tell the user, that the response was not an HTTP response. This should not happen unless there is some serious implemenetation error. */
-"de.cyface.error.oauthauthenticationerror.invalidResponse" = "Die Antwort war keine gültige HTTP Antwort. Dieser Fehler sollte normalerweise nicht passieren. Es kann sein, dass Sie eine fehlerbehaftete Zwischenversion verwenden. Bitte aktualisieren Sie die Anwendung oder warten Sie auf die neueste Version.";
-
-/* Tell the user that an invalid JWT token was encountered! */
-"de.cyface.error.oauthauthenticationerror.invalidtoken" = "Es wurde eine falsch formatierte Authentifizierung empfangen. Dies lässt sich meistens auf einen Fehler bei der Einstellung des Identitätsanbieters zurückführen. Bitte wenden Sie sich an den Anbieter dieser Anwendung, um weitere Hinweise zu erhalten.";
-
-/* Tell the user, that the internal auth state did not exist.
-Since it is gracefully initialized before used for the first time, this is an error, that should not happen in production.
-The cause of this error is provided as the first parameter. */
-"de.cyface.error.oauthauthenticationerror.missingAuthState" = "Die Anwendung konnte den Stand der Authentifizierung nicht ermitteln. Dies deutet auf einen ernsthaften Fehler in der Umsetzung der Anwendung hin. Es könnte sein, dass Sie eine fehlerbehaftete Zwischenversion haben. Bitte aktualisieren Sie die Anwendung oder warten Sie auf die nächste Aktualisierung. Falls Sie Hilfe brauchen, wenden Sie sich bitte an den Anbieter dieser Anwendung.";
-
-/* Tell the user, that OAuth was called in a wrong state. Namely there was no ViewController provided to return to, after successful authentication. */
-"de.cyface.error.oauthauthenticationerror.missingCallackController" = "Authentifizierung schlug fehl, weil die Anwendung nicht weiß, welchen Bildschirm sie nach Abschluss der Anmeldung anzeigen soll. Dies sollte nur auftreten, wenn Sie eine fehlerbehaftete Zwischenversion verwenden. Bitte aktualisieren Sie die Anwendung. Falls Sie Hilfe benötigen, wenden Sie sich bitte an den Anbieter dieser Anwendung.";
-
-/* This error should not happen on a properly developed system. Tell the user to call for support! */
-"de.cyface.error.oauthauthenticationerror.missingresponse" = "Authentifizierungsanfrage wurde ohne Antwort aber auch ohne Fehler abgebrochen. Es ist unmöglich mit einem der beiden Fälle fortzufahren. Dies deutet auf eine fehlerhafte Zwischenversion der Anwendung hin. Bitte aktualisieren Sie die Anwendung und wenden Sie sich an den Anbieter dieser Anwendung, wenn Sie Unterstützung benötigen.";
-
-/* Tell the user that you received no valid auth token on a refresh request. This should actually not happen and points to some serious implementation mistakes. */
-"de.cyface.error.oauthauthenticatorerror.tokenMissing" = "Die Authentifizierung konnte nicht erneuert werden.";
-
/* Tell the user that the e-mail address used for registration is not available anymore.
In such cases the user either uses an e-mail address not belonging to her/him/them or tries to reregister, which is not permitted.
To reset the password, the user needs to contact the Cyface support. */
@@ -125,15 +98,45 @@ The returned status code is provided as an Int as the first parameter! */
/* Explain that no valid voucher information was found for the current user. */
"de.cyface.error.rfrerror.missingVoucher" = "Ihr Gutschein konnte nicht geladen werden.";
-/* The system was unable to get a valid authentication token from the server. Either the server is not available or the user used invalid Credentials. */
-"de.cyface.error.rfrerror.unableToAuthenticate" = "Ihr Benutzerkonto konnte nicht authentifiziert werden. Möglicherweise ist der Authentifizierungsserver ausgefallen oder Sie haben ungültige Anmeldeinformationen angegeben. Bitte melden Sie sich ab und wieder an.";
-
/* Tell the user that a measurement could not be loaded. The device wide unique identifier of the measurement is provided as the first parameter. */
"de.cyface.error.rfrerror.unableToLoadMeasurement" = "Messung %@ konnte nicht geladen werden!";
/* Explain to the user, that the system was unable to load the voucher overview. */
"de.cyface.error.rfrerror.voucherOverviewFailed" = "Gutscheinübersicht konnte nicht geladen werden.";
+/* The subject of the participation E-Mail when sending a voucher. */
+"de.cyface.rfr.label.VoucherViewModel.mail_subject" = "Gewinnlos: \\(voucher.code)";
+
+/* Label telling the user when the next event happens. The date is provided as the first arguemnt. */
+"de.cyface.rfr.text.NoVoucher.next_event" = "Nächste Gewinnaktion ab %@";
+
+/* Label telling the user that there are no events at the moment. */
+"de.cyface.rfr.text.NoVoucher.no_events" = "Derzeit sind keine Gewinnaktionen verfügbar";
+
+/* Label telling the user that there are no events planned. */
+"de.cyface.rfr.text.NoVoucher.nothing_planned" = "Derzeit sind keine Gewinnaktionen geplant";
+
+/* A label containing the active voucher code, which is provided as the first argument. */
+"de.cyface.rfr.text.VoucherEnabled.code" = "Gutscheincode: %@";
+
+/* Tell the where to send a voucher code and what happens after sending it there. */
+"de.cyface.rfr.text.VoucherEnabled.game_explanation" = "Diesen Code an gewinnspiel@ready-for-robots.de schicken und an der Verlosung teilnehmen.";
+
+/* Button label for sending an E-Mail. */
+"de.cyface.rfr.text.VoucherEnabled.send_mail" = "E-Mail Senden";
+
+/* No comment provided by engineer. */
+"de.cyface.rfr.text.VoucherReached.show_voucher" = "Gewinnspiellos anzeigen";
+
+/* Tell the user that they achieved all goals to get the next voucher */
+"de.cyface.rfr.text.VoucherReached.voucher_active" = "Gewinnspiellos freigeschaltet";
+
+/* Tell the user how often they should pass town hall. The number is provided as the first argument */
+"de.cyface.rfr.text.VoucherRequirements.condition_town_hall" = "Bitte fahren Sie %d mal am Rathaus vorbei, um ein Gewinnlos zu erhalten!";
+
+/* Tell the user how many of their measurements they are still required to upload. The number of uploads required is provided as the first parameter. The second parameter are the uploads required alltogether. */
+"de.cyface.rfr.text.VoucherRequirements.condition_upload" = "Laden Sie bitte noch %d von %d Messungen hoch, um ein Gewinnlos zu erhalten!";
+
/* No comment provided by engineer. */
"E-Mail Adresse" = "E-Mail Adresse";
diff --git a/RFR-App/RFR/Resources/en.lproj/Localizable.strings b/RFR-App/RFR/Resources/en.lproj/Localizable.strings
index 2c775f69..b5ea2949 100644
--- a/RFR-App/RFR/Resources/en.lproj/Localizable.strings
+++ b/RFR-App/RFR/Resources/en.lproj/Localizable.strings
@@ -92,6 +92,39 @@ The returned status code is provided as an Int as the first parameter! */
/* Tell the user that a measurement could not be loaded. The device wide unique identifier of the measurement is provided as the first parameter. */
"de.cyface.error.rfrerror.unableToLoadMeasurement" = "Measurement %@ could not be loaded!";
+/* The subject of the participation E-Mail when sending a voucher. */
+"de.cyface.rfr.label.VoucherViewModel.mail_subject" = "Raffle Ticket: %@";
+
+/* Label telling the user when the next event happens. The date is provided as the first arguemnt. */
+"de.cyface.rfr.text.NoVoucher.next_event" = "Next giveaway starting at %s";
+
+/* Label telling the user that there are no events at the moment. */
+"de.cyface.rfr.text.NoVoucher.no_events" = "Currently, there are no giveaways available";
+
+/* Label telling the user that there are no events planned. */
+"de.cyface.rfr.text.NoVoucher.nothing_planned" = "Currently, there are no giveaways planned";
+
+/* A label containing the active voucher code, which is provided as the first argument. */
+"de.cyface.rfr.text.VoucherEnabled.code" = "Raffle Ticket: %@";
+
+/* Tell the where to send a voucher code and what happens after sending it there. */
+"de.cyface.rfr.text.VoucherEnabled.game_explanation" = "Send this Code to gewinnspiel@ready-for-robots.de for a chance to win!";
+
+/* Button label for sending an E-Mail. */
+"de.cyface.rfr.text.VoucherEnabled.send_mail" = "Send E-Mail";
+
+/* No comment provided by engineer. */
+"de.cyface.rfr.text.VoucherReached.show_voucher" = "Show Raffle Ticket";
+
+/* Tell the user that they achieved all goals to get the next voucher */
+"de.cyface.rfr.text.VoucherReached.voucher_active" = "Raffle Ticket Unlocked";
+
+/* Tell the user how often they should pass town hall. The number is provided as the first argument */
+"de.cyface.rfr.text.VoucherRequirements.condition_town_hall" = "Please cycle on %d days near the town hall to unlock the raffle ticket";
+
+/* Tell the user how many of their measurements they are still required to upload. The number of uploads required is provided as the first parameter. The second parameter are the uploads required alltogether. */
+"de.cyface.rfr.text.VoucherRequirements.condition_upload" = "Please upload %d of %d measurements, for a chance to a raffle ticket!";
+
/* No comment provided by engineer. */
"E-Mail Adresse" = "E-Mail Address";
diff --git a/RFR-App/RFR/Voucher/Events.swift b/RFR-App/RFR/Voucher/Events.swift
index ec20e055..b385e3e2 100644
--- a/RFR-App/RFR/Voucher/Events.swift
+++ b/RFR-App/RFR/Voucher/Events.swift
@@ -1,11 +1,24 @@
-//
-// Events.swift
-// RFR
-//
-// Created by Klemens Muthmann on 10.04.24.
-//
+/*
+ * Copyright 2024 Cyface GmbH
+ *
+ * This file is part of the Ready for Robots iOS App.
+ *
+ * The Ready for Robots iOS App is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Ready for Robots iOS App is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with the Ready for Robots iOS App. If not, see .
+ */
import Foundation
+/// Create the list of event ranges.
func createEvents() -> [ClosedRange] {
var start = DateComponents()
start.year = 2024
@@ -26,12 +39,15 @@ func createEvents() -> [ClosedRange] {
return [Calendar.current.date(from: start)!...Calendar.current.date(from: end)!]
}
+/// `true` if there is a current event; `false`otherwise.
func thereIsCurrentEvent() -> Bool {
return events.map { event in event.contains(Date.now) }.reduce(false) { $0 || $1}
}
+/// Provide the next event range.
func nextEvent() -> ClosedRange? {
return events.sorted { $0.lowerBound < $1.lowerBound }.first { $0.lowerBound > Date.now}
}
+/// The list of events in the system.
let events = createEvents()
diff --git a/RFR-App/RFR/Voucher/MailView.swift b/RFR-App/RFR/Voucher/MailView.swift
index aa12c9a9..b2cc2c6d 100644
--- a/RFR-App/RFR/Voucher/MailView.swift
+++ b/RFR-App/RFR/Voucher/MailView.swift
@@ -1,22 +1,53 @@
-//
-// MailView.swift
-// RFR
-//
-// Created by Klemens Muthmann on 08.04.24.
-//
-
+/*
+ * Copyright 2024 Cyface GmbH
+ *
+ * This file is part of the Ready for Robots iOS App.
+ *
+ * The Ready for Robots iOS App is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Ready for Robots iOS App is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with the Ready for Robots iOS App. If not, see .
+ */
import SwiftUI
import UIKit
import MessageUI
+/// The type of callbacks used when calling the systems E-Mail application
typealias MailViewCallback = ((Result) -> Void)?
+/**
+ The view showng when the user wants to send its voucher via E-Mail.
+
+ Since this is currently not supported by SwiftUI, it is implemented as a UIViewController using `UIViewControllerRepresentable`.
+
+ - Author: Klemens Muthmann
+ - Version: 1.0.0
+ - Since: 3.2.2
+ */
struct MailView: UIViewControllerRepresentable {
+ /// Specifies whether to show this view or not.
@Environment(\.presentationMode) var presentation
+ /// The data of the mail to send.
var data: ComposeMailData
+ /// Called when the mail was sent.
let callback: MailViewCallback
+ /**
+ The UIKit Coordinator for this representable.
+
+ - Author: Klemens Muthmann
+ - Version: 1.0.0
+ - Since: 3.2.2
+ */
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
@Binding var presentation: PresentationMode
var data: ComposeMailData
@@ -62,11 +93,19 @@ struct MailView: UIViewControllerRepresentable {
func updateUIViewController(_ uiViewController: MFMailComposeViewController, context: UIViewControllerRepresentableContext) {
}
+ /// `true` if mail sending is allowed, `false` otherwise.
static var canSendMail: Bool {
MFMailComposeViewController.canSendMail()
}
}
+/**
+ The data used to create the mail
+
+ - Author: Klemens Muthmann
+ - Version: 1.0.0
+ - Since: 3.2.2
+ */
struct ComposeMailData {
let subject: String
let recipients: [String]?
@@ -74,6 +113,13 @@ struct ComposeMailData {
let attachments: [AttachmentData]?
}
+/**
+ Data attached to the E-Mail.
+
+ - Author: Klemens Muthmann
+ - Version: 1.0.0
+ - Since: 3.2.2
+ */
struct AttachmentData {
let data: Data
let mimeType: String
diff --git a/RFR-App/RFR/Voucher/NoVoucher.swift b/RFR-App/RFR/Voucher/NoVoucher.swift
index 8f81828c..3572ed0d 100644
--- a/RFR-App/RFR/Voucher/NoVoucher.swift
+++ b/RFR-App/RFR/Voucher/NoVoucher.swift
@@ -1,14 +1,35 @@
-//
-// NoEvent.swift
-// RFR
-//
-// Created by Klemens Muthmann on 10.04.24.
-//
+/*
+ * Copyright 2024 Cyface GmbH
+ *
+ * This file is part of the Ready for Robots App.
+ *
+ * The Ready for Robots App is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The Ready for Robots App is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with the Ready for Robots App. If not, see .
+ */
import SwiftUI
+/**
+The view shown if no voucher has been enabled.
+
+ - Author: Klemens Muthmann
+ - Version: 1.0.0
+ - Since: 3.2.2
+ */
struct NoVoucher: View {
+ /// `true` if a voucher may be redeemed; `false` otherwise.
let voucherRedeemable: Bool
+ /// Formatter used to display dates in this view.
var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
@@ -17,11 +38,18 @@ struct NoVoucher: View {
var body: some View {
if let nextEvent = nextEvent() {
- Text("Nächste Gewinnaktion ab \(dateFormatter.string(from: nextEvent.lowerBound))")
+ Text(
+ String.localizedStringWithFormat(
+ NSLocalizedString(
+ "de.cyface.rfr.text.NoVoucher.next_event",
+ comment: "Label telling the user when the next event happens. The date is provided as the first arguemnt."
+ ), dateFormatter.string(from: nextEvent.lowerBound)
+ )
+ ) // "Nächste Gewinnaktion ab \(dateFormatter.string(from: nextEvent.lowerBound))"
} else if voucherRedeemable {
- Text("Derzeit sind keine Gewinnaktionen verfügbar")
+ Text("de.cyface.rfr.text.NoVoucher.no_events", comment: "Label telling the user that there are no events at the moment.") //Derzeit sind keine Gewinnaktionen verfügbar
} else {
- Text("Derzeit sind keine Gewinnaktionen geplant")
+ Text("de.cyface.rfr.text.NoVoucher.nothing_planned", comment: "Label telling the user that there are no events planned.") // Derzeit sind keine Gewinnaktionen geplant
}
}
}
diff --git a/RFR-App/RFR/Voucher/Voucher.swift b/RFR-App/RFR/Voucher/Voucher.swift
index 7c3571d1..dad93026 100644
--- a/RFR-App/RFR/Voucher/Voucher.swift
+++ b/RFR-App/RFR/Voucher/Voucher.swift
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 Cyface GmbH
+ * Copyright 2023-2024 Cyface GmbH
*
* This file is part of the Ready for Robots App.
*
@@ -23,7 +23,8 @@ import DataCapturing
Model object representing the collection of vouchers on the server.
- Author: Klemens Muthmann
- - Version: 1.0.0
+ - Version: 1.0.1
+ - Since: 3.2.2
*/
protocol Vouchers {
var count: Int { get async throws }
@@ -33,7 +34,7 @@ protocol Vouchers {
This class is responsible for creating the connection to the voucher API, retrieving vouchers and that state of the collection of vouchers.
- Author: Klemens Muthmann
- - Version: 1.0.0
+ - Version: 1.0.1
- Since: 3.1.2
*/
class VouchersApi: Vouchers {
diff --git a/RFR-App/RFR/Voucher/VoucherEnabled.swift b/RFR-App/RFR/Voucher/VoucherEnabled.swift
index 4ea0675b..be99f2f7 100644
--- a/RFR-App/RFR/Voucher/VoucherEnabled.swift
+++ b/RFR-App/RFR/Voucher/VoucherEnabled.swift
@@ -25,7 +25,7 @@ import DataCapturing
This view shows the actual voucher code.
- Author: Klemens Muthmann
- - Version: 1.0.0
+ - Version: 1.0.1
- Since: 3.1.2
*/
struct VoucherEnabled: View {
@@ -36,20 +36,28 @@ struct VoucherEnabled: View {
if
let voucher = viewModel.voucher {
VStack {
- Text("Gutscheincode: \(voucher.code)")
- .padding()
+ Text(
+ String.localizedStringWithFormat(
+ NSLocalizedString(
+ "de.cyface.rfr.text.VoucherEnabled.code",
+ comment: "A label containing the active voucher code, which is provided as the first argument."
+ ), voucher.code
+ )
+ ).padding()
Rectangle().frame(height: 1, alignment: .center).padding([.leading, .trailing]).foregroundColor(.gray)
- Text("Diesen Code an gewinnspiel@ready-for-robots.de schicken und an der Verlosung teilnehmen.")
+ Text(
+ "de.cyface.rfr.text.VoucherEnabled.game_explanation",
+ comment: "Tell the where to send a voucher code and what happens after sending it there."
+ )
.padding()
Button(action: {
viewModel.onSendEMailButtonPressed()
}, label: {
- Text("E-Mail Senden")
+ Text("de.cyface.rfr.text.VoucherEnabled.send_mail", comment: "Button label for sending an E-Mail.") // E-Mail Senden
})
.sheet(isPresented: $viewModel.showMailView, content: {
if let mailData = viewModel.mailData {
MailView(data: mailData) {_ in
- print("E-Mail send successfully!")
viewModel.showMailView.toggle()
}
}
diff --git a/RFR-App/RFR/Voucher/VoucherReached.swift b/RFR-App/RFR/Voucher/VoucherReached.swift
index a0e22c42..e8f726d8 100644
--- a/RFR-App/RFR/Voucher/VoucherReached.swift
+++ b/RFR-App/RFR/Voucher/VoucherReached.swift
@@ -25,7 +25,7 @@ import DataCapturing
It allows the user to actually claim one of the vouchers, if available.
- Author: Klemens Muthmann
- - Version: 1.0.0
+ - Version: 1.0.1
- Since: 3.1.2
*/
struct VoucherReached: View {
@@ -43,7 +43,10 @@ struct VoucherReached: View {
Divider()
HStack {
Image(systemName: "checkmark.seal.fill")
- Text("Gewinnspiellos freigeschaltet")
+ Text(
+ "de.cyface.rfr.text.VoucherReached.voucher_active",
+ comment: "Tell the user that they achieved all goals to get the next voucher"
+ )
.padding([.top, .bottom])
}
Button(action: {
@@ -55,11 +58,11 @@ struct VoucherReached: View {
}
}
}, label: {
- Text("Gewinnspiellos anzeigen".uppercased(with: .autoupdatingCurrent))
+ Text(String(localized: "de.cyface.rfr.text.VoucherReached.show_voucher").uppercased(with: .autoupdatingCurrent))
.frame(maxWidth: .infinity)
.foregroundColor(Color("ButtonText"))
- }
+ }
)
}
.buttonStyle(.borderedProminent)
diff --git a/RFR-App/RFR/Voucher/VoucherRequirements.swift b/RFR-App/RFR/Voucher/VoucherRequirements.swift
index 221182e5..cbf82326 100644
--- a/RFR-App/RFR/Voucher/VoucherRequirements.swift
+++ b/RFR-App/RFR/Voucher/VoucherRequirements.swift
@@ -21,19 +21,32 @@ import CoreLocation
import SwiftUI
import DataCapturing
+/**
+ A wrapper for the requirements for getting a voucher.
+
+ - Author: Klemens Muthmann
+ - Version: 1.0.0
+ - Since: 3.2.2
+ */
struct VoucherRequirements {
// MARK: - Properties
+ /// The number of days to have at least one measurement in the special region
let daysInSpecialRegion = 3
+ /// The coordinates of the special region.
let specialRegion = CLCircularRegion(
center: CLLocationCoordinate2D(latitude: 12.220760571276312, longitude: 51.395503403504705),
radius: CLLocationDistance(150),
identifier: "Schkeuditz Town Hall"
)
+ /// Access to the apps data storage to store load progress from.
let dataStoreStack: DataStoreStack
+ /// The number of days the user already did drive through the special region.
var daysInSpecialRegionFullFilled = 0
+ /// The number of valid measurements already uploaded.
var uploaded = 0
// MARK: - Methods
+ /// The view showing the progress towards the challenge goal.
@ViewBuilder
func progressView(voucherCount: Int) -> some View {
VStack {
@@ -41,19 +54,42 @@ struct VoucherRequirements {
Image(systemName: "rosette")
VStack {
if daysInSpecialRegionFullFilled < daysInSpecialRegion {
- Text("Bitte fahren Sie \(daysInSpecialRegion - daysInSpecialRegionFullFilled) mal am Rathaus vorbei, um ein Gewinnlos zu erhalten!")
+ Text(
+ String.localizedStringWithFormat(
+ NSLocalizedString(
+ "de.cyface.rfr.text.VoucherRequirements.condition_town_hall",
+ comment: "Tell the user how often they should pass town hall. The number is provided as the first argument"
+ ),
+ daysInSpecialRegion - daysInSpecialRegionFullFilled
+ )
+ //"Bitte fahren Sie \() mal am Rathaus vorbei, um ein Gewinnlos zu erhalten!"
+ )
} else {
- Text("Laden Sie bitte noch \(daysInSpecialRegion - uploaded) von \(daysInSpecialRegionFullFilled) Messungen hoch, um ein Gewinnlos zu erhalten!")
+ Text(
+ String.localizedStringWithFormat(
+ NSLocalizedString(
+ "de.cyface.rfr.text.VoucherRequirements.condition_upload",
+ comment: """
+Tell the user how many of their measurements they are still required to upload. The number of uploads required is provided as the first parameter. The second parameter are the uploads required alltogether.
+"""
+ ),
+ daysInSpecialRegion - uploaded,
+ daysInSpecialRegionFullFilled
+ )
+ //"Laden Sie bitte noch \(daysInSpecialRegion - uploaded) von \(daysInSpecialRegionFullFilled) Messungen hoch, um ein Gewinnlos zu erhalten!"
+ )
}
}
}
}
}
+ /// This is `true` if the user is qualified to recieve a new voucher.
func isQualifiedForVoucher() -> Bool {
return daysInSpecialRegionFullFilled >= daysInSpecialRegion
}
+ /// Refresh the progress from the measurements currently stored on the device.
mutating func refreshProgress() async throws {
try await withCheckedThrowingContinuation { continuation in
do {
@@ -93,6 +129,7 @@ struct VoucherRequirements {
}
// MARK: - Private Methods
+ /// Calculate the covered distance for one single track.
private func toDistance(track: TrackMO) -> Double {
return toDistance(
locations: track.typedLocations().sorted {
@@ -101,6 +138,7 @@ struct VoucherRequirements {
)
}
+ /// Calculate the distance between an array of geo locations ordered by time.
private func toDistance(locations: [GeoLocationMO]) -> Double {
var previousLocation: GeoLocationMO? = nil
var accumulatedDistance = 0.0
@@ -113,16 +151,28 @@ struct VoucherRequirements {
return accumulatedDistance
}
+ /// Calculate the distance covered by a measurement.
private func toDistance(measurement: MeasurementMO) -> Double {
return measurement.typedTracks().map { track in
toDistance(track: track)
}.reduce(0.0) { $0 + $1 }
}
+ /**
+ Since the natie date structure always requires a time component (and that time component will change the date based on the users time zone), this struct provides us the possibility to only store a date.
+
+ - Author: Klemens Muthmann
+ - Version: 1.0.0
+ - Since: 3.2.2
+ */
private struct DateWithOutTime: Equatable, Hashable {
+ /// The day in the month.
let day: Int
+ /// The month in the year.
let month: Int
+ /// The year AD.
let year: Int
+ /// Whether the measurement with that date was synchronized.
let synchronized: Bool
static func == (lhs: DateWithOutTime, rhs: DateWithOutTime) -> Bool {
diff --git a/RFR-App/RFR/Voucher/VoucherViewModel.swift b/RFR-App/RFR/Voucher/VoucherViewModel.swift
index 4415c205..2e72c4a8 100644
--- a/RFR-App/RFR/Voucher/VoucherViewModel.swift
+++ b/RFR-App/RFR/Voucher/VoucherViewModel.swift
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 Cyface GmbH
+ * Copyright 2023-2024 Cyface GmbH
*
* This file is part of the Ready for Robots App.
*
@@ -25,7 +25,7 @@ import MessageUI
View model used for the view showing the voucher.
- Author: Klemens Muthmann
- - Version: 1.0.0
+ - Version: 2.0.0
- Since: 3.1.2
*/
class VoucherViewModel: ObservableObject {
@@ -37,12 +37,15 @@ class VoucherViewModel: ObservableObject {
@Published var voucher: Voucher?
/// The number of available fouchers, shown as long as the current user did not acquire a voucher already.
@Published var voucherCount: Int = 0
+ /// `true` if the view to send the acquired voucher via E-Mail should display; `false` otherwise.
@Published var showMailView: Bool = false
+ /// The information making up an E-Mail.
@Published var mailData: ComposeMailData?
/// A handle to the `Vouchers` API, for retrieving voucher information from the server.
private let vouchers: Vouchers
/// An algorithm to calculate whether a user is eligleble for a voucher or not.
private var voucherRequirements: VoucherRequirements
+ /// `true` if a voucher is redeemable at the moment; `false` if the competition period is over.
private var voucherRedeemable: Bool {
var redeemDate = DateComponents()
redeemDate.year = 2024
@@ -91,7 +94,7 @@ class VoucherViewModel: ObservableObject {
}
// The following send E-Mail functionality is based on code from the following StackOverflowThread: https://stackoverflow.com/questions/25981422/how-to-open-mail-app-from-swift
-
+ /// This function is called if the user presses the send E-Mail button.
func onSendEMailButtonPressed() {
guard let voucher = voucher else {
return
@@ -99,7 +102,7 @@ class VoucherViewModel: ObservableObject {
// Modify following variables with your text / recipient
let recipientEmail = "gewinnspiel@ready-for-robots.de"
- let subject = "Gewinnlos: \(voucher.code)"
+ let subject = String(format: NSLocalizedString("de.cyface.rfr.label.VoucherViewModel.mail_subject", comment: "The subject of the participation E-Mail when sending a voucher."), voucher.code) //"Gewinnlos: \(voucher.code)"
let body = ""
if MFMailComposeViewController.canSendMail() {
@@ -110,7 +113,9 @@ class VoucherViewModel: ObservableObject {
}
}
-
+ /// Create a URL to send the E-Mail if the native mail application is not available.
+ ///
+ /// This tries to start GMail, Outlook, YahooMail, Spark or the default program registered for the mailto scheme.
private func createEmailUrl(to: String, subject: String, body: String) -> URL? {
let subjectEncoded = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let bodyEncoded = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
@@ -160,7 +165,9 @@ class VoucherViewModel: ObservableObject {
/// Thereafter show a button to acquire a voucher and finally show the voucher itself if one was still available.
@ViewBuilder
func view() -> some View {
- if voucherCount > 0 && !voucherRequirements.isQualifiedForVoucher() {
+ if !thereIsCurrentEvent() {
+ NoVoucher(voucherRedeemable: voucherRedeemable)
+ } else if voucherCount > 0 && !voucherRequirements.isQualifiedForVoucher() {
voucherRequirements.progressView(voucherCount: voucherCount).padding([.top, .bottom])
} else if voucherCount > 0 && voucherRequirements.isQualifiedForVoucher() && voucher == nil {
VoucherReached(viewModel: self).padding([.top, .bottom])