Skip to content

Commit

Permalink
Feature/App update (#136)
Browse files Browse the repository at this point in the history
* add update version notifications

* App update version views

* fix logout logic

* update tests

* Update UpdateRequiredView.swift

* remove debug prints

* switch the localization to default

* minor fixes after review

* add executable to RequestInterceptor

* code style review

* add prefetchDataForOffline to MainScreenViewModel

* remove appUpdateFeatureEnabled flag

* disable version button when is up to date

* fix merge conflicts

---------

Co-authored-by: stepanokdev <[email protected]>
  • Loading branch information
IvanStepanok and Stepanokdev authored Nov 3, 2023
1 parent 5bd9203 commit 55aa135
Show file tree
Hide file tree
Showing 42 changed files with 833 additions and 226 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import Foundation
import Core

//sourcery: AutoMockable
public protocol AuthorizationRouter: BaseRouter {}
public protocol AuthorizationRouter: BaseRouter {
func showUpdateRequiredView(showAccountLink: Bool)
}

// Mark - For testing and SwiftUI preview
#if DEBUG
public class AuthorizationRouterMock: BaseRouterMock, AuthorizationRouter {

public override init() {}

public func showUpdateRequiredView(showAccountLink: Bool) {}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ struct SignInView_Previews: PreviewProvider {
static var previews: some View {
let vm = SignInViewModel(
interactor: AuthInteractor.mock,
router: AuthorizationRouterMock(),
router: AuthorizationRouterMock(),
config: ConfigMock(),
analytics: AuthorizationAnalyticsMock(),
validator: Validator()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,21 @@ public class SignInViewModel: ObservableObject {
}

let router: AuthorizationRouter

private let config: Config
private let interactor: AuthInteractorProtocol
private let analytics: AuthorizationAnalytics
private let validator: Validator

public init(
interactor: AuthInteractorProtocol,
router: AuthorizationRouter,
config: Config,
analytics: AuthorizationAnalytics,
validator: Validator
) {
self.interactor = interactor
self.router = router
self.config = config
self.analytics = analytics
self.validator = validator
}
Expand All @@ -67,8 +69,10 @@ public class SignInViewModel: ObservableObject {
router.showMainOrWhatsNewScreen()
} catch let error {
isShowProgress = false
if let validationError = error.validationError,
let value = validationError.data?["error_description"] as? String {
if error.isUpdateRequeiredError {
router.showUpdateRequiredView(showAccountLink: false)
} else if let validationError = error.validationError,
let value = validationError.data?["error_description"] as? String {
errorMessage = value
} else if case APIError.invalidGrant = error {
errorMessage = CoreLocalization.Error.invalidCredentials
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ public class SignUpViewModel: ObservableObject {
isShowProgress = false
if error.isInternetError {
errorMessage = CoreLocalization.Error.slowOrNoInternetConnection
} else if error.isUpdateRequeiredError {
router.showUpdateRequiredView(showAccountLink: false)
} else {
errorMessage = CoreLocalization.Error.unknownError
}
Expand Down
18 changes: 18 additions & 0 deletions Authorization/AuthorizationTests/AuthorizationMock.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,12 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock {



open func showUpdateRequiredView(showAccountLink: Bool) {
addInvocation(.m_showUpdateRequiredView__showAccountLink_showAccountLink(Parameter<Bool>.value(`showAccountLink`)))
let perform = methodPerformValue(.m_showUpdateRequiredView__showAccountLink_showAccountLink(Parameter<Bool>.value(`showAccountLink`))) as? (Bool) -> Void
perform?(`showAccountLink`)
}

open func backToRoot(animated: Bool) {
addInvocation(.m_backToRoot__animated_animated(Parameter<Bool>.value(`animated`)))
let perform = methodPerformValue(.m_backToRoot__animated_animated(Parameter<Bool>.value(`animated`))) as? (Bool) -> Void
Expand Down Expand Up @@ -811,6 +817,7 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock {


fileprivate enum MethodType {
case m_showUpdateRequiredView__showAccountLink_showAccountLink(Parameter<Bool>)
case m_backToRoot__animated_animated(Parameter<Bool>)
case m_back__animated_animated(Parameter<Bool>)
case m_backWithFade
Expand All @@ -827,6 +834,11 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock {

static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult {
switch (lhs, rhs) {
case (.m_showUpdateRequiredView__showAccountLink_showAccountLink(let lhsShowaccountlink), .m_showUpdateRequiredView__showAccountLink_showAccountLink(let rhsShowaccountlink)):
var results: [Matcher.ParameterComparisonResult] = []
results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsShowaccountlink, rhs: rhsShowaccountlink, with: matcher), lhsShowaccountlink, rhsShowaccountlink, "showAccountLink"))
return Matcher.ComparisonResult(results)

case (.m_backToRoot__animated_animated(let lhsAnimated), .m_backToRoot__animated_animated(let rhsAnimated)):
var results: [Matcher.ParameterComparisonResult] = []
results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAnimated, rhs: rhsAnimated, with: matcher), lhsAnimated, rhsAnimated, "animated"))
Expand Down Expand Up @@ -896,6 +908,7 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock {

func intValue() -> Int {
switch self {
case let .m_showUpdateRequiredView__showAccountLink_showAccountLink(p0): return p0.intValue
case let .m_backToRoot__animated_animated(p0): return p0.intValue
case let .m_back__animated_animated(p0): return p0.intValue
case .m_backWithFade: return 0
Expand All @@ -913,6 +926,7 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock {
}
func assertionName() -> String {
switch self {
case .m_showUpdateRequiredView__showAccountLink_showAccountLink: return ".showUpdateRequiredView(showAccountLink:)"
case .m_backToRoot__animated_animated: return ".backToRoot(animated:)"
case .m_back__animated_animated: return ".back(animated:)"
case .m_backWithFade: return ".backWithFade()"
Expand Down Expand Up @@ -944,6 +958,7 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock {
public struct Verify {
fileprivate var method: MethodType

public static func showUpdateRequiredView(showAccountLink: Parameter<Bool>) -> Verify { return Verify(method: .m_showUpdateRequiredView__showAccountLink_showAccountLink(`showAccountLink`))}
public static func backToRoot(animated: Parameter<Bool>) -> Verify { return Verify(method: .m_backToRoot__animated_animated(`animated`))}
public static func back(animated: Parameter<Bool>) -> Verify { return Verify(method: .m_back__animated_animated(`animated`))}
public static func backWithFade() -> Verify { return Verify(method: .m_backWithFade)}
Expand All @@ -963,6 +978,9 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock {
fileprivate var method: MethodType
var performs: Any

public static func showUpdateRequiredView(showAccountLink: Parameter<Bool>, perform: @escaping (Bool) -> Void) -> Perform {
return Perform(method: .m_showUpdateRequiredView__showAccountLink_showAccountLink(`showAccountLink`), performs: perform)
}
public static func backToRoot(animated: Parameter<Bool>, perform: @escaping (Bool) -> Void) -> Perform {
return Perform(method: .m_backToRoot__animated_animated(`animated`), performs: perform)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ final class SignInViewModelTests: XCTestCase {
let analytics = AuthorizationAnalyticsMock()
let viewModel = SignInViewModel(
interactor: interactor,
router: router,
router: router,
config: ConfigMock(),
analytics: analytics,
validator: validator
)
Expand All @@ -51,6 +52,7 @@ final class SignInViewModelTests: XCTestCase {
let viewModel = SignInViewModel(
interactor: interactor,
router: router,
config: ConfigMock(),
analytics: analytics,
validator: validator
)
Expand All @@ -71,6 +73,7 @@ final class SignInViewModelTests: XCTestCase {
let viewModel = SignInViewModel(
interactor: interactor,
router: router,
config: ConfigMock(),
analytics: analytics,
validator: validator
)
Expand All @@ -96,6 +99,7 @@ final class SignInViewModelTests: XCTestCase {
let viewModel = SignInViewModel(
interactor: interactor,
router: router,
config: ConfigMock(),
analytics: analytics,
validator: validator
)
Expand Down Expand Up @@ -123,6 +127,7 @@ final class SignInViewModelTests: XCTestCase {
let viewModel = SignInViewModel(
interactor: interactor,
router: router,
config: ConfigMock(),
analytics: analytics,
validator: validator
)
Expand All @@ -146,6 +151,7 @@ final class SignInViewModelTests: XCTestCase {
let viewModel = SignInViewModel(
interactor: interactor,
router: router,
config: ConfigMock(),
analytics: analytics,
validator: validator
)
Expand All @@ -169,6 +175,7 @@ final class SignInViewModelTests: XCTestCase {
let viewModel = SignInViewModel(
interactor: interactor,
router: router,
config: ConfigMock(),
analytics: analytics,
validator: validator
)
Expand All @@ -194,6 +201,7 @@ final class SignInViewModelTests: XCTestCase {
let viewModel = SignInViewModel(
interactor: interactor,
router: router,
config: ConfigMock(),
analytics: analytics,
validator: validator
)
Expand All @@ -211,6 +219,7 @@ final class SignInViewModelTests: XCTestCase {
let viewModel = SignInViewModel(
interactor: interactor,
router: router,
config: ConfigMock(),
analytics: analytics,
validator: validator
)
Expand Down
12 changes: 12 additions & 0 deletions Core/Core/Assets.xcassets/warning_filled.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "warning_filled.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions Core/Core/Configuration/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public class Config {

public let feedbackEmail = "[email protected]"

private let appStoreId = "0000000000"
public var appStoreLink: String {
"itms-apps://itunes.apple.com/app/id\(appStoreId)?mt=8"
}
public let whatsNewEnabled: Bool = false

public init(baseURL: String, oAuthClientId: String) {
Expand Down
12 changes: 12 additions & 0 deletions Core/Core/Domain/Model/UserProfile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,16 @@ public struct UserProfile: Hashable {
self.shortBiography = shortBiography
self.isFullProfile = isFullProfile
}

public init() {
self.avatarUrl = ""
self.name = ""
self.username = ""
self.dateJoined = Date()
self.yearOfBirth = 0
self.country = ""
self.spokenLanguage = ""
self.shortBiography = ""
self.isFullProfile = true
}
}
3 changes: 3 additions & 0 deletions Core/Core/Extensions/Notification.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ import Foundation
public extension Notification.Name {
static let onCourseEnrolled = Notification.Name("onCourseEnrolled")
static let onTokenRefreshFailed = Notification.Name("onTokenRefreshFailed")
static let onActualVersionReceived = Notification.Name("onActualVersionReceived")
static let onAppUpgradeAccountSettingsTapped = Notification.Name("onAppUpgradeAccountSettingsTapped")
static let onNewVersionAvaliable = Notification.Name("onNewVersionAvaliable")
}
16 changes: 14 additions & 2 deletions Core/Core/Network/API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,25 @@ public final class API {
if !route.path.isEmpty {
url = url.appendingPathComponent(route.path)
}
return try await session.request(

let result = session.request(
url,
method: route.httpMethod,
parameters: parameters,
encoding: encoding,
headers: route.headers
).validateResponse().serializingData().value
).validateResponse().serializingData()

let latestVersion = await result.response.response?.headers["EDX-APP-LATEST-VERSION"]

if await result.response.response?.statusCode != 426 {
if let latestVersion = latestVersion {
NotificationCenter.default.post(name: .onActualVersionReceived, object: latestVersion)
}
}

return try await result.value

}

private func callCookies(
Expand Down
4 changes: 4 additions & 0 deletions Core/Core/Network/Alamofire+Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
import Alamofire

public extension Error {
var isUpdateRequeiredError: Bool {
self.asAFError?.responseCode == 426
}

var isInternetError: Bool {
guard let afError = self.asAFError,
let urlError = afError.underlyingError as? URLError else {
Expand Down
17 changes: 17 additions & 0 deletions Core/Core/Network/RequestInterceptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ final public class RequestInterceptor: Alamofire.RequestInterceptor {
urlRequest.setValue("\(config.tokenType.rawValue) \(token)", forHTTPHeaderField: "Authorization")
}

let userAgent: String = {
if let info = Bundle.main.infoDictionary {
let executable: AnyObject = info[kCFBundleExecutableKey as String] as AnyObject? ?? "Unknown" as AnyObject
let bundle: AnyObject = info[kCFBundleIdentifierKey as String] as AnyObject? ?? "Unknown" as AnyObject
let version: AnyObject = info["CFBundleShortVersionString"] as AnyObject? ?? "Unknown" as AnyObject
let os: AnyObject = ProcessInfo.processInfo.operatingSystemVersionString as AnyObject
var mutableUserAgent = NSMutableString(string: "\(executable)/\(bundle) (\(version); OS \(os))") as CFMutableString
let transform = NSString(string: "Any-Latin; Latin-ASCII; [:^ASCII:] Remove") as CFString
if CFStringTransform(mutableUserAgent, nil, transform, false) == true {
return mutableUserAgent as String
}
}
return "Alamofire"
}()

urlRequest.setValue(userAgent, forHTTPHeaderField: "User-Agent")

completion(.success(urlRequest))
}

Expand Down
1 change: 1 addition & 0 deletions Core/Core/SwiftGen/Assets.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ public enum CoreAssets {
public static let noCourseImage = ImageAsset(name: "noCourseImage")
public static let notAvaliable = ImageAsset(name: "notAvaliable")
public static let playVideo = ImageAsset(name: "playVideo")
public static let warningFilled = ImageAsset(name: "warning_filled")
}
// swiftlint:enable identifier_name line_length nesting type_body_length type_name

Expand Down
Loading

0 comments on commit 55aa135

Please sign in to comment.