Skip to content

Commit

Permalink
Merge pull request #205 from code4romania/develop
Browse files Browse the repository at this point in the history
Release for local elections 2020
  • Loading branch information
CristiHabliuc authored Sep 23, 2020
2 parents b7c1255 + b08973e commit 1b460ab
Show file tree
Hide file tree
Showing 99 changed files with 1,952 additions and 305 deletions.
8 changes: 8 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
# they will be requested for review when someone
# opens a pull request.
* @CristiHabliuc

# More details on creating a codeowners file:
# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners
11 changes: 4 additions & 7 deletions Config/CustomConfiguration.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,10 @@
PRODUCT_BUNDLE_IDENTIFIER = com.mycompany.MyOtherAppName
DEVELOPMENT_TEAM = ""
CODE_SIGN_STYLE = Manual
API_URL = https:/$()/mv-mobile-prod.azurewebsites.net/api // production
TEST_PHONE =
TEST_PIN =
API_URL = https:/$()/app-vmon-api-dev.azurewebsites.net/api
TEST_PHONE = 0722222222
TEST_PIN = 1234
ALLOWED_LANGUAGES = ro,en // you can override this in your project, but make sure you have correct localization
SUPPORT_PHONE = 0800080200
GUIDE_URL = https:/$()/fiecarevot.ro/wp-content/uploads/2019/11/Manual-prezidentiale.pdf

//API_URL = https:/$()/mv-mobile-test.azurewebsites.net/api // dev - use this in your LocalConfiguration.xcconfig
DISABLE_UPDATE_CHECK = false // override this to force disable the update check

#include? "LocalConfiguration.xcconfig"
79 changes: 62 additions & 17 deletions MonitorizareVot.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

22 changes: 18 additions & 4 deletions MonitorizareVot/APIManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ protocol APIManagerType: NSObject {
then callback: @escaping (APIError?) -> Void)
func sendPushToken(withToken token: String,
then callback: @escaping (APIError?) -> Void)
func fetchPollingStations(then callback: @escaping ([PollingStationResponse]?, APIError?) -> Void)
func fetchCounties(then callback: @escaping ([CountyResponse]?, APIError?) -> Void)
func fetchForms(diaspora: Bool, then callback: @escaping ([FormResponse]?, APIError?) -> Void)
func fetchForm(withId formId: Int,
then callback: @escaping ([FormSectionResponse]?, APIError?) -> Void)
Expand Down Expand Up @@ -55,13 +55,18 @@ class APIManager: NSObject, APIManagerType {
/// Use this to format dates to and from the API
lazy var apiDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
formatter.timeZone = TimeZone(abbreviation: "UTC")
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}()

func login(withPhone phone: String, pin: String, then callback: @escaping (APIError?) -> Void) {
if let errorMessage = checkConnectionError() {
callback(.generic(reason: errorMessage))
return
}

let url = ApiURL.login.url()
let udid = AccountManager.shared.udid
let request = LoginRequest(user: phone, password: pin, uniqueId: udid)
Expand Down Expand Up @@ -113,7 +118,7 @@ class APIManager: NSObject, APIManagerType {
}
}

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

