Skip to content

Commit

Permalink
Create AuthenticatedURLSession and refactor code
Browse files Browse the repository at this point in the history
  • Loading branch information
banghuazhao committed Nov 8, 2024
1 parent 099c8e4 commit 857f41c
Show file tree
Hide file tree
Showing 17 changed files with 172 additions and 109 deletions.
16 changes: 8 additions & 8 deletions AdRevenueWatch/Dependency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,31 @@ enum Dependency {
static var googleAuthUseCase: some GoogleAuthUseCaseProtocol {
GoogleAuthUseCase(
googleAuthRepository: GoogleAuthRepository.newRepo,
accessTokenRepository: KeychainAccessTokenRepository()
accessTokenProvider: KeychainAccessTokenProvider()
)
}

static var adMobAccountUseCase: some AdMobAccountUseCaseProtocol {
AdMobAccountUseCase(
adMobAccountRepository: AdMobAccountRepository.newRepo,
accessTokenRepository: KeychainAccessTokenRepository()
adMobAccountRepository: AdMobAccountRepository.newRepo
)
}

static var adMobReportUseCase: some AdMobReportUseCaseProtocol {
AdMobReportUseCase(
adMobReportRepository: AdMobReportRepository.newRepo,
accessTokenRepository: KeychainAccessTokenRepository()
adMobReportRepository: AdMobReportRepository.newRepo
)
}

static var accessTokenUseCase: some AccessTokenUseCaseProtocol {
AccessTokenUseCase(
repository: KeychainAccessTokenRepository()
accessTokenProvider: KeychainAccessTokenProvider()
)
}

static let sessionManager: some SessionManagerProtocol = SessionManager(accessTokenUseCase: accessTokenUseCase)
static let sessionManager: some SessionManagerProtocol = SessionManager(
accessTokenUseCase: accessTokenUseCase
)

static let appViewModel = AppViewModel(sessionManager: sessionManager)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
import Foundation

public protocol AdMobAccountRepositoryProtocol {
func fetchAccounts(accessToken: String) async throws -> [AdMobAccountEntity]
func fetchAccounts() async throws -> [AdMobAccountEntity]
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,12 @@ public protocol AdMobAccountUseCaseProtocol {

public struct AdMobAccountUseCase: AdMobAccountUseCaseProtocol {
private let adMobAccountRepository: any AdMobAccountRepositoryProtocol
private let accessTokenRepository: any AccessTokenRepositoryProtocol
public init(
adMobAccountRepository: some AdMobAccountRepositoryProtocol,
accessTokenRepository: some AccessTokenRepositoryProtocol
adMobAccountRepository: some AdMobAccountRepositoryProtocol
) {
self.adMobAccountRepository = adMobAccountRepository
self.accessTokenRepository = accessTokenRepository
}
public func fetchAccounts() async throws -> [AdMobAccountEntity] {
let accessToken = try accessTokenRepository.getAccessToken()
return try await adMobAccountRepository.fetchAccounts(accessToken: accessToken)
try await adMobAccountRepository.fetchAccounts()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import UIKit

public protocol AdMobReportRepositoryProtocol {
func fetchReport(
accessToken: String,
accountID: String,
reportRequest: AdMobReportRequestEntity
) async throws -> AdMobReportEntity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,17 @@ public protocol AdMobReportUseCaseProtocol {

public struct AdMobReportUseCase: AdMobReportUseCaseProtocol {
private let adMobReportRepository: any AdMobReportRepositoryProtocol
private let accessTokenRepository: any AccessTokenRepositoryProtocol

public init(
adMobReportRepository: some AdMobReportRepositoryProtocol,
accessTokenRepository: some AccessTokenRepositoryProtocol
adMobReportRepository: some AdMobReportRepositoryProtocol
) {
self.adMobReportRepository = adMobReportRepository
self.accessTokenRepository = accessTokenRepository
}

public func fetchReport(
accountID: String,
reportRequest: AdMobReportRequestEntity
) async throws -> AdMobReportEntity {
let accessToken = try accessTokenRepository.getAccessToken()
return try await adMobReportRepository.fetchReport(accessToken: accessToken, accountID: accountID, reportRequest: reportRequest)
return try await adMobReportRepository.fetchReport(accountID: accountID, reportRequest: reportRequest)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,20 @@ public protocol GoogleAuthUseCaseProtocol {

public struct GoogleAuthUseCase: GoogleAuthUseCaseProtocol {
private let googleAuthRepository: any GoogleAuthRepositoryProtocol
private let accessTokenRepository: any AccessTokenRepositoryProtocol
private let accessTokenProvider: any AccessTokenProvider

public init(
googleAuthRepository: some GoogleAuthRepositoryProtocol,
accessTokenRepository: some AccessTokenRepositoryProtocol
accessTokenProvider: some AccessTokenProvider
) {
self.googleAuthRepository = googleAuthRepository
self.accessTokenRepository = accessTokenRepository
self.accessTokenProvider = accessTokenProvider
}

@MainActor
public func signIn(presentingViewController: UIViewController) async throws {
let googleUserEntity = try await googleAuthRepository.signIn(presentingViewController: presentingViewController)
try accessTokenRepository.saveAccessToken(googleUserEntity.accessToken)
try accessTokenProvider.saveAccessToken(googleUserEntity.accessToken)
}

public func hasPreviousSignIn() -> Bool {
Expand All @@ -37,11 +37,11 @@ public struct GoogleAuthUseCase: GoogleAuthUseCaseProtocol {

public func restorePreviousSignIn() async throws {
let googleUserEntity = try await googleAuthRepository.restorePreviousSignIn()
try accessTokenRepository.saveAccessToken(googleUserEntity.accessToken)
try accessTokenProvider.saveAccessToken(googleUserEntity.accessToken)
}

public func signOut() async {
await googleAuthRepository.signOut()
try? accessTokenRepository.deleteAccessToken()
try? accessTokenProvider.deleteAccessToken()
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
//
// Created by Banghua Zhao on 03/10/2024
// Created by Banghua Zhao on 08/11/2024
// Copyright Apps Bay Limited. All rights reserved.
//


import Foundation

public protocol AccessTokenRepositoryProtocol {
public protocol AccessTokenProvider {
func saveAccessToken(_ token: String) throws
func getAccessToken() throws -> String
func deleteAccessToken() throws
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// Created by Banghua Zhao on 03/10/2024
// Copyright Apps Bay Limited. All rights reserved.
//

import Foundation

public protocol AccessTokenUseCaseProtocol {
var hasAccessToken: Bool { get }
}

public struct AccessTokenUseCase: AccessTokenUseCaseProtocol {
private let accessTokenProvider: AccessTokenProvider

public init(accessTokenProvider: AccessTokenProvider) {
self.accessTokenProvider = accessTokenProvider
}

public var hasAccessToken: Bool {
let accessToken = try? accessTokenProvider.getAccessToken()
return accessToken != nil
}
}
45 changes: 45 additions & 0 deletions AdRevenueWatch/Domain/Sources/Domain/Network/NetworkError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// Created by Banghua Zhao on 08/11/2024
// Copyright Apps Bay Limited. All rights reserved.
//


import Foundation

public enum NetworkError: Error {
case noAccessToken
case accessTokenExpired // Access token expired
case forbidden // User does not have permission
case notFound // Resource not found (404)
case serverError // Server error (5xx)
case invalidResponse // Invalid or unexpected response
case decodingError // JSON decoding failed
case networkFailure // General network failure (e.g., no internet)
case timeout // Request timed out
case unknown // Unknown error

// Provide a message for each error case
public var message: String {
switch self {
case .noAccessToken: return "No access token"
case .accessTokenExpired:
return "Access token expired."
case .forbidden:
return "You don't have permission to access this resource."
case .notFound:
return "The requested resource was not found."
case .serverError:
return "The server encountered an error. Please try again later."
case .invalidResponse:
return "Received an invalid response from the server."
case .decodingError:
return "Failed to decode the data. Please try again."
case .networkFailure:
return "Network connection lost. Please check your internet connection."
case .timeout:
return "The request timed out. Please try again."
case .unknown:
return "An unknown error occurred. Please try again."
}
}
}
11 changes: 11 additions & 0 deletions AdRevenueWatch/Domain/Sources/Domain/Network/NetworkSession.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// Created by Banghua Zhao on 08/11/2024
// Copyright Apps Bay Limited. All rights reserved.
//


import Foundation

public protocol NetworkSession {
func data(for request: URLRequest) async throws -> (Data, URLResponse)
}
7 changes: 1 addition & 6 deletions AdRevenueWatch/Presentation/Manager/SessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,6 @@ class SessionManager: SessionManagerProtocol, ObservableObject {
}

func refreshAuthenticationStatus() {
do {
_ = try accessTokenUseCase.getAccessToken()
continuation?.yield(true)
} catch {
continuation?.yield(false)
}
continuation?.yield(accessTokenUseCase.hasAccessToken)
}
}
36 changes: 11 additions & 25 deletions AdRevenueWatch/Presentation/ViewModel/AdMobReportViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,12 @@ import Foundation

@MainActor
class AdMobReportViewModel: ObservableObject {
private let googleAuthUseCase: any GoogleAuthUseCaseProtocol
private let adMobAccountUseCase: any AdMobAccountUseCaseProtocol
private let adMobReportUseCase: any AdMobReportUseCaseProtocol
private let sessionManager: any SessionManagerProtocol

@Published var state: State = .fetchingAccounts
enum State {
case fetchingAccounts
case fetchingReport
case reports
}

@Published var adMobPublisherIDs: [String] = []
@Published var selectedPublisherID: String = ""
@Published var adMobReportEntity: AdMobReportEntity?
@Published var totalEarningsData: TotalEarningData?

enum DateRangeOption: String, CaseIterable, Identifiable {
case todaySoFar = "Today so far"
case yesterdayVsLastWeek = "Yesterday vs same day last week"
Expand All @@ -33,10 +22,19 @@ class AdMobReportViewModel: ObservableObject {

var id: String { rawValue }
}

private let googleAuthUseCase: any GoogleAuthUseCaseProtocol
private let adMobAccountUseCase: any AdMobAccountUseCaseProtocol
private let adMobReportUseCase: any AdMobReportUseCaseProtocol
private let sessionManager: any SessionManagerProtocol

@Published private(set) var state: State = .fetchingAccounts
@Published private(set) var adMobPublisherIDs: [String] = []
@Published var selectedPublisherID: String = ""
@Published private(set) var adMobReportEntity: AdMobReportEntity?
@Published private(set) var totalEarningsData: TotalEarningData?
@Published var selectedDateRangeOption: DateRangeOption = .last7DaysVsPrevious7Days

@Published var adsMetricDatas: [AdMetricData]?
@Published private(set) var adsMetricDatas: [AdMetricData]?

init(
googleAuthUseCase: some GoogleAuthUseCaseProtocol = Dependency.googleAuthUseCase,
Expand Down Expand Up @@ -135,18 +133,6 @@ extension AdMobReportEntity {
)
}

// Helper function to check if a date is today
private func isDateToday(_ date: Date) -> Bool {
let calendar = Calendar.current
return calendar.isDateInToday(date)
}

// Helper function to check if a date is yesterday
private func isDateYesterday(_ date: Date) -> Bool {
let calendar = Calendar.current
return calendar.isDateInYesterday(date)
}

// Helper function to check if a date is in the current month
private func isDateInCurrentMonth(_ date: Date) -> Bool {
let calendar = Calendar.current
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation
import Domain
import Security

public struct KeychainAccessTokenRepository: AccessTokenRepositoryProtocol {
public struct KeychainAccessTokenProvider: AccessTokenProvider {
private let serviceName = "com.adRevenueWatch.accessToken"

public init() {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// Created by Banghua Zhao on 08/11/2024
// Copyright Apps Bay Limited. All rights reserved.
//

import Domain
import Foundation

public class AuthenticatedURLSession: NetworkSession {
private let session: URLSession
private let accessTokenProvider: AccessTokenProvider

public init(session: URLSession = .shared, accessTokenProvider: AccessTokenProvider) {
self.session = session
self.accessTokenProvider = accessTokenProvider
}

public func data(for request: URLRequest) async throws -> (Data, URLResponse) {
var authenticatedRequest = request

// Inject the token into the request header if available
if let token = try? accessTokenProvider.getAccessToken() {
authenticatedRequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
} else {
throw NetworkError.noAccessToken
}

// Perform the request
let (data, response) = try await session.data(for: authenticatedRequest)

// Handle 401 Unauthorized response
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 401 {
try accessTokenProvider.deleteAccessToken()
throw NetworkError.accessTokenExpired
}

return (data, response)
}
}
Loading

0 comments on commit 857f41c

Please sign in to comment.