Skip to content

Commit

Permalink
IOS-8970: [Visa] API mocks (#4495)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andoran90 authored Jan 23, 2025
1 parent 8bba0df commit df9812f
Show file tree
Hide file tree
Showing 17 changed files with 391 additions and 52 deletions.
2 changes: 1 addition & 1 deletion Tangem/App/Services/EnvironmentProvider/Feature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ enum Feature: String, Hashable, CaseIterable {
case learnToEarn
case onramp
case actionButtons
case visa
case visa // TODO: Remove all API mocks and menu presenter from VisaOnboardingViewModel when removing this toggle

var name: String {
switch self {
Expand Down
4 changes: 4 additions & 0 deletions Tangem/App/Services/EnvironmentProvider/FeaturesStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ class FeatureStorage {
@AppStorageCompat(FeatureStorageKeys.useVisaTestnet)
var isVisaTestnet = false

@AppStorageCompat(FeatureStorageKeys.useVisaAPIMocks)
var isVisaAPIMocksEnabled = false

private init() {}
}

Expand All @@ -55,4 +58,5 @@ private enum FeatureStorageKeys: String {
case performanceMonitorEnabled = "performance_monitor_enabled"
case mockedCardScannerEnabled = "mocked_card_scanner_enabled"
case useVisaTestnet = "use_visa_testnet"
case useVisaAPIMocks = "use_visa_api_mocks"
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ class VisaCardScanHandler {
private let visaUtilities = VisaUtilities()

init() {
let builder = VisaAPIServiceBuilder()
let builder = VisaAPIServiceBuilder(mockedAPI: FeatureStorage.instance.isVisaAPIMocksEnabled)
authorizationService = builder.buildAuthorizationService(urlSessionConfiguration: .defaultConfiguration, logger: AppLog.shared)
cardActivationStateProvider = builder.buildCardActivationStatusService(urlSessionConfiguration: .defaultConfiguration, logger: AppLog.shared)
cardActivationStateProvider = builder.buildCardActivationRemoteStateService(urlSessionConfiguration: .defaultConfiguration, logger: AppLog.shared)
}

deinit {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ final class EnvironmentSetupViewModel: ObservableObject {
set: { $0.isVisaTestnet = $1 }
)
),
DefaultToggleRowViewModel(
title: "Visa API Mocks",
isOn: BindingValue<Bool>(
root: featureStorage,
default: false,
get: { $0.isVisaAPIMocksEnabled },
set: { $0.isVisaAPIMocksEnabled = $1 }
)
),
]

pickerViewModels = [
Expand Down
20 changes: 19 additions & 1 deletion Tangem/Modules/Onboarding/Visa/VisaOnboardingViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ class VisaOnboardingViewModel: ObservableObject {
}

func openSupport() {
if FeatureStorage.instance.isVisaAPIMocksEnabled {
VisaMocksManager.instance.showMocksMenu(presenter: self)
} else {
openSupportSheet()
}
}

private func openSupportSheet() {
Analytics.log(.requestSupport, params: [.source: .onboarding])

UIApplication.shared.endEditing()
Expand Down Expand Up @@ -429,6 +437,16 @@ private extension VisaOnboardingViewModel {
}
}

// MARK: Development menu

// TODO: IOS-8843 Remove along side with Feature toggle

extension VisaOnboardingViewModel: VisaMockMenuPresenter {
func modalFromTop(_ vc: UIViewController) {
UIApplication.modalFromTop(vc)
}
}

#if DEBUG
extension VisaOnboardingViewModel {
static let coordinator = OnboardingCoordinator()
Expand All @@ -455,7 +473,7 @@ extension VisaOnboardingViewModel {

return .init(
input: cardInput,
visaActivationManager: VisaActivationManagerFactory().make(
visaActivationManager: VisaActivationManagerFactory(isMockedAPIEnabled: true).make(
initialActivationStatus: activationStatus,
tangemSdk: TangemSdkDefaultFactory().makeTangemSdk(),
urlSessionConfiguration: .default,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ struct VisaOnboardingViewModelBuilder {

let visaActivationManager: VisaActivationManager
if let initialActivationStatus {
visaActivationManager = VisaActivationManagerFactory().make(
let factory = VisaActivationManagerFactory(isMockedAPIEnabled: FeatureStorage.instance.isVisaAPIMocksEnabled)
visaActivationManager = factory.make(
initialActivationStatus: initialActivationStatus,
tangemSdk: TangemSdkDefaultFactory().makeTangemSdk(),
urlSessionConfiguration: .defaultConfiguration,
Expand Down
12 changes: 12 additions & 0 deletions TangemApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@
B08BC1BE2BF72C0B00041E95 /* VisaTokenInfoLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B08BC1BD2BF72C0B00041E95 /* VisaTokenInfoLoader.swift */; };
B08D3C142BF6269500C83299 /* visa_config.json in Resources */ = {isa = PBXBuildFile; fileRef = B08D3C132BF6269500C83299 /* visa_config.json */; };
B08D3C162BF628A500C83299 /* VisaConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B08D3C152BF628A500C83299 /* VisaConfigProvider.swift */; };
B08DB6132D3A6A4A00A8B63F /* VisaAPIMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = B08DB6122D3A6A4A00A8B63F /* VisaAPIMocks.swift */; };
B08E26AF29D5580E00346C43 /* SeedPhraseSuggestionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B08E26AE29D5580E00346C43 /* SeedPhraseSuggestionsView.swift */; };
B08EE5172BFDE0C400E8A07C /* WelcomeSearchTokensView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B08EE5162BFDE0C400E8A07C /* WelcomeSearchTokensView.swift */; };
B08EE5192BFDE12800E8A07C /* WelcomeSearchTokensViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B08EE5182BFDE12800E8A07C /* WelcomeSearchTokensViewModel.swift */; };
Expand Down Expand Up @@ -3751,6 +3752,7 @@
B08BC1BD2BF72C0B00041E95 /* VisaTokenInfoLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisaTokenInfoLoader.swift; sourceTree = "<group>"; };
B08D3C132BF6269500C83299 /* visa_config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = visa_config.json; path = "tangem-app-config/visa_config.json"; sourceTree = SOURCE_ROOT; };
B08D3C152BF628A500C83299 /* VisaConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisaConfigProvider.swift; sourceTree = "<group>"; };
B08DB6122D3A6A4A00A8B63F /* VisaAPIMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisaAPIMocks.swift; sourceTree = "<group>"; };
B08E26AE29D5580E00346C43 /* SeedPhraseSuggestionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedPhraseSuggestionsView.swift; sourceTree = "<group>"; };
B08EE5162BFDE0C400E8A07C /* WelcomeSearchTokensView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeSearchTokensView.swift; sourceTree = "<group>"; };
B08EE5182BFDE12800E8A07C /* WelcomeSearchTokensViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeSearchTokensViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -8408,6 +8410,14 @@
path = Config;
sourceTree = "<group>";
};
B08DB6112D3A69B300A8B63F /* Mocks */ = {
isa = PBXGroup;
children = (
B08DB6122D3A6A4A00A8B63F /* VisaAPIMocks.swift */,
);
path = Mocks;
sourceTree = "<group>";
};
B08EE5152BFDE0A600E8A07C /* SearchTokens */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -8522,6 +8532,7 @@
B050F4F22B5FB02B00B2E53C /* APIService */,
B099339B2B55194900240CDB /* BridgeInteractor */,
B051451A2B59353D00EDAD1E /* Models */,
B08DB6112D3A69B300A8B63F /* Mocks */,
B099339A2B55194000240CDB /* Settings */,
B05145152B5934E200EDAD1E /* Utilities */,
);
Expand Down Expand Up @@ -18735,6 +18746,7 @@
B01408312D0832FF00734888 /* CustomerInfoManagementModels.swift in Sources */,
B01408322D0832FF00734888 /* CustomerInfoManagementService.swift in Sources */,
B01356192D12B65E0028BD16 /* SignActivationOrderTask.swift in Sources */,
B08DB6132D3A6A4A00A8B63F /* VisaAPIMocks.swift in Sources */,
B01408332D0832FF00734888 /* CustomerInfoManagementAPITarget.swift in Sources */,
B06F03412B5A8C6300F04F0E /* VisaParserError.swift in Sources */,
B07232492D103AC400E30614 /* CardActivationRemoteStateTarget.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ public protocol VisaAuthorizationService {
func getAccessTokensForWalletAuth(signedChallenge: String, sessionId: String) async throws -> VisaAuthorizationTokens?
}

protocol AccessTokenRefreshService {
public protocol VisaAuthorizationTokenRefreshService {
func refreshAccessToken(refreshToken: String) async throws -> VisaAuthorizationTokens
}

class CommonVisaAuthorizationService {
struct CommonVisaAuthorizationService {
typealias AuthorizationAPIService = APIService<AuthorizationAPITarget, VisaAuthorizationAPIError>
private let apiService: AuthorizationAPIService

Expand Down Expand Up @@ -63,7 +63,7 @@ extension CommonVisaAuthorizationService: VisaAuthorizationService {
}
}

extension CommonVisaAuthorizationService: AccessTokenRefreshService {
extension CommonVisaAuthorizationService: VisaAuthorizationTokenRefreshService {
func refreshAccessToken(refreshToken: String) async throws -> VisaAuthorizationTokens {
try await apiService.request(.init(
target: .refreshAccessToken(refreshToken: refreshToken)
Expand Down
24 changes: 22 additions & 2 deletions TangemVisa/APIService/VisaAPIServiceBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import Foundation
import Moya

public struct VisaAPIServiceBuilder {
public init() {}
private let mockedAPI: Bool

public init(mockedAPI: Bool = false) {
self.mockedAPI = mockedAPI
}

public func buildTransactionHistoryService(isTestnet: Bool, urlSessionConfiguration: URLSessionConfiguration, logger: VisaLogger) -> VisaTransactionHistoryAPIService {
let logger = InternalLogger(logger: logger)
Expand All @@ -29,10 +33,26 @@ public struct VisaAPIServiceBuilder {

// Requirements are changed so this function will be also changed, but for now it is used for testing purposes
public func buildAuthorizationService(urlSessionConfiguration: URLSessionConfiguration, logger: VisaLogger) -> VisaAuthorizationService {
if mockedAPI {
return AuthorizationServiceMock()
}

return AuthorizationServiceBuilder().build(urlSessionConfiguration: urlSessionConfiguration, logger: logger)
}

public func buildAuthorizationTokenRefreshService(urlSessionConfiguration: URLSessionConfiguration, logger: VisaLogger) -> VisaAuthorizationTokenRefreshService {
if mockedAPI {
return AuthorizationServiceMock()
}

return AuthorizationServiceBuilder().build(urlSessionConfiguration: urlSessionConfiguration, logger: logger)
}

public func buildCardActivationStatusService(urlSessionConfiguration: URLSessionConfiguration, logger: VisaLogger) -> VisaCardActivationRemoteStateService {
public func buildCardActivationRemoteStateService(urlSessionConfiguration: URLSessionConfiguration, logger: VisaLogger) -> VisaCardActivationRemoteStateService {
if mockedAPI {
return CardActivationRemoteStateServiceMock()
}

let logger = InternalLogger(logger: logger)

return CommonCardActivationRemoteStateService(apiService: .init(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ protocol AuthorizationTokenHandler {
}

class CommonVisaAccessTokenHandler {
private let tokenRefreshService: AccessTokenRefreshService
private let tokenRefreshService: VisaAuthorizationTokenRefreshService
private weak var refreshTokenSaver: VisaRefreshTokenSaver?

private let scheduler: AsyncTaskScheduler = .init()
Expand All @@ -38,7 +38,7 @@ class CommonVisaAccessTokenHandler {

init(
accessTokenHolder: AccessTokenHolder,
tokenRefreshService: AccessTokenRefreshService,
tokenRefreshService: VisaAuthorizationTokenRefreshService,
logger: InternalLogger,
refreshTokenSaver: VisaRefreshTokenSaver?
) {
Expand Down
11 changes: 8 additions & 3 deletions TangemVisa/Activation/CardActivationOrderProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@
import Foundation
import JWTDecode

protocol CardActivationOrderProvider: AnyObject {
protocol CardActivationOrderProvider {
func provideActivationOrderForSign() async throws -> CardActivationOrder
func cancelOrderLoading()
}

struct CardActivationOrder {
let activationOrder: String
let dataToSign: Data
let dataToSignByCard: Data
let dataToSignByWallet: Data
}

final class CommonCardActivationOrderProvider {
Expand Down Expand Up @@ -64,7 +65,11 @@ extension CommonCardActivationOrderProvider: CardActivationOrderProvider {
if random % 2 == 0 {
throw "Not implemented"
} else {
return .init(activationOrder: "Activation order to sign", dataToSign: Data())
return .init(
activationOrder: "Activation order to sign",
dataToSignByCard: Data(),
dataToSignByWallet: Data()
)
}
}

Expand Down
2 changes: 2 additions & 0 deletions TangemVisa/Activation/VisaActivationError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public enum VisaActivationError: LocalizedError {
case accessCodeAlreadySet
case blockedForActivation
case invalidActivationState
case missingDerivationPath
case underlyingError(Error)

public var errorDescription: String? {
Expand All @@ -43,6 +44,7 @@ public enum VisaActivationError: LocalizedError {
case .accessCodeAlreadySet: return "Access code already set, wrong task used for activation"
case .blockedForActivation: return "This card cannot be activated. Please contact support for more information."
case .invalidActivationState: return "Invalid activation state. Please close activation proccess and scan card again"
case .missingDerivationPath: return "Something went wrong. Please contact support"
case .underlyingError(let error):
return "Underlying Visa Activation Error: \(error.localizedDescription)"
}
Expand Down
2 changes: 1 addition & 1 deletion TangemVisa/Activation/VisaActivationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ private extension CommonVisaActivationManager {
func signActivationOrder(activationInput: VisaCardActivationInput) async throws {
let activationOrder = try await cardActivationOrderProvider.provideActivationOrderForSign()

let signTask = SignActivationOrderTask(orderToSign: activationOrder.dataToSign)
let signTask = SignActivationOrderTask(orderToSign: activationOrder)
let signedActivationOrder: SignedActivationOrder = try await withCheckedThrowingContinuation { [weak self] continuation in
guard let self else {
continuation.resume(throwing: "Deinitialized")
Expand Down
66 changes: 46 additions & 20 deletions TangemVisa/Activation/VisaActivationManagerFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import TangemSdk
import Moya

public struct VisaActivationManagerFactory {
public init() {}
private let isMockedAPIEnabled: Bool
public init(isMockedAPIEnabled: Bool = false) {
self.isMockedAPIEnabled = isMockedAPIEnabled
}

public func make(
initialActivationStatus: VisaCardActivationStatus,
Expand All @@ -20,7 +23,12 @@ public struct VisaActivationManagerFactory {
logger: VisaLogger
) -> VisaActivationManager {
let internalLogger = InternalLogger(logger: logger)
let authorizationService = AuthorizationServiceBuilder().build(urlSessionConfiguration: urlSessionConfiguration, logger: logger)

let authorizationService = VisaAPIServiceBuilder(mockedAPI: isMockedAPIEnabled)
.buildAuthorizationService(
urlSessionConfiguration: urlSessionConfiguration,
logger: logger
)

let accessTokenHolder: AccessTokenHolder
if case .activationStarted(_, let authorizationTokens, _) = initialActivationStatus {
Expand All @@ -29,34 +37,52 @@ public struct VisaActivationManagerFactory {
accessTokenHolder = .init()
}

let authorizationTokenRefreshService = VisaAPIServiceBuilder(mockedAPI: isMockedAPIEnabled)
.buildAuthorizationTokenRefreshService(
urlSessionConfiguration: urlSessionConfiguration,
logger: logger
)

let tokenHandler = CommonVisaAccessTokenHandler(
accessTokenHolder: accessTokenHolder,
tokenRefreshService: authorizationService,
tokenRefreshService: authorizationTokenRefreshService,
logger: internalLogger,
refreshTokenSaver: nil
)

let customerInfoManagementService = CommonCustomerInfoManagementService(
authorizationTokenHandler: tokenHandler,
apiService: .init(
provider: MoyaProviderBuilder().buildProvider(configuration: urlSessionConfiguration),
logger: internalLogger,
decoder: JSONDecoderFactory().makeCIMDecoder()
)
)
let authorizationProcessor = CommonCardAuthorizationProcessor(
authorizationService: authorizationService,
logger: internalLogger
)
let activationOrderProvider = CommonCardActivationOrderProvider(
accessTokenProvider: tokenHandler,
customerInfoManagementService: customerInfoManagementService,
logger: internalLogger
)
let cardActivationRemoteStateService = VisaAPIServiceBuilder().buildCardActivationStatusService(
urlSessionConfiguration: urlSessionConfiguration,
logger: logger
)

let activationOrderProvider: CardActivationOrderProvider
if isMockedAPIEnabled {
activationOrderProvider = CardActivationTaskOrderProviderMock()
} else {
let customerInfoManagementService = CommonCustomerInfoManagementService(
authorizationTokenHandler: tokenHandler,
apiService: .init(
provider: MoyaProviderBuilder().buildProvider(configuration: urlSessionConfiguration),
logger: internalLogger,
decoder: JSONDecoderFactory().makeCIMDecoder()
)
)
activationOrderProvider = CommonCardActivationOrderProvider(
accessTokenProvider: tokenHandler,
customerInfoManagementService: customerInfoManagementService,
logger: internalLogger
)
}

let cardActivationRemoteStateService: VisaCardActivationRemoteStateService
if isMockedAPIEnabled {
cardActivationRemoteStateService = CardActivationRemoteStateServiceMock()
} else {
cardActivationRemoteStateService = VisaAPIServiceBuilder().buildCardActivationRemoteStateService(
urlSessionConfiguration: urlSessionConfiguration,
logger: logger
)
}

return CommonVisaActivationManager(
initialActivationStatus: initialActivationStatus,
Expand Down
Loading

0 comments on commit df9812f

Please sign in to comment.