Expand All @@ -124,7 +129,7 @@ class APIManager: NSObject, APIManagerType {
if statusCode == 200,
let data = response.data {
do {
let stations = try JSONDecoder().decode([PollingStationResponse].self, from: data)
let stations = try JSONDecoder().decode([CountyResponse].self, from: data)
callback(stations, nil)
} catch {
callback(nil, .incorrectFormat(reason: error.localizedDescription))
Expand Down Expand Up @@ -196,6 +201,11 @@ class APIManager: NSObject, APIManagerType {
}

func upload(pollingStation: UpdatePollingStationRequest, then callback: @escaping (APIError?) -> Void) {
if let errorMessage = checkConnectionError() {
callback(.generic(reason: errorMessage))
return
}

let url = ApiURL.pollingStation.url()
let auth = authorizationHeaders()
let headers = requestHeaders(withAuthHeaders: auth)
Expand Down Expand Up @@ -275,6 +285,10 @@ class APIManager: NSObject, APIManagerType {
}
}

private func checkConnectionError() -> String? {
ReachabilityManager.shared.isReachable ? nil : "Error.InternetConnection".localized
}

}

// MARK: - Helpers
Expand Down
21 changes: 18 additions & 3 deletions MonitorizareVot/ApiModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct UpdatePollingStationRequest: Codable {
var id: Int
var countyCode: String
var isUrbanArea: Bool
var leaveTime: String
var leaveTime: String?
var arrivalTime: String
var isPresidentFemale: Bool

Expand Down Expand Up @@ -90,12 +90,13 @@ struct LoginResponse: Codable {
}
}

struct PollingStationResponse: Codable {
struct CountyResponse: Codable {
var id: Int
var name: String
var code: String
var limit: Int
var numberOfPollingStations: Int
var diaspora: Bool?
var order: Int
}

struct FormListResponse: Codable {
Expand All @@ -111,12 +112,16 @@ struct FormResponse: Codable {
var code: String
var version: Int
var description: String
var order: Int?
var isDiasporaOnly: Bool?

enum CodingKeys: String, CodingKey {
case id
case code
case version = "ver"
case description
case order
case isDiasporaOnly = "diaspora"
}
}

Expand Down Expand Up @@ -164,3 +169,13 @@ struct QuestionOptionResponse: Codable {
}
}

struct AppInformationResponse: Decodable {
struct ResultResponse: Decodable {
var version: String
var releaseNotes: String
}

var resultCount: Int
var results: [ResultResponse]
}

2 changes: 1 addition & 1 deletion MonitorizareVot/ApiURL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ enum ApiURL {
var uri = ""
switch self {
case .login: uri = "/v1/access/authorize"
case .pollingStationList: uri = "/v1/polling-station"
case .pollingStationList: uri = "/v1/county"
case .pollingStation: uri = "/v1/polling-station"
case .forms: uri = "/v1/form"
case .form(let id): uri = "/v1/form/\(id)"
Expand Down
59 changes: 59 additions & 0 deletions MonitorizareVot/AppLanguageManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// AppLanguageManager.swift
// MonitorizareVot
//
// Created by Cristi Habliuc on 20/09/2020.
// Copyright © 2020 Code4Ro. All rights reserved.
//

import UIKit

class AppLanguageManager: NSObject {
static let shared = AppLanguageManager()

private(set) var supportedLanguages: [String] = ["en"]

var selectedLanguage: String? {
didSet {
save()
}
}

private override init() {
super.init()
load()
}

private func load() {
if let languagesInline = Bundle.main.infoDictionary?["ALLOWED_LANGUAGES"] as? String {
supportedLanguages = languagesInline.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
} else {
supportedLanguages = ["en"] // default to english
}

if let savedLanguage = PreferencesManager.shared.languageLocale {
selectedLanguage = savedLanguage
} else {
// select the current system language if it's in the list
let systemLanguage = Locale.current.identifier
if supportedLanguages.contains(systemLanguage) {
selectedLanguage = systemLanguage
// also save it for later
save()
}
}

}

func languageName(forIdentifier identifier: String) -> String {
return Locale(identifier: identifier)
.localizedString(forLanguageCode: identifier)?.capitalized ?? identifier.capitalized
}

func save() {
guard let selectedLanguage = selectedLanguage else { return }
PreferencesManager.shared.languageLocale = selectedLanguage
PreferencesManager.shared.languageName = languageName(forIdentifier: selectedLanguage)
}

}
54 changes: 54 additions & 0 deletions MonitorizareVot/AppRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,29 @@ class AppRouter: NSObject {
} else {
goToLogin()
}

RemoteConfigManager.shared.afterLoad {
self.checkForNewVersion()
}
}

func checkForNewVersion() {
guard RemoteConfigManager.shared.value(of: .checkAppUpdateAvailable).boolValue == true else { return }
AppUpdateManager.shared.checkForNewVersion { (isAvailable, response, error) in
if let error = error {
DebugLog("Can't check for new version: \(error.localizedDescription)")
} else {
if isAvailable,
let response = response {
DebugLog("New Version available: \(response.version)")
let currentVersion = AppUpdateManager.shared.currentVersion
let newVersion = response.version
self.showNewVersionDialog(currentVersion: currentVersion, newVersion: newVersion)
} else {
DebugLog("Already on latest version: \(response?.version ?? "?")")
}
}
}
}

func goToLogin() {
Expand Down Expand Up @@ -103,4 +126,35 @@ class AppRouter: NSObject {
let navigation = UINavigationController(rootViewController: controller)
split.showDetailViewController(navigation, sender: nil)
}

private func showNewVersionDialog(currentVersion: String, newVersion: String) {
let alert = UIAlertController(title: "Title.NewVersion".localized,
message: "AlertMessage_NewVersion".localized,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK".localized,
style: .default,
handler:
{ action in
self.openAppUrl()
if RemoteConfigManager.shared.value(of: .forceAppUpdate).boolValue == true {
self.showNewVersionDialog(currentVersion: currentVersion, newVersion: newVersion)
}
}))
window?.rootViewController?.present(alert, animated: true, completion: nil)
}

func showAppMenu() {
let menu = MenuViewController()
let nav = MVNavigationViewController(rootViewController: menu)
nav.shouldHideBackButtonTitle = true
nav.modalPresentationStyle = .fullScreen
window?.rootViewController?.present(nav, animated: true, completion: nil)
}

private func openAppUrl() {
let url = AppUpdateManager.shared.applicationURL
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.openURL(url)
}
}
}
92 changes: 92 additions & 0 deletions MonitorizareVot/AppUpdateManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//
// AppUpdateManager.swift
// MonitorizareVot
//
// Created by Cristi Habliuc on 14/12/2019.
// Copyright © 2019 Code4Ro. All rights reserved.
//

import UIKit
import Alamofire

/// This manager's sole purpose is to check if there's a new version of the app available on the app store
/// You can adjust its functionality via RemoteConfig or completely disable it using the local xcconfig setting
/// `DISABLE_UPDATE_CHECK` (set it to true)
class AppUpdateManager: NSObject {
static let shared = AppUpdateManager()

var currentVersion: String {
guard let infoDict = Bundle.main.infoDictionary,
let currentVer = infoDict["CFBundleShortVersionString"] as? String else {
fatalError("No current ver found, this shouldn't happen")
}
return currentVer
}

var applicationURL: URL {
return URL(string: "https://apps.apple.com/app/id1183063109")!
}

var isUpdateCheckCompletelyDisabled: Bool {
if let infoDict = Bundle.main.infoDictionary,
let disableFlag = infoDict["DISABLE_UPDATE_CHECK"] as? String,
disableFlag == "true" {
return true
}
return false
}

private override init() {}

/// It's okay to have the error messages not localized, since they'll never reach the user. Debugging purposes only
enum UpdateError: Error {
case noResults
case unknown(reason: String)

var localizedDescription: String {
switch self {
case .noResults: return "No information on latest version"
case .unknown(let reason): return reason
}
}
}

typealias NewVersionCallback = (_ isNewVersionAvailable: Bool, _ result: AppInformationResponse.ResultResponse?, _ error: UpdateError?) -> Void

func checkForNewVersion(then callback: @escaping NewVersionCallback) {
guard !isUpdateCheckCompletelyDisabled else {
DebugLog("Update check disabled via DISABLE_UPDATE_CHECK flag. Ignoring...")
return
}

guard let infoDict = Bundle.main.infoDictionary,
let bundleId = infoDict["CFBundleIdentifier"] as? String else {
fatalError("No app id found, this shouldn't happen")
}

let currentVer = currentVersion
let urlString = "https://itunes.apple.com/lookup?bundleId=" + bundleId
guard let url = URL(string: urlString) else {
fatalError("Invalid check url: \(urlString)")
}

Alamofire
.request(url, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: [:])
.response { response in
if response.response?.statusCode == 200,
let data = response.data {
do {
let response = try JSONDecoder().decode(AppInformationResponse.self, from: data)
if let result = response.results.first {
let isNewVersionAvailable = result.version.compare(currentVer, options: .numeric, range: nil, locale: nil) == .orderedDescending
callback(isNewVersionAvailable, result, nil)
return
}
} catch {
callback(false, nil, .unknown(reason: "Couldn't decode response"))
}
}
callback(false, nil, .unknown(reason: "Unknown reason"))
}
}
}
2 changes: 1 addition & 1 deletion MonitorizareVot/ApplicationData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class ApplicationData: NSObject {
// for diaspora we might have different forms, so first check if the user is in diaspora or not
var isCountyDiaspora = false
if let countyCode = PreferencesManager.shared.county,
let stationCounty = LocalStorage.shared.getPollingStationResponse(withCode: countyCode) {
let stationCounty = LocalStorage.shared.getCounty(withCode: countyCode) {
isCountyDiaspora = stationCounty.diaspora ?? false
}

Expand Down
Loading

0 comments on commit 1b460ab

Please sign in to comment.