Skip to content
This repository was archived by the owner on May 26, 2025. It is now read-only.

Commit 1b460ab

Browse files
Merge pull request #205 from code4romania/develop
Release for local elections 2020
2 parents b7c1255 + b08973e commit 1b460ab

File tree

99 files changed

+1952
-305
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

99 files changed

+1952
-305
lines changed

.github/CODEOWNERS

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# These owners will be the default owners for everything in
2+
# the repo. Unless a later match takes precedence,
3+
# they will be requested for review when someone
4+
# opens a pull request.
5+
* @CristiHabliuc
6+
7+
# More details on creating a codeowners file:
8+
# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners

Config/CustomConfiguration.xcconfig

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,10 @@
1212
PRODUCT_BUNDLE_IDENTIFIER = com.mycompany.MyOtherAppName
1313
DEVELOPMENT_TEAM = ""
1414
CODE_SIGN_STYLE = Manual
15-
API_URL = https:/$()/mv-mobile-prod.azurewebsites.net/api // production
16-
TEST_PHONE =
17-
TEST_PIN =
15+
API_URL = https:/$()/app-vmon-api-dev.azurewebsites.net/api
16+
TEST_PHONE = 0722222222
17+
TEST_PIN = 1234
1818
ALLOWED_LANGUAGES = ro,en // you can override this in your project, but make sure you have correct localization
19-
SUPPORT_PHONE = 0800080200
20-
GUIDE_URL = https:/$()/fiecarevot.ro/wp-content/uploads/2019/11/Manual-prezidentiale.pdf
21-
22-
//API_URL = https:/$()/mv-mobile-test.azurewebsites.net/api // dev - use this in your LocalConfiguration.xcconfig
19+
DISABLE_UPDATE_CHECK = false // override this to force disable the update check
2320

2421
#include? "LocalConfiguration.xcconfig"

MonitorizareVot.xcodeproj/project.pbxproj

Lines changed: 62 additions & 17 deletions
Large diffs are not rendered by default.

