diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..ed65ec7d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,113 @@ +DS_Store +.DS_Store +CleanSwift/ + +# Created by https://www.toptal.com/developers/gitignore/api/xcode,swift,cocoapods +# Edit at https://www.toptal.com/developers/gitignore?templates=xcode,swift,cocoapods + +### CocoaPods ### +## CocoaPods GitIgnore Template + +# CocoaPods - Only use to conserve bandwidth / Save time on Pushing +# - Also handy if you have a large number of dependant pods +# - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE +Pods/ + +### Swift ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# Pods/ +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +### Xcode ### + +## Xcode 8 and earlier + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno +**/xcshareddata/WorkspaceSettings.xcsettings + +# End of https://www.toptal.com/developers/gitignore/api/xcode,swift,cocoapods \ No newline at end of file diff --git a/MyBankApp/MyBankApp.xcodeproj/project.pbxproj b/MyBankApp/MyBankApp.xcodeproj/project.pbxproj new file mode 100644 index 000000000..ad3dfa1da --- /dev/null +++ b/MyBankApp/MyBankApp.xcodeproj/project.pbxproj @@ -0,0 +1,1081 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 1999FDEFC6310BFE84329E86 /* Pods_MyBankAppTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C337E965EE80D701CC09ED0D /* Pods_MyBankAppTests.framework */; }; + 1D3DCA54D867A351A2E52696 /* Pods_MyBankApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CEB3D9AED855921215D3A98 /* Pods_MyBankApp.framework */; }; + 4F5A4BD5DE23F65DE5F0A06E /* Pods_MyBankApp_MyBankAppUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 39BB90D19EE7C0FCD034B3EF /* Pods_MyBankApp_MyBankAppUITests.framework */; }; + 5E8FB8DA2A53185300B5A792 /* LoginRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E8FB8D92A53185300B5A792 /* LoginRequest.swift */; }; + 5E8FB8DC2A5318A400B5A792 /* LoginWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E8FB8DB2A5318A400B5A792 /* LoginWorker.swift */; }; + 5E8FB8DE2A531A4300B5A792 /* LoginWorkerProtocolSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E8FB8DD2A531A4300B5A792 /* LoginWorkerProtocolSpy.swift */; }; + 5E8FB8E02A531CF800B5A792 /* LoginWorkerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E8FB8DF2A531CF800B5A792 /* LoginWorkerTests.swift */; }; + 5E8FB8E22A53224B00B5A792 /* HomeWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E8FB8E12A53224A00B5A792 /* HomeWorker.swift */; }; + 5EA5DF402A52959300147F4A /* URLSessionProtocolSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA5DF3F2A52959300147F4A /* URLSessionProtocolSpy.swift */; }; + 5EA5DF422A52965F00147F4A /* NetworkServiceImplTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA5DF412A52965F00147F4A /* NetworkServiceImplTests.swift */; }; + 5EA5DF452A52A37200147F4A /* DispatchQueueProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA5DF442A52A37100147F4A /* DispatchQueueProtocol.swift */; }; + 5EEFAB4E2A50982B00A000EE /* NetworkProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFAB4D2A50982B00A000EE /* NetworkProtocols.swift */; }; + 5EEFAB572A50A81900A000EE /* LoginService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFAB562A50A81900A000EE /* LoginService.swift */; }; + 5EEFAB592A50A84300A000EE /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFAB582A50A84300A000EE /* NetworkError.swift */; }; + 5EEFAB5B2A50A85B00A000EE /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFAB5A2A50A85B00A000EE /* HTTPMethod.swift */; }; + 5EEFAB5D2A50A88600A000EE /* NetworkServiceImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFAB5C2A50A88600A000EE /* NetworkServiceImpl.swift */; }; + 5EEFAB622A50A8E500A000EE /* HomeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFAB612A50A8E500A000EE /* HomeResponse.swift */; }; + 5EEFAB642A50A8F900A000EE /* HomeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFAB632A50A8F900A000EE /* HomeService.swift */; }; + 5EEFAB672A50A99C00A000EE /* LoginResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFAB662A50A99C00A000EE /* LoginResponse.swift */; }; + 5EEFAB6C2A50B16E00A000EE /* NetworkServiceSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFAB6B2A50B16E00A000EE /* NetworkServiceSpy.swift */; }; + 5EEFAB6F2A50B21700A000EE /* LoginServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFAB6E2A50B21700A000EE /* LoginServiceTests.swift */; }; + 5EEFAB722A50B80200A000EE /* HomeServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFAB712A50B80200A000EE /* HomeServiceTests.swift */; }; + 5EEFAB802A50CB8500A000EE /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFAB7F2A50CB8500A000EE /* LoginViewController.swift */; }; + 5EEFAB8F2A50CD8B00A000EE /* LoginProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFAB8E2A50CD8B00A000EE /* LoginProtocols.swift */; }; + 5EEFAB922A50D04B00A000EE /* LoginInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFAB912A50D04B00A000EE /* LoginInteractor.swift */; }; + 5EEFAB942A50D06700A000EE /* LoginPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFAB932A50D06700A000EE /* LoginPresenter.swift */; }; + 5EEFAB962A50D0C500A000EE /* LoginRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFAB952A50D0C500A000EE /* LoginRouter.swift */; }; + 5EEFAB982A50D15200A000EE /* LoginConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFAB972A50D15200A000EE /* LoginConfigurator.swift */; }; + 5EEFABB42A51E56D00A000EE /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABB32A51E56D00A000EE /* UIColor+Extension.swift */; }; + 5EEFABB82A51E5CF00A000EE /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABB72A51E5CF00A000EE /* HomeViewController.swift */; }; + 5EEFABBA2A51E5F800A000EE /* HomeProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABB92A51E5F800A000EE /* HomeProtocols.swift */; }; + 5EEFABBC2A51E6C800A000EE /* HomePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABBB2A51E6C800A000EE /* HomePresenter.swift */; }; + 5EEFABBE2A51E71200A000EE /* HomeInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABBD2A51E71200A000EE /* HomeInteractor.swift */; }; + 5EEFABC02A51EADB00A000EE /* HomeRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABBF2A51EADB00A000EE /* HomeRouter.swift */; }; + 5EEFABC22A51EB1D00A000EE /* HomeConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABC12A51EB1D00A000EE /* HomeConfigurator.swift */; }; + 5EEFABC72A52133B00A000EE /* StatementTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABC62A52133B00A000EE /* StatementTableViewCell.swift */; }; + 5EEFABD52A52179A00A000EE /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABD42A52179A00A000EE /* Double+Extension.swift */; }; + 5EEFABD72A52181C00A000EE /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABD62A52181C00A000EE /* String+Extension.swift */; }; + 5EEFABD92A52195600A000EE /* UIFont+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABD82A52195600A000EE /* UIFont+Extension.swift */; }; + 5EEFABDE2A522A2B00A000EE /* DispatchQueueSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABDD2A522A2B00A000EE /* DispatchQueueSpy.swift */; }; + 5EEFABE02A522B5000A000EE /* LoginViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABDF2A522B5000A000EE /* LoginViewControllerTests.swift */; }; + 5EEFABE32A522C5400A000EE /* LoginBusinessLogicSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABE22A522C5400A000EE /* LoginBusinessLogicSpy.swift */; }; + 5EEFABE52A525A2100A000EE /* LoginRouterSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABE42A525A2100A000EE /* LoginRouterSpy.swift */; }; + 5EEFABE72A52620100A000EE /* LoginPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABE62A52620100A000EE /* LoginPresenterTests.swift */; }; + 5EEFABE92A52630E00A000EE /* LoginDisplayLogicSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABE82A52630E00A000EE /* LoginDisplayLogicSpy.swift */; }; + 5EEFABEB2A5265C700A000EE /* LoginInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABEA2A5265C700A000EE /* LoginInteractorTests.swift */; }; + 5EEFABED2A52667500A000EE /* LoginServiceSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABEC2A52667500A000EE /* LoginServiceSpy.swift */; }; + 5EEFABF02A52671E00A000EE /* LoginResponse+Fixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABEF2A52671E00A000EE /* LoginResponse+Fixture.swift */; }; + 5EEFABF22A5267E500A000EE /* LoginPresentationLogicSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABF12A5267E500A000EE /* LoginPresentationLogicSpy.swift */; }; + 5EEFABF82A526D2900A000EE /* LoginRouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABF72A526D2900A000EE /* LoginRouterTests.swift */; }; + 5EEFABFA2A526D9300A000EE /* NavigationControllerSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABF92A526D9300A000EE /* NavigationControllerSpy.swift */; }; + 5EEFABFC2A52770E00A000EE /* LoginViewControllerSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABFB2A52770E00A000EE /* LoginViewControllerSpy.swift */; }; + 5EEFABFE2A52836900A000EE /* LoginConfiguratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABFD2A52836900A000EE /* LoginConfiguratorTests.swift */; }; + 5EEFAC002A52854E00A000EE /* Mirror+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EEFABFF2A52854E00A000EE /* Mirror+Extension.swift */; }; + 5EF1ACAE2A4E47C6002EA972 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EF1ACAD2A4E47C6002EA972 /* AppDelegate.swift */; }; + 5EF1ACB02A4E47C6002EA972 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EF1ACAF2A4E47C6002EA972 /* SceneDelegate.swift */; }; + 5EF1ACB52A4E47C6002EA972 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5EF1ACB32A4E47C6002EA972 /* Main.storyboard */; }; + 5EF1ACB72A4E47C9002EA972 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5EF1ACB62A4E47C9002EA972 /* Assets.xcassets */; }; + 5EF1ACBA2A4E47C9002EA972 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5EF1ACB82A4E47C9002EA972 /* LaunchScreen.storyboard */; }; + 5EF1ACCF2A4E47C9002EA972 /* MyBankAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EF1ACCE2A4E47C9002EA972 /* MyBankAppUITests.swift */; }; + 5EF1ACD12A4E47C9002EA972 /* MyBankAppUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EF1ACD02A4E47C9002EA972 /* MyBankAppUITestsLaunchTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 5EF1ACC12A4E47C9002EA972 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5EF1ACA22A4E47C6002EA972 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5EF1ACA92A4E47C6002EA972; + remoteInfo = MyBankApp; + }; + 5EF1ACCB2A4E47C9002EA972 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5EF1ACA22A4E47C6002EA972 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5EF1ACA92A4E47C6002EA972; + remoteInfo = MyBankApp; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 1D934480DE6FAFDC4888F38E /* Pods-MyBankApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MyBankApp.debug.xcconfig"; path = "Target Support Files/Pods-MyBankApp/Pods-MyBankApp.debug.xcconfig"; sourceTree = ""; }; + 20FA089B898C9F88264E0F2B /* Pods-MyBankApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MyBankApp.release.xcconfig"; path = "Target Support Files/Pods-MyBankApp/Pods-MyBankApp.release.xcconfig"; sourceTree = ""; }; + 364B4B14C6AB11DA11030F62 /* Pods-MyBankAppTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MyBankAppTests.debug.xcconfig"; path = "Target Support Files/Pods-MyBankAppTests/Pods-MyBankAppTests.debug.xcconfig"; sourceTree = ""; }; + 39BB90D19EE7C0FCD034B3EF /* Pods_MyBankApp_MyBankAppUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MyBankApp_MyBankAppUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3CEB3D9AED855921215D3A98 /* Pods_MyBankApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MyBankApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4FF8791C0A8C3F3ECC7E00B1 /* Pods-MyBankApp-MyBankAppUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MyBankApp-MyBankAppUITests.release.xcconfig"; path = "Target Support Files/Pods-MyBankApp-MyBankAppUITests/Pods-MyBankApp-MyBankAppUITests.release.xcconfig"; sourceTree = ""; }; + 5E8FB8D92A53185300B5A792 /* LoginRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginRequest.swift; sourceTree = ""; }; + 5E8FB8DB2A5318A400B5A792 /* LoginWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginWorker.swift; sourceTree = ""; }; + 5E8FB8DD2A531A4300B5A792 /* LoginWorkerProtocolSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginWorkerProtocolSpy.swift; sourceTree = ""; }; + 5E8FB8DF2A531CF800B5A792 /* LoginWorkerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginWorkerTests.swift; sourceTree = ""; }; + 5E8FB8E12A53224A00B5A792 /* HomeWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeWorker.swift; sourceTree = ""; }; + 5EA5DF3F2A52959300147F4A /* URLSessionProtocolSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionProtocolSpy.swift; sourceTree = ""; }; + 5EA5DF412A52965F00147F4A /* NetworkServiceImplTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkServiceImplTests.swift; sourceTree = ""; }; + 5EA5DF442A52A37100147F4A /* DispatchQueueProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchQueueProtocol.swift; sourceTree = ""; }; + 5EEFAB4D2A50982B00A000EE /* NetworkProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtocols.swift; sourceTree = ""; }; + 5EEFAB562A50A81900A000EE /* LoginService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginService.swift; sourceTree = ""; }; + 5EEFAB582A50A84300A000EE /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; + 5EEFAB5A2A50A85B00A000EE /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; + 5EEFAB5C2A50A88600A000EE /* NetworkServiceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkServiceImpl.swift; sourceTree = ""; }; + 5EEFAB612A50A8E500A000EE /* HomeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeResponse.swift; sourceTree = ""; }; + 5EEFAB632A50A8F900A000EE /* HomeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeService.swift; sourceTree = ""; }; + 5EEFAB662A50A99C00A000EE /* LoginResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginResponse.swift; sourceTree = ""; }; + 5EEFAB6B2A50B16E00A000EE /* NetworkServiceSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkServiceSpy.swift; sourceTree = ""; }; + 5EEFAB6E2A50B21700A000EE /* LoginServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginServiceTests.swift; sourceTree = ""; }; + 5EEFAB712A50B80200A000EE /* HomeServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeServiceTests.swift; sourceTree = ""; }; + 5EEFAB7F2A50CB8500A000EE /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; + 5EEFAB8E2A50CD8B00A000EE /* LoginProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginProtocols.swift; sourceTree = ""; }; + 5EEFAB912A50D04B00A000EE /* LoginInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginInteractor.swift; sourceTree = ""; }; + 5EEFAB932A50D06700A000EE /* LoginPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginPresenter.swift; sourceTree = ""; }; + 5EEFAB952A50D0C500A000EE /* LoginRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginRouter.swift; sourceTree = ""; }; + 5EEFAB972A50D15200A000EE /* LoginConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginConfigurator.swift; sourceTree = ""; }; + 5EEFABB32A51E56D00A000EE /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = ""; }; + 5EEFABB72A51E5CF00A000EE /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; + 5EEFABB92A51E5F800A000EE /* HomeProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeProtocols.swift; sourceTree = ""; }; + 5EEFABBB2A51E6C800A000EE /* HomePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePresenter.swift; sourceTree = ""; }; + 5EEFABBD2A51E71200A000EE /* HomeInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeInteractor.swift; sourceTree = ""; }; + 5EEFABBF2A51EADB00A000EE /* HomeRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeRouter.swift; sourceTree = ""; }; + 5EEFABC12A51EB1D00A000EE /* HomeConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeConfigurator.swift; sourceTree = ""; }; + 5EEFABC62A52133B00A000EE /* StatementTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatementTableViewCell.swift; sourceTree = ""; }; + 5EEFABD42A52179A00A000EE /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; }; + 5EEFABD62A52181C00A000EE /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; + 5EEFABD82A52195600A000EE /* UIFont+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Extension.swift"; sourceTree = ""; }; + 5EEFABDD2A522A2B00A000EE /* DispatchQueueSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchQueueSpy.swift; sourceTree = ""; }; + 5EEFABDF2A522B5000A000EE /* LoginViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewControllerTests.swift; sourceTree = ""; }; + 5EEFABE22A522C5400A000EE /* LoginBusinessLogicSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginBusinessLogicSpy.swift; sourceTree = ""; }; + 5EEFABE42A525A2100A000EE /* LoginRouterSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginRouterSpy.swift; sourceTree = ""; }; + 5EEFABE62A52620100A000EE /* LoginPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginPresenterTests.swift; sourceTree = ""; }; + 5EEFABE82A52630E00A000EE /* LoginDisplayLogicSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginDisplayLogicSpy.swift; sourceTree = ""; }; + 5EEFABEA2A5265C700A000EE /* LoginInteractorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginInteractorTests.swift; sourceTree = ""; }; + 5EEFABEC2A52667500A000EE /* LoginServiceSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginServiceSpy.swift; sourceTree = ""; }; + 5EEFABEF2A52671E00A000EE /* LoginResponse+Fixture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoginResponse+Fixture.swift"; sourceTree = ""; }; + 5EEFABF12A5267E500A000EE /* LoginPresentationLogicSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginPresentationLogicSpy.swift; sourceTree = ""; }; + 5EEFABF72A526D2900A000EE /* LoginRouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginRouterTests.swift; sourceTree = ""; }; + 5EEFABF92A526D9300A000EE /* NavigationControllerSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationControllerSpy.swift; sourceTree = ""; }; + 5EEFABFB2A52770E00A000EE /* LoginViewControllerSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewControllerSpy.swift; sourceTree = ""; }; + 5EEFABFD2A52836900A000EE /* LoginConfiguratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginConfiguratorTests.swift; sourceTree = ""; }; + 5EEFABFF2A52854E00A000EE /* Mirror+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mirror+Extension.swift"; sourceTree = ""; }; + 5EF1ACAA2A4E47C6002EA972 /* MyBankApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MyBankApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 5EF1ACAD2A4E47C6002EA972 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 5EF1ACAF2A4E47C6002EA972 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 5EF1ACB42A4E47C6002EA972 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 5EF1ACB62A4E47C9002EA972 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 5EF1ACB92A4E47C9002EA972 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 5EF1ACBB2A4E47C9002EA972 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 5EF1ACC02A4E47C9002EA972 /* MyBankAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MyBankAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 5EF1ACCA2A4E47C9002EA972 /* MyBankAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MyBankAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 5EF1ACCE2A4E47C9002EA972 /* MyBankAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyBankAppUITests.swift; sourceTree = ""; }; + 5EF1ACD02A4E47C9002EA972 /* MyBankAppUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyBankAppUITestsLaunchTests.swift; sourceTree = ""; }; + C337E965EE80D701CC09ED0D /* Pods_MyBankAppTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MyBankAppTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D66AB948202BF58D8CE4C347 /* Pods-MyBankApp-MyBankAppUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MyBankApp-MyBankAppUITests.debug.xcconfig"; path = "Target Support Files/Pods-MyBankApp-MyBankAppUITests/Pods-MyBankApp-MyBankAppUITests.debug.xcconfig"; sourceTree = ""; }; + E33569CFF49BE73CD4CE261C /* Pods-MyBankAppTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MyBankAppTests.release.xcconfig"; path = "Target Support Files/Pods-MyBankAppTests/Pods-MyBankAppTests.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5EF1ACA72A4E47C6002EA972 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1D3DCA54D867A351A2E52696 /* Pods_MyBankApp.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5EF1ACBD2A4E47C9002EA972 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1999FDEFC6310BFE84329E86 /* Pods_MyBankAppTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5EF1ACC72A4E47C9002EA972 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4F5A4BD5DE23F65DE5F0A06E /* Pods_MyBankApp_MyBankAppUITests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0B4CB77341CFCCE73463B88C /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3CEB3D9AED855921215D3A98 /* Pods_MyBankApp.framework */, + 39BB90D19EE7C0FCD034B3EF /* Pods_MyBankApp_MyBankAppUITests.framework */, + C337E965EE80D701CC09ED0D /* Pods_MyBankAppTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 3F0964CD2917D53AD2E5358D /* Pods */ = { + isa = PBXGroup; + children = ( + 1D934480DE6FAFDC4888F38E /* Pods-MyBankApp.debug.xcconfig */, + 20FA089B898C9F88264E0F2B /* Pods-MyBankApp.release.xcconfig */, + D66AB948202BF58D8CE4C347 /* Pods-MyBankApp-MyBankAppUITests.debug.xcconfig */, + 4FF8791C0A8C3F3ECC7E00B1 /* Pods-MyBankApp-MyBankAppUITests.release.xcconfig */, + 364B4B14C6AB11DA11030F62 /* Pods-MyBankAppTests.debug.xcconfig */, + E33569CFF49BE73CD4CE261C /* Pods-MyBankAppTests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 5EA5DF432A52A35800147F4A /* Common */ = { + isa = PBXGroup; + children = ( + 5EA5DF442A52A37100147F4A /* DispatchQueueProtocol.swift */, + ); + path = Common; + sourceTree = ""; + }; + 5EEFAB442A50739500A000EE /* Network */ = { + isa = PBXGroup; + children = ( + 5EEFAB4D2A50982B00A000EE /* NetworkProtocols.swift */, + 5EEFAB582A50A84300A000EE /* NetworkError.swift */, + 5EEFAB5A2A50A85B00A000EE /* HTTPMethod.swift */, + 5EEFAB5C2A50A88600A000EE /* NetworkServiceImpl.swift */, + ); + path = Network; + sourceTree = ""; + }; + 5EEFAB5E2A50A8BE00A000EE /* Login */ = { + isa = PBXGroup; + children = ( + 5EEFAB902A50CECB00A000EE /* Service */, + 5EEFAB8D2A50CD7600A000EE /* Scene */, + 5EEFAB652A50A99300A000EE /* Models */, + ); + path = Login; + sourceTree = ""; + }; + 5EEFAB5F2A50A8CB00A000EE /* Home */ = { + isa = PBXGroup; + children = ( + 5EEFABC52A52132E00A000EE /* Cells */, + 5EEFABB62A51E5BD00A000EE /* Scene */, + 5EEFAB602A50A8DE00A000EE /* Models */, + 5EEFABB52A51E5B100A000EE /* Service */, + ); + path = Home; + sourceTree = ""; + }; + 5EEFAB602A50A8DE00A000EE /* Models */ = { + isa = PBXGroup; + children = ( + 5EEFAB612A50A8E500A000EE /* HomeResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; + 5EEFAB652A50A99300A000EE /* Models */ = { + isa = PBXGroup; + children = ( + 5EEFAB662A50A99C00A000EE /* LoginResponse.swift */, + 5E8FB8D92A53185300B5A792 /* LoginRequest.swift */, + ); + path = Models; + sourceTree = ""; + }; + 5EEFAB6A2A50B15700A000EE /* Network */ = { + isa = PBXGroup; + children = ( + 5EEFAB6B2A50B16E00A000EE /* NetworkServiceSpy.swift */, + 5EA5DF3F2A52959300147F4A /* URLSessionProtocolSpy.swift */, + 5EA5DF412A52965F00147F4A /* NetworkServiceImplTests.swift */, + ); + path = Network; + sourceTree = ""; + }; + 5EEFAB6D2A50B20A00A000EE /* Login */ = { + isa = PBXGroup; + children = ( + 5EEFABEE2A52671300A000EE /* Fixtures */, + 5EEFABE12A522C4900A000EE /* Spies */, + 5EEFAB6E2A50B21700A000EE /* LoginServiceTests.swift */, + 5EEFABDF2A522B5000A000EE /* LoginViewControllerTests.swift */, + 5EEFABE62A52620100A000EE /* LoginPresenterTests.swift */, + 5EEFABEA2A5265C700A000EE /* LoginInteractorTests.swift */, + 5EEFABF72A526D2900A000EE /* LoginRouterTests.swift */, + 5EEFABFD2A52836900A000EE /* LoginConfiguratorTests.swift */, + 5E8FB8DF2A531CF800B5A792 /* LoginWorkerTests.swift */, + ); + path = Login; + sourceTree = ""; + }; + 5EEFAB702A50B7F900A000EE /* Home */ = { + isa = PBXGroup; + children = ( + 5EEFAB712A50B80200A000EE /* HomeServiceTests.swift */, + ); + path = Home; + sourceTree = ""; + }; + 5EEFAB8D2A50CD7600A000EE /* Scene */ = { + isa = PBXGroup; + children = ( + 5EEFAB8E2A50CD8B00A000EE /* LoginProtocols.swift */, + 5EEFAB7F2A50CB8500A000EE /* LoginViewController.swift */, + 5EEFAB932A50D06700A000EE /* LoginPresenter.swift */, + 5EEFAB912A50D04B00A000EE /* LoginInteractor.swift */, + 5EEFAB952A50D0C500A000EE /* LoginRouter.swift */, + 5EEFAB972A50D15200A000EE /* LoginConfigurator.swift */, + 5E8FB8DB2A5318A400B5A792 /* LoginWorker.swift */, + ); + path = Scene; + sourceTree = ""; + }; + 5EEFAB902A50CECB00A000EE /* Service */ = { + isa = PBXGroup; + children = ( + 5EEFAB562A50A81900A000EE /* LoginService.swift */, + ); + path = Service; + sourceTree = ""; + }; + 5EEFABB22A51E56300A000EE /* Extensions */ = { + isa = PBXGroup; + children = ( + 5EEFABB32A51E56D00A000EE /* UIColor+Extension.swift */, + 5EEFABD42A52179A00A000EE /* Double+Extension.swift */, + 5EEFABD62A52181C00A000EE /* String+Extension.swift */, + 5EEFABD82A52195600A000EE /* UIFont+Extension.swift */, + 5EEFABFF2A52854E00A000EE /* Mirror+Extension.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 5EEFABB52A51E5B100A000EE /* Service */ = { + isa = PBXGroup; + children = ( + 5EEFAB632A50A8F900A000EE /* HomeService.swift */, + ); + path = Service; + sourceTree = ""; + }; + 5EEFABB62A51E5BD00A000EE /* Scene */ = { + isa = PBXGroup; + children = ( + 5EEFABB92A51E5F800A000EE /* HomeProtocols.swift */, + 5EEFABB72A51E5CF00A000EE /* HomeViewController.swift */, + 5EEFABBB2A51E6C800A000EE /* HomePresenter.swift */, + 5EEFABBD2A51E71200A000EE /* HomeInteractor.swift */, + 5EEFABBF2A51EADB00A000EE /* HomeRouter.swift */, + 5EEFABC12A51EB1D00A000EE /* HomeConfigurator.swift */, + 5E8FB8E12A53224A00B5A792 /* HomeWorker.swift */, + ); + path = Scene; + sourceTree = ""; + }; + 5EEFABC52A52132E00A000EE /* Cells */ = { + isa = PBXGroup; + children = ( + 5EEFABC62A52133B00A000EE /* StatementTableViewCell.swift */, + ); + path = Cells; + sourceTree = ""; + }; + 5EEFABDC2A522A1200A000EE /* Common */ = { + isa = PBXGroup; + children = ( + 5EEFABDD2A522A2B00A000EE /* DispatchQueueSpy.swift */, + 5EEFABF92A526D9300A000EE /* NavigationControllerSpy.swift */, + ); + path = Common; + sourceTree = ""; + }; + 5EEFABE12A522C4900A000EE /* Spies */ = { + isa = PBXGroup; + children = ( + 5EEFABE22A522C5400A000EE /* LoginBusinessLogicSpy.swift */, + 5EEFABE42A525A2100A000EE /* LoginRouterSpy.swift */, + 5EEFABE82A52630E00A000EE /* LoginDisplayLogicSpy.swift */, + 5EEFABEC2A52667500A000EE /* LoginServiceSpy.swift */, + 5EEFABF12A5267E500A000EE /* LoginPresentationLogicSpy.swift */, + 5EEFABFB2A52770E00A000EE /* LoginViewControllerSpy.swift */, + 5E8FB8DD2A531A4300B5A792 /* LoginWorkerProtocolSpy.swift */, + ); + path = Spies; + sourceTree = ""; + }; + 5EEFABEE2A52671300A000EE /* Fixtures */ = { + isa = PBXGroup; + children = ( + 5EEFABEF2A52671E00A000EE /* LoginResponse+Fixture.swift */, + ); + path = Fixtures; + sourceTree = ""; + }; + 5EF1ACA12A4E47C6002EA972 = { + isa = PBXGroup; + children = ( + 5EF1ACAC2A4E47C6002EA972 /* MyBankApp */, + 5EF1ACC32A4E47C9002EA972 /* MyBankAppTests */, + 5EF1ACCD2A4E47C9002EA972 /* MyBankAppUITests */, + 5EF1ACAB2A4E47C6002EA972 /* Products */, + 3F0964CD2917D53AD2E5358D /* Pods */, + 0B4CB77341CFCCE73463B88C /* Frameworks */, + ); + sourceTree = ""; + }; + 5EF1ACAB2A4E47C6002EA972 /* Products */ = { + isa = PBXGroup; + children = ( + 5EF1ACAA2A4E47C6002EA972 /* MyBankApp.app */, + 5EF1ACC02A4E47C9002EA972 /* MyBankAppTests.xctest */, + 5EF1ACCA2A4E47C9002EA972 /* MyBankAppUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 5EF1ACAC2A4E47C6002EA972 /* MyBankApp */ = { + isa = PBXGroup; + children = ( + 5EA5DF432A52A35800147F4A /* Common */, + 5EEFABB22A51E56300A000EE /* Extensions */, + 5EEFAB5E2A50A8BE00A000EE /* Login */, + 5EEFAB5F2A50A8CB00A000EE /* Home */, + 5EEFAB442A50739500A000EE /* Network */, + 5EF1ACAD2A4E47C6002EA972 /* AppDelegate.swift */, + 5EF1ACAF2A4E47C6002EA972 /* SceneDelegate.swift */, + 5EF1ACB32A4E47C6002EA972 /* Main.storyboard */, + 5EF1ACB62A4E47C9002EA972 /* Assets.xcassets */, + 5EF1ACB82A4E47C9002EA972 /* LaunchScreen.storyboard */, + 5EF1ACBB2A4E47C9002EA972 /* Info.plist */, + ); + path = MyBankApp; + sourceTree = ""; + }; + 5EF1ACC32A4E47C9002EA972 /* MyBankAppTests */ = { + isa = PBXGroup; + children = ( + 5EEFABDC2A522A1200A000EE /* Common */, + 5EEFAB702A50B7F900A000EE /* Home */, + 5EEFAB6D2A50B20A00A000EE /* Login */, + 5EEFAB6A2A50B15700A000EE /* Network */, + ); + path = MyBankAppTests; + sourceTree = ""; + }; + 5EF1ACCD2A4E47C9002EA972 /* MyBankAppUITests */ = { + isa = PBXGroup; + children = ( + 5EF1ACCE2A4E47C9002EA972 /* MyBankAppUITests.swift */, + 5EF1ACD02A4E47C9002EA972 /* MyBankAppUITestsLaunchTests.swift */, + ); + path = MyBankAppUITests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 5EF1ACA92A4E47C6002EA972 /* MyBankApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5EF1ACD42A4E47C9002EA972 /* Build configuration list for PBXNativeTarget "MyBankApp" */; + buildPhases = ( + B586E5958C28FD1C7110E56C /* [CP] Check Pods Manifest.lock */, + 5EF1ACA62A4E47C6002EA972 /* Sources */, + 5EF1ACA72A4E47C6002EA972 /* Frameworks */, + 5EF1ACA82A4E47C6002EA972 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MyBankApp; + productName = MyBankApp; + productReference = 5EF1ACAA2A4E47C6002EA972 /* MyBankApp.app */; + productType = "com.apple.product-type.application"; + }; + 5EF1ACBF2A4E47C9002EA972 /* MyBankAppTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5EF1ACD72A4E47C9002EA972 /* Build configuration list for PBXNativeTarget "MyBankAppTests" */; + buildPhases = ( + BA99DD3DC452AA0EBA74C6B3 /* [CP] Check Pods Manifest.lock */, + 5EF1ACBC2A4E47C9002EA972 /* Sources */, + 5EF1ACBD2A4E47C9002EA972 /* Frameworks */, + 5EF1ACBE2A4E47C9002EA972 /* Resources */, + CCACCCB6ECA48F6DA40BC4EC /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 5EF1ACC22A4E47C9002EA972 /* PBXTargetDependency */, + ); + name = MyBankAppTests; + productName = MyBankAppTests; + productReference = 5EF1ACC02A4E47C9002EA972 /* MyBankAppTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 5EF1ACC92A4E47C9002EA972 /* MyBankAppUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5EF1ACDA2A4E47C9002EA972 /* Build configuration list for PBXNativeTarget "MyBankAppUITests" */; + buildPhases = ( + 1142FE0886F0FDC6A8D23367 /* [CP] Check Pods Manifest.lock */, + 5EF1ACC62A4E47C9002EA972 /* Sources */, + 5EF1ACC72A4E47C9002EA972 /* Frameworks */, + 5EF1ACC82A4E47C9002EA972 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 5EF1ACCC2A4E47C9002EA972 /* PBXTargetDependency */, + ); + name = MyBankAppUITests; + productName = MyBankAppUITests; + productReference = 5EF1ACCA2A4E47C9002EA972 /* MyBankAppUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 5EF1ACA22A4E47C6002EA972 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1430; + LastUpgradeCheck = 1430; + TargetAttributes = { + 5EF1ACA92A4E47C6002EA972 = { + CreatedOnToolsVersion = 14.3.1; + }; + 5EF1ACBF2A4E47C9002EA972 = { + CreatedOnToolsVersion = 14.3.1; + TestTargetID = 5EF1ACA92A4E47C6002EA972; + }; + 5EF1ACC92A4E47C9002EA972 = { + CreatedOnToolsVersion = 14.3.1; + TestTargetID = 5EF1ACA92A4E47C6002EA972; + }; + }; + }; + buildConfigurationList = 5EF1ACA52A4E47C6002EA972 /* Build configuration list for PBXProject "MyBankApp" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 5EF1ACA12A4E47C6002EA972; + productRefGroup = 5EF1ACAB2A4E47C6002EA972 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 5EF1ACA92A4E47C6002EA972 /* MyBankApp */, + 5EF1ACBF2A4E47C9002EA972 /* MyBankAppTests */, + 5EF1ACC92A4E47C9002EA972 /* MyBankAppUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 5EF1ACA82A4E47C6002EA972 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5EF1ACBA2A4E47C9002EA972 /* LaunchScreen.storyboard in Resources */, + 5EF1ACB72A4E47C9002EA972 /* Assets.xcassets in Resources */, + 5EF1ACB52A4E47C6002EA972 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5EF1ACBE2A4E47C9002EA972 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5EF1ACC82A4E47C9002EA972 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1142FE0886F0FDC6A8D23367 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-MyBankApp-MyBankAppUITests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + B586E5958C28FD1C7110E56C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-MyBankApp-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + BA99DD3DC452AA0EBA74C6B3 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-MyBankAppTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + CCACCCB6ECA48F6DA40BC4EC /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-MyBankAppTests/Pods-MyBankAppTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-MyBankAppTests/Pods-MyBankAppTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-MyBankAppTests/Pods-MyBankAppTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 5EF1ACA62A4E47C6002EA972 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5EEFABBC2A51E6C800A000EE /* HomePresenter.swift in Sources */, + 5EEFABBE2A51E71200A000EE /* HomeInteractor.swift in Sources */, + 5EEFAB622A50A8E500A000EE /* HomeResponse.swift in Sources */, + 5E8FB8DC2A5318A400B5A792 /* LoginWorker.swift in Sources */, + 5E8FB8DA2A53185300B5A792 /* LoginRequest.swift in Sources */, + 5EEFAB5D2A50A88600A000EE /* NetworkServiceImpl.swift in Sources */, + 5EEFABD52A52179A00A000EE /* Double+Extension.swift in Sources */, + 5EEFABB82A51E5CF00A000EE /* HomeViewController.swift in Sources */, + 5EA5DF452A52A37200147F4A /* DispatchQueueProtocol.swift in Sources */, + 5EEFAB922A50D04B00A000EE /* LoginInteractor.swift in Sources */, + 5EEFABB42A51E56D00A000EE /* UIColor+Extension.swift in Sources */, + 5EEFABC72A52133B00A000EE /* StatementTableViewCell.swift in Sources */, + 5EEFAC002A52854E00A000EE /* Mirror+Extension.swift in Sources */, + 5EEFAB4E2A50982B00A000EE /* NetworkProtocols.swift in Sources */, + 5E8FB8E22A53224B00B5A792 /* HomeWorker.swift in Sources */, + 5EEFAB802A50CB8500A000EE /* LoginViewController.swift in Sources */, + 5EEFABD72A52181C00A000EE /* String+Extension.swift in Sources */, + 5EEFAB962A50D0C500A000EE /* LoginRouter.swift in Sources */, + 5EEFABC02A51EADB00A000EE /* HomeRouter.swift in Sources */, + 5EEFAB8F2A50CD8B00A000EE /* LoginProtocols.swift in Sources */, + 5EEFABD92A52195600A000EE /* UIFont+Extension.swift in Sources */, + 5EEFAB642A50A8F900A000EE /* HomeService.swift in Sources */, + 5EEFAB5B2A50A85B00A000EE /* HTTPMethod.swift in Sources */, + 5EEFAB942A50D06700A000EE /* LoginPresenter.swift in Sources */, + 5EEFAB572A50A81900A000EE /* LoginService.swift in Sources */, + 5EEFAB592A50A84300A000EE /* NetworkError.swift in Sources */, + 5EF1ACAE2A4E47C6002EA972 /* AppDelegate.swift in Sources */, + 5EEFABBA2A51E5F800A000EE /* HomeProtocols.swift in Sources */, + 5EEFAB672A50A99C00A000EE /* LoginResponse.swift in Sources */, + 5EEFABC22A51EB1D00A000EE /* HomeConfigurator.swift in Sources */, + 5EEFAB982A50D15200A000EE /* LoginConfigurator.swift in Sources */, + 5EF1ACB02A4E47C6002EA972 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5EF1ACBC2A4E47C9002EA972 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5EEFABFE2A52836900A000EE /* LoginConfiguratorTests.swift in Sources */, + 5EEFABE72A52620100A000EE /* LoginPresenterTests.swift in Sources */, + 5EEFABE52A525A2100A000EE /* LoginRouterSpy.swift in Sources */, + 5EEFAB6F2A50B21700A000EE /* LoginServiceTests.swift in Sources */, + 5EEFAB722A50B80200A000EE /* HomeServiceTests.swift in Sources */, + 5EEFABF22A5267E500A000EE /* LoginPresentationLogicSpy.swift in Sources */, + 5EEFABDE2A522A2B00A000EE /* DispatchQueueSpy.swift in Sources */, + 5EEFABFA2A526D9300A000EE /* NavigationControllerSpy.swift in Sources */, + 5EEFABF82A526D2900A000EE /* LoginRouterTests.swift in Sources */, + 5EEFABE92A52630E00A000EE /* LoginDisplayLogicSpy.swift in Sources */, + 5EEFABE32A522C5400A000EE /* LoginBusinessLogicSpy.swift in Sources */, + 5EEFABE02A522B5000A000EE /* LoginViewControllerTests.swift in Sources */, + 5E8FB8DE2A531A4300B5A792 /* LoginWorkerProtocolSpy.swift in Sources */, + 5EEFABFC2A52770E00A000EE /* LoginViewControllerSpy.swift in Sources */, + 5EA5DF402A52959300147F4A /* URLSessionProtocolSpy.swift in Sources */, + 5EA5DF422A52965F00147F4A /* NetworkServiceImplTests.swift in Sources */, + 5EEFABF02A52671E00A000EE /* LoginResponse+Fixture.swift in Sources */, + 5EEFABEB2A5265C700A000EE /* LoginInteractorTests.swift in Sources */, + 5E8FB8E02A531CF800B5A792 /* LoginWorkerTests.swift in Sources */, + 5EEFAB6C2A50B16E00A000EE /* NetworkServiceSpy.swift in Sources */, + 5EEFABED2A52667500A000EE /* LoginServiceSpy.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5EF1ACC62A4E47C9002EA972 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5EF1ACD12A4E47C9002EA972 /* MyBankAppUITestsLaunchTests.swift in Sources */, + 5EF1ACCF2A4E47C9002EA972 /* MyBankAppUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 5EF1ACC22A4E47C9002EA972 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5EF1ACA92A4E47C6002EA972 /* MyBankApp */; + targetProxy = 5EF1ACC12A4E47C9002EA972 /* PBXContainerItemProxy */; + }; + 5EF1ACCC2A4E47C9002EA972 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5EF1ACA92A4E47C6002EA972 /* MyBankApp */; + targetProxy = 5EF1ACCB2A4E47C9002EA972 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 5EF1ACB32A4E47C6002EA972 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 5EF1ACB42A4E47C6002EA972 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 5EF1ACB82A4E47C9002EA972 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 5EF1ACB92A4E47C9002EA972 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 5EF1ACD22A4E47C9002EA972 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 5EF1ACD32A4E47C9002EA972 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 5EF1ACD52A4E47C9002EA972 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1D934480DE6FAFDC4888F38E /* Pods-MyBankApp.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = D67XBEEN3L; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MyBankApp/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = fusco.MyBankApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 5EF1ACD62A4E47C9002EA972 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 20FA089B898C9F88264E0F2B /* Pods-MyBankApp.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = D67XBEEN3L; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MyBankApp/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = fusco.MyBankApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 5EF1ACD82A4E47C9002EA972 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 364B4B14C6AB11DA11030F62 /* Pods-MyBankAppTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = D67XBEEN3L; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = fusco.MyBankAppTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MyBankApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MyBankApp"; + }; + name = Debug; + }; + 5EF1ACD92A4E47C9002EA972 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E33569CFF49BE73CD4CE261C /* Pods-MyBankAppTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = D67XBEEN3L; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = fusco.MyBankAppTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MyBankApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MyBankApp"; + }; + name = Release; + }; + 5EF1ACDB2A4E47C9002EA972 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D66AB948202BF58D8CE4C347 /* Pods-MyBankApp-MyBankAppUITests.debug.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = D67XBEEN3L; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = fusco.MyBankAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = MyBankApp; + }; + name = Debug; + }; + 5EF1ACDC2A4E47C9002EA972 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4FF8791C0A8C3F3ECC7E00B1 /* Pods-MyBankApp-MyBankAppUITests.release.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = D67XBEEN3L; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = fusco.MyBankAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = MyBankApp; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 5EF1ACA52A4E47C6002EA972 /* Build configuration list for PBXProject "MyBankApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5EF1ACD22A4E47C9002EA972 /* Debug */, + 5EF1ACD32A4E47C9002EA972 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5EF1ACD42A4E47C9002EA972 /* Build configuration list for PBXNativeTarget "MyBankApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5EF1ACD52A4E47C9002EA972 /* Debug */, + 5EF1ACD62A4E47C9002EA972 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5EF1ACD72A4E47C9002EA972 /* Build configuration list for PBXNativeTarget "MyBankAppTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5EF1ACD82A4E47C9002EA972 /* Debug */, + 5EF1ACD92A4E47C9002EA972 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5EF1ACDA2A4E47C9002EA972 /* Build configuration list for PBXNativeTarget "MyBankAppUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5EF1ACDB2A4E47C9002EA972 /* Debug */, + 5EF1ACDC2A4E47C9002EA972 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 5EF1ACA22A4E47C6002EA972 /* Project object */; +} diff --git a/MyBankApp/MyBankApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/MyBankApp/MyBankApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/MyBankApp/MyBankApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/MyBankApp/MyBankApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/MyBankApp/MyBankApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/MyBankApp/MyBankApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/MyBankApp/MyBankApp.xcodeproj/project.xcworkspace/xcuserdata/matheusfusco.xcuserdatad/UserInterfaceState.xcuserstate b/MyBankApp/MyBankApp.xcodeproj/project.xcworkspace/xcuserdata/matheusfusco.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 000000000..b9374fd90 Binary files /dev/null and b/MyBankApp/MyBankApp.xcodeproj/project.xcworkspace/xcuserdata/matheusfusco.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/MyBankApp/MyBankApp.xcodeproj/xcshareddata/xcschemes/MyBankApp.xcscheme b/MyBankApp/MyBankApp.xcodeproj/xcshareddata/xcschemes/MyBankApp.xcscheme new file mode 100644 index 000000000..e32995892 --- /dev/null +++ b/MyBankApp/MyBankApp.xcodeproj/xcshareddata/xcschemes/MyBankApp.xcscheme @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MyBankApp/MyBankApp.xcodeproj/xcuserdata/matheusfusco.xcuserdatad/xcschemes/xcschememanagement.plist b/MyBankApp/MyBankApp.xcodeproj/xcuserdata/matheusfusco.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 000000000..4d76879df --- /dev/null +++ b/MyBankApp/MyBankApp.xcodeproj/xcuserdata/matheusfusco.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,32 @@ + + + + + SchemeUserState + + MyBankApp.xcscheme_^#shared#^_ + + orderHint + 6 + + + SuppressBuildableAutocreation + + 5EF1ACA92A4E47C6002EA972 + + primary + + + 5EF1ACBF2A4E47C9002EA972 + + primary + + + 5EF1ACC92A4E47C9002EA972 + + primary + + + + + diff --git a/MyBankApp/MyBankApp.xcworkspace/contents.xcworkspacedata b/MyBankApp/MyBankApp.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..5cb6c87a6 --- /dev/null +++ b/MyBankApp/MyBankApp.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/MyBankApp/MyBankApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/MyBankApp/MyBankApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/MyBankApp/MyBankApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/MyBankApp/MyBankApp/AppDelegate.swift b/MyBankApp/MyBankApp/AppDelegate.swift new file mode 100644 index 000000000..5353f0413 --- /dev/null +++ b/MyBankApp/MyBankApp/AppDelegate.swift @@ -0,0 +1,32 @@ +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + window = UIWindow(frame: UIScreen.main.bounds) + let rootViewController = UINavigationController(rootViewController: LoginConfigurator().setup()) + window?.rootViewController = rootViewController + window?.makeKeyAndVisible() + return true + } + + // MARK: UISceneSession Lifecycle + @available(iOS 13.0, *) + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + @available(iOS 13.0, *) + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + +} + diff --git a/MyBankApp/MyBankApp/Assets.xcassets/AccentColor.colorset/Contents.json b/MyBankApp/MyBankApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/MyBankApp/MyBankApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyBankApp/MyBankApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/MyBankApp/MyBankApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..13613e3ee --- /dev/null +++ b/MyBankApp/MyBankApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyBankApp/MyBankApp/Assets.xcassets/Contents.json b/MyBankApp/MyBankApp/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/MyBankApp/MyBankApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyBankApp/MyBankApp/Assets.xcassets/bankLogo.imageset/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@1x.png b/MyBankApp/MyBankApp/Assets.xcassets/bankLogo.imageset/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@1x.png new file mode 100644 index 000000000..466ef1c09 Binary files /dev/null and b/MyBankApp/MyBankApp/Assets.xcassets/bankLogo.imageset/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@1x.png differ diff --git a/MyBankApp/MyBankApp/Assets.xcassets/bankLogo.imageset/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@2x.png b/MyBankApp/MyBankApp/Assets.xcassets/bankLogo.imageset/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@2x.png new file mode 100644 index 000000000..cb118e91b Binary files /dev/null and b/MyBankApp/MyBankApp/Assets.xcassets/bankLogo.imageset/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@2x.png differ diff --git a/MyBankApp/MyBankApp/Assets.xcassets/bankLogo.imageset/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@3x.png b/MyBankApp/MyBankApp/Assets.xcassets/bankLogo.imageset/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@3x.png new file mode 100644 index 000000000..a650b8199 Binary files /dev/null and b/MyBankApp/MyBankApp/Assets.xcassets/bankLogo.imageset/CDEBC6CD-73C4-47CB-B879-8A426D8163E1@3x.png differ diff --git a/MyBankApp/MyBankApp/Assets.xcassets/bankLogo.imageset/Contents.json b/MyBankApp/MyBankApp/Assets.xcassets/bankLogo.imageset/Contents.json new file mode 100644 index 000000000..4cf613711 --- /dev/null +++ b/MyBankApp/MyBankApp/Assets.xcassets/bankLogo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "CDEBC6CD-73C4-47CB-B879-8A426D8163E1@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "CDEBC6CD-73C4-47CB-B879-8A426D8163E1@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "CDEBC6CD-73C4-47CB-B879-8A426D8163E1@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyBankApp/MyBankApp/Assets.xcassets/ic-exitButton.imageset/Contents.json b/MyBankApp/MyBankApp/Assets.xcassets/ic-exitButton.imageset/Contents.json new file mode 100644 index 000000000..91fdf6f86 --- /dev/null +++ b/MyBankApp/MyBankApp/Assets.xcassets/ic-exitButton.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "logout 2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyBankApp/MyBankApp/Assets.xcassets/ic-exitButton.imageset/logout 2.png b/MyBankApp/MyBankApp/Assets.xcassets/ic-exitButton.imageset/logout 2.png new file mode 100644 index 000000000..de1e4ae3c Binary files /dev/null and b/MyBankApp/MyBankApp/Assets.xcassets/ic-exitButton.imageset/logout 2.png differ diff --git a/MyBankApp/MyBankApp/Base.lproj/LaunchScreen.storyboard b/MyBankApp/MyBankApp/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000..865e9329f --- /dev/null +++ b/MyBankApp/MyBankApp/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MyBankApp/MyBankApp/Base.lproj/Main.storyboard b/MyBankApp/MyBankApp/Base.lproj/Main.storyboard new file mode 100644 index 000000000..25a763858 --- /dev/null +++ b/MyBankApp/MyBankApp/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MyBankApp/MyBankApp/Common/DispatchQueueProtocol.swift b/MyBankApp/MyBankApp/Common/DispatchQueueProtocol.swift new file mode 100644 index 000000000..b7ac2f774 --- /dev/null +++ b/MyBankApp/MyBankApp/Common/DispatchQueueProtocol.swift @@ -0,0 +1,11 @@ +import Foundation + +protocol DispatchQueueProtocol { + func async(execute work: @escaping () -> Void) +} + +final class MainDispatchQueueWrapper: DispatchQueueProtocol { + func async(execute work: @escaping () -> Void) { + DispatchQueue.main.async(execute: work) + } +} diff --git a/MyBankApp/MyBankApp/Extensions/Double+Extension.swift b/MyBankApp/MyBankApp/Extensions/Double+Extension.swift new file mode 100644 index 000000000..ce6385c08 --- /dev/null +++ b/MyBankApp/MyBankApp/Extensions/Double+Extension.swift @@ -0,0 +1,11 @@ +import UIKit + +extension Double { + func formatCurrency() -> String? { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.locale = Locale(identifier: "pt_BR") + + return formatter.string(from: NSNumber(value: self)) + } +} diff --git a/MyBankApp/MyBankApp/Extensions/Mirror+Extension.swift b/MyBankApp/MyBankApp/Extensions/Mirror+Extension.swift new file mode 100644 index 000000000..261fd13bf --- /dev/null +++ b/MyBankApp/MyBankApp/Extensions/Mirror+Extension.swift @@ -0,0 +1,9 @@ +extension Mirror { + func firstChild(of type: T.Type, in label: String? = nil) -> T? { + return children.lazy.compactMap { + guard let value = $0.value as? T else { return nil } + guard let label = label else { return value } + return $0.label == label ? value : nil + }.first + } +} diff --git a/MyBankApp/MyBankApp/Extensions/String+Extension.swift b/MyBankApp/MyBankApp/Extensions/String+Extension.swift new file mode 100644 index 000000000..bf07c225b --- /dev/null +++ b/MyBankApp/MyBankApp/Extensions/String+Extension.swift @@ -0,0 +1,64 @@ +import UIKit + +extension String { + var containsUppercaseLetter: Bool { + let uppercaseLetterRegex = ".*[A-Z]+.*" + let uppercaseLetterPredicate = NSPredicate(format: "SELF MATCHES %@", uppercaseLetterRegex) + return uppercaseLetterPredicate.evaluate(with: self) + } + + var containsSpecialCharacter: Bool { + let specialCharacterRegex = ".*[^A-Za-z0-9].*" + let specialCharacterPredicate = NSPredicate(format: "SELF MATCHES %@", specialCharacterRegex) + return specialCharacterPredicate.evaluate(with: self) + } + + func isValidEmail() -> Bool { + let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + + let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) + return emailPred.evaluate(with: self) + } + + func convertToValidDate() -> String? { + let inputFormatter = DateFormatter() + inputFormatter.dateFormat = "ddMMyyyy" + + guard let date = inputFormatter.date(from: self) else { + return nil + } + + let outputFormatter = DateFormatter() + outputFormatter.dateFormat = "dd/MM/yyyy" + + let formattedDate = outputFormatter.string(from: date) + return formattedDate + } + + func isValidCPF() -> Bool { + let numbers = self.compactMap({Int(String($0))}) + guard numbers.count == 11 && Set(numbers).count != 1 else { return false } + let soma1 = 11 - ( numbers[0] * 10 + + numbers[1] * 9 + + numbers[2] * 8 + + numbers[3] * 7 + + numbers[4] * 6 + + numbers[5] * 5 + + numbers[6] * 4 + + numbers[7] * 3 + + numbers[8] * 2 ) % 11 + let dv1 = soma1 > 9 ? 0 : soma1 + let soma2 = 11 - ( numbers[0] * 11 + + numbers[1] * 10 + + numbers[2] * 9 + + numbers[3] * 8 + + numbers[4] * 7 + + numbers[5] * 6 + + numbers[6] * 5 + + numbers[7] * 4 + + numbers[8] * 3 + + numbers[9] * 2 ) % 11 + let dv2 = soma2 > 9 ? 0 : soma2 + return dv1 == numbers[9] && dv2 == numbers[10] + } +} diff --git a/MyBankApp/MyBankApp/Extensions/UIColor+Extension.swift b/MyBankApp/MyBankApp/Extensions/UIColor+Extension.swift new file mode 100644 index 000000000..b6fd76a34 --- /dev/null +++ b/MyBankApp/MyBankApp/Extensions/UIColor+Extension.swift @@ -0,0 +1,20 @@ +import UIKit + +extension UIColor { + convenience init(hex: String) { + var hexFormatted: String = hex.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).uppercased() + + if hexFormatted.hasPrefix("#") { + hexFormatted = String(hexFormatted.dropFirst()) + } + + var rgbValue: UInt64 = 0 + Scanner(string: hexFormatted).scanHexInt64(&rgbValue) + + let r = (rgbValue & 0xFF0000) >> 16 + let g = (rgbValue & 0x00FF00) >> 8 + let b = rgbValue & 0x0000FF + + self.init(red: CGFloat(r) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: 1.0) + } +} diff --git a/MyBankApp/MyBankApp/Extensions/UIFont+Extension.swift b/MyBankApp/MyBankApp/Extensions/UIFont+Extension.swift new file mode 100644 index 000000000..4554537b7 --- /dev/null +++ b/MyBankApp/MyBankApp/Extensions/UIFont+Extension.swift @@ -0,0 +1,13 @@ +import UIKit + +extension UIFont { + + func set(_ fontName: MyBankFont, size: CGFloat) -> UIFont { + return UIFont(name: fontName.rawValue, size: size) ?? UIFont(name: "Arial", size: size)! + } + + enum MyBankFont: String { + case HelveticaNeue = "HelveticaNeue" + case HelveticaNeueLight = "HelveticaNeue-Light" + } +} diff --git a/MyBankApp/MyBankApp/Home/Cells/StatementTableViewCell.swift b/MyBankApp/MyBankApp/Home/Cells/StatementTableViewCell.swift new file mode 100644 index 000000000..7700f6457 --- /dev/null +++ b/MyBankApp/MyBankApp/Home/Cells/StatementTableViewCell.swift @@ -0,0 +1,107 @@ +import UIKit + +final class StatementCell: UITableViewCell { + private let backgroundContainerView: UIView = { + let view = UIView() + view.backgroundColor = .white + view.layer.cornerRadius = 8 + return view + }() + + private let typeLabel: UILabel = { + let label = UILabel() + label.font = UIFont().set(.HelveticaNeue, size: 16) + label.textColor = UIColor(hex: "#A8B4C4") + return label + }() + + private let detailLabel: UILabel = { + let label = UILabel() + label.font = UIFont().set(.HelveticaNeue, size: 16) + label.textColor = UIColor(hex: "#485465") + label.numberOfLines = 0 + return label + }() + + private let dateLabel: UILabel = { + let label = UILabel() + label.font = UIFont().set(.HelveticaNeue, size: 16) + label.textColor = UIColor(hex: "#A8B4C4") + return label + }() + + private let valueLabel: UILabel = { + let label = UILabel() + label.font = UIFont().set(.HelveticaNeue, size: 20) + label.textColor = UIColor(hex: "#485465") + return label + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + addSubviews() + setupConstraints() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(with statement: Statement) { + typeLabel.text = statement.type + detailLabel.text = statement.detail + dateLabel.text = statement.date.convertToValidDate() + valueLabel.text = statement.value.formatCurrency() + } + + private func addSubviews() { + contentView.addSubview(backgroundContainerView) + backgroundContainerView.addSubview(typeLabel) + backgroundContainerView.addSubview(detailLabel) + backgroundContainerView.addSubview(dateLabel) + backgroundContainerView.addSubview(valueLabel) + } + + private func setupConstraints() { + setupBackgroundContainerViewConstraints() + setupTypeLabelConstraints() + setupDetailLabelConstraints() + setupDateLabelConstraints() + setupValueLabelConstraints() + } + + private func setupBackgroundContainerViewConstraints() { + backgroundContainerView.translatesAutoresizingMaskIntoConstraints = false + backgroundContainerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8).isActive = true + backgroundContainerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8).isActive = true + backgroundContainerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8).isActive = true + backgroundContainerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8).isActive = true + } + + private func setupTypeLabelConstraints() { + typeLabel.translatesAutoresizingMaskIntoConstraints = false + typeLabel.topAnchor.constraint(equalTo: backgroundContainerView.topAnchor, constant: 18).isActive = true + typeLabel.leadingAnchor.constraint(equalTo: backgroundContainerView.leadingAnchor, constant: 20).isActive = true + } + + private func setupDetailLabelConstraints() { + detailLabel.translatesAutoresizingMaskIntoConstraints = false + detailLabel.topAnchor.constraint(equalTo: typeLabel.bottomAnchor, constant: 18).isActive = true + detailLabel.leadingAnchor.constraint(equalTo: backgroundContainerView.leadingAnchor, constant: 20).isActive = true + detailLabel.trailingAnchor.constraint(equalTo: backgroundContainerView.trailingAnchor, constant: -20).isActive = true + } + + private func setupDateLabelConstraints() { + dateLabel.translatesAutoresizingMaskIntoConstraints = false + dateLabel.topAnchor.constraint(equalTo: backgroundContainerView.topAnchor, constant: 18).isActive = true + dateLabel.trailingAnchor.constraint(equalTo: backgroundContainerView.trailingAnchor, constant: -20).isActive = true + } + + private func setupValueLabelConstraints() { + valueLabel.translatesAutoresizingMaskIntoConstraints = false + valueLabel.topAnchor.constraint(equalTo: dateLabel.bottomAnchor, constant: 13).isActive = true + valueLabel.trailingAnchor.constraint(equalTo: backgroundContainerView.trailingAnchor, constant: -20).isActive = true + valueLabel.bottomAnchor.constraint(equalTo: backgroundContainerView.bottomAnchor, constant: -18).isActive = true + } +} diff --git a/MyBankApp/MyBankApp/Home/Models/HomeResponse.swift b/MyBankApp/MyBankApp/Home/Models/HomeResponse.swift new file mode 100644 index 000000000..9a4712152 --- /dev/null +++ b/MyBankApp/MyBankApp/Home/Models/HomeResponse.swift @@ -0,0 +1,13 @@ +import Foundation + +struct HomeResponse: Decodable { + let statement: [Statement] +} + +struct Statement: Decodable, Equatable { + let id: Int + let type: String + let date: String + let detail: String + let value: Double +} diff --git a/MyBankApp/MyBankApp/Home/Scene/HomeConfigurator.swift b/MyBankApp/MyBankApp/Home/Scene/HomeConfigurator.swift new file mode 100644 index 000000000..2d595ab2a --- /dev/null +++ b/MyBankApp/MyBankApp/Home/Scene/HomeConfigurator.swift @@ -0,0 +1,21 @@ +import UIKit + +final class HomeConfigurator { + func setup() -> HomeViewController { + let viewController = HomeViewController() + let interactor = HomeInteractor() + let presenter = HomePresenter() + let router = HomeRouter() + + viewController.interactor = interactor + viewController.router = router + + interactor.presenter = presenter + presenter.viewController = viewController + + router.viewController = viewController + router.dataStore = interactor + + return viewController + } +} diff --git a/MyBankApp/MyBankApp/Home/Scene/HomeInteractor.swift b/MyBankApp/MyBankApp/Home/Scene/HomeInteractor.swift new file mode 100644 index 000000000..e27bc2847 --- /dev/null +++ b/MyBankApp/MyBankApp/Home/Scene/HomeInteractor.swift @@ -0,0 +1,28 @@ +import Foundation + +final class HomeInteractor: HomeBusinessLogic, HomeDataStore { + + var user: LoginResponse? + + var presenter: HomePresentationLogic? + let worker: HomeWorkerProtocol + + init(presenter: HomePresentationLogic? = nil, worker: HomeWorkerProtocol = HomeWorker()) { + self.presenter = presenter + self.worker = worker + } + + func fetchUserStatements() { + worker.fetchStatements(for: user?.userId ?? "") { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let response): + print("Statements:", response.statement) + self.presenter?.presentStatementsSuccess(response.statement) + case .failure(let error): + print("Statements error:", error.localizedDescription) + self.presenter?.presentStatementsError(message: "") + } + } + } +} diff --git a/MyBankApp/MyBankApp/Home/Scene/HomePresenter.swift b/MyBankApp/MyBankApp/Home/Scene/HomePresenter.swift new file mode 100644 index 000000000..5b4f99281 --- /dev/null +++ b/MyBankApp/MyBankApp/Home/Scene/HomePresenter.swift @@ -0,0 +1,13 @@ +import Foundation + +final class HomePresenter: HomePresentationLogic { + weak var viewController: HomeDisplayLogic? + + func presentStatementsSuccess(_ statements: [Statement]) { + viewController?.displayStatements(statements) + } + + func presentStatementsError(message: String) { + viewController?.displayStatementsError(message: message) + } +} diff --git a/MyBankApp/MyBankApp/Home/Scene/HomeProtocols.swift b/MyBankApp/MyBankApp/Home/Scene/HomeProtocols.swift new file mode 100644 index 000000000..d77020a6f --- /dev/null +++ b/MyBankApp/MyBankApp/Home/Scene/HomeProtocols.swift @@ -0,0 +1,33 @@ +import Foundation + +// MARK: - HomeDisplayLogic +protocol HomeDisplayLogic: AnyObject { + func displayStatements(_ statements: [Statement]) + func displayStatementsError(message: String) +} + +// MARK: - HomeInteractor +protocol HomeBusinessLogic { + func fetchUserStatements() +} + +// MARK: - HomePresenter +protocol HomePresentationLogic { + func presentStatementsSuccess(_ statements: [Statement]) + func presentStatementsError(message: String) +} + +// MARK: - HomeRouter +protocol HomeRoutingLogic { + func logout() +} + +// MARK: - HomeDataStore +protocol HomeDataStore { + var user: LoginResponse? { get set } +} + +// MARK: - LoginDataPassing +protocol HomeDataPassing { + var dataStore: HomeDataStore? { get } +} diff --git a/MyBankApp/MyBankApp/Home/Scene/HomeRouter.swift b/MyBankApp/MyBankApp/Home/Scene/HomeRouter.swift new file mode 100644 index 000000000..42f4f8d57 --- /dev/null +++ b/MyBankApp/MyBankApp/Home/Scene/HomeRouter.swift @@ -0,0 +1,10 @@ +import UIKit + +final class HomeRouter: NSObject, HomeRoutingLogic, HomeDataPassing { + weak var viewController: HomeViewController? + var dataStore: HomeDataStore? + + func logout() { + viewController?.navigationController?.popViewController(animated: true) + } +} diff --git a/MyBankApp/MyBankApp/Home/Scene/HomeViewController.swift b/MyBankApp/MyBankApp/Home/Scene/HomeViewController.swift new file mode 100644 index 000000000..65caae9ba --- /dev/null +++ b/MyBankApp/MyBankApp/Home/Scene/HomeViewController.swift @@ -0,0 +1,235 @@ +import UIKit + +final class HomeViewController: UIViewController { + private let containerView = UIView() + private let blueSquareView = UIView() + private let exitButton = UIButton(type: .system) + private let nameLabel = UILabel() + private let accountLabel = UILabel() + private let accountNumberLabel = UILabel() + private let balanceLabel = UILabel() + private let balanceValueLabel = UILabel() + private let tableView = UITableView() + + var interactor: HomeBusinessLogic? + var router: (NSObjectProtocol & HomeRoutingLogic & HomeDataPassing)? + var dispatchQueue: DispatchQueueProtocol + + private var statements: [Statement] = [] + + init(interactor: HomeBusinessLogic? = nil, router: HomeRouter? = nil, dispatchQueue: DispatchQueueProtocol = MainDispatchQueueWrapper()) { + self.interactor = interactor + self.router = router + self.dispatchQueue = dispatchQueue + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + navigationController?.interactivePopGestureRecognizer?.isEnabled = false + setupViews() + setupUserInfo() + interactor?.fetchUserStatements() + } + + // MARK: SetupUI + private func addSubviews() { + view.addSubview(containerView) + containerView.addSubview(blueSquareView) + containerView.addSubview(exitButton) + containerView.addSubview(nameLabel) + containerView.addSubview(accountLabel) + containerView.addSubview(accountNumberLabel) + containerView.addSubview(balanceLabel) + containerView.addSubview(balanceValueLabel) + containerView.addSubview(tableView) + } + + private func setupContainerView() { + containerView.translatesAutoresizingMaskIntoConstraints = false + containerView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true + } + + private func setupSquareView() { + blueSquareView.translatesAutoresizingMaskIntoConstraints = false + blueSquareView.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true + blueSquareView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true + blueSquareView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true + blueSquareView.heightAnchor.constraint(equalToConstant: 250).isActive = true + blueSquareView.backgroundColor = UIColor(hex: "#3B49EE") + } + + private func setupExitButton() { + exitButton.translatesAutoresizingMaskIntoConstraints = false + exitButton.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 60).isActive = true + exitButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20).isActive = true + exitButton.widthAnchor.constraint(equalToConstant: 54).isActive = true + exitButton.heightAnchor.constraint(equalToConstant: 54).isActive = true + exitButton.setImage(UIImage(named: "ic-exitButton"), for: .normal) + exitButton.tintColor = UIColor(hex: "#FFFFFF") + if #available(iOS 14.0, *) { + exitButton.addAction(UIAction(handler: { [weak self] _ in + self?.exitButtonTapped() + }), for: .touchUpInside) + } else { + // Check if iOS version is 11 or earlier + if #available(iOS 12.0, *) { + exitButton.addTarget(self, action: #selector(exitButtonTapped), for: .touchUpInside) + } else { + // Set a fallback action for iOS versions 11 or earlier + exitButton.addTarget(self, action: #selector(exitButtonTapped), for: .touchDown) + } + } + } + + private func setupUserNameLabel() { + nameLabel.font = UIFont().set(.HelveticaNeueLight, size: 25) + nameLabel.textColor = UIColor(hex: "#FFFFFF") + nameLabel.text = "-" + } + + private func setupUserNameLabelConstraints() { + nameLabel.translatesAutoresizingMaskIntoConstraints = false + nameLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 60).isActive = true + nameLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 18).isActive = true + nameLabel.trailingAnchor.constraint(equalTo: exitButton.leadingAnchor, constant: -18).isActive = true + nameLabel.heightAnchor.constraint(equalToConstant: 29).isActive = true + } + + private func setupAccountLabel() { + accountLabel.font = UIFont().set(.HelveticaNeue, size: 13) + accountLabel.textColor = UIColor(hex: "#FFFFFF") + accountLabel.text = "Conta" + } + + private func setupAccountLabelConstraints() { + accountLabel.translatesAutoresizingMaskIntoConstraints = false + accountLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 28).isActive = true + accountLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 18).isActive = true + accountLabel.heightAnchor.constraint(equalToConstant: 13).isActive = true + } + + private func setupAccountNumberLabel() { + accountNumberLabel.font = UIFont().set(.HelveticaNeueLight, size: 25) + accountNumberLabel.textColor = UIColor(hex: "#FFFFFF") + } + + private func setupAccountNumberLabelConstraints() { + accountNumberLabel.translatesAutoresizingMaskIntoConstraints = false + accountNumberLabel.topAnchor.constraint(equalTo: accountLabel.bottomAnchor, constant: 6).isActive = true + accountNumberLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 18).isActive = true + accountNumberLabel.heightAnchor.constraint(equalToConstant: 29).isActive = true + } + + private func setupBalanceLabel() { + balanceLabel.font = UIFont().set(.HelveticaNeue, size: 13) + balanceLabel.textColor = UIColor(hex: "#FFFFFF") + balanceLabel.text = "Saldo" + } + + private func setupBalanceLabelConstraints() { + balanceLabel.translatesAutoresizingMaskIntoConstraints = false + balanceLabel.topAnchor.constraint(equalTo: accountNumberLabel.bottomAnchor, constant: 21).isActive = true + balanceLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 18).isActive = true + balanceLabel.heightAnchor.constraint(equalToConstant: 13).isActive = true + } + + private func setupBalanceValueLabel() { + balanceValueLabel.font = UIFont().set(.HelveticaNeueLight, size: 25) + balanceValueLabel.textColor = UIColor(hex: "#FFFFFF") + } + + private func setupBalanceValueLabelConstraints() { + balanceValueLabel.translatesAutoresizingMaskIntoConstraints = false + balanceValueLabel.topAnchor.constraint(equalTo: balanceLabel.bottomAnchor, constant: 6).isActive = true + balanceValueLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 18).isActive = true + balanceValueLabel.heightAnchor.constraint(equalToConstant: 29).isActive = true + } + + private func setupTableView() { + tableView.delegate = self + tableView.dataSource = self + tableView.register(StatementCell.self, forCellReuseIdentifier: "StatementCell") + } + + private func setupTableViewConstraints() { + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.topAnchor.constraint(equalTo: blueSquareView.bottomAnchor).isActive = true + tableView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true + tableView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true + tableView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true + } + + private func setupViews() { + addSubviews() + setupContainerView() + setupSquareView() + setupExitButton() + setupUserNameLabel() + setupUserNameLabelConstraints() + setupAccountLabel() + setupAccountLabelConstraints() + setupAccountNumberLabel() + setupAccountNumberLabelConstraints() + setupBalanceLabel() + setupBalanceLabelConstraints() + setupBalanceValueLabel() + setupBalanceValueLabelConstraints() + setupTableView() + setupTableViewConstraints() + } + + private func setupUserInfo() { + guard let user = router?.dataStore?.user else { return } + nameLabel.text = user.name + accountNumberLabel.text = user.accountNumber + balanceValueLabel.text = user.balance + } + + @objc private func exitButtonTapped() { + router?.logout() + } +} + +// MARK: - HomeDisplayLogic Methods +extension HomeViewController: HomeDisplayLogic { + func displayStatements(_ statements: [Statement]) { + dispatchQueue.async { + self.statements = statements + self.tableView.reloadData() + } + } + + func displayStatementsError(message: String) { + + } +} + +// MARK: - UITableView Methods +extension HomeViewController: UITableViewDelegate, UITableViewDataSource { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return statements.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "StatementCell", for: indexPath) as? StatementCell else { + fatalError("Failed to dequeue StatementCell") + } + + let statement = statements[indexPath.row] + cell.configure(with: statement) + + return cell + } +} diff --git a/MyBankApp/MyBankApp/Home/Scene/HomeWorker.swift b/MyBankApp/MyBankApp/Home/Scene/HomeWorker.swift new file mode 100644 index 000000000..1cdaa4c1a --- /dev/null +++ b/MyBankApp/MyBankApp/Home/Scene/HomeWorker.swift @@ -0,0 +1,18 @@ +import Foundation + +protocol HomeWorkerProtocol { + func fetchStatements(for id: String, completion: @escaping (Result) -> Void) +} + +final class HomeWorker: HomeWorkerProtocol { + + private var homeService: HomeServiceProtocol + + init(homeService: HomeServiceProtocol = HomeService()) { + self.homeService = homeService + } + + func fetchStatements(for id: String, completion: @escaping (Result) -> Void) { + homeService.fetchStatements(for: id, completion: completion) + } +} diff --git a/MyBankApp/MyBankApp/Home/Service/HomeService.swift b/MyBankApp/MyBankApp/Home/Service/HomeService.swift new file mode 100644 index 000000000..9dc60ee80 --- /dev/null +++ b/MyBankApp/MyBankApp/Home/Service/HomeService.swift @@ -0,0 +1,32 @@ +import Foundation + +protocol HomeServiceProtocol { + func fetchStatements(for id: String, completion: @escaping (Result) -> Void) +} + +final class HomeService: HomeServiceProtocol { + private let networkService: NetworkService + + init(networkService: NetworkService = NetworkServiceImpl()) { + self.networkService = networkService + } + + func fetchStatements( + for id: String, + completion: @escaping (Result) -> Void + ) { + /// id should be used at the end of the URL(.../statements/{id}), but as the API working without it, i will not add the id at the end + guard let url = URL(string: "https://6092aef785ff5100172136c2.mockapi.io/api/statements") else { + completion(.failure(.invalidResponse)) + return + } + + networkService.request( + url: url, + method: .get, + bodyParameters: nil + ) { (result: Result) in + completion(result) + } + } +} diff --git a/MyBankApp/MyBankApp/Info.plist b/MyBankApp/MyBankApp/Info.plist new file mode 100644 index 000000000..dd3c9afda --- /dev/null +++ b/MyBankApp/MyBankApp/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/MyBankApp/MyBankApp/Login/Models/LoginRequest.swift b/MyBankApp/MyBankApp/Login/Models/LoginRequest.swift new file mode 100644 index 000000000..2e0bc77f1 --- /dev/null +++ b/MyBankApp/MyBankApp/Login/Models/LoginRequest.swift @@ -0,0 +1,6 @@ +import Foundation + +struct LoginRequest: Equatable, Codable { + let username: String + let password: String +} diff --git a/MyBankApp/MyBankApp/Login/Models/LoginResponse.swift b/MyBankApp/MyBankApp/Login/Models/LoginResponse.swift new file mode 100644 index 000000000..9d94a2636 --- /dev/null +++ b/MyBankApp/MyBankApp/Login/Models/LoginResponse.swift @@ -0,0 +1,11 @@ +import Foundation + +struct LoginResponse: Codable, Equatable { + let userId: String + let email: String + let cpf: String + let name: String + let accountNumber: String + let agency: String + let balance: String +} diff --git a/MyBankApp/MyBankApp/Login/Scene/LoginConfigurator.swift b/MyBankApp/MyBankApp/Login/Scene/LoginConfigurator.swift new file mode 100644 index 000000000..08eca675c --- /dev/null +++ b/MyBankApp/MyBankApp/Login/Scene/LoginConfigurator.swift @@ -0,0 +1,21 @@ +import UIKit + +final class LoginConfigurator { + func setup() -> LoginViewController { + let viewController = LoginViewController() + let interactor = LoginInteractor() + let presenter = LoginPresenter() + let router = LoginRouter() + + viewController.interactor = interactor + viewController.router = router + + interactor.presenter = presenter + presenter.viewController = viewController + + router.viewController = viewController + router.dataStore = interactor + + return viewController + } +} diff --git a/MyBankApp/MyBankApp/Login/Scene/LoginInteractor.swift b/MyBankApp/MyBankApp/Login/Scene/LoginInteractor.swift new file mode 100644 index 000000000..b55355fd8 --- /dev/null +++ b/MyBankApp/MyBankApp/Login/Scene/LoginInteractor.swift @@ -0,0 +1,30 @@ +import Foundation + +final class LoginInteractor: LoginBusinessLogic, LoginDataStore { + var presenter: LoginPresentationLogic? + var worker: LoginWorkerProtocol + + var user: LoginResponse? + + init( + presenter: LoginPresentationLogic? = nil, + worker: LoginWorkerProtocol = LoginWorker() + ) { + self.presenter = presenter + self.worker = worker + } + + func login(request: LoginRequest) { + /// Uncomment this line, and comment the loginService implementation above if Login API is not working, so the app on the simulator can work properly + presenter?.presentLoginSuccess() +// worker.login(request: request) { [weak self] result in +// switch result { +// case .success(let response): +// self?.user = response +// self?.presenter?.presentLoginSuccess() +// case .failure(let error): +// self?.presenter?.presentLoginError(message: error.localizedDescription) +// } +// } + } +} diff --git a/MyBankApp/MyBankApp/Login/Scene/LoginPresenter.swift b/MyBankApp/MyBankApp/Login/Scene/LoginPresenter.swift new file mode 100644 index 000000000..a7d4b95cc --- /dev/null +++ b/MyBankApp/MyBankApp/Login/Scene/LoginPresenter.swift @@ -0,0 +1,17 @@ +import Foundation + +final class LoginPresenter: LoginPresentationLogic { + weak var viewController: LoginDisplayLogic? + + init(viewController: LoginDisplayLogic? = nil) { + self.viewController = viewController + } + + func presentLoginSuccess() { + viewController?.displayLoginSuccess() + } + + func presentLoginError(message: String) { + viewController?.displayLoginError(message: message) + } +} diff --git a/MyBankApp/MyBankApp/Login/Scene/LoginProtocols.swift b/MyBankApp/MyBankApp/Login/Scene/LoginProtocols.swift new file mode 100644 index 000000000..32e61cc17 --- /dev/null +++ b/MyBankApp/MyBankApp/Login/Scene/LoginProtocols.swift @@ -0,0 +1,33 @@ +import Foundation + +// MARK: - LoginDisplayLogic +protocol LoginDisplayLogic: AnyObject { + func displayLoginSuccess() + func displayLoginError(message: String) +} + +// MARK: - LoginInteractor +protocol LoginBusinessLogic { + func login(request: LoginRequest) +} + +// MARK: - LoginPresenter +protocol LoginPresentationLogic { + func presentLoginSuccess() + func presentLoginError(message: String) +} + +// MARK: - LoginRouter +protocol LoginRoutingLogic { + func routeToHome() +} + +// MARK: - LoginDataStore +protocol LoginDataStore { + var user: LoginResponse? { get set } +} + +// MARK: - LoginDataPassing +protocol LoginDataPassing { + var dataStore: LoginDataStore? { get } +} diff --git a/MyBankApp/MyBankApp/Login/Scene/LoginRouter.swift b/MyBankApp/MyBankApp/Login/Scene/LoginRouter.swift new file mode 100644 index 000000000..bf83a911b --- /dev/null +++ b/MyBankApp/MyBankApp/Login/Scene/LoginRouter.swift @@ -0,0 +1,23 @@ +import UIKit + +class LoginRouter: NSObject, LoginRoutingLogic, LoginDataPassing { + weak var viewController: LoginViewController? + var dataStore: LoginDataStore? + + func routeToHome() { + let homeViewController = HomeConfigurator().setup() + var homeDataStore = homeViewController.router?.dataStore + let mockUserResponse = LoginResponse( + userId: "1", + email: "jose_da_silta@teste.com6", + cpf: "123.456.789-10", + name: "José da Silva Teste", + accountNumber: "2050 / 01.111222-4", + agency: "0", + balance: "R$ 1.000,00" + ) + homeDataStore?.user = mockUserResponse//dataStore?.user /// Uncomment this line if login API comes back to work + homeViewController.navigationItem.setHidesBackButton(true, animated: false) + viewController?.navigationController?.pushViewController(homeViewController, animated: true) + } +} diff --git a/MyBankApp/MyBankApp/Login/Scene/LoginViewController.swift b/MyBankApp/MyBankApp/Login/Scene/LoginViewController.swift new file mode 100644 index 000000000..8e037ba61 --- /dev/null +++ b/MyBankApp/MyBankApp/Login/Scene/LoginViewController.swift @@ -0,0 +1,162 @@ +import UIKit + +class LoginViewController: UIViewController { + var interactor: LoginBusinessLogic? + var router: (NSObjectProtocol & LoginRoutingLogic & LoginDataPassing)? + private var dispatchQueue: DispatchQueueProtocol + + // MARK: UI Elements + private let imageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "bankLogo") + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + let usernameTextField: UITextField = { + let textField = UITextField() + textField.placeholder = "Username" + textField.borderStyle = .roundedRect + textField.translatesAutoresizingMaskIntoConstraints = false + return textField + }() + + let passwordTextField: UITextField = { + let textField = UITextField() + textField.placeholder = "Password" + textField.borderStyle = .roundedRect + textField.isSecureTextEntry = true + textField.translatesAutoresizingMaskIntoConstraints = false + return textField + }() + + private let loginButton: UIButton = { + let button = UIButton() + button.setTitle("Login", for: .normal) + button.backgroundColor = .blue + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside) + return button + }() + + init( + interactor: LoginBusinessLogic? = nil, + router: LoginRouter? = nil, + dispatchQueue: DispatchQueueProtocol = MainDispatchQueueWrapper() + ) { + self.interactor = interactor + self.router = router + self.dispatchQueue = dispatchQueue + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: View Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + } + + // MARK: UI Setup + private func setupUI() { + view.backgroundColor = .white + addSubviews() + setupImageViewConstraints() + setupStackViewConstraints() + setupLoginButtonConstraints() + } + + private func addSubviews() { + view.addSubview(imageView) + view.addSubview(usernameTextField) + view.addSubview(passwordTextField) + view.addSubview(loginButton) + } + + private func setupImageViewConstraints() { + imageView.widthAnchor.constraint(equalToConstant: 100).isActive = true + imageView.heightAnchor.constraint(equalToConstant: 65).isActive = true + imageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40).isActive = true + imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + } + + private func setupStackViewConstraints() { + usernameTextField.heightAnchor.constraint(equalToConstant: 50).isActive = true + passwordTextField.heightAnchor.constraint(equalToConstant: 50).isActive = true + + let stackView = UIStackView(arrangedSubviews: [usernameTextField, passwordTextField]) + stackView.axis = .vertical + stackView.spacing = 16 + stackView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(stackView) + stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16).isActive = true + stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16).isActive = true + stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true + } + + private func setupLoginButtonConstraints() { + loginButton.heightAnchor.constraint(equalToConstant: 50).isActive = true + loginButton.widthAnchor.constraint(equalToConstant: 185).isActive = true + loginButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + loginButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40).isActive = true + } + + // MARK: Actions + @objc func loginButtonTapped() { + guard let username = usernameTextField.text, + let password = passwordTextField.text, + !username.isEmpty, + !password.isEmpty, + validateForm() + else { + displayLoginError(message: "Please fill user and password fields correctly!") + return + } + let request = LoginRequest(username: username, password: password) + interactor?.login(request: request) + } + + private func validateUserTextField() -> Bool { + let emailRegex = "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" + let numberRegex = "^\\d{11}$" + let userText = usernameTextField.text ?? "" + + if userText.isValidEmail() { + return true + } else if userText.isValidCPF() { + return true + } else { + return false + } + } + + private func validatePasswordTextField() -> Bool { + let password = passwordTextField.text ?? "" + return password.count >= 3 && password.containsUppercaseLetter && password.containsSpecialCharacter + } + + private func validateForm() -> Bool { + let isUserValid = validateUserTextField() + let isPasswordValid = validatePasswordTextField() + + return isUserValid && isPasswordValid + } +} + +extension LoginViewController: LoginDisplayLogic { + func displayLoginSuccess() { + router?.routeToHome() + } + + func displayLoginError(message: String) { + let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + dispatchQueue.async { [weak self] in + self?.present(alertController, animated: true, completion: nil) + } + } +} diff --git a/MyBankApp/MyBankApp/Login/Scene/LoginWorker.swift b/MyBankApp/MyBankApp/Login/Scene/LoginWorker.swift new file mode 100644 index 000000000..1b8b28a57 --- /dev/null +++ b/MyBankApp/MyBankApp/Login/Scene/LoginWorker.swift @@ -0,0 +1,18 @@ +import Foundation + +protocol LoginWorkerProtocol { + func login(request: LoginRequest, completion: @escaping (Result) -> Void) +} + +final class LoginWorker: LoginWorkerProtocol { + + private var loginService: LoginServiceProtocol + + init(loginService: LoginServiceProtocol = LoginService()) { + self.loginService = loginService + } + + func login(request: LoginRequest, completion: @escaping (Result) -> Void) { + loginService.login(user: request.username, password: request.password, completion: completion) + } +} diff --git a/MyBankApp/MyBankApp/Login/Service/LoginService.swift b/MyBankApp/MyBankApp/Login/Service/LoginService.swift new file mode 100644 index 000000000..df41a9dc5 --- /dev/null +++ b/MyBankApp/MyBankApp/Login/Service/LoginService.swift @@ -0,0 +1,34 @@ +import Foundation + +protocol LoginServiceProtocol { + func login(user: String, password: String, completion: @escaping (Result) -> Void) +} + +final class LoginService: LoginServiceProtocol { + private let networkService: NetworkService + + init(networkService: NetworkService = NetworkServiceImpl()) { + self.networkService = networkService + } + + func login( + user: String, + password: String, + completion: @escaping (Result) -> Void + ) { + guard let url = URL(string: "https://6092aef785ff5100172136c2.mockapi.io/api/login") else { + completion(.failure(.invalidResponse)) + return + } + + let bodyParameters = ["user": user, "password": password] + + networkService.request( + url: url, + method: .post, + bodyParameters: bodyParameters + ) { (result: Result) in + completion(result) + } + } +} diff --git a/MyBankApp/MyBankApp/Network/HTTPMethod.swift b/MyBankApp/MyBankApp/Network/HTTPMethod.swift new file mode 100644 index 000000000..e45d8bf46 --- /dev/null +++ b/MyBankApp/MyBankApp/Network/HTTPMethod.swift @@ -0,0 +1,6 @@ +import Foundation + +enum HTTPMethod: String { + case get = "GET" + case post = "POST" +} diff --git a/MyBankApp/MyBankApp/Network/NetworkError.swift b/MyBankApp/MyBankApp/Network/NetworkError.swift new file mode 100644 index 000000000..d4632277b --- /dev/null +++ b/MyBankApp/MyBankApp/Network/NetworkError.swift @@ -0,0 +1,24 @@ +import Foundation + +enum NetworkError: Error { + case requestFailed + case invalidResponse + case decodingFailed + case noData + case unknownError + + var localizedDescription: String { + switch self { + case .requestFailed: + return "Request failed" + case .invalidResponse: + return "Invalid response received" + case .decodingFailed: + return "Decoding failed" + case .noData: + return "No Data" + case .unknownError: + return "Unknown Error" + } + } +} diff --git a/MyBankApp/MyBankApp/Network/NetworkProtocols.swift b/MyBankApp/MyBankApp/Network/NetworkProtocols.swift new file mode 100644 index 000000000..9f02f0202 --- /dev/null +++ b/MyBankApp/MyBankApp/Network/NetworkProtocols.swift @@ -0,0 +1,26 @@ +import Foundation + +protocol NetworkService { + func request( + url: URL, + method: HTTPMethod, + bodyParameters: [String: Any]?, + completion: @escaping (Result) -> Void + ) +} + +protocol URLSessionProtocol { + func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTaskProtocol +} + +protocol URLSessionDataTaskProtocol { + func resume() +} + +extension URLSession: URLSessionProtocol { + func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTaskProtocol { + return dataTask(with: request, completionHandler: completionHandler) as URLSessionDataTask + } +} + +extension URLSessionDataTask: URLSessionDataTaskProtocol {} diff --git a/MyBankApp/MyBankApp/Network/NetworkServiceImpl.swift b/MyBankApp/MyBankApp/Network/NetworkServiceImpl.swift new file mode 100644 index 000000000..c63ed24e5 --- /dev/null +++ b/MyBankApp/MyBankApp/Network/NetworkServiceImpl.swift @@ -0,0 +1,54 @@ +import Foundation + +final class NetworkServiceImpl: NetworkService { + private let urlSession: URLSessionProtocol + + init(urlSession: URLSessionProtocol = URLSession.shared) { + self.urlSession = urlSession + } + + func request( + url: URL, + method: HTTPMethod, + bodyParameters: [String: Any]?, + completion: @escaping (Result) -> Void + ) { + var request = URLRequest(url: url) + request.httpMethod = method.rawValue + + // Set body parameters if available + if let bodyParameters = bodyParameters { + request.httpBody = try? JSONSerialization.data(withJSONObject: bodyParameters) + } + + let task = urlSession.dataTask(with: request) { data, response, error in + // Check for network errors + if let _ = error { + completion(.failure(NetworkError.requestFailed)) + return + } + + // Check for invalid response + guard let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode) else { + completion(.failure(NetworkError.invalidResponse)) + return + } + + // Parse the response data + guard let responseData = data else { + completion(.failure(NetworkError.decodingFailed)) + return + } + + do { + let decodedObject = try JSONDecoder().decode(T.self, from: responseData) + completion(.success(decodedObject)) + } catch { + completion(.failure(NetworkError.decodingFailed)) + } + } + + task.resume() + } +} diff --git a/MyBankApp/MyBankApp/SceneDelegate.swift b/MyBankApp/MyBankApp/SceneDelegate.swift new file mode 100644 index 000000000..902d4c68d --- /dev/null +++ b/MyBankApp/MyBankApp/SceneDelegate.swift @@ -0,0 +1,51 @@ +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + @available(iOS 13.0, *) + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + window = UIWindow(windowScene: windowScene) + let rootViewController = UINavigationController(rootViewController: LoginConfigurator().setup()) + window?.rootViewController = rootViewController + window?.makeKeyAndVisible() + } + + @available(iOS 13.0, *) + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + @available(iOS 13.0, *) + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + @available(iOS 13.0, *) + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + @available(iOS 13.0, *) + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + @available(iOS 13.0, *) + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/MyBankApp/MyBankAppTests/Common/DispatchQueueSpy.swift b/MyBankApp/MyBankAppTests/Common/DispatchQueueSpy.swift new file mode 100644 index 000000000..9a37f4f49 --- /dev/null +++ b/MyBankApp/MyBankAppTests/Common/DispatchQueueSpy.swift @@ -0,0 +1,9 @@ +import XCTest +@testable import MyBankApp + +typealias DispatchQueueDummy = DispatchQueueSpy +class DispatchQueueSpy: DispatchQueueProtocol { + func async(execute work: @escaping () -> Void) { + work() + } +} diff --git a/MyBankApp/MyBankAppTests/Common/NavigationControllerSpy.swift b/MyBankApp/MyBankAppTests/Common/NavigationControllerSpy.swift new file mode 100644 index 000000000..b39eaed10 --- /dev/null +++ b/MyBankApp/MyBankAppTests/Common/NavigationControllerSpy.swift @@ -0,0 +1,116 @@ +import UIKit + +final class NavigationControllerSpy: UINavigationController { + enum Methods: Equatable { + case show(UIViewController, Any?) + case presentViewController(UIViewController, animated: Bool) + case popToViewController(UIViewController, animated: Bool) + case pushViewController(UIViewController, animated: Bool) + case popToRootViewController(animated: Bool) + case popViewController(animated: Bool) + case dismiss(animated: Bool) + case setViewControllers([UIViewController], animated: Bool) + // MARK: View Type Helpers + case presentViewControllerType(UIViewController.Type, animated: Bool) + case popToViewControllerType(UIViewController.Type, animated: Bool) + case pushViewControllerType(UIViewController.Type, animated: Bool) + } + + private(set) var calledMethods: [Methods] = [] + private func called(_ method: Methods) { + calledMethods.append(method) + } + + public init() { + super.init(nibName: nil, bundle: nil) + } + + public override init(rootViewController: UIViewController) { + super.init(rootViewController: rootViewController) + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + nil + } + + // MARK: - pushViewController + public override func pushViewController(_ viewController: UIViewController, animated: Bool) { + called(.pushViewController(viewController, animated: animated)) + super.pushViewController(viewController, animated: animated) + } + + // MARK: - popToRootViewController + public var popToRootViewControllerViewControllersToBeReturned: [UIViewController]? + public override func popToRootViewController(animated: Bool) -> [UIViewController]? { + called(.popToRootViewController(animated: animated)) + return popToRootViewControllerViewControllersToBeReturned + } + + // MARK: - setViewControllers + public var setViewControllersToBeReturned: [UIViewController]? + public override func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) { + super.setViewControllers(viewControllers, animated: animated) + called(.setViewControllers(viewControllers, animated: animated)) + setViewControllersToBeReturned = viewControllers + } + + // MARK: - popViewController + public var popViewControllerToBeReturned: UIViewController? + public override func popViewController(animated: Bool) -> UIViewController? { + called(.popViewController(animated: animated)) + return popViewControllerToBeReturned + } + + // MARK: - show + public override func show(_ vc: UIViewController, sender: Any?) { + called(.show(vc, sender)) + } + + // MARK: - popToViewController + public var popToViewControllersToBeReturned: [UIViewController]? + public override func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? { + called(.popToViewController(viewController, animated: animated)) + return popToViewControllersToBeReturned + } + + // MARK: - present + public override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) { + called(.presentViewController(viewControllerToPresent, animated: flag)) + completion?() + } + + // MARK: - dismiss + public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + called(.dismiss(animated: flag)) + completion?() + } +} + +extension NavigationControllerSpy.Methods { + static func == (_ lhs: Self, _ rhs: Self) -> Bool { + switch (lhs, rhs) { + case let (.presentViewControllerType(vcType, lhsA), .presentViewController(vc, rhsA)), + let (.popToViewControllerType(vcType, lhsA), .popToViewController(vc, rhsA)), + let (.pushViewControllerType(vcType, lhsA), .pushViewController(vc, rhsA)), + let (.presentViewController(vc, lhsA), .presentViewControllerType(vcType, rhsA)), + let (.popToViewController(vc, lhsA), .popToViewControllerType(vcType, rhsA)), + let (.pushViewController(vc, lhsA), .pushViewControllerType(vcType, rhsA)): + return lhsA == rhsA && String(describing: vc.classForCoder) == String(describing: vcType) + case let (.presentViewControllerType(lhsT, lhsA), .presentViewControllerType(rhsT, rhsA)), + let (.popToViewControllerType(lhsT, lhsA), .popToViewControllerType(rhsT, rhsA)), + let (.pushViewControllerType(lhsT, lhsA), .pushViewControllerType(rhsT, rhsA)): + return lhsA == rhsA && String(describing: lhsT) == String(describing: rhsT) + case let (.presentViewController(lhsVC, lhsA), .presentViewController(rhsVC, rhsA)), + let (.popToViewController(lhsVC, lhsA), .popToViewController(rhsVC, rhsA)), + let (.pushViewController(lhsVC, lhsA), .pushViewController(rhsVC, rhsA)): + return lhsVC == rhsVC && lhsA == rhsA + case let (.popToRootViewController(lhs), .popToRootViewController(rhs)), + let (.popViewController(lhs), .popViewController(rhs)), + let (.dismiss(lhs), .dismiss(rhs)): + return lhs == rhs + default: + return false + } + } +} diff --git a/MyBankApp/MyBankAppTests/Home/HomeServiceTests.swift b/MyBankApp/MyBankAppTests/Home/HomeServiceTests.swift new file mode 100644 index 000000000..d45420810 --- /dev/null +++ b/MyBankApp/MyBankAppTests/Home/HomeServiceTests.swift @@ -0,0 +1,81 @@ +@testable import MyBankApp +import XCTest + +final class HomeServiceTests: XCTestCase { + private var sut: HomeService! + private var networkServiceSpy: NetworkServiceSpy! + + override func setUp() { + super.setUp() + networkServiceSpy = NetworkServiceSpy() + sut = HomeService(networkService: networkServiceSpy) + } + + override func tearDown() { + sut = nil + networkServiceSpy = nil + super.tearDown() + } + + func test_fetchStatements_givenSuccessResponse_whenValidUrlAndValidMethod_shouldSucceedWithValidStatements() { + // Given + let expectedStatements = [ + Statement(id: 1, type: "Expense", date: "2023-06-30", detail: "Groceries", value: 50.0), + Statement(id: 2, type: "Income", date: "2023-06-29", detail: "Salary", value: 2000.0) + ] + + let responseJSON = """ + { + "statement": [ + {"id": 1, "type": "Expense", "date": "2023-06-30", "detail": "Groceries", "value": 50.0}, + {"id": 2, "type": "Income", "date": "2023-06-29", "detail": "Salary", "value": 2000.0} + ] + } + """ + + let responseData = responseJSON.data(using: .utf8)! + networkServiceSpy.mockedResult = .success(responseData) + + // When + var capturedResult: Result? + sut.fetchStatements(for: "user123") { result in + capturedResult = result + } + + // Then + XCTAssertEqual(networkServiceSpy.capturedURL?.absoluteString, "https://6092aef785ff5100172136c2.mockapi.io/api/statements") + XCTAssertEqual(networkServiceSpy.capturedMethod, .get) + switch capturedResult { + case .success(let statements): + XCTAssertEqual(statements.statement, expectedStatements) + case .failure(let error): + XCTFail("Expected success, but received failure with error: \(error)") + case .none: + XCTFail("Result is nil") + } + } + + func test_fetchStatements_givenFailureResponse_shouldFailWithExpectedError() { + // Given + let expectedError = NetworkError.requestFailed + networkServiceSpy.mockedResult = .failure(expectedError) + + // When + var capturedResult: Result? + sut.fetchStatements(for: "user123") { result in + capturedResult = result + } + + // Then + XCTAssertEqual(networkServiceSpy.capturedURL?.absoluteString, "https://6092aef785ff5100172136c2.mockapi.io/api/statements") + XCTAssertEqual(networkServiceSpy.capturedMethod, .get) + switch capturedResult { + case .success(let statements): + XCTFail("Expected failure, but received success with statements: \(statements)") + case .failure(let error): + XCTAssertEqual(error, expectedError) + case .none: + XCTFail("Result is nil") + } + } +} diff --git a/MyBankApp/MyBankAppTests/Login/Fixtures/LoginResponse+Fixture.swift b/MyBankApp/MyBankAppTests/Login/Fixtures/LoginResponse+Fixture.swift new file mode 100644 index 000000000..69756f72f --- /dev/null +++ b/MyBankApp/MyBankAppTests/Login/Fixtures/LoginResponse+Fixture.swift @@ -0,0 +1,23 @@ +@testable import MyBankApp + +extension LoginResponse { + static func fixture( + userId: String = "1", + email: String = "test@test.com", + cpf: String = "123.456.789-10", + name: String = "Test", + accountNumber: String = "123456", + agency: String = "123", + balance: String = "R$ 1.000,00" + ) -> LoginResponse { + .init( + userId: userId, + email: email, + cpf: cpf, + name: name, + accountNumber: accountNumber, + agency: agency, + balance: balance + ) + } +} diff --git a/MyBankApp/MyBankAppTests/Login/LoginConfiguratorTests.swift b/MyBankApp/MyBankAppTests/Login/LoginConfiguratorTests.swift new file mode 100644 index 000000000..a431289ca --- /dev/null +++ b/MyBankApp/MyBankAppTests/Login/LoginConfiguratorTests.swift @@ -0,0 +1,62 @@ +import XCTest +@testable import MyBankApp + +final class LoginConfiguratorTests: XCTestCase { + + private var sut: LoginConfigurator! + + override func setUp() { + super.setUp() + sut = LoginConfigurator() + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + func test_setup() { + // When + let viewController = sut.setup() + + // Then + XCTAssertNotNil(viewController) + + let interactor = Mirror(reflecting: viewController).firstChild(of: LoginInteractor.self, in: "interactor") + XCTAssertNotNil(interactor) + XCTAssertTrue(interactor?.presenter is LoginPresenter) + + let presenter = Mirror(reflecting: interactor!).firstChild(of: LoginPresenter.self, in: "presenter") + XCTAssertNotNil(presenter) + XCTAssertTrue(presenter?.viewController === viewController) + + let router = Mirror(reflecting: viewController).firstChild(of: LoginRouter.self, in: "router") + XCTAssertNotNil(router) + XCTAssertTrue(router?.viewController === viewController) + XCTAssertTrue(router?.dataStore as? LoginInteractor === interactor) + } + +/// * +/// This next test is another approach for the same purpose +/// * +/// func test_setup2() { +/// // When +/// let viewController = sut.setup() +/// +/// // Then +/// XCTAssertNotNil(viewController) +/// +/// let interactor = viewController.interactor as? LoginInteractor +/// XCTAssertNotNil(interactor) +/// XCTAssertTrue(interactor?.presenter is LoginPresenter) +/// +/// let presenter = interactor?.presenter as? LoginPresenter +/// XCTAssertNotNil(presenter) +/// XCTAssertTrue(presenter?.viewController === viewController) +/// +/// let router = viewController.router as? LoginRouter +/// XCTAssertNotNil(router) +/// XCTAssertTrue(router?.viewController === viewController) +/// } +} + diff --git a/MyBankApp/MyBankAppTests/Login/LoginInteractorTests.swift b/MyBankApp/MyBankAppTests/Login/LoginInteractorTests.swift new file mode 100644 index 000000000..2bee85793 --- /dev/null +++ b/MyBankApp/MyBankAppTests/Login/LoginInteractorTests.swift @@ -0,0 +1,40 @@ +import XCTest +@testable import MyBankApp + +final class LoginInteractorTests: XCTestCase { + private var sut: LoginInteractor! + private var presenterSpy: LoginPresentationLogicSpy! + private var workerSpy: LoginWorkerProtocolSpy! + + override func setUp() { + super.setUp() + presenterSpy = LoginPresentationLogicSpy() + workerSpy = LoginWorkerProtocolSpy() + sut = LoginInteractor(presenter: presenterSpy, worker: workerSpy) + } + + override func tearDown() { + presenterSpy = nil + workerSpy = nil + sut = nil + super.tearDown() + } + + func test_login_givenLoginSuccess_shouldCallPresentLoginSuccess() { + workerSpy.completionToBeReturned = .success(.fixture()) + + sut.login(request: LoginRequest(username: "", password: "")) + + XCTAssertNotNil(sut.user) + XCTAssertEqual(presenterSpy.calledMethods, [.presentLoginSuccess]) + } + + func test_login_givenLoginFailure_shouldCallPresentLoginError() { + workerSpy.completionToBeReturned = .failure(.requestFailed) + + sut.login(request: LoginRequest(username: "", password: "")) + + XCTAssertNil(sut.user) + XCTAssertEqual(presenterSpy.calledMethods, [.presentLoginError(message: "Request failed")]) + } +} diff --git a/MyBankApp/MyBankAppTests/Login/LoginPresenterTests.swift b/MyBankApp/MyBankAppTests/Login/LoginPresenterTests.swift new file mode 100644 index 000000000..2ca226d91 --- /dev/null +++ b/MyBankApp/MyBankAppTests/Login/LoginPresenterTests.swift @@ -0,0 +1,31 @@ +import XCTest +@testable import MyBankApp + +final class LoginPresenterTests: XCTestCase { + private var sut: LoginPresenter! + private var viewControllerSpy: LoginDisplayLogicSpy! + + override func setUp() { + super.setUp() + viewControllerSpy = LoginDisplayLogicSpy() + sut = LoginPresenter(viewController: viewControllerSpy) + } + + override func tearDown() { + viewControllerSpy = nil + sut = nil + super.tearDown() + } + + func test_presentLoginSuccess_shouldCallViewControllerpresentLoginSuccess() { + sut.presentLoginSuccess() + + XCTAssertEqual(viewControllerSpy.calledMethods, [.displayLoginSuccess]) + } + + func test_presentLoginError_shouldCallViewControllerpresentLoginErrorWithMessage() { + sut.presentLoginError(message: "message") + + XCTAssertEqual(viewControllerSpy.calledMethods, [.displayLoginError(message: "message")]) + } +} diff --git a/MyBankApp/MyBankAppTests/Login/LoginRouterTests.swift b/MyBankApp/MyBankAppTests/Login/LoginRouterTests.swift new file mode 100644 index 000000000..898091dce --- /dev/null +++ b/MyBankApp/MyBankAppTests/Login/LoginRouterTests.swift @@ -0,0 +1,30 @@ +import XCTest +@testable import MyBankApp + +final class LoginRouterTests: XCTestCase { + private var sut: LoginRouter! + var viewController: LoginViewControllerSpy! + + override func setUp() { + super.setUp() + sut = LoginRouter() + viewController = LoginViewControllerSpy() + sut.viewController = viewController + } + + override func tearDown() { + viewController = nil + sut = nil + super.tearDown() + } + + func test_routeToHome_viewControllerShouldRouteToHomeViewController() { + let navigationControllerSpy = NavigationControllerSpy() + viewController.navigationControllerSpy = navigationControllerSpy + + sut.routeToHome() + + XCTAssertEqual(navigationControllerSpy.calledMethods, [.pushViewControllerType(HomeViewController.self, animated: true)]) + } + +} diff --git a/MyBankApp/MyBankAppTests/Login/LoginServiceTests.swift b/MyBankApp/MyBankAppTests/Login/LoginServiceTests.swift new file mode 100644 index 000000000..d0af84468 --- /dev/null +++ b/MyBankApp/MyBankAppTests/Login/LoginServiceTests.swift @@ -0,0 +1,91 @@ +@testable import MyBankApp +import XCTest + +final class LoginServiceTests: XCTestCase { + private var sut: LoginService! + private var networkServiceSpy: NetworkServiceSpy! + + override func setUp() { + super.setUp() + networkServiceSpy = NetworkServiceSpy() + sut = LoginService(networkService: networkServiceSpy) + } + + override func tearDown() { + sut = nil + networkServiceSpy = nil + super.tearDown() + } + + func test_givenLoginSuccess_shouldReturnValidLoginResponse() { + // Given + let expectedID = "user123" + let expectedEmail = "fulano@teste.com" + let expectedCpf = "123.456.789-10" + let expectedName = "Fulano de Tail" + let expectedAccountNumber = "123456" + let expectedAgency = "123" + let expectedBalance = "R$ 1.000,00" + let responseJSON = """ + { + "userId": "\(expectedID)", + "email": "\(expectedEmail)", + "cpf": "\(expectedCpf)", + "name": "\(expectedName)", + "accountNumber": "\(expectedAccountNumber)", + "agency": "\(expectedAgency)", + "balance": "\(expectedBalance)" + } + """ + let responseData = responseJSON.data(using: .utf8)! + networkServiceSpy.mockedResult = .success(responseData) + + // When + var capturedResult: Result? + sut.login(user: "testuser", password: "testpassword") { result in + capturedResult = result + } + + // Then + XCTAssertEqual(networkServiceSpy.capturedURL?.absoluteString, "https://6092aef785ff5100172136c2.mockapi.io/api/login") + XCTAssertEqual(networkServiceSpy.capturedMethod, .post) + switch capturedResult { + case let .success(response): + XCTAssertEqual(response.userId, expectedID) + XCTAssertEqual(response.email, expectedEmail) + XCTAssertEqual(response.cpf, expectedCpf) + XCTAssertEqual(response.name, expectedName) + XCTAssertEqual(response.accountNumber, expectedAccountNumber) + XCTAssertEqual(response.agency, expectedAgency) + XCTAssertEqual(response.balance, expectedBalance) + case .failure(_): + XCTFail() + case .none: + XCTFail() + } + } + + func test_givenLoginFailure_shouldFailWithExpectedError() { + // Given + let expectedError = NetworkError.requestFailed + networkServiceSpy.mockedResult = .failure(expectedError) + + // When + var capturedResult: Result? + sut.login(user: "testuser", password: "testpassword") { result in + capturedResult = result + } + + // Then + XCTAssertEqual(networkServiceSpy.capturedURL?.absoluteString, "https://6092aef785ff5100172136c2.mockapi.io/api/login") + XCTAssertEqual(networkServiceSpy.capturedMethod, .post) + switch capturedResult { + case .success(_): + XCTFail() + case let .failure(error): + XCTAssertEqual(error, expectedError) + case .none: + XCTFail() + } + } +} diff --git a/MyBankApp/MyBankAppTests/Login/LoginViewControllerTests.swift b/MyBankApp/MyBankAppTests/Login/LoginViewControllerTests.swift new file mode 100644 index 000000000..6bd2b94cb --- /dev/null +++ b/MyBankApp/MyBankAppTests/Login/LoginViewControllerTests.swift @@ -0,0 +1,88 @@ +import XCTest +import SnapshotTesting +@testable import MyBankApp + +final class LoginViewControllerTests: XCTestCase { + + private var sut: LoginViewController! + private var interactorSpy: LoginBusinessLogicSpy! + private var routerSpy: LoginRouterSpy! + + override func setUp() { + super.setUp() + interactorSpy = LoginBusinessLogicSpy() + routerSpy = LoginRouterSpy() + sut = LoginViewController( + interactor: interactorSpy, + router: routerSpy + ) + isRecording = false + } + + override func tearDown() { + interactorSpy = nil + routerSpy = nil + sut = nil + super.tearDown() + } + + func test_viewDidLoad_shouldMatchSnapshot() { + sut.viewDidLoad() + + assertSnapshot(matching: sut, as: .image) + } + + func test_givenDisplayLoginSuccess_shouldRouteToHome() { + sut.displayLoginSuccess() + + XCTAssertEqual(routerSpy.calledMethods, [.routeToHome]) + } + + func test_givenUserNameWithValidCPFlAndPasswordAreFilled_shouldCallInteractorLogin() { + //Given + sut.usernameTextField.text = "699.876.200-35" + sut.passwordTextField.text = "T3st!ng" + + //When + sut.loginButtonTapped() + + //Then + XCTAssertEqual(interactorSpy.calledMethods, [.login(LoginRequest(username: "699.876.200-35", password: "T3st!ng"))]) + } + + func test_givenUserNameWithInvalidEmailAndPasswordAreFilled_shouldCallInteractorLogin() { + //Given + sut.usernameTextField.text = "test@testcom" + sut.passwordTextField.text = "T3st!ng" + + //When + sut.loginButtonTapped() + + //Then + XCTAssertEqual(interactorSpy.calledMethods, []) + } + + func test_givenUserNameAndPasswordAreFilled_shouldCallInteractorLogin() { + //Given + sut.usernameTextField.text = "test@test.com" + sut.passwordTextField.text = "T3st!ng" + + //When + sut.loginButtonTapped() + + //Then + XCTAssertEqual(interactorSpy.calledMethods, [.login(LoginRequest(username: "699.876.200-35", password: "T3st!ng"))]) + } + + func test_givenUserNameOrPasswordAreNotFilled_shouldNotCallInteractorLogin() { + //Given + sut.usernameTextField.text = "test@test.com" + sut.passwordTextField.text = "" + + //When + sut.loginButtonTapped() + + //Then + XCTAssertEqual(interactorSpy.calledMethods, []) + } +} diff --git a/MyBankApp/MyBankAppTests/Login/LoginWorkerTests.swift b/MyBankApp/MyBankAppTests/Login/LoginWorkerTests.swift new file mode 100644 index 000000000..7952e82cf --- /dev/null +++ b/MyBankApp/MyBankAppTests/Login/LoginWorkerTests.swift @@ -0,0 +1,64 @@ +@testable import MyBankApp +import XCTest + +final class LoginWorkerTests: XCTestCase { + private var sut: LoginWorker! + private var loginServiceSpy: LoginServiceSpy! + + override func setUp() { + super.setUp() + loginServiceSpy = LoginServiceSpy() + sut = LoginWorker(loginService: loginServiceSpy) + } + + override func tearDown() { + sut = nil + loginServiceSpy = nil + super.tearDown() + } + + func testLoginWithValidCredentials() { + // Given + let request = LoginRequest(username: "", password: "") + let expectedResponse = LoginResponse.fixture() + + loginServiceSpy.completionToBeReturned = .success(expectedResponse) + + // When + let expectation = XCTestExpectation(description: "Login request") + sut.login(request: request) { result in + // Then + switch result { + case .success(let response): + XCTAssertEqual(response, expectedResponse) + case .failure(let error): + XCTFail("Login request failed with error: \(error)") + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1.0) + } + + func testLoginWithInvalidCredentials() { + // Given + let request = LoginRequest(username: "", password: "") + + loginServiceSpy.completionToBeReturned = .failure(.invalidResponse) + + // When + let expectation = XCTestExpectation(description: "Login request") + sut.login(request: request) { result in + // Then + switch result { + case .success(_): + XCTFail("Login request should fail with invalid credentials") + case .failure(let error): + XCTAssertEqual(error, .invalidResponse) + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1.0) + } +} diff --git a/MyBankApp/MyBankAppTests/Login/Spies/LoginBusinessLogicSpy.swift b/MyBankApp/MyBankAppTests/Login/Spies/LoginBusinessLogicSpy.swift new file mode 100644 index 000000000..729bb4480 --- /dev/null +++ b/MyBankApp/MyBankAppTests/Login/Spies/LoginBusinessLogicSpy.swift @@ -0,0 +1,14 @@ +import XCTest +@testable import MyBankApp + +final class LoginBusinessLogicSpy: LoginBusinessLogic { + enum Methods: Equatable { + case login(LoginRequest) + } + + private(set) var calledMethods: [Methods] = [] + + func login(request: LoginRequest) { + calledMethods.append(.login(request)) + } +} diff --git a/MyBankApp/MyBankAppTests/Login/Spies/LoginDisplayLogicSpy.swift b/MyBankApp/MyBankAppTests/Login/Spies/LoginDisplayLogicSpy.swift new file mode 100644 index 000000000..7894811ba --- /dev/null +++ b/MyBankApp/MyBankAppTests/Login/Spies/LoginDisplayLogicSpy.swift @@ -0,0 +1,19 @@ +import XCTest +@testable import MyBankApp + +final class LoginDisplayLogicSpy: LoginDisplayLogic { + enum Method: Equatable { + case displayLoginSuccess + case displayLoginError(message: String) + } + + private(set) var calledMethods: [Method] = [] + + func displayLoginSuccess() { + calledMethods.append(.displayLoginSuccess) + } + + func displayLoginError(message: String) { + calledMethods.append(.displayLoginError(message: message)) + } +} diff --git a/MyBankApp/MyBankAppTests/Login/Spies/LoginPresentationLogicSpy.swift b/MyBankApp/MyBankAppTests/Login/Spies/LoginPresentationLogicSpy.swift new file mode 100644 index 000000000..04ae33aba --- /dev/null +++ b/MyBankApp/MyBankAppTests/Login/Spies/LoginPresentationLogicSpy.swift @@ -0,0 +1,18 @@ +import XCTest +@testable import MyBankApp + +final class LoginPresentationLogicSpy: LoginPresentationLogic { + enum Method: Equatable { + case presentLoginSuccess + case presentLoginError(message: String) + } + private(set) var calledMethods: [Method] = [] + + func presentLoginSuccess() { + calledMethods.append(.presentLoginSuccess) + } + + func presentLoginError(message: String) { + calledMethods.append(.presentLoginError(message: message)) + } +} diff --git a/MyBankApp/MyBankAppTests/Login/Spies/LoginRouterSpy.swift b/MyBankApp/MyBankAppTests/Login/Spies/LoginRouterSpy.swift new file mode 100644 index 000000000..34f734123 --- /dev/null +++ b/MyBankApp/MyBankAppTests/Login/Spies/LoginRouterSpy.swift @@ -0,0 +1,14 @@ +import XCTest +@testable import MyBankApp + +final class LoginRouterSpy: LoginRouter { + enum Method: Equatable { + case routeToHome + } + + private(set) var calledMethods: [Method] = [] + + override func routeToHome() { + calledMethods.append(.routeToHome) + } +} diff --git a/MyBankApp/MyBankAppTests/Login/Spies/LoginServiceSpy.swift b/MyBankApp/MyBankAppTests/Login/Spies/LoginServiceSpy.swift new file mode 100644 index 000000000..08680a88f --- /dev/null +++ b/MyBankApp/MyBankAppTests/Login/Spies/LoginServiceSpy.swift @@ -0,0 +1,15 @@ +import XCTest +@testable import MyBankApp + +final class LoginServiceSpy: LoginServiceProtocol { + enum Method: Equatable { + case login(user: String, password: String) + } + private(set) var calledMethods: [Method] = [] + + var completionToBeReturned: Result = .success(.fixture()) + func login(user: String, password: String, completion: @escaping (Result) -> Void) { + calledMethods.append(.login(user: user, password: password)) + completion(completionToBeReturned) + } +} diff --git a/MyBankApp/MyBankAppTests/Login/Spies/LoginViewControllerSpy.swift b/MyBankApp/MyBankAppTests/Login/Spies/LoginViewControllerSpy.swift new file mode 100644 index 000000000..ec0ed7d26 --- /dev/null +++ b/MyBankApp/MyBankAppTests/Login/Spies/LoginViewControllerSpy.swift @@ -0,0 +1,10 @@ +import XCTest +@testable import MyBankApp + +final class LoginViewControllerSpy: LoginViewController { + var navigationControllerSpy: NavigationControllerSpy? + + override var navigationController: UINavigationController? { + return navigationControllerSpy + } +} diff --git a/MyBankApp/MyBankAppTests/Login/Spies/LoginWorkerProtocolSpy.swift b/MyBankApp/MyBankAppTests/Login/Spies/LoginWorkerProtocolSpy.swift new file mode 100644 index 000000000..608fa298c --- /dev/null +++ b/MyBankApp/MyBankAppTests/Login/Spies/LoginWorkerProtocolSpy.swift @@ -0,0 +1,15 @@ +@testable import MyBankApp + +final class LoginWorkerProtocolSpy: LoginWorkerProtocol { + enum Method: Equatable { + case login(LoginRequest) + } + + private(set) var calledMethods: [Method] = [] + + var completionToBeReturned: Result = .success(.fixture()) + func login(request: MyBankApp.LoginRequest, completion: @escaping (Result) -> Void) { + calledMethods.append(.login(request)) + completion(completionToBeReturned) + } +} diff --git a/MyBankApp/MyBankAppTests/Login/__Snapshots__/LoginViewControllerTests/test_viewDidLoad_shouldMatchSnapshot.1.png b/MyBankApp/MyBankAppTests/Login/__Snapshots__/LoginViewControllerTests/test_viewDidLoad_shouldMatchSnapshot.1.png new file mode 100644 index 000000000..b5ec2527d Binary files /dev/null and b/MyBankApp/MyBankAppTests/Login/__Snapshots__/LoginViewControllerTests/test_viewDidLoad_shouldMatchSnapshot.1.png differ diff --git a/MyBankApp/MyBankAppTests/Network/NetworkServiceImplTests.swift b/MyBankApp/MyBankAppTests/Network/NetworkServiceImplTests.swift new file mode 100644 index 000000000..f837c3c79 --- /dev/null +++ b/MyBankApp/MyBankAppTests/Network/NetworkServiceImplTests.swift @@ -0,0 +1,109 @@ +import XCTest +@testable import MyBankApp + +final class NetworkServiceImplTests: XCTestCase { + + var sut: NetworkServiceImpl! + var urlSessionMock: URLSessionSpy! + + override func setUp() { + super.setUp() + urlSessionMock = URLSessionSpy() + sut = NetworkServiceImpl(urlSession: urlSessionMock) + } + + override func tearDown() { + sut = nil + urlSessionMock = nil + super.tearDown() + } + + func test_request_givenSuccessfulResponseWithValidData_shouldReturnSuccess() { + // Given + let expectedData = try? JSONEncoder().encode(LoginResponse.fixture()) + let expectedResponse = HTTPURLResponse(url: URL(string: "https://example.com")!, statusCode: 200, httpVersion: nil, headerFields: nil) + urlSessionMock.mockedData = expectedData + urlSessionMock.mockedResponse = expectedResponse + + // When + sut.request(url: URL(string: "https://example.com")!, method: .get, bodyParameters: nil) { (result: Result) in + // Then + switch result { + case .success(let data): + XCTAssertEqual(data, LoginResponse.fixture()) + case .failure(_): + XCTFail("Expected successful response") + } + } + } + + func test_request_givenNetworkError_shouldReturnFailureWithRequestFailed() { + // Given + let expectedError = NSError(domain: "TestErrorDomain", code: 123, userInfo: nil) + urlSessionMock.mockedError = expectedError + + // When + sut.request(url: URL(string: "https://example.com")!, method: .get, bodyParameters: nil) { (result: Result) in + // Then + switch result { + case .success(_): + XCTFail("Expected network error") + case .failure(let error): + XCTAssertEqual(error, NetworkError.requestFailed) + } + } + } + + func test_request_givenInvalidStatusCode_shouldReturnFailureWithInvalidResponse() { + // Given + let expectedResponse = HTTPURLResponse(url: URL(string: "https://example.com")!, statusCode: 404, httpVersion: nil, headerFields: nil) + urlSessionMock.mockedResponse = expectedResponse + + // When + sut.request(url: URL(string: "https://example.com")!, method: .get, bodyParameters: nil) { (result: Result) in + // Then + switch result { + case .success(_): + XCTFail("Expected invalid response error") + case .failure(let error): + XCTAssertEqual(error, NetworkError.invalidResponse) + } + } + } + + func test_request_givenResponseSuccessButMissingResponseData_shouldReturnFailureWithDecodingFailed() { + // Given + let expectedResponse = HTTPURLResponse(url: URL(string: "https://example.com")!, statusCode: 200, httpVersion: nil, headerFields: nil) + urlSessionMock.mockedResponse = expectedResponse + + // When + sut.request(url: URL(string: "https://example.com")!, method: .get, bodyParameters: nil) { (result: Result) in + // Then + switch result { + case .success(_): + XCTFail("Expected missing response data error") + case .failure(let error): + XCTAssertEqual(error, NetworkError.decodingFailed) + } + } + } + + func test_request_givenSuccessButWithEmptyResponseData_shouldReturnFailureWithDecodingFailed() { + // Given + let expectedResponse = HTTPURLResponse(url: URL(string: "https://example.com")!, statusCode: 200, httpVersion: nil, headerFields: nil) + let emptyData = Data() + urlSessionMock.mockedData = emptyData + urlSessionMock.mockedResponse = expectedResponse + + // When + sut.request(url: URL(string: "https://example.com")!, method: .get, bodyParameters: nil) { (result: Result) in + // Then + switch result { + case .success(_): + XCTFail("Expected empty response data error") + case .failure(let error): + XCTAssertEqual(error, NetworkError.decodingFailed) + } + } + } +} diff --git a/MyBankApp/MyBankAppTests/Network/NetworkServiceSpy.swift b/MyBankApp/MyBankAppTests/Network/NetworkServiceSpy.swift new file mode 100644 index 000000000..31419360a --- /dev/null +++ b/MyBankApp/MyBankAppTests/Network/NetworkServiceSpy.swift @@ -0,0 +1,35 @@ +@testable import MyBankApp +import XCTest + +typealias NetworkServiceDummy = NetworkServiceSpy + +final class NetworkServiceSpy: NetworkService { + var capturedURL: URL? + var capturedMethod: HTTPMethod? + var capturedBodyParameters: [String: Any]? + var mockedResult: Result? + + func request(url: URL, method: HTTPMethod, bodyParameters: [String: Any]?, completion: @escaping (Result) -> Void) where T: Decodable { + capturedURL = url + capturedMethod = method + capturedBodyParameters = bodyParameters + + switch mockedResult { + case .success(let data): + if let data = data { + do { + let decodedObject = try JSONDecoder().decode(T.self, from: data) + completion(.success(decodedObject)) + } catch { + completion(.failure(.decodingFailed)) + } + } else { + completion(.failure(.noData)) + } + case .failure(_): + completion(.failure(.requestFailed)) + case .none: + completion(.failure(.unknownError)) + } + } +} diff --git a/MyBankApp/MyBankAppTests/Network/URLSessionProtocolSpy.swift b/MyBankApp/MyBankAppTests/Network/URLSessionProtocolSpy.swift new file mode 100644 index 000000000..0b80fe673 --- /dev/null +++ b/MyBankApp/MyBankAppTests/Network/URLSessionProtocolSpy.swift @@ -0,0 +1,34 @@ +@testable import MyBankApp +import UIKit +import Foundation + +final class URLSessionDataTaskMock: URLSessionDataTaskProtocol { + private let completionHandler: () -> Void + + init(completionHandler: @escaping () -> Void) { + self.completionHandler = completionHandler + } + + func resume() { + completionHandler() + } +} + +final class URLSessionSpy: URLSessionProtocol { + var mockedData: Data? + var mockedResponse: URLResponse? + var mockedError: Error? + + func dataTask( + with request: URLRequest, + completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void + ) -> URLSessionDataTaskProtocol { + let data = mockedData + let response = mockedResponse + let error = mockedError + + return URLSessionDataTaskMock { [weak self] in + completionHandler(data, response, error) + } + } +} diff --git a/MyBankApp/MyBankAppUITests/MyBankAppUITests.swift b/MyBankApp/MyBankAppUITests/MyBankAppUITests.swift new file mode 100644 index 000000000..cf40341fc --- /dev/null +++ b/MyBankApp/MyBankAppUITests/MyBankAppUITests.swift @@ -0,0 +1,34 @@ +import XCTest + +final class MyBankAppUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/MyBankApp/MyBankAppUITests/MyBankAppUITestsLaunchTests.swift b/MyBankApp/MyBankAppUITests/MyBankAppUITestsLaunchTests.swift new file mode 100644 index 000000000..90286c88f --- /dev/null +++ b/MyBankApp/MyBankAppUITests/MyBankAppUITestsLaunchTests.swift @@ -0,0 +1,25 @@ +import XCTest + +final class MyBankAppUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/MyBankApp/Podfile b/MyBankApp/Podfile new file mode 100644 index 000000000..ba7b8a807 --- /dev/null +++ b/MyBankApp/Podfile @@ -0,0 +1,22 @@ +# Uncomment the next line to define a global platform for your project + platform :ios, '11.0' + +target 'MyBankApp' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + # Pods for MyBankApp + # + # + + target 'MyBankAppTests' do + inherit! :search_paths + # Pods for testing + pod 'SnapshotTesting' + end + + target 'MyBankAppUITests' do + # Pods for testing + end + +end diff --git a/MyBankApp/Podfile.lock b/MyBankApp/Podfile.lock new file mode 100644 index 000000000..2e169087f --- /dev/null +++ b/MyBankApp/Podfile.lock @@ -0,0 +1,16 @@ +PODS: + - SnapshotTesting (1.9.0) + +DEPENDENCIES: + - SnapshotTesting + +SPEC REPOS: + trunk: + - SnapshotTesting + +SPEC CHECKSUMS: + SnapshotTesting: 6141c48b6aa76ead61431ca665c14ab9a066c53b + +PODFILE CHECKSUM: ba0d6c1df54ed8d5dfdaf032779ee7c6deff107b + +COCOAPODS: 1.12.1 diff --git a/README.md b/README.md index c059fa6a5..29f2eae9c 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,41 @@ -# Show me the code +# Matheus Fusco - Desafio Banco Safra - Kaspper -Esse repositório contem todo o material necessário para realizar o teste: -- A especificação do layout está na pasta 'bank_app_layout' abrindo o index.html, os icones estão na pasta 'assets' +Repositório com o código referente ao desafio proposto para a vaga de iOS no Banco Safra - Kaspper. -- Os dados da Api estão mockados, os exemplos e a especificação dos serviços (login e statements) se encontram no arquivo BankApp.postman_collection.json ( é necessário instalar o postman e importar a colection https://www.getpostman.com/apps) +# Setup -![Image of Yaktocat](https://github.com/SantanderTecnologia/TesteiOSv2/blob/master/telas.png) +Solução adotada para gerenciador de dependência: `Cocoapods` +- Manual de instalação em [link](https://cocoapods.org). +- Entrar na pasta via terminal .../TesteiOSV2/MyBankApp/ e rodar o comando `pod install` +- Após a instalação, rodar o comando `open MyBankApp.xcworkspace` +- PS: Acabei não utilizando nenhuma dependência (pelo menos por enquanto), deixei o mais nativo possível, incluindo na parte de chamadas -### # DESAFIO: +# Como rodar -Na primeira tela teremos um formulario de login, o campo user deve aceitar email ou cpf, -o campo password deve validar se a senha tem pelo menos uma letra maiuscula, um caracter especial e um caracter alfanumérico. -Apos a validação, realizar o login no endpoint https://6092aef785ff5100172136c2.mockapi.io/api/login e exibir os dados de retorno na próxima tela. -O ultimo usuário logado deve ser salvo de forma segura localmente, e exibido na tela de login se houver algum salvo. +- Após a abertuda o `MyBankApp.xcworkspace`, rodar o app com o comando `Command + R` +- PS: Para que a navegação funcione corretamente, no arquivo `LoginInteractor` é necessário descomentar a linha 19, e comentar as linhas 21 até 32. Motivo: Não consegui fazer a API de Login funcionar, então deixei a linha 19 pra fins de navegação do aplicativo -Na segunda tela será exibido os dados formatados do retorno do login e será necessário fazer um segundo request para obter os lançamentos do usuário, no endpoint https://6092aef785ff5100172136c2.mockapi.io/api/statements/{idUser} que retornará uma lista de lançamentos +# Testes -### # Avaliação +Solução adotada para os testes unitários: `XCTests` +Solução adotada para testes de UI: `SnapshotTesting` +- Após a abertura do projeto, rodar `Command + U` -Você será avaliado pela usabilidade, por respeitar o design e pela arquitetura do app. É esperado que você consiga explicar as decisões que tomou durante o desenvolvimento através de commits. +## Observações -Obrigatórios: +- Testes referentes a `Home` ainda não foram finalizados, constando somente o teste da `HomeService` +- Testes referentes a tela de `Login` foram todos finalizados, incluindo todas camadas da arquitetura +- Testes das camadas de `Network` foram todos finalizados +- Para que os testes funcionem corretamente, no arquivo `LoginInteractor` é necessário comentar a linha 19, e descomentar as linhas 21 até 32. Motivo: Não consegui fazer a API de Login funcionar, então deixei a linha 19 pra fins de navegação do aplicativo -* Swift 3.0 ou superior -* Autolayout -* O app deve funcionar no iOS 11 -* Testes unitários, pode usar a ferramenta que você tem mais experiência, só nos explique o que ele tem de bom. -* Arquitetura a ser utilizada: Swift Clean ([https://clean-swift.com/handbook/](https://clean-swift.com/handbook/) && [https://github.com/Clean-Swift/CleanStore](https://github.com/Clean-Swift/CleanStore) -* Uso do git. +# Observações Gerais -### # Observações gerais +- Nunca havia utilizado a arquitetura CleanSwift, espero que tenha feito um bom código +- A API de Login não estava funcionando, defini um retorno `LoginResponse` e seus respectivos campos e utilizei um mock para apresentar a tela `Home` +- A API de `/statements` não funcionava com o `/{id}` no final, então modifiquei a URL para igual a collection enviada para que funcionasse o retorno (sem o `/{id}` no final) -Adicione um arquivo [README.md](http://README.md) com os procedimentos para executar o projeto. -Pedimos que trabalhe sozinho e não divulgue o resultado na internet. +# Próximos passos -Faça um fork desse desse repositório em seu Github e ao finalizar nos envie um Pull Request com o resultado, por favor informe por qual empresa você esta se candidatando. - -# Importante: não há prazo de entrega, faça com qualidade! - -# BOA SORTE! +- Finalizar os testes referente a `Home`, incluindo testes unitários +- Ajustar validação dos campos de texto e retornos exibidos ao usuário +- Adicionar `Loading` tanto na tela de `Login` quanto na `Home` e tratar possíveis cenários de erro na tela \ No newline at end of file