MonitorizareVot/APIManager.swift

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ protocol APIManagerType: NSObject {
1717
then callback: @escaping (APIError?) -> Void)
1818
func sendPushToken(withToken token: String,
1919
then callback: @escaping (APIError?) -> Void)
20-
func fetchPollingStations(then callback: @escaping ([PollingStationResponse]?, APIError?) -> Void)
20+
func fetchCounties(then callback: @escaping ([CountyResponse]?, APIError?) -> Void)
2121
func fetchForms(diaspora: Bool, then callback: @escaping ([FormResponse]?, APIError?) -> Void)
2222
func fetchForm(withId formId: Int,
2323
then callback: @escaping ([FormSectionResponse]?, APIError?) -> Void)
@@ -55,13 +55,18 @@ class APIManager: NSObject, APIManagerType {
5555
/// Use this to format dates to and from the API
5656
lazy var apiDateFormatter: DateFormatter = {
5757
let formatter = DateFormatter()
58-
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
58+
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
5959
formatter.timeZone = TimeZone(abbreviation: "UTC")
6060
formatter.locale = Locale(identifier: "en_US_POSIX")
6161
return formatter
6262
}()
6363

6464
func login(withPhone phone: String, pin: String, then callback: @escaping (APIError?) -> Void) {
65+
if let errorMessage = checkConnectionError() {
66+
callback(.generic(reason: errorMessage))
67+
return
68+
}
69+
6570
let url = ApiURL.login.url()
6671
let udid = AccountManager.shared.udid
6772
let request = LoginRequest(user: phone, password: pin, uniqueId: udid)
@@ -113,7 +118,7 @@ class APIManager: NSObject, APIManagerType {
113118
}
114119
}
115120

116-
func fetchPollingStations(then callback: @escaping ([PollingStationResponse]?, APIError?) -> Void) {
121+
func fetchCounties(then callback: @escaping ([CountyResponse]?, APIError?) -> Void) {
117122
let url = ApiURL.pollingStationList.url()
118123
let headers = authorizationHeaders()
119124

@@ -124,7 +129,7 @@ class APIManager: NSObject, APIManagerType {
124129
if statusCode == 200,
125130
let data = response.data {
126131
do {
127-
let stations = try JSONDecoder().decode([PollingStationResponse].self, from: data)
132+
let stations = try JSONDecoder().decode([CountyResponse].self, from: data)
128133
callback(stations, nil)
129134
} catch {
130135
callback(nil, .incorrectFormat(reason: error.localizedDescription))
@@ -196,6 +201,11 @@ class APIManager: NSObject, APIManagerType {
196201
}
197202

198203
func upload(pollingStation: UpdatePollingStationRequest, then callback: @escaping (APIError?) -> Void) {
204+
if let errorMessage = checkConnectionError() {
205+
callback(.generic(reason: errorMessage))
206+
return
207+
}
208+
199209
let url = ApiURL.pollingStation.url()
200210
let auth = authorizationHeaders()
201211
let headers = requestHeaders(withAuthHeaders: auth)
@@ -275,6 +285,10 @@ class APIManager: NSObject, APIManagerType {
275285
}
276286
}
277287

288+
private func checkConnectionError() -> String? {
289+
ReachabilityManager.shared.isReachable ? nil : "Error.InternetConnection".localized
290+
}
291+
278292
}
279293

280294
// MARK: - Helpers

MonitorizareVot/ApiModels.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ struct UpdatePollingStationRequest: Codable {
2222
var id: Int
2323
var countyCode: String
2424
var isUrbanArea: Bool
25-
var leaveTime: String
25+
var leaveTime: String?
2626
var arrivalTime: String
2727
var isPresidentFemale: Bool
2828

@@ -90,12 +90,13 @@ struct LoginResponse: Codable {
9090
}
9191
}
9292

93-
struct PollingStationResponse: Codable {
93+
struct CountyResponse: Codable {
9494
var id: Int
9595
var name: String
9696
var code: String
97-
var limit: Int
97+
var numberOfPollingStations: Int
9898
var diaspora: Bool?
99+
var order: Int
99100
}
100101

101102
struct FormListResponse: Codable {
@@ -111,12 +112,16 @@ struct FormResponse: Codable {
111112
var code: String
112113
var version: Int
113114
var description: String
115+
var order: Int?
116+
var isDiasporaOnly: Bool?
114117

115118
enum CodingKeys: String, CodingKey {
116119
case id
117120
case code
118121
case version = "ver"
119122
case description
123+
case order
124+
case isDiasporaOnly = "diaspora"
120125
}
121126
}
122127

@@ -164,3 +169,13 @@ struct QuestionOptionResponse: Codable {
164169
}
165170
}
166171

172+
struct AppInformationResponse: Decodable {
173+
struct ResultResponse: Decodable {
174+
var version: String
175+
var releaseNotes: String
176+
}
177+
178+
var resultCount: Int
179+
var results: [ResultResponse]
180+
}
181+

MonitorizareVot/ApiURL.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ enum ApiURL {
2222
var uri = ""
2323
switch self {
2424
case .login: uri = "/v1/access/authorize"
25-
case .pollingStationList: uri = "/v1/polling-station"
25+
case .pollingStationList: uri = "/v1/county"
2626
case .pollingStation: uri = "/v1/polling-station"
2727
case .forms: uri = "/v1/form"
2828
case .form(let id): uri = "/v1/form/\(id)"
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//
2+
// AppLanguageManager.swift
3+
// MonitorizareVot
4+
//
5+
// Created by Cristi Habliuc on 20/09/2020.
6+
// Copyright © 2020 Code4Ro. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
class AppLanguageManager: NSObject {
12+
static let shared = AppLanguageManager()
13+
14+
private(set) var supportedLanguages: [String] = ["en"]
15+
16+
var selectedLanguage: String? {
17+
didSet {
18+
save()
19+
}
20+
}
21+
22+
private override init() {
23+
super.init()
24+
load()
25+
}
26+
27+
private func load() {
28+
if let languagesInline = Bundle.main.infoDictionary?["ALLOWED_LANGUAGES"] as? String {
29+
supportedLanguages = languagesInline.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
30+
} else {
31+
supportedLanguages = ["en"] // default to english
32+
}
33+
34+
if let savedLanguage = PreferencesManager.shared.languageLocale {
35+
selectedLanguage = savedLanguage
36+
} else {
37+
// select the current system language if it's in the list
38+
let systemLanguage = Locale.current.identifier
39+
if supportedLanguages.contains(systemLanguage) {
40+
selectedLanguage = systemLanguage
41+
// also save it for later
42+
save()
43+
}
44+
}
45+
46+
}
47+
48+
func languageName(forIdentifier identifier: String) -> String {
49+
return Locale(identifier: identifier)
50+
.localizedString(forLanguageCode: identifier)?.capitalized ?? identifier.capitalized
51+
}
52+
53+
func save() {
54+
guard let selectedLanguage = selectedLanguage else { return }
55+
PreferencesManager.shared.languageLocale = selectedLanguage
56+
PreferencesManager.shared.languageName = languageName(forIdentifier: selectedLanguage)
57+
}
58+
59+
}

MonitorizareVot/AppRouter.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,29 @@ class AppRouter: NSObject {
2828
} else {
2929
goToLogin()
3030
}
31+
32+
RemoteConfigManager.shared.afterLoad {
33+
self.checkForNewVersion()
34+
}
35+
}
36+
37+
func checkForNewVersion() {
38+
guard RemoteConfigManager.shared.value(of: .checkAppUpdateAvailable).boolValue == true else { return }
39+
AppUpdateManager.shared.checkForNewVersion { (isAvailable, response, error) in
40+
if let error = error {
41+
DebugLog("Can't check for new version: \(error.localizedDescription)")
42+
} else {
43+
if isAvailable,
44+
let response = response {
45+
DebugLog("New Version available: \(response.version)")
46+
let currentVersion = AppUpdateManager.shared.currentVersion
47+
let newVersion = response.version
48+
self.showNewVersionDialog(currentVersion: currentVersion, newVersion: newVersion)
49+
} else {
50+
DebugLog("Already on latest version: \(response?.version ?? "?")")
51+
}
52+
}
53+
}
3154
}
3255

3356
func goToLogin() {
@@ -103,4 +126,35 @@ class AppRouter: NSObject {
103126
let navigation = UINavigationController(rootViewController: controller)
104127
split.showDetailViewController(navigation, sender: nil)
105128
}
129+
130+
private func showNewVersionDialog(currentVersion: String, newVersion: String) {
131+
let alert = UIAlertController(title: "Title.NewVersion".localized,
132+
message: "AlertMessage_NewVersion".localized,
133+
preferredStyle: .alert)
134+
alert.addAction(UIAlertAction(title: "OK".localized,
135+
style: .default,
136+
handler:
137+
{ action in
138+
self.openAppUrl()
139+
if RemoteConfigManager.shared.value(of: .forceAppUpdate).boolValue == true {
140+
self.showNewVersionDialog(currentVersion: currentVersion, newVersion: newVersion)
141+
}
142+
}))
143+
window?.rootViewController?.present(alert, animated: true, completion: nil)
144+
}
145+
146+
func showAppMenu() {
147+
let menu = MenuViewController()
148+
let nav = MVNavigationViewController(rootViewController: menu)
149+
nav.shouldHideBackButtonTitle = true
150+
nav.modalPresentationStyle = .fullScreen
151+
window?.rootViewController?.present(nav, animated: true, completion: nil)
152+
}
153+
154+
private func openAppUrl() {
155+
let url = AppUpdateManager.shared.applicationURL
156+
if UIApplication.shared.canOpenURL(url) {
157+
UIApplication.shared.openURL(url)
158+
}
159+
}
106160
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//
2+
// AppUpdateManager.swift
3+
// MonitorizareVot
4+
//
5+
// Created by Cristi Habliuc on 14/12/2019.
6+
// Copyright © 2019 Code4Ro. All rights reserved.
7+
//
8+
9+
import UIKit
10+
import Alamofire
11+
12+
/// This manager's sole purpose is to check if there's a new version of the app available on the app store
13+
/// You can adjust its functionality via RemoteConfig or completely disable it using the local xcconfig setting
14+
/// `DISABLE_UPDATE_CHECK` (set it to true)
15+
class AppUpdateManager: NSObject {
16+
static let shared = AppUpdateManager()
17+
18+
var currentVersion: String {
19+
guard let infoDict = Bundle.main.infoDictionary,
20+
let currentVer = infoDict["CFBundleShortVersionString"] as? String else {
21+
fatalError("No current ver found, this shouldn't happen")
22+
}
23+
return currentVer
24+
}
25+
26+
var applicationURL: URL {
27+
return URL(string: "https://apps.apple.com/app/id1183063109")!
28+
}
29+
30+
var isUpdateCheckCompletelyDisabled: Bool {
31+
if let infoDict = Bundle.main.infoDictionary,
32+
let disableFlag = infoDict["DISABLE_UPDATE_CHECK"] as? String,
33+
disableFlag == "true" {
34+
return true
35+
}
36+
return false
37+
}
38+
39+
private override init() {}
40+
41+
/// It's okay to have the error messages not localized, since they'll never reach the user. Debugging purposes only
42+
enum UpdateError: Error {
43+
case noResults
44+
case unknown(reason: String)
45+
46+
var localizedDescription: String {
47+
switch self {
48+
case .noResults: return "No information on latest version"
49+
case .unknown(let reason): return reason
50+
}
51+
}
52+
}
53+
54+
typealias NewVersionCallback = (_ isNewVersionAvailable: Bool, _ result: AppInformationResponse.ResultResponse?, _ error: UpdateError?) -> Void
55+
56+
func checkForNewVersion(then callback: @escaping NewVersionCallback) {
57+
guard !isUpdateCheckCompletelyDisabled else {
58+
DebugLog("Update check disabled via DISABLE_UPDATE_CHECK flag. Ignoring...")
59+
return
60+
}
61+
62+
guard let infoDict = Bundle.main.infoDictionary,
63+
let bundleId = infoDict["CFBundleIdentifier"] as? String else {
64+
fatalError("No app id found, this shouldn't happen")
65+
}
66+
67+
let currentVer = currentVersion
68+
let urlString = "https://itunes.apple.com/lookup?bundleId=" + bundleId
69+
guard let url = URL(string: urlString) else {
70+
fatalError("Invalid check url: \(urlString)")
71+
}
72+
73+
Alamofire
74+
.request(url, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: [:])
75+
.response { response in
76+
if response.response?.statusCode == 200,
77+
let data = response.data {
78+
do {
79+
let response = try JSONDecoder().decode(AppInformationResponse.self, from: data)
80+
if let result = response.results.first {
81+
let isNewVersionAvailable = result.version.compare(currentVer, options: .numeric, range: nil, locale: nil) == .orderedDescending
82+
callback(isNewVersionAvailable, result, nil)
83+
return
84+
}
85+
} catch {
86+
callback(false, nil, .unknown(reason: "Couldn't decode response"))
87+
}
88+
}
89+
callback(false, nil, .unknown(reason: "Unknown reason"))
90+
}
91+
}
92+
}

MonitorizareVot/ApplicationData.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class ApplicationData: NSObject {
2121
// for diaspora we might have different forms, so first check if the user is in diaspora or not
2222
var isCountyDiaspora = false
2323
if let countyCode = PreferencesManager.shared.county,
24-
let stationCounty = LocalStorage.shared.getPollingStationResponse(withCode: countyCode) {
24+
let stationCounty = LocalStorage.shared.getCounty(withCode: countyCode) {
2525
isCountyDiaspora = stationCounty.diaspora ?? false
2626
}
2727

0 commit comments

Comments
 (0)