diff --git a/AptoUISDK.podspec b/AptoUISDK.podspec index a4cb532..57ad8b6 100644 --- a/AptoUISDK.podspec +++ b/AptoUISDK.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = "AptoUISDK" - s.version = "2.12.0" + s.version = "2.13.0" s.summary = "The Apto UI platform iOS SDK." s.description = <<-DESC Apto iOS UI SDK provides a UI flow that allows to easily integrate the platform in your app. @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.homepage = "https://github.com/AptoPayments/apto-ui-sdk-ios.git" s.license = { :type => 'MIT', :file => 'LICENSE' } s.authors = { "Ivan Oliver" => "ivan@aptopayments.com", "Takeichi Kanzaki" => "takeichi@aptopayments.com" } - s.source = { :git => "https://github.com/AptoPayments/apto-ui-sdk-ios.git", :tag => "2.12.0" } + s.source = { :git => "https://github.com/AptoPayments/apto-ui-sdk-ios.git", :tag => "2.13.0" } s.platform = :ios s.ios.deployment_target = '10.0' @@ -32,7 +32,7 @@ Pod::Spec.new do |s| s.resources = ["Pod/Assets/*.png", "Pod/Assets/*.css", "Pod/Localization/*.lproj", "Pod/Assets/*.xcassets", "Pod/Fonts/*.ttf", "Pod/CHANGELOG_ui.md"] s.frameworks = 'UIKit', 'CoreLocation', 'Accelerate', 'AudioToolbox', 'AVFoundation', 'CoreGraphics', 'CoreMedia', 'CoreVideo', 'Foundation', 'MobileCoreServices', 'OpenGLES', 'QuartzCore', 'Security', 'LocalAuthentication', 'CallKit' - s.dependency 'AptoSDK', '3.4.0' + s.dependency 'AptoSDK', '3.5.0' s.dependency 'AptoPCI', '2.1.0' s.dependency 'SnapKit', '~> 5.0' s.dependency 'Bond', '~> 7.6' diff --git a/CHANGELOG_ui.md b/CHANGELOG_ui.md new file mode 100644 index 0000000..456b46b --- /dev/null +++ b/CHANGELOG_ui.md @@ -0,0 +1,18 @@ +## CHANGELOG AptoUISDK + +# 2021-07-09 Version 2.13.0 +- feature: add alpha version of In App Card Provisioning. Add a card to Apple Wallet. +- improvement: added Load Funds Disclosure screen +- fix: improved load funds screen for smaller devices +- fix: error copies in add card screen + +# 2021-05-26 Version 2.12.0 +- improvement: validated SSN field format +- improvement: updated AlamoFire dependency to the 5.4.3 version. +- fix: resolved issue making the OTP alert dialog not showing up. +- improvement: removed wrong error messages on user logout. +- fix: added the card logo on the main card screen + +# 2021-04-26 Version 2.11.0 +- feature: added new `Exchange rates` disclaimer agreement on Card Settings, if available. +- fix: removes `Monthly Statements` from Account settings. diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 5e9db0b..0f135c1 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -7,7 +7,7 @@ PODS: - AptoObjCSDK (1.1.0): - AptoSDK - AptoPCI (2.1.0) - - AptoSDK (3.4.0): + - AptoSDK (3.5.0): - Alamofire (~> 5.4.3) - AlamofireNetworkActivityIndicator (~> 3.1) - Bond (~> 7.6) @@ -17,9 +17,9 @@ PODS: - PhoneNumberKit (~> 3.2) - SwiftyJSON (~> 5.0) - TrustKit (~> 1.6) - - AptoUISDK (2.11.1): + - AptoUISDK (2.12.0): - AptoPCI (= 2.1.0) - - AptoSDK (= 3.4.0) + - AptoSDK (= 3.5.0) - Bond (~> 7.6) - Down (~> 0.8.0) - GoogleKit (~> 0.3) @@ -115,8 +115,8 @@ SPEC CHECKSUMS: AlamofireNetworkActivityLogger: 162ab8aee00e6267a4304d7cc134e13ccfe3bcc5 AptoObjCSDK: 818ed46d0b6328d9ec76f32ab39e51d8ed8dffb1 AptoPCI: 8a85a014534b5d9036170e3698309de4fa51bc88 - AptoSDK: 95b488240ddf27c607a0b4189c22c4681aeaa308 - AptoUISDK: 6a140476051a2212677187d8e170578bc7c1e84c + AptoSDK: 86f48d3afc08271b23261620f2728a4ec57960b6 + AptoUISDK: e959de25e2b185f0f5b260fbb5c7e619b6ce4328 Bond: 87075c7f20f37ea3e73dc7057fce839787bf46eb Branch: 65d05ffb137ef504777cff6bd4fb6f770f17145a Differ: 3b6bd78e2b20cc795d9a86f7641d087524e4273e diff --git a/Example/ShiftSDK.xcodeproj/project.pbxproj b/Example/ShiftSDK.xcodeproj/project.pbxproj index 64414b4..92cea33 100644 --- a/Example/ShiftSDK.xcodeproj/project.pbxproj +++ b/Example/ShiftSDK.xcodeproj/project.pbxproj @@ -127,7 +127,6 @@ D99BD09BF570ED3CE7A53606 /* IssueCardInteractorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D99BDCF7398ED5703351D598 /* IssueCardInteractorTest.swift */; }; D99BD0AE38A93FD6A99C6C96 /* VoIPModuleTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D99BD00B83C53D45044B1FA1 /* VoIPModuleTest.swift */; }; D99BD0C95CE9E838E24A3C30 /* WebBrowserTestDoubles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D99BDB8C13937EEAD8C14217 /* WebBrowserTestDoubles.swift */; }; - D99BD0D3B76E2A681556D3A1 /* AptoPlatformTestDoubles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D99BDE8FA3C180E4E7030125 /* AptoPlatformTestDoubles.swift */; }; D99BD0FA5F61BF9AEA515941 /* CardSettingsTestDoubles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D99BDCC0B321CDE36B8C93E5 /* CardSettingsTestDoubles.swift */; }; D99BD106FA7689F59B3B745F /* IssueCardModuleTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D99BD042115A0913247DF648 /* IssueCardModuleTest.swift */; }; D99BD10C4AF529884446BCD4 /* InMemoryUserDefaultsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D99BD5D1CAEA83B9B60373F1 /* InMemoryUserDefaultsStorage.swift */; }; @@ -241,6 +240,13 @@ E571DB11260DE62600A5C533 /* UIButton+TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E571DB10260DE62600A5C533 /* UIButton+TestHelpers.swift */; }; E571DB16260DE64F00A5C533 /* UIControl+TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E571DB15260DE64F00A5C533 /* UIControl+TestHelpers.swift */; }; E57DF760265CECA000E2A9CC /* ImageCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E57DF75F265CECA000E2A9CC /* ImageCacheTests.swift */; }; + E5987A88262831D2008CFDBA /* ApplePayIAPStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5987A87262831D2008CFDBA /* ApplePayIAPStorageTests.swift */; }; + E59879D426263412008CFDBA /* ApplePayRowItemViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E59879D326263412008CFDBA /* ApplePayRowItemViewTests.swift */; }; + E5987A3D2627287E008CFDBA /* ApplePayButtonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5987A3C2627287E008CFDBA /* ApplePayButtonTests.swift */; }; + E5987A88262831D2008CFDBA /* ApplePayIAPStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5987A87262831D2008CFDBA /* ApplePayIAPStorageTests.swift */; }; + E5987BB42629C692008CFDBA /* ApplePayIAPViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5987BB32629C692008CFDBA /* ApplePayIAPViewControllerTests.swift */; }; + E5987BD52629CCDB008CFDBA /* AptoPlatformTestDoubles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D99BDE8FA3C180E4E7030125 /* AptoPlatformTestDoubles.swift */; }; + E5987BD92629CD99008CFDBA /* AptoPlatformTestDoubles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D99BDE8FA3C180E4E7030125 /* AptoPlatformTestDoubles.swift */; }; E59923E425C19F6C0078B5DA /* DirectDepositViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E59923E325C19F6C0078B5DA /* DirectDepositViewTests.swift */; }; E5A6933C25B5F49B00ED8149 /* ACHAccountStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5A692C625B5D29700ED8149 /* ACHAccountStorageTests.swift */; }; E5A6934125B980C100ED8149 /* StorageTransportSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5A6934025B980C100ED8149 /* StorageTransportSpy.swift */; }; @@ -248,6 +254,8 @@ E5A6C4F425C2BC730079009F /* XCTestCase+MemoryLeakTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5A6C4F325C2BC730079009F /* XCTestCase+MemoryLeakTracking.swift */; }; E5B2E3092609EFE40095F997 /* OrderPhysicalCardViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5B2E3082609EFE40095F997 /* OrderPhysicalCardViewTests.swift */; }; E5BE11E125D5524E0035F132 /* AgreementStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5BE11E025D5524E0035F132 /* AgreementStorageTests.swift */; }; + E5C7A3BF267B54480065BFD0 /* AddCardOnboardingViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5C7A3BE267B54480065BFD0 /* AddCardOnboardingViewControllerTests.swift */; }; + E5CF2E292678EA8D005516B0 /* AddCardOnboardingViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CF2E282678EA8D005516B0 /* AddCardOnboardingViewTests.swift */; }; E5D61E0E260B986F0085EA2B /* OrderPhysicalCardSuccessViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5D61E0D260B986F0085EA2B /* OrderPhysicalCardSuccessViewTests.swift */; }; E5D61E38260CA1AB0085EA2B /* OrderPhysicalCardViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5D61E37260CA1AB0085EA2B /* OrderPhysicalCardViewControllerTests.swift */; }; E5DD0B6925BF2121005FDF08 /* AddMoneyViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5DD0B6825BF2121005FDF08 /* AddMoneyViewControllerTests.swift */; }; @@ -573,6 +581,11 @@ E571DB10260DE62600A5C533 /* UIButton+TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+TestHelpers.swift"; sourceTree = ""; }; E571DB15260DE64F00A5C533 /* UIControl+TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIControl+TestHelpers.swift"; sourceTree = ""; }; E57DF75F265CECA000E2A9CC /* ImageCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCacheTests.swift; sourceTree = ""; }; + E5987A87262831D2008CFDBA /* ApplePayIAPStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePayIAPStorageTests.swift; sourceTree = ""; }; + E59879D326263412008CFDBA /* ApplePayRowItemViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePayRowItemViewTests.swift; sourceTree = ""; }; + E5987A3C2627287E008CFDBA /* ApplePayButtonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePayButtonTests.swift; sourceTree = ""; }; + E5987A87262831D2008CFDBA /* ApplePayIAPStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePayIAPStorageTests.swift; sourceTree = ""; }; + E5987BB32629C692008CFDBA /* ApplePayIAPViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePayIAPViewControllerTests.swift; sourceTree = ""; }; E59923E325C19F6C0078B5DA /* DirectDepositViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectDepositViewTests.swift; sourceTree = ""; }; E5A692C625B5D29700ED8149 /* ACHAccountStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACHAccountStorageTests.swift; sourceTree = ""; }; E5A6934025B980C100ED8149 /* StorageTransportSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageTransportSpy.swift; sourceTree = ""; }; @@ -580,6 +593,8 @@ E5A6C4F325C2BC730079009F /* XCTestCase+MemoryLeakTracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+MemoryLeakTracking.swift"; sourceTree = ""; }; E5B2E3082609EFE40095F997 /* OrderPhysicalCardViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderPhysicalCardViewTests.swift; sourceTree = ""; }; E5BE11E025D5524E0035F132 /* AgreementStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgreementStorageTests.swift; sourceTree = ""; }; + E5C7A3BE267B54480065BFD0 /* AddCardOnboardingViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCardOnboardingViewControllerTests.swift; sourceTree = ""; }; + E5CF2E282678EA8D005516B0 /* AddCardOnboardingViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCardOnboardingViewTests.swift; sourceTree = ""; }; E5D61E0D260B986F0085EA2B /* OrderPhysicalCardSuccessViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderPhysicalCardSuccessViewTests.swift; sourceTree = ""; }; E5D61E37260CA1AB0085EA2B /* OrderPhysicalCardViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderPhysicalCardViewControllerTests.swift; sourceTree = ""; }; E5DD0ABE25BB6590005FDF08 /* AgreementStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgreementStorageTests.swift; sourceTree = ""; }; @@ -908,6 +923,8 @@ 338E36F124B887D30072C7CE /* Features */ = { isa = PBXGroup; children = ( + E5CF2E272678EA76005516B0 /* AddCardOnboarding */, + E59879D2262633F9008CFDBA /* ApplePayProvisioning */, E5B2E3072609EF6A0095F997 /* OrderPhysicalCard */, E5DD0B7325C0151C005FDF08 /* AddMoney */, ); @@ -1556,6 +1573,7 @@ D99BDE7810FD4CC74825371B /* shiftcardkit */ = { isa = PBXGroup; children = ( + E5987BB22629C67D008CFDBA /* ApplePay */, E5DD0B6725BF2107005FDF08 /* AddMoney */, D99BDB13D84D77B629D014BB /* CardMonthlyStats */, D99BDE0EE5E45792839A1209 /* CardProductSelector */, @@ -1614,6 +1632,7 @@ D99BD1889D23F50523AE94F8 /* UserStorageTestDoubles.swift */, 3C58BF36242A6A9300175697 /* UserTokenStorageTest.swift */, D99BD4B61FB3A0D943C778B8 /* UserTokenStorageTestDoubles.swift */, + E5987A87262831D2008CFDBA /* ApplePayIAPStorageTests.swift */, ); path = Storages; sourceTree = ""; @@ -1651,6 +1670,23 @@ path = Components; sourceTree = ""; }; + E59879D2262633F9008CFDBA /* ApplePayProvisioning */ = { + isa = PBXGroup; + children = ( + E59879D326263412008CFDBA /* ApplePayRowItemViewTests.swift */, + E5987A3C2627287E008CFDBA /* ApplePayButtonTests.swift */, + ); + path = ApplePayProvisioning; + sourceTree = ""; + }; + E5987BB22629C67D008CFDBA /* ApplePay */ = { + isa = PBXGroup; + children = ( + E5987BB32629C692008CFDBA /* ApplePayIAPViewControllerTests.swift */, + ); + path = ApplePay; + sourceTree = ""; + }; E5A6C4F225C2BC4E0079009F /* Helpers */ = { isa = PBXGroup; children = ( @@ -1670,6 +1706,14 @@ path = OrderPhysicalCard; sourceTree = ""; }; + E5CF2E272678EA76005516B0 /* AddCardOnboarding */ = { + isa = PBXGroup; + children = ( + E5CF2E282678EA8D005516B0 /* AddCardOnboardingViewTests.swift */, + ); + path = AddCardOnboarding; + sourceTree = ""; + }; E5D61E36260CA16C0085EA2B /* OrderPhysicalCard */ = { isa = PBXGroup; children = ( @@ -1683,6 +1727,7 @@ children = ( E5DD0B6825BF2121005FDF08 /* AddMoneyViewControllerTests.swift */, E5A6C4ED25C1F8CB0079009F /* DirectDepositViewControllerTests.swift */, + E5C7A3BE267B54480065BFD0 /* AddCardOnboardingViewControllerTests.swift */, ); path = AddMoney; sourceTree = ""; @@ -1833,7 +1878,6 @@ }; 334E132B2464606100BADC89 = { CreatedOnToolsVersion = 11.4.1; - DevelopmentTeam = 3ZGLM2XX3U; ProvisioningStyle = Automatic; TestTargetID = 2C0F05B61C565F6F00F1C0E5; }; @@ -2103,13 +2147,16 @@ 3C05CD54238D63E800AA7CC9 /* VerifyPasscodePresenterTest.swift in Sources */, 3C05CD5A238D85F600AA7CC9 /* VerifyPasscodeModuleTest.swift in Sources */, 33BC263F24CB0348002E32EB /* PaymentSourcesStorageTestDoubles.swift in Sources */, + E5C7A3BF267B54480065BFD0 /* AddCardOnboardingViewControllerTests.swift in Sources */, 3CEBF04E215A908000B077BD /* DataConfirmationPresenterTest.swift in Sources */, D99BD19670282880C2D4670B /* FullScreenDisclaimerTestDoubles.swift in Sources */, 2C61F4C42551DDA700A0707E /* ManageCardTestDoubles.swift in Sources */, + E5987A88262831D2008CFDBA /* ApplePayIAPStorageTests.swift in Sources */, D99BD62AFD7D84FCA5EAB0ED /* FullScreenDisclaimerPresenterTest.swift in Sources */, 3C65E0BA217E10BB000207B1 /* PhysicalCardActivationSucceedTestDoubles.swift in Sources */, D99BD99B174D90E4B8C97F85 /* FullScreenDisclaimerModuleTest.swift in Sources */, D99BD23C6667DB4E1566431C /* AuthModuleTest.swift in Sources */, + E5987BB42629C692008CFDBA /* ApplePayIAPViewControllerTests.swift in Sources */, 3C03E9072338F36C00C565F1 /* MonthlyStatementsListTestDoubles.swift in Sources */, 3CD2DBF723F55E1A00C2EDE1 /* ChangePasscodePresenterTest.swift in Sources */, D99BD18C7F82C6C36CC05F59 /* ModelDataProvider.swift in Sources */, @@ -2146,7 +2193,6 @@ D99BD4B1C65D30B428CA97DD /* ServerMaintenanceErrorPresenterTest.swift in Sources */, D99BD5DE2B51A91002A694EB /* ServerMaintenanceErrorModuleTest.swift in Sources */, D99BD687B765050BE2C7F994 /* StorageLocatorFake.swift in Sources */, - D99BD0D3B76E2A681556D3A1 /* AptoPlatformTestDoubles.swift in Sources */, 2C38C3D7258B761B00D3DC48 /* SetPassCodeInteractorTest.swift in Sources */, D99BD31F56E362E159AACE4F /* ContentPresenterTestDoubles.swift in Sources */, D99BD466641EDE6E07ABF939 /* ContentPresenterInteractorTest.swift in Sources */, @@ -2205,6 +2251,7 @@ E5A6C4EE25C1F8CB0079009F /* DirectDepositViewControllerTests.swift in Sources */, D99BDFFC7AD558DB67D945B7 /* CardWaitListModuleTest.swift in Sources */, D99BD40B54B9F02B27F45C37 /* NotificationGroupTest.swift in Sources */, + E5987BD52629CCDB008CFDBA /* AptoPlatformTestDoubles.swift in Sources */, D99BD656AC57498ABC4E58D7 /* NotificationPreferencesTest.swift in Sources */, 3C05CD50238D38BC00AA7CC9 /* VerifyPasscodeInteractorTest.swift in Sources */, E5ED3AED260CAA70008A8442 /* LocalCacheFileManager.swift in Sources */, @@ -2332,10 +2379,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E5987BD92629CD99008CFDBA /* AptoPlatformTestDoubles.swift in Sources */, E5B2E3092609EFE40095F997 /* OrderPhysicalCardViewTests.swift in Sources */, E59923E425C19F6C0078B5DA /* DirectDepositViewTests.swift in Sources */, + E59879D426263412008CFDBA /* ApplePayRowItemViewTests.swift in Sources */, E5DD0B7525C0153C005FDF08 /* AddMoneyViewTests.swift in Sources */, + E5987A3D2627287E008CFDBA /* ApplePayButtonTests.swift in Sources */, 334E133C246461D500BADC89 /* ModelDataProvider.swift in Sources */, + E5CF2E292678EA8D005516B0 /* AddCardOnboardingViewTests.swift in Sources */, E5D61E0E260B986F0085EA2B /* OrderPhysicalCardSuccessViewTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2644,8 +2695,8 @@ CODE_SIGN_ENTITLEMENTS = Link/Link.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 32; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_TEAM = QWTPF9R98K; GCC_GENERATE_DEBUGGING_SYMBOLS = NO; GCC_OPTIMIZATION_LEVEL = 0; @@ -2665,7 +2716,7 @@ ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.aptopayments.aptocard.local; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PROVISIONING_PROFILE_SPECIFIER = "Apto Card Development with Apple Pay"; RUN_LINTER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Demo/tools/Demo-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -2749,7 +2800,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_TEAM = ""; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; @@ -2854,7 +2905,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_TEAM = ""; GCC_GENERATE_DEBUGGING_SYMBOLS = NO; GCC_OPTIMIZATION_LEVEL = 0; @@ -2960,7 +3011,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_TEAM = QWTPF9R98K; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; @@ -3064,10 +3115,9 @@ CODE_SIGN_IDENTITY = "Apple Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 32; + CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_TEAM = QWTPF9R98K; ENABLE_TESTABILITY = NO; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; HEADER_SEARCH_PATHS = "$(inherited)"; @@ -3082,12 +3132,12 @@ "${PODS_ROOT}", ); MARKETING_VERSION = 1.0.0; - ONLY_ACTIVE_ARCH = YES; + ONLY_ACTIVE_ARCH = NO; OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = com.aptopayments.aptocard; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; - PROVISIONING_PROFILE_SPECIFIER = "Apto Payments Demo App"; + PROVISIONING_PROFILE_SPECIFIER = "Apto Payments Card App with Apple Pay"; RUN_LINTER = ""; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_BRIDGING_HEADER = "Demo/tools/Demo-Bridging-Header.h"; @@ -3422,7 +3472,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 3ZGLM2XX3U; + DEVELOPMENT_TEAM = ""; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; diff --git a/Example/SnapshotTests/Features/AddCardOnboarding/AddCardOnboardingViewTests.swift b/Example/SnapshotTests/Features/AddCardOnboarding/AddCardOnboardingViewTests.swift new file mode 100644 index 0000000..ae77890 --- /dev/null +++ b/Example/SnapshotTests/Features/AddCardOnboarding/AddCardOnboardingViewTests.swift @@ -0,0 +1,29 @@ +// +// AddCardOnboardingViewTests.swift +// SnapshotTests +// +// Created by Fabio Cuomo on 15/6/21. +// Copyright © 2021 CocoaPods. All rights reserved. +// + +import XCTest +import SnapshotTesting +import SnapKit + +@testable import AptoUISDK +@testable import AptoSDK + +class AddCardOnboardingViewTests: XCTestCase { + + func test_addOnboardingView_rendersViewWithInformation() { + let view = AddCardOnboardingView(uiconfig: UIConfig.default) + view.configure(firstParagraph: "You can instantly transfer funds from your existing bank debit card to your <> card account. Please note that transfers are reviewed and they can be delayed or declined if we suspect risks.", + secondParagraph: "This transaction will appear in your bank account statement as: <>") + view.snp.makeConstraints { make in + make.height.equalTo(896) + make.width.equalTo(414) + } + assertSnapshot(matching: view, as: .image) + } + +} diff --git a/Example/SnapshotTests/Features/AddCardOnboarding/__Snapshots__/AddCardOnboardingViewTests/test_addOnboardingView_rendersViewWithInformation.1.png b/Example/SnapshotTests/Features/AddCardOnboarding/__Snapshots__/AddCardOnboardingViewTests/test_addOnboardingView_rendersViewWithInformation.1.png new file mode 100644 index 0000000..33a3d5c Binary files /dev/null and b/Example/SnapshotTests/Features/AddCardOnboarding/__Snapshots__/AddCardOnboardingViewTests/test_addOnboardingView_rendersViewWithInformation.1.png differ diff --git a/Example/SnapshotTests/Features/ApplePayProvisioning/ApplePayButtonTests.swift b/Example/SnapshotTests/Features/ApplePayProvisioning/ApplePayButtonTests.swift new file mode 100644 index 0000000..4bd183d --- /dev/null +++ b/Example/SnapshotTests/Features/ApplePayProvisioning/ApplePayButtonTests.swift @@ -0,0 +1,33 @@ +// +// ApplePayButtonTests.swift +// SnapshotTests +// +// Created by Fabio Cuomo on 14/4/21. +// Copyright © 2021 CocoaPods. All rights reserved. +// + +import XCTest +import SnapshotTesting +import SnapKit +import AptoUISDK + +@testable import AptoSDK + +class ApplePayButtonTests: XCTestCase { + + func test_applePayButton_rendersView() { + let helper = InAppProvisioningHelper() + let buttonView = helper.appleWalletButton() + + let view = UIView() + view.backgroundColor = .white + view.addSubview(buttonView) + + buttonView.snp.makeConstraints { $0.center.equalToSuperview() } + view.snp.makeConstraints { make in + make.height.equalTo(896) + make.width.equalTo(414) + } + assertSnapshot(matching: view, as: .image) + } +} diff --git a/Example/SnapshotTests/Features/ApplePayProvisioning/ApplePayRowItemViewTests.swift b/Example/SnapshotTests/Features/ApplePayProvisioning/ApplePayRowItemViewTests.swift new file mode 100644 index 0000000..0dbf97f --- /dev/null +++ b/Example/SnapshotTests/Features/ApplePayProvisioning/ApplePayRowItemViewTests.swift @@ -0,0 +1,28 @@ +// +// ApplePayRowItemViewTests.swift +// SnapshotTests +// +// Created by Fabio Cuomo on 13/4/21. +// Copyright © 2021 CocoaPods. All rights reserved. +// + +import XCTest +import SnapshotTesting +import SnapKit + +@testable import AptoUISDK +@testable import AptoSDK + +class ApplePayRowItemViewTests: XCTestCase { + + func test_applePayRow_rendersView() { + let view = ApplePayRowItemView(with: "card_settings.apple_pay.add_to_wallet.title".podLocalized(), + uiconfig: UIConfig.default) + + view.snp.makeConstraints { make in + make.height.equalTo(896) + make.width.equalTo(414) + } + assertSnapshot(matching: view, as: .image) + } +} diff --git a/Example/SnapshotTests/Features/ApplePayProvisioning/__Snapshots__/ApplePayButtonTests/test_applePayButton_rendersView.1.png b/Example/SnapshotTests/Features/ApplePayProvisioning/__Snapshots__/ApplePayButtonTests/test_applePayButton_rendersView.1.png new file mode 100644 index 0000000..e3896f8 Binary files /dev/null and b/Example/SnapshotTests/Features/ApplePayProvisioning/__Snapshots__/ApplePayButtonTests/test_applePayButton_rendersView.1.png differ diff --git a/Example/SnapshotTests/Features/ApplePayProvisioning/__Snapshots__/ApplePayRowItemViewTests/test_applePayRow_rendersView.1.png b/Example/SnapshotTests/Features/ApplePayProvisioning/__Snapshots__/ApplePayRowItemViewTests/test_applePayRow_rendersView.1.png new file mode 100644 index 0000000..7226570 Binary files /dev/null and b/Example/SnapshotTests/Features/ApplePayProvisioning/__Snapshots__/ApplePayRowItemViewTests/test_applePayRow_rendersView.1.png differ diff --git a/Example/SnapshotTests/Features/ApplePayProvisioning/__Snapshots__/ApplePaySplashViewTests/test_applePaySplashScreen_rendersView.1.png b/Example/SnapshotTests/Features/ApplePayProvisioning/__Snapshots__/ApplePaySplashViewTests/test_applePaySplashScreen_rendersView.1.png new file mode 100644 index 0000000..adc1068 Binary files /dev/null and b/Example/SnapshotTests/Features/ApplePayProvisioning/__Snapshots__/ApplePaySplashViewTests/test_applePaySplashScreen_rendersView.1.png differ diff --git a/Example/SnapshotTests/Features/ApplePayProvisioning/__Snapshots__/ApplePaySplashViewTests/test_splashScreenViewController_rendersView.iPhone8.png b/Example/SnapshotTests/Features/ApplePayProvisioning/__Snapshots__/ApplePaySplashViewTests/test_splashScreenViewController_rendersView.iPhone8.png new file mode 100644 index 0000000..5fc2ef8 Binary files /dev/null and b/Example/SnapshotTests/Features/ApplePayProvisioning/__Snapshots__/ApplePaySplashViewTests/test_splashScreenViewController_rendersView.iPhone8.png differ diff --git a/Example/SnapshotTests/Features/ApplePayProvisioning/__Snapshots__/ApplePaySplashViewTests/test_splashScreenViewController_rendersView.iPhoneX.png b/Example/SnapshotTests/Features/ApplePayProvisioning/__Snapshots__/ApplePaySplashViewTests/test_splashScreenViewController_rendersView.iPhoneX.png new file mode 100644 index 0000000..d1796a0 Binary files /dev/null and b/Example/SnapshotTests/Features/ApplePayProvisioning/__Snapshots__/ApplePaySplashViewTests/test_splashScreenViewController_rendersView.iPhoneX.png differ diff --git a/Example/SnapshotTests/Features/ApplePayProvisioning/__Snapshots__/ApplePaySplashViewTests/test_splashScreenViewController_rendersView.iPhoneXsMax.png b/Example/SnapshotTests/Features/ApplePayProvisioning/__Snapshots__/ApplePaySplashViewTests/test_splashScreenViewController_rendersView.iPhoneXsMax.png new file mode 100644 index 0000000..5579533 Binary files /dev/null and b/Example/SnapshotTests/Features/ApplePayProvisioning/__Snapshots__/ApplePaySplashViewTests/test_splashScreenViewController_rendersView.iPhoneXsMax.png differ diff --git a/Example/UnitTests/common/Loader/CardLoaderSpy.swift b/Example/UnitTests/common/Loader/CardLoaderSpy.swift index b17530e..66734e9 100644 --- a/Example/UnitTests/common/Loader/CardLoaderSpy.swift +++ b/Example/UnitTests/common/Loader/CardLoaderSpy.swift @@ -11,17 +11,29 @@ import Foundation class CardLoaderSpy: AptoPlatformFake { private var cardCompletions = [(Result) -> Void]() - + private var cardProductCompletions = [(Result) -> Void]() + var loadCardInfoCallCount: Int { cardCompletions.count } - + var loadCardProductCallCount: Int { + cardProductCompletions.count + } + override func fetchCard(_ cardId: String, forceRefresh: Bool, retrieveBalances: Bool, callback: @escaping Result.Callback) { cardCompletions.append(callback) } + override func fetchCardProduct(cardProductId: String, forceRefresh: Bool, callback: @escaping Result.Callback) { + cardProductCompletions.append(callback) + } + func completeCardInfoLoading(with card: Card = ModelDataProvider.provider.cardWithACHAccount, at index: Int = 0) { cardCompletions[index](.success(card)) } + + func completeCardProductLoading(with cardProduct: CardProduct = ModelDataProvider.provider.cardProduct, at index: Int = 0) { + cardProductCompletions[index](.success(cardProduct)) + } } diff --git a/Example/UnitTests/common/Manager/AptoPlatformTestDoubles.swift b/Example/UnitTests/common/Manager/AptoPlatformTestDoubles.swift index 999a7cd..4ea99d8 100644 --- a/Example/UnitTests/common/Manager/AptoPlatformTestDoubles.swift +++ b/Example/UnitTests/common/Manager/AptoPlatformTestDoubles.swift @@ -757,4 +757,6 @@ var nextIsShowDetailedCardActivityEnabledResult = true func orderPhysicalCard(_ cardId: String, callback: @escaping (Result) -> Void) {} func getOrderPhysicalCardConfig(_ cardId: String, callback: @escaping (Result) -> Void) {} + func startApplePayInAppProvisioning(cardId: String, certificates: [Data], nonce: Data, nonceSignature: Data, callback: @escaping (ApplePayIAPResult) -> Void) {} + } diff --git a/Example/UnitTests/common/ModelDataProvider/ModelDataProvider.swift b/Example/UnitTests/common/ModelDataProvider/ModelDataProvider.swift index 572c082..4c8e06b 100644 --- a/Example/UnitTests/common/ModelDataProvider/ModelDataProvider.swift +++ b/Example/UnitTests/common/ModelDataProvider/ModelDataProvider.swift @@ -230,7 +230,7 @@ class ModelDataProvider { let features = CardFeatures(setPin: FeatureAction(source: .ivr(ivr), status: .enabled), getPin: FeatureAction(source: .ivr(ivr), status: .enabled), allowedBalanceTypes: [balanceType], activation: nil, ivrSupport: ivr, funding: nil, - passCode: nil, achAccount: nil) + passCode: nil, achAccount: nil, inAppProvisioning: nil) let card = Card(accountId: "card_id", cardProductId: "card_product_id", cardNetwork: .other, @@ -258,7 +258,7 @@ class ModelDataProvider { let features = CardFeatures(setPin: FeatureAction(source: .voIP, status: .enabled), getPin: FeatureAction(source: .voIP, status: .enabled), allowedBalanceTypes: [balanceType], activation: nil, ivrSupport: ivr, funding: nil, - passCode: nil, achAccount: nil) + passCode: nil, achAccount: nil, inAppProvisioning: nil) let card = Card(accountId: "card_id", cardProductId: "card_product_id", cardNetwork: .other, @@ -286,7 +286,7 @@ class ModelDataProvider { let features = CardFeatures(setPin: FeatureAction(source: .api, status: .enabled), getPin: FeatureAction(source: .api, status: .enabled), allowedBalanceTypes: [balanceType], activation: nil, ivrSupport: ivr, funding: nil, - passCode: nil, achAccount: nil) + passCode: nil, achAccount: nil, inAppProvisioning: nil) let card = Card(accountId: "card_id", cardProductId: "card_product_id", cardNetwork: .other, @@ -314,7 +314,7 @@ class ModelDataProvider { let features = CardFeatures(setPin: FeatureAction(source: .api, status: .enabled), getPin: FeatureAction(source: .ivr(ivr), status: .enabled), allowedBalanceTypes: [balanceType], activation: nil, ivrSupport: ivr, funding: nil, - passCode: nil, achAccount: nil) + passCode: nil, achAccount: nil, inAppProvisioning: nil) let card = Card(accountId: "card_id", cardProductId: "card_product_id", cardNetwork: .other, @@ -343,7 +343,7 @@ class ModelDataProvider { getPin: FeatureAction(source: .ivr(ivr), status: .enabled), allowedBalanceTypes: [balanceType], activation: nil, ivrSupport: ivr, funding: nil, passCode: PassCode(status: .enabled, passCodeSet: false, verificationRequired: true), - achAccount: nil) + achAccount: nil, inAppProvisioning: nil) let card = Card(accountId: "card_id", cardProductId: "card_product_id", cardNetwork: .other, @@ -375,7 +375,8 @@ class ModelDataProvider { isAccountProvisioned: true, disclaimer: disclaimer, achAccountDetails: accountDetails) - let features = CardFeatures(setPin: nil, getPin: nil, allowedBalanceTypes: nil, activation: nil, ivrSupport: nil, funding: nil, passCode: nil, achAccount: bankAccount) + let features = CardFeatures(setPin: nil, getPin: nil, allowedBalanceTypes: nil, activation: nil, ivrSupport: nil, funding: nil, + passCode: nil, achAccount: bankAccount, inAppProvisioning: nil) let card = Card(accountId: "card_id", cardProductId: "card_product_id", cardNetwork: .visa, @@ -394,7 +395,34 @@ class ModelDataProvider { verified: nil) return card }() - + + lazy var cardWithFunding: Card = { + let keys = ["evolve_eua"] + let url = URL(string: "https://public-media-prd-usw1-shift.s3-us-west-1.amazonaws.com/developer_portal/disclaimers/instance_issuance_disclaimer.html")! + let content = Content.externalURL(url) + let funding = Funding(status: .enabled, + cardNetworks: [.mastercard], + limits: FundingLimits(daily: FundingSingleLimit(max: Amount(value: 1000, currency: "EUR"))), softDescriptor: "Soft Descriptor") + let features = CardFeatures(setPin: nil, getPin: nil, allowedBalanceTypes: nil, activation: nil, ivrSupport: nil, funding: nil, passCode: nil, achAccount: nil, inAppProvisioning: nil) + let card = Card(accountId: "card_id", + cardProductId: "card_product_id", + cardNetwork: .visa, + cardIssuer: .shift, + cardBrand: "Marvel Card", + state: .active, + cardHolder: "Holder Name", + lastFourDigits: "7890", + spendableToday: nil, + nativeSpendableToday: nil, + totalBalance: nil, + nativeTotalBalance: nil, + kyc: .passed, + orderedStatus: .ordered, + features: features, + verified: nil) + return card + }() + lazy var cardWithDataToRenderACreditCardView: Card = { let cardBackground = CardBackgroundStyle.color(color: UIColor.colorFromHexString("2f3237") ?? .black) let style = CardStyle(background: cardBackground, textColor: "ffffff", balanceSelectorImage: nil, cardLogo: nil) @@ -456,7 +484,7 @@ class ModelDataProvider { let features = CardFeatures(setPin: FeatureAction(source: .unknown, status: .enabled), getPin: FeatureAction(source: .unknown, status: .enabled), allowedBalanceTypes: [balanceType], activation: nil, ivrSupport: ivr, funding: nil, - passCode: nil, achAccount: nil) + passCode: nil, achAccount: nil, inAppProvisioning: nil) let card = Card(accountId: "card_id", cardProductId: "card_product_id", cardNetwork: .other, diff --git a/Example/UnitTests/common/ServiceLocator/StorageLocatorFake.swift b/Example/UnitTests/common/ServiceLocator/StorageLocatorFake.swift index 76cad52..a191ed8 100644 --- a/Example/UnitTests/common/ServiceLocator/StorageLocatorFake.swift +++ b/Example/UnitTests/common/ServiceLocator/StorageLocatorFake.swift @@ -81,6 +81,11 @@ class StorageLocatorFake: StorageLocatorProtocol { func achAccountStorage(transport: JSONTransport) -> ACHAccountStorageProtocol { return achAccountStorageSpy } + + lazy var applePayIAPStorageSpy = ApplePayIAPStorageSpy() + func applePayIAPStorage(transport: JSONTransport) -> ApplePayIAPStorageProtocol { + return applePayIAPStorageSpy + } lazy var cardApplicationStorageSpy = CardApplicationStorageSpy() func cardApplicationStorage(transport: JSONTransport) -> CardApplicationsStorageProtocol { diff --git a/Example/UnitTests/ledgecore/Storages/ApplePayIAPStorageTests.swift b/Example/UnitTests/ledgecore/Storages/ApplePayIAPStorageTests.swift new file mode 100644 index 0000000..0fe143f --- /dev/null +++ b/Example/UnitTests/ledgecore/Storages/ApplePayIAPStorageTests.swift @@ -0,0 +1,136 @@ +// +// ApplePayInAppProvisioningStorageTests.swift +// UnitTests +// +// Created by Fabio Cuomo on 15/4/21. +// Copyright © 2021 CocoaPods. All rights reserved. +// + +import XCTest +import SwiftyJSON +@testable import AptoSDK + +class ApplePayIAPStorageTests: XCTestCase { + + private let apiKey = "api_key" + private let userToken = "user_token" + private let cardId = "crd_1234567890" + + func test_inAppProvisioning_requestsDataFromURL() { + let (sut, transport) = makeSUT() + let urlParameters = [":account_id": cardId] + let url = URLWrapper(baseUrl: transport.environment.baseUrl(), url: .applePayInAppProvisioning, urlParameters: urlParameters) + + sut.inAppProvisioning(apiKey, userToken: userToken, cardId: cardId, payload: makeInAppProvisioningInputData()) { _ in } + + XCTAssertTrue(transport.postCalled) + XCTAssertEqual(transport.requestesURLs, [try url.asURL()]) + } + + func test_inAppProvisioningTwice_requestsDataFromURLTwice() { + let (sut, transport) = makeSUT() + let urlParameters = [":account_id": cardId] + let url = URLWrapper(baseUrl: transport.environment.baseUrl(), url: .applePayInAppProvisioning, urlParameters: urlParameters) + + sut.inAppProvisioning(apiKey, userToken: userToken, cardId: cardId, payload: makeInAppProvisioningInputData()) { _ in } + sut.inAppProvisioning(apiKey, userToken: userToken, cardId: cardId, payload: makeInAppProvisioningInputData()) { _ in } + + XCTAssertTrue(transport.postCalled) + XCTAssertEqual(transport.requestesURLs, [try url.asURL(), try url.asURL()]) + } + + func test_inAppProvisioning_deliversErrorOnClientError() { + let (sut, transport) = makeSUT() + let clientError = NSError(domain: "APTO DOMAIN ERROR", code: 0) + + expect(sut, + toCompleteWith: .failure(clientError), + apiKey: apiKey, + userToken: userToken, + inputData: makeInAppProvisioningInputData()) { + transport.complete(with: clientError) + } + } + + func test_inAppProvisioning_deliversErrorOnInvalidJSONResponse() throws { + let (sut, transport) = makeSUT() + let json = try JSON(data: Data("[]".utf8)) + + expect(sut, + toCompleteWith: .failure(ServiceError(code: .jsonError)), + apiKey: apiKey, + userToken: userToken, + inputData: makeInAppProvisioningInputData()) { + transport.complete(withResult: json) + } + } + + func test_inAppProvisioning_deliversApplePayIAPResultOnValidJSONResponse() throws { + let (sut, transport) = makeSUT() + let item = makeIAPResult() + + expect(sut, + toCompleteWith: .success(item.issuerResponse), + apiKey: apiKey, + userToken: userToken, + inputData: makeInAppProvisioningInputData()) { + transport.complete(withResult: item.json) + } + } + + // MARK: Private Helper methods + private func makeSUT() -> (sut: ApplePayIAPStorage, transport: StorageTransportSpy) { + let transport = StorageTransportSpy() + let sut = ApplePayIAPStorage(transport: transport) + return (sut, transport) + } + + private func expect(_ sut: ApplePayIAPStorage, + toCompleteWith result: ApplePayIAPResult, + apiKey: String, + userToken: String, + inputData: ApplePayIAPInputData, + when action: () -> Void) { + + var capturedResults = [ApplePayIAPResult]() + sut.inAppProvisioning(apiKey, userToken: userToken, cardId: cardId, payload: makeInAppProvisioningInputData()) { capturedResults.append($0) } + + action() + + XCTAssertEqual(capturedResults, [result]) + } + + private func makeIAPResult() -> (issuerResponse: ApplePayIAPIssuerResponse, json: JSON) { + let iapResponse = ApplePayIAPIssuerResponse(encryptedPassData: anyData(), + activationData: anyData(), + ephemeralPublicKey: anyData()) + + let json: JSON = [ + "encrypted_pass_data": "YW55IGRhdGE=", + "activation_data": "YW55IGRhdGE=", + "ephemeral_public_key": "YW55IGRhdGE=" + ] + + return (iapResponse, json) + } + + private func makeInAppProvisioningInputData() -> ApplePayIAPInputData { + ApplePayIAPInputData(certificates: [anyBase64EncodedString(), anyBase64EncodedString()], nonce: anyBase64EncodedString(), nonceSignature: anyBase64EncodedString()) + } + + private func anyData() -> Data { + Data(base64Encoded: "YW55IGRhdGE=", options: [])! + } + + private func anyString() -> String { + "any string" + } + + private func anyBase64EncodedString() -> String { + "YW55IGRhdGE=" + } +} + +class ApplePayIAPStorageSpy: ApplePayIAPStorageProtocol { + func inAppProvisioning(_ apiKey: String, userToken: String, cardId: String, payload: ApplePayIAPInputData, completion: @escaping (ApplePayIAPResult) -> Void) {} +} diff --git a/Example/UnitTests/shiftcardkit/AddMoney/AddCardOnboardingViewControllerTests.swift b/Example/UnitTests/shiftcardkit/AddMoney/AddCardOnboardingViewControllerTests.swift new file mode 100644 index 0000000..6cd516c --- /dev/null +++ b/Example/UnitTests/shiftcardkit/AddMoney/AddCardOnboardingViewControllerTests.swift @@ -0,0 +1,81 @@ +// +// AddCardOnboardingViewControllerTests.swift +// UnitTests +// +// Created by Fabio Cuomo on 17/6/21. +// Copyright © 2021 CocoaPods. All rights reserved. +// + +import XCTest +@testable import AptoSDK +@testable import AptoUISDK + +class AddCardOnboardingViewControllerTests: XCTestCase { + + let cardId = "crd_1234567890" + + func test_init_doesNotFetchCardInfo() { + let (_, loader) = makeSUT() + + XCTAssertEqual(loader.loadCardInfoCallCount, 0) + } + + func test_viewDidLoad_fetchesCardInfo() { + let (sut, loader) = makeSUT() + + sut.loadViewIfNeeded() + + XCTAssertEqual(loader.loadCardInfoCallCount, 1) + } + + func test_fetchCardInfoCompletion_fetchesCardProductInfo() { + let (sut, loader) = makeSUT() + + sut.loadViewIfNeeded() + loader.completeCardInfoLoading() + loader.completeCardProductLoading() + + XCTAssertEqual(loader.loadCardInfoCallCount, 1) + XCTAssertEqual(loader.loadCardProductCallCount, 1) + } + + func test_loadCardCompletion_rendersSuccessfullyLoadedInfo() { + let card = ModelDataProvider.provider.cardWithFunding + let cardProduct = ModelDataProvider.provider.cardProduct + + let (sut, loader) = makeSUT() + + sut.loadViewIfNeeded() + loader.completeCardInfoLoading(with: card) + loader.completeCardProductLoading(with: cardProduct) + + let header = "Debit Card" + let firstParagraph = "You can instantly transfer funds from your existing bank debit card to your <> card account. Please note that transfers are reviewed and they can be delayed or declined if we suspect risks.".replace(["<>": cardProduct.name]) + let secondParagraph = "This transaction will appear in your bank account statement as: <>".replace(["<>": card.features?.funding?.softDescriptor ?? ""]) + XCTAssertEqual(sut.header, header) + XCTAssertEqual(sut.firstParagraph, firstParagraph) + XCTAssertEqual(sut.secondParagraph, secondParagraph) + } + + // MARK: - Helpers + private func makeSUT(file: StaticString = #file, line: UInt = #line) -> (sut: AddCardOnboardingViewController, loader: CardLoaderSpy) { + let loader = CardLoaderSpy() + let viewModel = AddCardOnboardingViewModel(cardId: cardId, loader: loader) + let sut = AddCardOnboardingViewController(uiConfiguration: UIConfig.default, viewModel: viewModel) + trackForMemoryLeaks(loader, file: file, line: line) + trackForMemoryLeaks(sut, file: file, line: line) + return (sut, loader) + } +} + +private extension AddCardOnboardingViewController { + var header: String? { + return mainView.headerLabel.text + } + var firstParagraph: String? { + return mainView.firstParagraphLabel.text + } + var secondParagraph: String? { + return mainView.secondParagraphLabel.text + } +} diff --git a/Example/UnitTests/shiftcardkit/ApplePay/ApplePayIAPViewControllerTests.swift b/Example/UnitTests/shiftcardkit/ApplePay/ApplePayIAPViewControllerTests.swift new file mode 100644 index 0000000..ed81cf5 --- /dev/null +++ b/Example/UnitTests/shiftcardkit/ApplePay/ApplePayIAPViewControllerTests.swift @@ -0,0 +1,44 @@ +// +// ApplePayIAPViewControllerTests.swift +// UnitTests +// +// Created by Fabio Cuomo on 16/4/21. +// Copyright © 2021 CocoaPods. All rights reserved. +// + +import XCTest + +@testable import AptoUISDK +@testable import AptoSDK + +class ApplePayIAPViewControllerTests: XCTestCase { + + let cardId = "crd_1234567890" + + func test_init_doesNotFetchCard() { + let (_, loader) = makeSUT() + + XCTAssertEqual(loader.loadCardInfoCallCount, 0) + } + + func test_viewDidLoad_fetchCardData() { + let (sut, loader) = makeSUT() + + sut.loadViewIfNeeded() + + XCTAssertEqual(loader.loadCardInfoCallCount, 1) + } + + // MARK: - Helpers + private func makeSUT(file: StaticString = #file, line: UInt = #line) + -> + (sut: ApplePayIAPViewController, loader: CardLoaderSpy) { + let loader = CardLoaderSpy() + let viewModel = ApplePayIAPViewModel(cardId: cardId, loader: loader) + let sut = ApplePayIAPViewController(viewModel: viewModel, uiConfiguration: UIConfig.default) + trackForMemoryLeaks(loader, file: file, line: line) + trackForMemoryLeaks(sut, file: file, line: line) + return (sut, loader) + } + +} diff --git a/Example/UnitTests/shiftcardkit/CardSettings/CardSettingsTestDoubles.swift b/Example/UnitTests/shiftcardkit/CardSettings/CardSettingsTestDoubles.swift index d89a8b2..48040c0 100644 --- a/Example/UnitTests/shiftcardkit/CardSettings/CardSettingsTestDoubles.swift +++ b/Example/UnitTests/shiftcardkit/CardSettings/CardSettingsTestDoubles.swift @@ -23,6 +23,8 @@ class CardSettingsModuleSpy: UIModuleSpy, CardSettingsRouterProtocol, CardSettin func showOrderPhysicalCard(_ card: Card, completion: OrderPhysicalCardUIComposer.OrderedCompletion?) {} + func showApplePayIAP(cardId: String, completion: ApplePayIAPUIComposer.IAPCompletion?) {} + private(set) var closeFromShiftCardSettingsCalled = false func closeFromShiftCardSettings() { closeFromShiftCardSettingsCalled = true @@ -159,6 +161,7 @@ class CardSettingsPresenterSpy: CardSettingsPresenterProtocol { } func didTapOnOrderPhysicalCard() {} + func didTapOnApplePayIAP() {} private(set) var viewLoadedCalled = false func viewLoaded() { diff --git a/Example/UnitTests/shiftcardkit/ManageCardModule/ManageCard/ManageCardTestDoubles.swift b/Example/UnitTests/shiftcardkit/ManageCardModule/ManageCard/ManageCardTestDoubles.swift index 30f8d57..ea86093 100644 --- a/Example/UnitTests/shiftcardkit/ManageCardModule/ManageCard/ManageCardTestDoubles.swift +++ b/Example/UnitTests/shiftcardkit/ManageCardModule/ManageCard/ManageCardTestDoubles.swift @@ -57,6 +57,8 @@ class ManageCardModuleSpy: UIModuleSpy, ManageCardRouterProtocol { func physicalActivationSucceed() { physicalActivationSucceedCalled = true } + + func enrollApplePayProvisioning() {} } class ManageCardInteractorSpy: ManageCardInteractorProtocol { @@ -283,6 +285,8 @@ class ManageCardPresenterSpy: ManageCardPresenterProtocol { func activatePhysicalCardTapped() { activatePhysicalCardTappedCalled = true } + + func initInAppProvisioningEnrollProcess() {} } class ManageCardViewSpy: ViewControllerSpy, ManageCardViewProtocol { diff --git a/Example/fastlane/Fastfile b/Example/fastlane/Fastfile index 6033b6d..7d9e4d6 100644 --- a/Example/fastlane/Fastfile +++ b/Example/fastlane/Fastfile @@ -271,6 +271,7 @@ def update_core_repo sh("cp -r ../../Pod/Localization #{repo_loc}") sh("mkdir -p #{repo_code}") sh("cp -r ../../Pod/Classes/core #{repo_code}") + sh("cp ../../Pod/CHANGELOG_core.md #{repo}/CHANGELOG_core.md") end def update_ui_repo @@ -295,7 +296,7 @@ def update_ui_repo sh("mkdir -p #{repo_code}") sh("cp -r ../../Pod/Classes/ui #{repo_code}") sh("cp -r ../../Example #{repo}") - sh("cp ../../README-UI.md #{repo}/README.md") + sh("cp ../../Pod/CHANGELOG_ui.md #{repo}/CHANGELOG_ui.md") # Clean Example App sh("rm -rf #{repo}/Example/Pods | true") diff --git a/Example/link/Link.entitlements b/Example/link/Link.entitlements index 41428bf..1f54e68 100644 --- a/Example/link/Link.entitlements +++ b/Example/link/Link.entitlements @@ -9,5 +9,11 @@ vs5sa.test-app.link vs5sa-alternate.test-app.link + com.apple.developer.payment-pass-provisioning + + com.apple.developer.pass-type-identifiers + + $(TeamIdentifierPrefix)* + diff --git a/Pod/Assets/Assets.xcassets/ApplePay/Contents.json b/Pod/Assets/Assets.xcassets/ApplePay/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Pod/Assets/Assets.xcassets/ApplePay/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Pod/Assets/Assets.xcassets/ApplePay/apple_pay_mark.imageset/Contents.json b/Pod/Assets/Assets.xcassets/ApplePay/apple_pay_mark.imageset/Contents.json new file mode 100644 index 0000000..104fa37 --- /dev/null +++ b/Pod/Assets/Assets.xcassets/ApplePay/apple_pay_mark.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "apple_pay_mark.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Pod/Assets/Assets.xcassets/ApplePay/apple_pay_mark.imageset/apple_pay_mark.svg b/Pod/Assets/Assets.xcassets/ApplePay/apple_pay_mark.imageset/apple_pay_mark.svg new file mode 100644 index 0000000..0c6ecaf --- /dev/null +++ b/Pod/Assets/Assets.xcassets/ApplePay/apple_pay_mark.imageset/apple_pay_mark.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Pod/Assets/Assets.xcassets/Icons/Contents.json b/Pod/Assets/Assets.xcassets/Icons/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Pod/Assets/Assets.xcassets/Icons/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Pod/Assets/Assets.xcassets/Icons/ic_info.imageset/Contents.json b/Pod/Assets/Assets.xcassets/Icons/ic_info.imageset/Contents.json new file mode 100644 index 0000000..8d5db2d --- /dev/null +++ b/Pod/Assets/Assets.xcassets/Icons/ic_info.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ic_info.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Pod/Assets/Assets.xcassets/Icons/ic_info.imageset/ic_info.pdf b/Pod/Assets/Assets.xcassets/Icons/ic_info.imageset/ic_info.pdf new file mode 100644 index 0000000..91f5ae9 Binary files /dev/null and b/Pod/Assets/Assets.xcassets/Icons/ic_info.imageset/ic_info.pdf differ diff --git a/Pod/Classes/ui/CardKit/AccountSettingsModule/AccountSettingsViewControllerTheme2.swift b/Pod/Classes/ui/CardKit/AccountSettingsModule/AccountSettingsViewControllerTheme2.swift index 5409bc8..dea3ba3 100644 --- a/Pod/Classes/ui/CardKit/AccountSettingsModule/AccountSettingsViewControllerTheme2.swift +++ b/Pod/Classes/ui/CardKit/AccountSettingsModule/AccountSettingsViewControllerTheme2.swift @@ -178,8 +178,8 @@ private extension AccountSettingsViewControllerTheme2 { func createBiometricButton(_ biometryType: BiometryType) -> FormRowView { let title = biometryType == .faceID - ? "account_settings.security.face_id.title".podLocalized() - : "account_settings.security.touch_id.title" + ? "account_settings.security.face_id.title".podLocalized() + : "account_settings.security.touch_id.title".podLocalized() let subtitle = biometryType == .faceID ? "account_settings.security.face_id.description".podLocalized() : "account_settings.security.touch_id.description".podLocalized() diff --git a/Pod/Classes/ui/CardKit/AddMoney/AddCardOnBoardingUIComposer.swift b/Pod/Classes/ui/CardKit/AddMoney/AddCardOnBoardingUIComposer.swift new file mode 100644 index 0000000..2df66cb --- /dev/null +++ b/Pod/Classes/ui/CardKit/AddMoney/AddCardOnBoardingUIComposer.swift @@ -0,0 +1,24 @@ +// +// AddCardOnBoardingUIComposer.swift +// AptoUISDK +// +// Created by Fabio Cuomo on 23/6/21. +// + +import Foundation +import AptoSDK + +struct AddCardOnBoardingUIComposer { + public static func compose(with card: Card, + extraContent: ExtraContent?, + platform: AptoPlatformProtocol, + actionCompletion: @escaping () -> Void, + closeCompletion: @escaping (UIViewController) -> Void) -> AddCardOnboardingViewController { + let viewModel = AddCardOnboardingViewModel(cardId: card.accountId, loader: platform) + let controller = AddCardOnboardingViewController(uiConfiguration: UIConfig.default, viewModel: viewModel) + controller.modalPresentationStyle = .formSheet + controller.actionButtonCompletion = actionCompletion + controller.closeButtonCompletion = closeCompletion + return controller + } +} diff --git a/Pod/Classes/ui/CardKit/AddMoney/Helper/LoadFundsOnBoardingHelper.swift b/Pod/Classes/ui/CardKit/AddMoney/Helper/LoadFundsOnBoardingHelper.swift new file mode 100644 index 0000000..2d77563 --- /dev/null +++ b/Pod/Classes/ui/CardKit/AddMoney/Helper/LoadFundsOnBoardingHelper.swift @@ -0,0 +1,19 @@ +// +// LoadFundsOnBoardingHelper.swift +// AptoSDK +// +// Created by Fabio Cuomo on 17/6/21. +// + +import Foundation + +public struct LoadFundsOnBoardingHelper { + private static let OnBoardingScreen = "com.apto.load.funds.onboarding.presented" + public static func shouldPresentOnBoarding(userDefaults: UserDefaults = .standard) -> Bool { + return userDefaults.bool(forKey: OnBoardingScreen) == false + } + + public static func markAsPresented(userDefaults: UserDefaults = .standard) { + userDefaults.set(true, forKey: OnBoardingScreen) + } +} diff --git a/Pod/Classes/ui/CardKit/AddMoney/View/AddCardOnboardingView.swift b/Pod/Classes/ui/CardKit/AddMoney/View/AddCardOnboardingView.swift new file mode 100644 index 0000000..4a1947d --- /dev/null +++ b/Pod/Classes/ui/CardKit/AddMoney/View/AddCardOnboardingView.swift @@ -0,0 +1,110 @@ +// +// AddCardOnboardingView.swift +// AptoUISDK +// +// Created by Fabio Cuomo on 15/6/21. +// + +import Foundation + +import UIKit +import SnapKit +import AptoSDK + +public final class AddCardOnboardingView: UIView { + let uiConfiguration: UIConfig + + private(set) lazy var headerLabel: UILabel = { + let label = ComponentCatalog.topBarTitleLabelWith(text: "", + textAlignment: .left, + uiConfig: uiConfiguration) + label.textColor = uiConfiguration.textTopBarSecondaryColor + label.font = UITheme2FontProvider(fontDescriptors: nil).topBarTitleBigFont + label.text = "load_funds.add_card.onboarding.title".podLocalized() + return label + }() + private(set) lazy var firstParagraphLabel: UILabel = { + let label = ComponentCatalog.formListLabelWith(text: "", + textAlignment: .left, + uiConfig: uiConfiguration) + label.textColor = uiConfiguration.textTopBarSecondaryColor + label.font = uiConfiguration.fontProvider.formLabelFont + label.numberOfLines = 0 + return label + }() + private(set) lazy var secondParagraphLabel: UILabel = { + let label = ComponentCatalog.formListLabelWith(text: "", + textAlignment: .left, + uiConfig: uiConfiguration) + label.textColor = uiConfiguration.textTopBarSecondaryColor + label.font = uiConfiguration.fontProvider.formLabelFont + label.numberOfLines = 0 + return label + }() + private(set) lazy var actionButton: UIButton = { + let button = UIButton() + button.backgroundColor = uiConfiguration.uiPrimaryColor + button.setTitleColor(.white, for: .normal) + button.setTitle("load_funds.add_card.onboarding.primary_cta".podLocalized(), for: .normal) + button.titleLabel?.font = UITheme2FontProvider(fontDescriptors: nil).mainItemLightFont + return button + }() + + init(uiconfig: UIConfig) { + self.uiConfiguration = uiconfig + super.init(frame: .zero) + setupViews() + setupConstraints() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + private func setupViews() { + backgroundColor = uiConfiguration.textMessageColor + isOpaque = false + + [headerLabel, firstParagraphLabel, secondParagraphLabel, actionButton].forEach(addSubview) + actionButton.layer.cornerRadius = 25 + } + + private func setupConstraints() { + headerLabel.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(20) + make.top.equalToSuperview().offset(52) + } + firstParagraphLabel.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(20) + make.top.equalTo(headerLabel.snp.bottom).offset(28) + } + secondParagraphLabel.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(20) + make.top.equalTo(firstParagraphLabel.snp.bottom).offset(20) + } + actionButton.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(20) + make.height.equalTo(48) + make.bottom.equalTo(bottomConstraint).inset(34) + } + } + + // MARK: Public methods + public func configure(firstParagraph: String, secondParagraph: String) { + firstParagraphLabel.text = firstParagraph + secondParagraphLabel.text = secondParagraph + } + + func hideView() { + [headerLabel, firstParagraphLabel, secondParagraphLabel, actionButton].forEach { view in + view.alpha = 0 + } + } + + func showView() { + [headerLabel, firstParagraphLabel, secondParagraphLabel, actionButton].forEach { view in + view.alpha = 1 + } + } +} + + diff --git a/Pod/Classes/ui/CardKit/AddMoney/ViewController/AddCardOnboardingViewController.swift b/Pod/Classes/ui/CardKit/AddMoney/ViewController/AddCardOnboardingViewController.swift new file mode 100644 index 0000000..df1295a --- /dev/null +++ b/Pod/Classes/ui/CardKit/AddMoney/ViewController/AddCardOnboardingViewController.swift @@ -0,0 +1,79 @@ +// +// AddCardOnboardingViewController.swift +// AptoUISDK +// +// Created by Fabio Cuomo on 15/6/21. +// + +import AptoSDK + +class AddCardOnboardingViewController: ShiftViewController { + private(set) lazy var mainView = AddCardOnboardingView(uiconfig: uiConfiguration) + private let viewModel: AddCardOnboardingViewModel + public typealias CloseCompletionResult = ((UIViewController) -> Void) + var closeButtonCompletion: CloseCompletionResult? + var actionButtonCompletion: (() -> Void)? + + init(uiConfiguration: UIConfig, viewModel: AddCardOnboardingViewModel) { + self.viewModel = viewModel + super.init(uiConfiguration: uiConfiguration) + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func loadView() { + self.view = mainView + } + + override func viewDidLoad() { + super.viewDidLoad() + setupBinding() + viewModel.fetchInfo() + } + + override func closeTapped() { + closeButtonCompletion?(self) + dismiss(animated: true) + } + + // MARK: Private methods + private func setupBinding() { + viewModel.onCardLoadingStateChange = { [weak self] isLoading in + if isLoading { + self?.showLoadingSpinner() + self?.mainView.hideView() + } else { + self?.hideActivityIndicator() + } + } + + viewModel.onCardInfoLoadedSuccessfully = { [weak self] cardData in + if let cardName = cardData.cardName, let descriptor = cardData.softDescriptor { + let p1 = "load_funds.add_card.onboarding.explanation".podLocalized().replace(["<>" : cardName]) + let p2 = "load_funds.add_card.onboarding.explanation_2".podLocalized().replace(["<>" : descriptor]) + self?.mainView.configure(firstParagraph: p1, secondParagraph: p2) + } + } + + viewModel.onErrorCardLoading = { [weak self] error in + self?.show(error: error) + } + + mainView.actionButton.addTarget(self, action: #selector(didTapOnActionButton), for: .touchUpInside) + } + + private func hideActivityIndicator() { + hideLoadingSpinner() + UIView.animate(withDuration: 0.3) { [weak self] in + self?.mainView.showView() + } + } + + @objc func didTapOnActionButton() { + LoadFundsOnBoardingHelper.markAsPresented() + dismiss(animated: true) { [weak self] in + self?.actionButtonCompletion?() + } + } +} diff --git a/Pod/Classes/ui/CardKit/AddMoney/ViewModel/AddCardOnboardingViewModel.swift b/Pod/Classes/ui/CardKit/AddMoney/ViewModel/AddCardOnboardingViewModel.swift new file mode 100644 index 0000000..7d6c78c --- /dev/null +++ b/Pod/Classes/ui/CardKit/AddMoney/ViewModel/AddCardOnboardingViewModel.swift @@ -0,0 +1,64 @@ +// +// AddCardOnboardingViewModel.swift +// AptoUISDK +// +// Created by Fabio Cuomo on 15/6/21. +// + +import Foundation +import AptoSDK + +struct OnBoardingCardData { + let cardName: String? + let softDescriptor: String? +} + +final class AddCardOnboardingViewModel { + private let loader: AptoPlatformProtocol + private let cardId: String + typealias Observer = (T) -> Void + + var onCardLoadingStateChange: Observer? + var onErrorCardLoading: Observer? + var onCardInfoLoadedSuccessfully: Observer? + + init(cardId: String, loader: AptoPlatformProtocol) { + self.loader = loader + self.cardId = cardId + } + + public func fetchInfo() { + onCardLoadingStateChange?(true) + loader + .fetchCard(cardId, + forceRefresh: false, + retrieveBalances: false, + callback: { [weak self] cardResult in + switch cardResult { + case .success(let card): + let descriptor = card.features?.funding?.softDescriptor ?? "" + guard let cardProductId = card.cardProductId else { + self?.onCardInfoLoadedSuccessfully?(OnBoardingCardData(cardName: "", softDescriptor: descriptor)) + return + } + self?.fetchCompanyName(cardProductId: cardProductId, descriptor: descriptor) + case .failure(let error): + self?.onErrorCardLoading?(error) + } + self?.onCardLoadingStateChange?(false) + }) + } + + public func fetchCompanyName(cardProductId: String, descriptor: String) { + loader.fetchCardProduct(cardProductId: cardProductId, + forceRefresh: false) { [weak self] result in + switch result { + case .success(let cardProduct): + self?.onCardInfoLoadedSuccessfully?(OnBoardingCardData(cardName: cardProduct.name, softDescriptor: descriptor)) + case .failure(let error): + self?.onErrorCardLoading?(error) + } + } + } + +} diff --git a/Pod/Classes/ui/CardKit/ApplePay/IAPActivation/ApplePayIAPUIComposer.swift b/Pod/Classes/ui/CardKit/ApplePay/IAPActivation/ApplePayIAPUIComposer.swift new file mode 100644 index 0000000..a76af70 --- /dev/null +++ b/Pod/Classes/ui/CardKit/ApplePay/IAPActivation/ApplePayIAPUIComposer.swift @@ -0,0 +1,29 @@ +// +// ApplePayIAPUIComposer.swift +// AptoUISDK +// +// Created by Fabio Cuomo on 16/4/21. +// + +import Foundation + +import AptoSDK + +final class ApplePayIAPUIComposer { + private init() {} + public typealias ContinueCompletion = (() -> Void) + public typealias IAPCompletion = (() -> Void) + + public static func composedWith(cardId: String, + cardLoader: AptoPlatformProtocol, + uiConfiguration: UIConfig, + iapCompletion: IAPCompletion? = nil) -> ApplePayIAPViewController { + let viewModel = ApplePayIAPViewModel(cardId: cardId, loader: cardLoader) + let controller = ApplePayIAPViewController(viewModel: viewModel, uiConfiguration: UIConfig.default) + controller.didFinishInAppProvisioning = { controller, pass, error in + controller.dismiss(animated: true) + iapCompletion?() + } + return controller + } +} diff --git a/Pod/Classes/ui/CardKit/ApplePay/IAPActivation/ApplePayIAPViewController.swift b/Pod/Classes/ui/CardKit/ApplePay/IAPActivation/ApplePayIAPViewController.swift new file mode 100644 index 0000000..a5a48cd --- /dev/null +++ b/Pod/Classes/ui/CardKit/ApplePay/IAPActivation/ApplePayIAPViewController.swift @@ -0,0 +1,73 @@ +// +// ApplePayIAPViewControllerTests.swift +// AptoUISDK +// +// Created by Fabio Cuomo on 15/4/21. +// + +import AptoSDK +import SnapKit +import PassKit + +final class ApplePayIAPViewController: ShiftViewController { + var onCompletion: (() -> Void)? + private let viewModel: ApplePayIAPViewModel + var didFinishInAppProvisioning: ((PKAddPaymentPassViewController, PKPaymentPass?, Error?) -> Void)? + + init(viewModel: ApplePayIAPViewModel, uiConfiguration: UIConfig) { + self.viewModel = viewModel + super.init(uiConfiguration: uiConfiguration) + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func viewDidLoad() { + super.viewDidLoad() + navigationItem.setHidesBackButton(true, animated: false) + setupBindings() + viewModel.loadCard() + } + + private func setupBindings() { + viewModel.onCardFetched = { [weak self] card in + guard let configuration = PKAddPaymentPassRequestConfiguration(encryptionScheme: .ECC_V2) else { + self?.show(error: BackendError(code: .incorrectParameters)) + return + } + configuration.cardholderName = card.cardHolder + configuration.primaryAccountSuffix = card.lastFourDigits + + guard let paymentPassViewController = PKAddPaymentPassViewController(requestConfiguration: configuration, delegate: self) else { + self?.show(error: BackendError(code: .undefinedError)) + return + } + self?.addChild(paymentPassViewController) + self?.view.addSubview(paymentPassViewController.view) + paymentPassViewController.didMove(toParent: self) + } + } + + override func closeTapped() { + dismiss(animated: true) + } +} + +extension ApplePayIAPViewController: PKAddPaymentPassViewControllerDelegate { + func addPaymentPassViewController(_ controller: PKAddPaymentPassViewController, + generateRequestWithCertificateChain certificates: [Data], + nonce: Data, nonceSignature: Data, + completionHandler handler: @escaping (PKAddPaymentPassRequest) -> Void) { + viewModel.sendRequestData(certificates: certificates, nonce: nonce, nonceSignature: nonceSignature) { issuerResponse in + let request = PKAddPaymentPassRequest() + request.activationData = issuerResponse.activationData + request.ephemeralPublicKey = issuerResponse.ephemeralPublicKey + request.encryptedPassData = issuerResponse.encryptedPassData + handler(request) + } + } + + func addPaymentPassViewController(_ controller: PKAddPaymentPassViewController, didFinishAdding pass: PKPaymentPass?, error: Error?) { + didFinishInAppProvisioning?(controller, pass, error) + } +} diff --git a/Pod/Classes/ui/CardKit/ApplePay/IAPActivation/ApplePayIAPViewModel.swift b/Pod/Classes/ui/CardKit/ApplePay/IAPActivation/ApplePayIAPViewModel.swift new file mode 100644 index 0000000..bf0086a --- /dev/null +++ b/Pod/Classes/ui/CardKit/ApplePay/IAPActivation/ApplePayIAPViewModel.swift @@ -0,0 +1,55 @@ +// +// ApplePaySplashViewModel.swift +// AptoUISDK +// +// Created by Fabio Cuomo on 16/4/21. +// + +import Foundation +import AptoSDK + +final class ApplePayIAPViewModel { + typealias Observer = (T) -> Void + + private let loader: AptoPlatformProtocol + private let cardId: String + var onLoadingStateChange: Observer? + var onCardFetched: Observer? + var onCardError: Observer? + var onPayloadFetched: Observer? + var onPayloadError: Observer? + + init(cardId: String, loader: AptoPlatformProtocol) { + self.cardId = cardId + self.loader = loader + } + + func loadCard() { + onLoadingStateChange?(true) + loader.fetchCard(cardId, forceRefresh: false) { [weak self] result in + switch result { + case .success(let card): + self?.onCardFetched?(card) + case .failure(let error): + self?.onCardError?(error) + } + self?.onLoadingStateChange?(false) + } + } + + func sendRequestData(certificates: [Data], nonce: Data, nonceSignature: Data, completion: @escaping (ApplePayIAPIssuerResponse) -> Void) { + onLoadingStateChange?(true) + loader.startApplePayInAppProvisioning(cardId: cardId, + certificates: certificates, + nonce: nonce, + nonceSignature: nonceSignature) { [weak self] result in + switch result { + case .success(let responsePayload): + completion(responsePayload) + self?.onPayloadFetched?(responsePayload) + case .failure(let error): + self?.onPayloadError?(error) + } + } + } +} diff --git a/Pod/Classes/ui/CardKit/ApplePay/InAppProvisioningHelper.swift b/Pod/Classes/ui/CardKit/ApplePay/InAppProvisioningHelper.swift new file mode 100644 index 0000000..506d692 --- /dev/null +++ b/Pod/Classes/ui/CardKit/ApplePay/InAppProvisioningHelper.swift @@ -0,0 +1,37 @@ +// +// InAppProvisioningHelper.swift +// AptoUISDK +// +// Created by Fabio Cuomo on 14/4/21. +// + +import PassKit +import AptoSDK + +public protocol ApplePayInAppProvisioningProtocol { + func shouldShowAppleWalletButton(iapEnabled: Bool) -> Bool + func appleWalletButton() -> UIButton +} + +public typealias AppleWalletButtonAction = (() -> Void) + +public class InAppProvisioningHelper: ApplePayInAppProvisioningProtocol { + + static let appleWalletButtonTag = 223311 + static let appleWalletContainerViewTag = 223312 + + public init() {} + + public func shouldShowAppleWalletButton(iapEnabled: Bool) -> Bool { + let pLib = PKPassLibrary() + let passes = pLib.passes(of: .payment) + return iapEnabled && PKAddPaymentPassViewController.canAddPaymentPass() && passes.count == 0 + } + + public func appleWalletButton() -> UIButton { + let button = PKAddPassButton(addPassButtonStyle: .blackOutline) + button.translatesAutoresizingMaskIntoConstraints = false + button.tag = InAppProvisioningHelper.appleWalletButtonTag + return button + } +} diff --git a/Pod/Classes/ui/CardKit/CardSettingsModule/CardSettings/CardSettingsPresenter.swift b/Pod/Classes/ui/CardKit/CardSettingsModule/CardSettings/CardSettingsPresenter.swift index 75ac975..b41dd8f 100644 --- a/Pod/Classes/ui/CardKit/CardSettingsModule/CardSettings/CardSettingsPresenter.swift +++ b/Pod/Classes/ui/CardKit/CardSettingsModule/CardSettings/CardSettingsPresenter.swift @@ -135,19 +135,21 @@ class CardSettingsPresenter: CardSettingsPresenterProtocol { } } - fileprivate func refreshData() { - viewModel.locked.send(card.state != .active) - viewModel.buttonsVisibility.send(CardSettingsButtonsVisibility( - showChangePin: card.features?.setPin?.status == .enabled, - showGetPin: card.features?.getPin?.status == .enabled, - showSetPassCode: card.features?.passCode?.status == .enabled, - showIVRSupport: card.features?.ivrSupport?.status == .enabled, - showDetailedCardActivity: config.showDetailedCardActivity, - isShowDetailedCardActivityEnabled: interactor.isShowDetailedCardActivityEnabled(), - showMonthlyStatements: config.showMonthlyStatements, - showAddFundsFeature: card.features?.funding?.status == .enabled, - showOrderPhysicalCard: card.orderedStatus == .available)) - } + fileprivate func refreshData() { + viewModel.locked.send(card.state != .active) + let iapEnabled = card.features?.inAppProvisioning?.status == .enabled + viewModel.buttonsVisibility.send(CardSettingsButtonsVisibility( + showChangePin: card.features?.setPin?.status == .enabled, + showGetPin: card.features?.getPin?.status == .enabled, + showSetPassCode: card.features?.passCode?.status == .enabled, + showIVRSupport: card.features?.ivrSupport?.status == .enabled, + showDetailedCardActivity: config.showDetailedCardActivity, + isShowDetailedCardActivityEnabled: interactor.isShowDetailedCardActivityEnabled(), + showMonthlyStatements: config.showMonthlyStatements, + showAddFundsFeature: card.features?.funding?.status == .enabled, + showOrderPhysicalCard: card.orderedStatus == .available, + showAppleWalletRow: InAppProvisioningHelper().shouldShowAppleWalletButton(iapEnabled: iapEnabled))) + } func closeTapped() { router.closeFromShiftCardSettings() @@ -223,6 +225,13 @@ class CardSettingsPresenter: CardSettingsPresenterProtocol { } } + func didTapOnApplePayIAP() { + let cardId = card.accountId + router.showApplePayIAP(cardId: cardId) { [weak self] in + self?.refreshCardData(cardId) + } + } + func lostCardTapped() { reportLostCardAction.run { [unowned self] result in switch result { diff --git a/Pod/Classes/ui/CardKit/CardSettingsModule/CardSettings/CardSettingsViewControllerTheme2.swift b/Pod/Classes/ui/CardKit/CardSettingsModule/CardSettings/CardSettingsViewControllerTheme2.swift index 280cae4..1dbd4a5 100644 --- a/Pod/Classes/ui/CardKit/CardSettingsModule/CardSettings/CardSettingsViewControllerTheme2.swift +++ b/Pod/Classes/ui/CardKit/CardSettingsModule/CardSettings/CardSettingsViewControllerTheme2.swift @@ -133,8 +133,9 @@ private extension CardSettingsViewControllerTheme2 { self.createGetPinRow(showButton: buttonsVisibility.showGetPin), self.createSetPassCodeRow(showButton: buttonsVisibility.showSetPassCode), self.setUpShowCardInfoRow(), - self.setUpLockCardRow(lastItem: buttonsVisibility.showOrderPhysicalCard == false), - self.addShowOrderPhysicalCardRow(showButton: buttonsVisibility.showOrderPhysicalCard) + self.showAppleWallet(showButton: buttonsVisibility.showAppleWalletRow), + self.addShowOrderPhysicalCardRow(showButton: buttonsVisibility.showOrderPhysicalCard), + self.setUpLockCardRow(), ].compactMap { return $0 } let transactionsRows = [ self.createTransactionsTitle(showDetailedCardActivity: buttonsVisibility.showDetailedCardActivity), @@ -314,7 +315,7 @@ private extension CardSettingsViewControllerTheme2 { } } - func setUpLockCardRow(lastItem: Bool) -> FormRowSwitchTitleSubtitleView? { + func setUpLockCardRow() -> FormRowSwitchTitleSubtitleView? { let title = "card_settings.settings.lock_card.title".podLocalized() let subtitle = "card_settings.settings.lock_card.description".podLocalized() lockCardRow = FormBuilder.titleSubtitleSwitchRowWith(title: title, @@ -327,7 +328,7 @@ private extension CardSettingsViewControllerTheme2 { if let locked = presenter.viewModel.locked.value { set(lockedSwitch: locked) } - lockCardRow?.showSplitter = !lastItem + lockCardRow?.showSplitter = false return lockCardRow } @@ -386,7 +387,17 @@ private extension CardSettingsViewControllerTheme2 { uiConfig: uiConfiguration) { [weak self] in self?.presenter.didTapOnOrderPhysicalCard() } - addOrderPhysicalCardRow.showSplitter = false + addOrderPhysicalCardRow.showSplitter = true return addOrderPhysicalCardRow } + + func showAppleWallet(showButton: Bool) -> FormRowView? { + guard showButton else { return nil } + let applePayRow = ApplePayRowItemView(with: "card_settings.apple_pay.add_to_wallet.title".podLocalized(), + uiconfig: uiConfiguration) { [presenter] in + presenter.didTapOnApplePayIAP() + } + applePayRow.showSplitter = true + return applePayRow + } } diff --git a/Pod/Classes/ui/CardKit/CardSettingsModule/CardSettingsContract.swift b/Pod/Classes/ui/CardKit/CardSettingsModule/CardSettingsContract.swift index eae5140..6526721 100644 --- a/Pod/Classes/ui/CardKit/CardSettingsModule/CardSettingsContract.swift +++ b/Pod/Classes/ui/CardKit/CardSettingsModule/CardSettingsContract.swift @@ -34,6 +34,7 @@ protocol CardSettingsRouterProtocol: class { declineCompletion: @escaping () -> Void) func showAddMoneyBottomSheet(card: Card, extraContent: ExtraContent?) func showOrderPhysicalCard(_ card: Card, completion: OrderPhysicalCardUIComposer.OrderedCompletion?) + func showApplePayIAP(cardId: String, completion: ApplePayIAPUIComposer.IAPCompletion?) } extension CardSettingsRouterProtocol { @@ -83,13 +84,14 @@ struct CardSettingsButtonsVisibility { let showMonthlyStatements: Bool let showAddFundsFeature: Bool let showOrderPhysicalCard: Bool + let showAppleWalletRow: Bool } extension CardSettingsButtonsVisibility { init() { self.init(showChangePin: false, showGetPin: false, showSetPassCode: false, showIVRSupport: false, showDetailedCardActivity: false, isShowDetailedCardActivityEnabled: false, showMonthlyStatements: false, - showAddFundsFeature: false, showOrderPhysicalCard: false) + showAddFundsFeature: false, showOrderPhysicalCard: false, showAppleWalletRow: false) } } @@ -124,6 +126,7 @@ protocol CardSettingsPresenterProtocol: class { func monthlyStatementsTapped() func didTapOnLoadFunds() func didTapOnOrderPhysicalCard() + func didTapOnApplePayIAP() } public struct ExtraContent { diff --git a/Pod/Classes/ui/CardKit/CardSettingsModule/CardSettingsModule.swift b/Pod/Classes/ui/CardKit/CardSettingsModule/CardSettingsModule.swift index 7c84bf0..9ba72b7 100644 --- a/Pod/Classes/ui/CardKit/CardSettingsModule/CardSettingsModule.swift +++ b/Pod/Classes/ui/CardKit/CardSettingsModule/CardSettingsModule.swift @@ -182,8 +182,34 @@ extension CardSettingsModule: CardSettingsRouterProtocol { } func showAddFunds(for card: Card, extraContent: ExtraContent? = nil) { + let viewController = addFundsUIComposer(with: card, extraContent: extraContent) + navigationController?.pushViewController(viewController, animated: true, completion: nil) + } + + private func addFundsUIComposer(with card: Card, extraContent: ExtraContent? = nil) -> AddFundsViewController { let viewModel = AddFundsViewModel(card: card) let viewController = AddFundsViewController(viewModel: viewModel, uiConfig: uiConfig) + viewController.onPaymentSourceLoaded = { + if LoadFundsOnBoardingHelper.shouldPresentOnBoarding() { + + let actionCompletion = { [weak self] in + if let addCardController = self?.addCardUIComposer() { + self?.present(viewController: addCardController, animated: true, embedInNavigationController: true, completion: {}) + } + } + let closeCompletion: AddCardOnboardingViewController.CloseCompletionResult = { controller in + if let nc = controller.presentingViewController as? UINavigationController { + nc.popViewController(animated: false) + } + } + let controller = AddCardOnBoardingUIComposer.compose(with: card, + extraContent: extraContent, + platform: self.serviceLocator.platform, + actionCompletion: actionCompletion, + closeCompletion: closeCompletion) + self.present(viewController: controller, animated: true, embedInNavigationController: true, completion: {}) + } + } let fundsNavigator = AddFundsNavigator( from: viewController, uiConfig: uiConfig, @@ -197,7 +223,17 @@ extension CardSettingsModule: CardSettingsRouterProtocol { } } viewModel.navigator = fundsNavigator - navigationController?.pushViewController(viewController, animated: true, completion: nil) + return viewController + } + + private func addCardUIComposer() -> AddCardViewController { + let cardNetworks = card.features?.funding?.cardNetworks ?? [] + let viewModel = AddCardViewModel(cardNetworks: cardNetworks) + let controller = AddCardViewController(viewModel: viewModel, uiConfig: uiConfig, cardNetworks: cardNetworks) + controller.closeCompletion = { addCardController in + addCardController.dismiss(animated: true) + } + return controller } func showACHAccountAgreements(disclaimer: Content, @@ -259,4 +295,11 @@ extension CardSettingsModule: CardSettingsRouterProtocol { cardConfigErrorCompletion: errorCompletion) present(viewController: viewController, animated: true, embedInNavigationController: true, completion: {}) } + + func showApplePayIAP(cardId: String, completion: ApplePayIAPUIComposer.IAPCompletion? = nil) { + let viewController = ApplePayIAPUIComposer.composedWith(cardId: cardId, + cardLoader: serviceLocator.platform, uiConfiguration: UIConfig.default, + iapCompletion: completion) + present(viewController: viewController, animated: true, completion: {}) + } } diff --git a/Pod/Classes/ui/CardKit/ManageCardModule/ManageCard/ManageCardPresenter.swift b/Pod/Classes/ui/CardKit/ManageCardModule/ManageCard/ManageCardPresenter.swift index 1d4ab26..b0ad2c0 100644 --- a/Pod/Classes/ui/CardKit/ManageCardModule/ManageCard/ManageCardPresenter.swift +++ b/Pod/Classes/ui/CardKit/ManageCardModule/ManageCard/ManageCardPresenter.swift @@ -157,6 +157,10 @@ class ManageCardPresenter: ManageCardPresenterProtocol { analyticsManager?.track(event: Event.manageCardGetPINNue) } + func initInAppProvisioningEnrollProcess() { + router.enrollApplePayProvisioning() + } + // MARK: - Private methods func retrieveFundingSource() { diff --git a/Pod/Classes/ui/CardKit/ManageCardModule/ManageCard/ManageCardViewControllerTheme2.swift b/Pod/Classes/ui/CardKit/ManageCardModule/ManageCard/ManageCardViewControllerTheme2.swift index 8ebe186..ca8a8aa 100644 --- a/Pod/Classes/ui/CardKit/ManageCardModule/ManageCard/ManageCardViewControllerTheme2.swift +++ b/Pod/Classes/ui/CardKit/ManageCardModule/ManageCard/ManageCardViewControllerTheme2.swift @@ -333,21 +333,36 @@ private extension ManageCardViewControllerTheme2 { } } - func setUpEmptyCaseView() { - emptyCaseView.subviews.forEach { $0.removeFromSuperview() } - emptyCaseView.backgroundColor = .clear - let message = "manage_card.transaction_list.empty_case.title".podLocalized() - let label = ComponentCatalog.sectionTitleLabelWith(text: message, textAlignment: .center, uiConfig: uiConfiguration) - label.textColor = uiConfiguration.textTertiaryColor - label.numberOfLines = 0 - emptyCaseView.addSubview(label) - label.snp.makeConstraints { make in - make.left.right.equalToSuperview().inset(16) - make.centerY.equalToSuperview().offset(-16) + func setUpEmptyCaseView(iapEnabled: Bool) { + emptyCaseView.subviews.forEach { $0.removeFromSuperview() } + emptyCaseView.backgroundColor = .clear + let message = "manage_card.transaction_list.empty_case.title".podLocalized() + let label = ComponentCatalog.sectionTitleLabelWith(text: message, textAlignment: .center, uiConfig: uiConfiguration) + label.textColor = uiConfiguration.textTertiaryColor + label.numberOfLines = 0 + emptyCaseView.addSubview(label) + label.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(16) + make.centerY.equalToSuperview().offset(-16) + } + view.bringSubviewToFront(emptyCaseView) + + if InAppProvisioningHelper().shouldShowAppleWalletButton(iapEnabled: iapEnabled) { + setupAppleWalletButton() + } } - view.bringSubviewToFront(emptyCaseView) - } + private func setupAppleWalletButton() { + let button = InAppProvisioningHelper().appleWalletButton() + emptyCaseView.addSubview(button) + button.snp.makeConstraints { make in + make.top.equalToSuperview().offset(30) + make.centerX.equalToSuperview() + } + button.bringSubviewToFront(emptyCaseView) + button.addTarget(self, action: #selector(didTapOnWalletButton), for: .touchUpInside) + } + func setUpActivateCardView() { activateCardView.delegate = self activateCardView.backgroundColor = uiConfiguration.uiBackgroundSecondaryColor @@ -372,6 +387,10 @@ private extension ManageCardViewControllerTheme2 { navigationItem.rightBarButtonItems = items } } + + @objc func didTapOnWalletButton() { + presenter.initInAppProvisioningEnrollProcess() + } } // MARK: - View model subscriptions @@ -457,6 +476,14 @@ private extension ManageCardViewControllerTheme2 { self.setUpNavigationBarActions(isStatsFeatureEnabled: isStatsFeatureEnabled, isAccountSettingsEnabled: isAccountSettingsEnabled) }.dispose(in: disposeBag) + + combineLatest(viewModel.transactionsLoaded, viewModel.card).observeNext { [weak self, viewModel] transactionLoaded, card in + guard let self = self else { return } + if transactionLoaded && viewModel.transactions.numberOfItemsInAllSections == 0, + let iapStatus = card?.features?.inAppProvisioning?.status { + self.showEmptyCase(iapEnabled: iapStatus == .enabled) + } + }.dispose(in: disposeBag) } func updateUI() { @@ -471,7 +498,6 @@ private extension ManageCardViewControllerTheme2 { emptyCaseView.isHidden = !shouldShowEmptyCase if shouldShowEmptyCase { view.backgroundColor = uiConfiguration.uiBackgroundSecondaryColor - showEmptyCase() } else { view.backgroundColor = uiConfiguration.uiNavigationSecondaryColor @@ -480,18 +506,18 @@ private extension ManageCardViewControllerTheme2 { } } - func showEmptyCase() { - bottomBackgroundView.backgroundColor = uiConfiguration.uiBackgroundSecondaryColor - emptyCaseView.removeFromSuperview() - emptyCaseView.snp.removeConstraints() - view.addSubview(emptyCaseView) - emptyCaseView.snp.makeConstraints { make in - let topConstraint = transactionsList.visibleCells.last?.snp.bottom ?? view.snp.top - make.top.equalTo(topConstraint) - make.left.right.bottom.equalToSuperview() + func showEmptyCase(iapEnabled: Bool) { + bottomBackgroundView.backgroundColor = uiConfiguration.uiBackgroundSecondaryColor + emptyCaseView.removeFromSuperview() + emptyCaseView.snp.removeConstraints() + view.addSubview(emptyCaseView) + emptyCaseView.snp.makeConstraints { make in + let topConstraint = transactionsList.visibleCells.last?.snp.bottom ?? view.snp.top + make.top.equalTo(topConstraint) + make.left.right.bottom.equalToSuperview() + } + setUpEmptyCaseView(iapEnabled: iapEnabled) } - setUpEmptyCaseView() - } func buildActivatePhysicalCardButtonItem() -> UIBarButtonItem { let topBarButtonItem = TopBarButtonItem( diff --git a/Pod/Classes/ui/CardKit/ManageCardModule/ManageCardContract.swift b/Pod/Classes/ui/CardKit/ManageCardModule/ManageCardContract.swift index 9b0afad..a908ff9 100644 --- a/Pod/Classes/ui/CardKit/ManageCardModule/ManageCardContract.swift +++ b/Pod/Classes/ui/CardKit/ManageCardModule/ManageCardContract.swift @@ -17,6 +17,7 @@ protocol ManageCardRouterProtocol: class { func showCardStatsTappedInManageCardViewer() func showTransactionDetails(transaction: Transaction) func physicalActivationSucceed() + func enrollApplePayProvisioning() } protocol ManageCardViewProtocol: ViewControllerProtocol { @@ -61,6 +62,7 @@ protocol ManageCardEventHandler: class { func moreTransactionsTapped(completion: @escaping (_ noMoreTransactions: Bool) -> Void) func transactionSelected(indexPath: IndexPath) func activatePhysicalCardTapped() + func initInAppProvisioningEnrollProcess() } open class ManageCardViewModel { diff --git a/Pod/Classes/ui/CardKit/ManageCardModule/ManageCardModule.swift b/Pod/Classes/ui/CardKit/ManageCardModule/ManageCardModule.swift index 6bac821..a45e7dc 100644 --- a/Pod/Classes/ui/CardKit/ManageCardModule/ManageCardModule.swift +++ b/Pod/Classes/ui/CardKit/ManageCardModule/ManageCardModule.swift @@ -347,6 +347,13 @@ extension ManageCardModule: ManageCardRouterProtocol { self.transactionDetailsPresenter = presenter return viewController } + + func enrollApplePayProvisioning() { + let iapController = ApplePayIAPUIComposer.composedWith(cardId: card.accountId, + cardLoader: platform, + uiConfiguration: UIConfig.default) + present(viewController: iapController, animated: true) { } + } } extension ManageCardModule: ShiftCardTransactionDetailsRouterProtocol { diff --git a/Pod/Classes/ui/Features/Payments/Add Card/View/AddCardViewController.swift b/Pod/Classes/ui/Features/Payments/Add Card/View/AddCardViewController.swift index 8cd8cca..9e6a79f 100644 --- a/Pod/Classes/ui/Features/Payments/Add Card/View/AddCardViewController.swift +++ b/Pod/Classes/ui/Features/Payments/Add Card/View/AddCardViewController.swift @@ -11,7 +11,8 @@ final class AddCardViewController: UIViewController { let addCardView = AddCardView(uiConfig: uiConfig, cardNetworks: self.cardNetworks) return addCardView }() - + var closeCompletion: ((UIViewController) -> Void)? + init(viewModel: AddCardViewModelType, uiConfig: UIConfig, cardNetworks: [CardNetwork]) { self.viewModel = viewModel self.uiConfig = uiConfig @@ -48,7 +49,11 @@ final class AddCardViewController: UIViewController { } override func closeTapped() { - viewModel.input.didTapOnClose() + if let closeCompletion = closeCompletion { + closeCompletion(self) + } else { + viewModel.input.didTapOnClose() + } } private func configureNavigationBar() { diff --git a/Pod/Classes/ui/Features/Payments/Add Funds/Navigator/AddFundsNavigator.swift b/Pod/Classes/ui/Features/Payments/Add Funds/Navigator/AddFundsNavigator.swift index 1d9c12c..8156913 100644 --- a/Pod/Classes/ui/Features/Payments/Add Funds/Navigator/AddFundsNavigator.swift +++ b/Pod/Classes/ui/Features/Payments/Add Funds/Navigator/AddFundsNavigator.swift @@ -73,6 +73,7 @@ final class AddFundsNavigator: AddFundsNavigatorType { self?.from.navigationController?.popViewController(animated: true) } viewModel.navigator = transferStatusNavigator + navigationController.modalPresentationStyle = .fullScreen from.navigationController?.present(navigationController, animated: true) } } diff --git a/Pod/Classes/ui/Features/Payments/Add Funds/View Model/AddFundsViewModel.swift b/Pod/Classes/ui/Features/Payments/Add Funds/View Model/AddFundsViewModel.swift index b4e9a22..65e3a35 100644 --- a/Pod/Classes/ui/Features/Payments/Add Funds/View Model/AddFundsViewModel.swift +++ b/Pod/Classes/ui/Features/Payments/Add Funds/View Model/AddFundsViewModel.swift @@ -52,7 +52,9 @@ final class AddFundsViewModel: ViewModel { guard self.currentPaymentSource != nil, let value = value, let amount = Double(value), - amount > 0 else + amount > 0, + let lastChar = value.last, + lastChar != "." else { self.nextButtonEnabled.send(false) return diff --git a/Pod/Classes/ui/Features/Payments/Add Funds/View/AddFundsView.swift b/Pod/Classes/ui/Features/Payments/Add Funds/View/AddFundsView.swift index 711c40a..ec95b23 100644 --- a/Pod/Classes/ui/Features/Payments/Add Funds/View/AddFundsView.swift +++ b/Pod/Classes/ui/Features/Payments/Add Funds/View/AddFundsView.swift @@ -6,7 +6,8 @@ import AptoSDK final class AddFundsView: UIView { - static let maxAllowedDigit = 4 + static let maxAllowedDigit = 5 + static let maxAllowedDigitAfterDot = 2 private lazy var textField: UITextField = { let textField = ComponentCatalog.textFieldWith(placeholder: "$0",placeholderColor: uiConfig.textSecondaryColor, font: .boldSystemFont(ofSize: 44), textColor: uiConfig.textPrimaryColor) @@ -175,14 +176,23 @@ final class AddFundsView: UIView { extension AddFundsView: UITextFieldDelegate { func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { dailyLimitError("", show: false) - if (!string.isEmpty && !Character(string).isNumber) { + + guard let text = textField.text else { return false } + if !shouldAllowDot(lastChar: string, text: text) { + return false + } + if (!string.isEmpty && !Character(string).isNumber && string != ".") { return false } - guard let text = textField.text else { return false } - - if text.count == 1 && string.isEmpty { - didChangeAmountValue?(nil) + if text.contains(".") { + let sides = text.split(separator: ".") + let rightSideCnt = sides.count == 2 ? sides[1].count + 1 : 1 + if rightSideCnt <= AddFundsView.maxAllowedDigitAfterDot { + didChangeAmountValue?(updateAmountIfNeeded(lastChar: string, text: text + string)) + } else { + didChangeAmountValue?(updateAmountIfNeeded(lastChar: string, text: text)) + } } else { if text.count + 1 <= AddFundsView.maxAllowedDigit { didChangeAmountValue?(updateAmountIfNeeded(lastChar: string, text: text + string)) @@ -190,15 +200,32 @@ extension AddFundsView: UITextFieldDelegate { didChangeAmountValue?(updateAmountIfNeeded(lastChar: string, text: text)) } } - - if string.isEmpty && text.count <= AddFundsView.maxAllowedDigit { - return true - } - return text.count < AddFundsView.maxAllowedDigit + + if string.isEmpty { return true } + return shouldAddCharacter(lastChar: string, text: text) } private func updateAmountIfNeeded(lastChar: String, text: String) -> String { let amount = lastChar.isEmpty ? String(text.dropLast()) : text return Double(amount) != nil ? amount : "" } + + private func shouldAllowDot(lastChar: String, text: String) -> Bool { + let input = text + lastChar + let dotCount = input.filter { $0 == "." }.count + return dotCount <= 1 + } + + private func shouldAddCharacter(lastChar: String, text: String) -> Bool { + let input = text + lastChar + let sides = input.split(separator: ".") + switch sides.count { + case 1: + return sides[0].count < AddFundsView.maxAllowedDigit + case 2: + return sides[0].count < AddFundsView.maxAllowedDigit && sides[1].count <= AddFundsView.maxAllowedDigitAfterDot + default: + return false + } + } } diff --git a/Pod/Classes/ui/Features/Payments/Add Funds/View/AddFundsViewController.swift b/Pod/Classes/ui/Features/Payments/Add Funds/View/AddFundsViewController.swift index f151751..602510b 100644 --- a/Pod/Classes/ui/Features/Payments/Add Funds/View/AddFundsViewController.swift +++ b/Pod/Classes/ui/Features/Payments/Add Funds/View/AddFundsViewController.swift @@ -10,6 +10,8 @@ final class AddFundsViewController: UIViewController { let addFundsView = AddFundsView(uiConfig: uiConfig) return addFundsView }() + var onPaymentSourceLoaded: (() -> Void)? + init(viewModel: AddFundsViewModelType, uiConfig: UIConfig) { self.viewModel = viewModel @@ -48,6 +50,9 @@ final class AddFundsViewController: UIViewController { case .loading: self?.showLoadingView(uiConfig: self?.uiConfig) case .loaded(let paymentSource): + if paymentSource == nil { + self?.onPaymentSourceLoaded?() + } self?.addFundsView.set(current: paymentSource) case .error(let error): self?.show(error: error, uiConfig: self?.uiConfig) @@ -55,7 +60,7 @@ final class AddFundsViewController: UIViewController { }.dispose(in: disposeBag) addFundsView.didTapOnChangeCurrentCard = { [weak self] in - self?.viewModel.input.didTapOnChangeCard() + self?.showAddCardScreen() } addFundsView.didChangeAmountValue = { [weak self] value in @@ -68,4 +73,8 @@ final class AddFundsViewController: UIViewController { addFundsView.dailyLimitError(String(limit), show: true) } } + + func showAddCardScreen() { + viewModel.input.didTapOnChangeCard() + } } diff --git a/Pod/Classes/ui/Features/Payments/Transfer Status/View/TransferStatusView.swift b/Pod/Classes/ui/Features/Payments/Transfer Status/View/TransferStatusView.swift index eb183fb..e736f33 100644 --- a/Pod/Classes/ui/Features/Payments/Transfer Status/View/TransferStatusView.swift +++ b/Pod/Classes/ui/Features/Payments/Transfer Status/View/TransferStatusView.swift @@ -105,46 +105,54 @@ final class TransferStatusView: UIView { self.transferStateLabel.text = title } - private func setupConstraints() { - scrollView.snp.makeConstraints { constraints in - constraints.leading.equalToSuperview() - constraints.trailing.equalToSuperview() - constraints.top.equalToSuperview() - constraints.bottom.equalTo(closeButton.snp.top).inset(16) + private func setupConstraints() { + scrollView.snp.makeConstraints { constraints in + constraints.leading.equalToSuperview() + constraints.trailing.equalToSuperview() + constraints.top.equalToSuperview() + constraints.bottom.equalTo(closeButton.snp.top).inset(16) + } + + statusIcon.snp.makeConstraints { constraints in + constraints.size.equalTo(CGSize(width: 64, height: 64)) + constraints.centerX.equalToSuperview() + constraints.top.equalToSuperview().inset(self.statusIconTopConstraint()) + } + + transferStateLabel.snp.makeConstraints { constraints in + constraints.centerX.equalToSuperview() + constraints.leading.greaterThanOrEqualToSuperview().offset(16) + constraints.trailing.lessThanOrEqualToSuperview().offset(16) + constraints.top.equalTo(statusIcon.snp.bottom).offset(16) + } + + disclaimerLabel.snp.makeConstraints { constraints in + constraints.leading.equalTo(self).offset(16) + constraints.trailing.equalTo(self).inset(16) + constraints.top.equalTo(tableView.snp.bottom).offset(16) + constraints.bottom.lessThanOrEqualToSuperview().inset(32) + } + + tableView.snp.makeConstraints { constraints in + constraints.leading.equalTo(self) + constraints.trailing.equalTo(self) + constraints.height.equalTo(250) + constraints.top.equalTo(transferStateLabel.snp.bottom).offset(self.tableViewTopConstraint()) + } + + closeButton.snp.makeConstraints { constraints in + constraints.leading.equalToSuperview().inset(16) + constraints.trailing.equalToSuperview().inset(16) + constraints.bottom.equalToSuperview().inset(32) + constraints.height.equalTo(52) + } } - statusIcon.snp.makeConstraints { constraints in - constraints.size.equalTo(CGSize(width: 64, height: 64)) - constraints.centerX.equalToSuperview() - constraints.top.equalToSuperview().inset(100) + private func statusIconTopConstraint() -> CGFloat { + UIScreen.main.bounds.height > 667 ? 100 : 30 } - - transferStateLabel.snp.makeConstraints { constraints in - constraints.centerX.equalToSuperview() - constraints.leading.greaterThanOrEqualToSuperview().offset(16) - constraints.trailing.lessThanOrEqualToSuperview().offset(16) - constraints.top.equalTo(statusIcon.snp.bottom).offset(16) - } - - disclaimerLabel.snp.makeConstraints { constraints in - constraints.leading.equalTo(self).offset(16) - constraints.trailing.equalTo(self).inset(16) - constraints.top.equalTo(tableView.snp.bottom).offset(16) - constraints.bottom.lessThanOrEqualToSuperview().inset(32) - } - - tableView.snp.makeConstraints { constraints in - constraints.leading.equalTo(self) - constraints.trailing.equalTo(self) - constraints.height.equalTo(250) - constraints.top.equalTo(transferStateLabel.snp.bottom).offset(100) - } - - closeButton.snp.makeConstraints { constraints in - constraints.leading.equalToSuperview().inset(16) - constraints.trailing.equalToSuperview().inset(16) - constraints.bottom.equalToSuperview().inset(32) - constraints.height.equalTo(52) + + private func tableViewTopConstraint() -> CGFloat { + UIScreen.main.bounds.height > 667 ? 100 : 60 } - } } diff --git a/Pod/Classes/ui/UIKit/FormBuilder/ApplePayRowItemView.swift b/Pod/Classes/ui/UIKit/FormBuilder/ApplePayRowItemView.swift new file mode 100644 index 0000000..f7f1e04 --- /dev/null +++ b/Pod/Classes/ui/UIKit/FormBuilder/ApplePayRowItemView.swift @@ -0,0 +1,59 @@ +// +// ApplePayRowItemView.swift +// AptoUISDK +// +// Created by Fabio Cuomo on 13/4/21. +// + +import UIKit +import SnapKit +import AptoSDK + +final class ApplePayRowItemView: FormRowView { + private(set) lazy var titleLabel = ComponentCatalog.mainItemLightLabelWith(text: title, uiConfig: uiconfig) + private let appleMarkImageView = UIImageView(image: UIImage.imageFromPodBundle("apple_pay_mark")) + + private let title: String + private let uiconfig: UIConfig + + typealias ClickHandlerAction = (() -> Void) + + init(with title: String, + uiconfig: UIConfig, + clickHandler: ClickHandlerAction? = nil) { + self.title = title + self.uiconfig = uiconfig + super.init(showSplitter: true, height: 72) + setupViews() + setupConstraints() + setupBinding(handler: clickHandler) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + private func setupViews() { + backgroundColor = .white + [titleLabel, appleMarkImageView].forEach(addSubview) + } + + private func setupConstraints() { + appleMarkImageView.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.left.equalToSuperview().offset(32) + make.width.equalTo(50) + make.height.equalTo(32) + } + + titleLabel.snp.makeConstraints { make in + make.left.equalTo(appleMarkImageView.snp.right).offset(16) + make.centerY.equalToSuperview() + } + } + + private func setupBinding(handler: ClickHandlerAction?) { + if let handler = handler { + addTapGestureRecognizer(action: handler) + } + } +} diff --git a/Pod/Classes/ui/UIKit/FormBuilder/FormRowTextInputView.swift b/Pod/Classes/ui/UIKit/FormBuilder/FormRowTextInputView.swift index 0493ea8..69bbc56 100644 --- a/Pod/Classes/ui/UIKit/FormBuilder/FormRowTextInputView.swift +++ b/Pod/Classes/ui/UIKit/FormBuilder/FormRowTextInputView.swift @@ -161,10 +161,6 @@ open class FormRowTextInputView: FormRowLeftLabelView, UITextFieldDelegate { super.presentNonPassedValidationResult(reason) textField.textColor = uiConfig.uiErrorColor shakeTextField() - textField.show(message: reason, - title: "", - isError: true, - uiConfig: UIConfig.default, tapHandler: nil) } override func presentPassedValidationResult() { @@ -190,6 +186,10 @@ open class FormRowTextInputView: FormRowLeftLabelView, UITextFieldDelegate { } } + public func updateValidator(validator: DataValidator) { + textValidator = validator + } + // MARK: - Private methods and attributes private func setUpTextField() { diff --git a/Pod/Classes/ui/UserKit/UserDataCollectorModule/Steps/DataCollectorAddressStep.swift b/Pod/Classes/ui/UserKit/UserDataCollectorModule/Steps/DataCollectorAddressStep.swift index 0d04016..3fe6ba5 100644 --- a/Pod/Classes/ui/UserKit/UserDataCollectorModule/Steps/DataCollectorAddressStep.swift +++ b/Pod/Classes/ui/UserKit/UserDataCollectorModule/Steps/DataCollectorAddressStep.swift @@ -70,6 +70,9 @@ class AddressStep: DataCollectorBaseStep, DataCollectorStepProtocol { self.address.region.send(address?.region.value) self.address.zip.send(address?.zip.value) }.dispose(in: disposeBag) + addressField.valid.observeNext { [unowned self] valid in + self.aptUnitField?.isHidden = !valid + }.dispose(in: disposeBag) validatableRows.append(addressField) return addressField } diff --git a/Pod/Classes/ui/UserKit/UserDataCollectorModule/Steps/DataCollectorBirthdaySSNStep.swift b/Pod/Classes/ui/UserKit/UserDataCollectorModule/Steps/DataCollectorBirthdaySSNStep.swift deleted file mode 100644 index c1f8970..0000000 --- a/Pod/Classes/ui/UserKit/UserDataCollectorModule/Steps/DataCollectorBirthdaySSNStep.swift +++ /dev/null @@ -1,221 +0,0 @@ -// -// DataCollectorBirthdaySSNStep.swift -// AptoSDK -// -// Created by Ivan Oliver Martínez on 06/03/16. -// -// - -import AptoSDK -import Bond -import ReactiveKit -import SnapKit - -class BirthdaySSNStep: DataCollectorBaseStep, DataCollectorStepProtocol { - var title = "collect_user_data.dob.title".podLocalized() - fileprivate let linkHandler: LinkHandler? - fileprivate let mode: UserDataCollectorFinalStepMode - fileprivate var birthdayField: FormRowDatePickerView! // swiftlint:disable:this implicitly_unwrapped_optional - fileprivate var numberField: FormRowTextInputView! // swiftlint:disable:this implicitly_unwrapped_optional - private var countryField: FormRowCountryPickerView? - private var documentTypeField: FormRowIdDocumentTypePickerView? - - private let disposeBag = DisposeBag() - private let userData: DataPointList - private let requiredData: RequiredDataPointList - private let secondaryCredentialType: DataPointType - private var showBirthdate = true - private var showIdDocument = true - private var showOptionalIdDocument = false - private let allowedDocuments: [Country: [IdDocumentType]] - private lazy var allowedCountries: [Country] = { - return Array(allowedDocuments.keys) - }() - - init(requiredData: RequiredDataPointList, - secondaryCredentialType: DataPointType, - userData: DataPointList, - mode: UserDataCollectorFinalStepMode, - uiConfig: UIConfig, - linkHandler: LinkHandler?) { - self.userData = userData - self.requiredData = requiredData - self.linkHandler = linkHandler - self.mode = mode - self.secondaryCredentialType = secondaryCredentialType - if let dataPoint = requiredData.getRequiredDataPointOf(type: .idDocument), - let config = dataPoint.configuration as? AllowedIdDocumentTypesConfiguration, - !config.allowedDocumentTypes.isEmpty { - self.allowedDocuments = config.allowedDocumentTypes - } - else { - self.allowedDocuments = [Country.defaultCountry: [IdDocumentType.ssn]] - } - super.init(uiConfig: uiConfig) - } - - override func setupRows() -> [FormRowView] { - calculateFieldsVisibility() - - return [ - FormRowSeparatorView(backgroundColor: UIColor.clear, height: CGFloat(48)), - createBirthdayField(), - createCountryPickerField(), - createDocumentTypePickerField(), - createNumberField(), - setUpOptionalIdDocument(), - FormRowSeparatorView(backgroundColor: UIColor.clear, height: CGFloat(20)) - ].compactMap { return $0 } - } - - fileprivate func calculateFieldsVisibility() { - // Calculate if the Id Document Fields should be shown - if let showSSNRequiredDataPoint = requiredData.getRequiredDataPointOf(type: .idDocument) { - showIdDocument = true - showOptionalIdDocument = showSSNRequiredDataPoint.optional - } - else { - showIdDocument = false - showOptionalIdDocument = false - } - showBirthdate = requiredData.getRequiredDataPointOf(type: .birthDate) != nil - } -} - -private extension BirthdaySSNStep { - func createBirthdayField() -> FormRowDatePickerView? { - guard showBirthdate == true else { return nil } - - let birthDateDataPoint = userData.birthDateDataPoint - let failReason = "issue_card.issue_card.error.dob_too_young".podLocalized() - let minimumDob = Calendar.current.date(byAdding: .year, value: -18, to: Date()) - let dateValidator = MaximumDateValidator(maximumDate: minimumDob ?? Date(), failReasonMessage: failReason) - birthdayField = FormBuilder.datePickerRowWith(label: "collect_user_data.dob.dob.title".podLocalized(), - placeholder: "collect_user_data.dob.dob.placeholder".podLocalized(), - format: .dateOnly, - value: birthDateDataPoint.date.value, - accessibilityLabel: "Birthdate Input Field", - validator: dateValidator, - firstFormField: true, - uiConfig: uiConfig) - birthDateDataPoint.date.bidirectionalBind(to: birthdayField.bndDate).dispose(in: disposeBag) - validatableRows.append(birthdayField) - return birthdayField - } - - func createCountryPickerField() -> FormRowCountryPickerView? { - guard showIdDocument == true else { return nil } - - let idDocumentDataPoint = userData.idDocumentDataPoint - guard allowedCountries.count > 1 else { - idDocumentDataPoint.country.send(allowedCountries.first) - return nil - } - - let countryField = FormBuilder.countryPickerRow(label: "collect_user_data.dob.doc_country.title".podLocalized(), - allowedCountries: allowedCountries, - uiConfig: uiConfig) - countryField.bndValue.observeNext { [unowned self] country in - idDocumentDataPoint.country.send(country) - guard let allowedDocumentTypes = self.allowedDocuments[country] else { - fatalError("No document types configured for country \(country.name)") - } - self.documentTypeField?.allowedDocumentTypes = allowedDocumentTypes - }.dispose(in: disposeBag) - self.countryField = countryField - return countryField - } - - func createDocumentTypePickerField() -> FormRowIdDocumentTypePickerView? { - guard showIdDocument == true else { return nil } - - let idDocumentDataPoint = userData.idDocumentDataPoint - let currentCountry = idDocumentDataPoint.country.value ?? allowedCountries[0] - guard let allowedDocumentTypes = allowedDocuments[currentCountry], - !allowedDocuments.isEmpty else { - fatalError("No document types configured for country \(currentCountry.name)") - } - guard allowedCountries.count > 1 || allowedDocumentTypes.count > 1 else { - idDocumentDataPoint.documentType.send(allowedDocumentTypes[0]) - return nil - } - let label = "collect_user_data.dob.doc_type.title".podLocalized() - let documentTypeField = FormBuilder.idDocumentTypePickerRow(label: label, - allowedDocumentTypes: allowedDocumentTypes, - uiConfig: uiConfig) - documentTypeField.bndValue.observeNext { documentType in - idDocumentDataPoint.documentType.send(documentType) - }.dispose(in: disposeBag) - self.documentTypeField = documentTypeField - return documentTypeField - } - - func createNumberField() -> FormRowTextInputView? { - guard showIdDocument == true else { return nil } - - let idDocumentDataPoint = userData.idDocumentDataPoint - let initiallyReadOnly = mode == .updateUser - let validator = SSNUSFormatValidator(failReasonMessage: "birthday-collector.id-document.invalid".podLocalized()) - let placeholder = "collect_user_data.dob.doc_id.placeholder".podLocalized() - var label = "collect_user_data.dob.doc_id.title".podLocalized() - let currentCountry = idDocumentDataPoint.country.value ?? allowedCountries[0] - if let allowedDocumentTypes = allowedDocuments[currentCountry], - (allowedCountries.count == 1 && allowedDocumentTypes.count == 1) { - label.append(" (\(allowedDocumentTypes[0].localizedDescription))") - } - numberField = FormBuilder.standardTextInputRowWith(label: label, - placeholder: placeholder, - value: "", - accessibilityLabel: "Id document Input Field", - validator: validator, - initiallyReadonly: initiallyReadOnly, - uiConfig: uiConfig) - idDocumentDataPoint.value.bidirectionalBind(to: numberField.bndValue) - validatableRows.append(numberField) - - return numberField - } - - func setUpOptionalIdDocument() -> FormRowCheckView? { - guard showOptionalIdDocument == true else { return nil } - let idDocumentDataPoint = userData.idDocumentDataPoint - let text = "collect_user_data.dob.doc_id.not-specified.title".podLocalized() - let label = ComponentCatalog.formListLabelWith(text: text, - uiConfig: uiConfig) - let documentNotSpecified = FormRowCheckView(label: label, height: 20) - documentNotSpecified.checkIcon.tintColor = uiConfig.uiPrimaryColor - rows.append(documentNotSpecified) - if let notSpecified = idDocumentDataPoint.notSpecified { - documentNotSpecified.bndValue.send(notSpecified) - numberField.bndValue.send(nil) - self.validatableRows = self.validatableRows.compactMap { ($0 == self.numberField) ? nil : $0 } - self.setupStepValidation() - } - documentNotSpecified.bndValue.observeNext { checked in - idDocumentDataPoint.notSpecified = checked - idDocumentDataPoint.country.send(nil) - idDocumentDataPoint.value.send(nil) - idDocumentDataPoint.documentType.send(nil) - self.numberField.isEnabled = !checked - self.countryField?.isEnabled = !checked - self.documentTypeField?.isEnabled = !checked - if checked { - self.numberField.bndValue.send(nil) - self.validatableRows = self.validatableRows.compactMap { - ($0 == self.numberField || $0 == self.countryField || $0 == self.documentTypeField) ? nil : $0 - } - } - else { - self.validatableRows.append(self.numberField) - if let countryField = self.countryField { - self.validatableRows.append(countryField) - } - if let documentTypeField = self.documentTypeField { - self.validatableRows.append(documentTypeField) - } - } - self.setupStepValidation() - }.dispose(in: disposeBag) - return documentNotSpecified - } -} diff --git a/Pod/Classes/ui/UserKit/UserDataCollectorModule/Steps/DataCollectorBirthdayStep.swift b/Pod/Classes/ui/UserKit/UserDataCollectorModule/Steps/DataCollectorBirthdayStep.swift new file mode 100644 index 0000000..45b0b04 --- /dev/null +++ b/Pod/Classes/ui/UserKit/UserDataCollectorModule/Steps/DataCollectorBirthdayStep.swift @@ -0,0 +1,74 @@ +// +// DataCollectorBirthdayStep.swift +// AptoSDK +// +// Created by Ivan Oliver Martínez on 06/03/16. +// +// + +import AptoSDK +import Bond +import ReactiveKit +import SnapKit + +class BirthdayStep: DataCollectorBaseStep, DataCollectorStepProtocol { + var title = "collect_user_data.dob.title".podLocalized() + fileprivate let linkHandler: LinkHandler? + fileprivate let mode: UserDataCollectorFinalStepMode + fileprivate var birthdayField: FormRowDatePickerView! // swiftlint:disable:this implicitly_unwrapped_optional + + private let disposeBag = DisposeBag() + private let userData: DataPointList + private let requiredData: RequiredDataPointList + private let secondaryCredentialType: DataPointType + private var showBirthdate = true + + init(requiredData: RequiredDataPointList, + secondaryCredentialType: DataPointType, + userData: DataPointList, + mode: UserDataCollectorFinalStepMode, + uiConfig: UIConfig, + linkHandler: LinkHandler?) { + self.userData = userData + self.requiredData = requiredData + self.linkHandler = linkHandler + self.mode = mode + self.secondaryCredentialType = secondaryCredentialType + super.init(uiConfig: uiConfig) + } + + override func setupRows() -> [FormRowView] { + calculateFieldsVisibility() + + return [ + createBirthdayField(), + FormRowSeparatorView(backgroundColor: UIColor.clear, height: CGFloat(20)) + ].compactMap { return $0 } + } + + private func calculateFieldsVisibility() { + showBirthdate = requiredData.getRequiredDataPointOf(type: .birthDate) != nil + } +} + +private extension BirthdayStep { + func createBirthdayField() -> FormRowDatePickerView? { + guard showBirthdate == true else { return nil } + + let birthDateDataPoint = userData.birthDateDataPoint + let failReason = "issue_card.issue_card.error.dob_too_young".podLocalized() + let minimumDob = Calendar.current.date(byAdding: .year, value: -18, to: Date()) + let dateValidator = MaximumDateValidator(maximumDate: minimumDob ?? Date(), failReasonMessage: failReason) + birthdayField = FormBuilder.datePickerRowWith(label: "collect_user_data.dob.dob.title".podLocalized(), + placeholder: "collect_user_data.dob.dob.placeholder".podLocalized(), + format: .dateOnly, + value: birthDateDataPoint.date.value, + accessibilityLabel: "Birthdate Input Field", + validator: dateValidator, + firstFormField: true, + uiConfig: uiConfig) + birthDateDataPoint.date.bidirectionalBind(to: birthdayField.bndDate).dispose(in: disposeBag) + validatableRows.append(birthdayField) + return birthdayField + } +} diff --git a/Pod/Classes/ui/UserKit/UserDataCollectorModule/Steps/DataCollectorSSNStep.swift b/Pod/Classes/ui/UserKit/UserDataCollectorModule/Steps/DataCollectorSSNStep.swift new file mode 100644 index 0000000..d316e23 --- /dev/null +++ b/Pod/Classes/ui/UserKit/UserDataCollectorModule/Steps/DataCollectorSSNStep.swift @@ -0,0 +1,254 @@ +// +// DataCollectorSSNStep.swift +// AptoUISDK +// +// Created by Fabio Cuomo on 22/5/21. +// + +import AptoSDK +import Bond +import ReactiveKit +import SnapKit + +class SSNStep: DataCollectorBaseStep, DataCollectorStepProtocol { + var title = "collect_user_data.dob.title".podLocalized() + + private let userData: DataPointList + private let requiredData: RequiredDataPointList + private let linkHandler: LinkHandler? + private let mode: UserDataCollectorFinalStepMode + private let secondaryCredentialType: DataPointType + private let allowedDocuments: [Country: [IdDocumentType]] + private lazy var allowedCountries: [Country] = { + return Array(allowedDocuments.keys) + }() + private var countryField: FormRowCountryPickerView? + private var documentTypeField: FormRowIdDocumentTypePickerView? + private var numberField: FormRowTextInputView! // swiftlint:disable:this implicitly_unwrapped_optional + + private var showIdDocument = true + private var showOptionalIdDocument = false + + private let disposeBag = DisposeBag() + private var numberTextField: FormRowTextInputView? + + init(requiredData: RequiredDataPointList, + secondaryCredentialType: DataPointType, + userData: DataPointList, + mode: UserDataCollectorFinalStepMode, + uiConfig: UIConfig, + linkHandler: LinkHandler?) { + self.userData = userData + self.requiredData = requiredData + self.linkHandler = linkHandler + self.mode = mode + self.secondaryCredentialType = secondaryCredentialType + if let dataPoint = requiredData.getRequiredDataPointOf(type: .idDocument), + let config = dataPoint.configuration as? AllowedIdDocumentTypesConfiguration, + !config.allowedDocumentTypes.isEmpty { + self.allowedDocuments = config.allowedDocumentTypes + } + else { + self.allowedDocuments = [Country.defaultCountry: [IdDocumentType.ssn]] + } + super.init(uiConfig: uiConfig) + } + + override func setupRows() -> [FormRowView] { + calculateFieldsVisibility() + return [ + FormRowSeparatorView(backgroundColor: UIColor.clear, height: CGFloat(48)), + createCountryPickerField(), + createDocumentTypePickerField(), + createNumberField(), + setUpOptionalIdDocument(), + FormRowSeparatorView(backgroundColor: UIColor.clear, height: CGFloat(20)) + ].compactMap { return $0 } + } + + private func calculateFieldsVisibility() { + // Calculate if the Id Document Fields should be shown + if let showSSNRequiredDataPoint = requiredData.getRequiredDataPointOf(type: .idDocument) { + showIdDocument = true + showOptionalIdDocument = showSSNRequiredDataPoint.optional + } + else { + showIdDocument = false + showOptionalIdDocument = false + } + } + + // MARK: Priate UI methods + private func createCountryPickerField() -> FormRowCountryPickerView? { + guard showIdDocument == true else { return nil } + + let idDocumentDataPoint = userData.idDocumentDataPoint + guard allowedCountries.count > 1 else { + idDocumentDataPoint.country.send(allowedCountries.first) + return nil + } + + let countryField = FormBuilder.countryPickerRow(label: "collect_user_data.dob.doc_country.title".podLocalized(), + allowedCountries: allowedCountries, + uiConfig: uiConfig) + countryField.bndValue.observeNext { [unowned self] country in + idDocumentDataPoint.country.send(country) + guard let allowedDocumentTypes = self.allowedDocuments[country] else { + fatalError("No document types configured for country \(country.name)") + } + self.documentTypeField?.allowedDocumentTypes = allowedDocumentTypes + }.dispose(in: disposeBag) + self.countryField = countryField + return countryField + } + + private func createDocumentTypePickerField() -> FormRowIdDocumentTypePickerView? { + guard showIdDocument == true else { return nil } + + let idDocumentDataPoint = userData.idDocumentDataPoint + let currentCountry = idDocumentDataPoint.country.value ?? allowedCountries[0] + guard let allowedDocumentTypes = allowedDocuments[currentCountry], + !allowedDocuments.isEmpty else { + fatalError("No document types configured for country \(currentCountry.name)") + } + guard allowedCountries.count > 1 || allowedDocumentTypes.count > 1 else { + idDocumentDataPoint.documentType.send(allowedDocumentTypes[0]) + return nil + } + let label = "collect_user_data.dob.doc_type.title".podLocalized() + let documentTypeField = FormBuilder.idDocumentTypePickerRow(label: label, + allowedDocumentTypes: allowedDocumentTypes, + uiConfig: uiConfig) + documentTypeField.bndValue.observeNext { [weak self] documentType in + guard let self = self else { return } + idDocumentDataPoint.documentType.send(documentType) + guard let numberTextField = self.numberTextField else { return } + numberTextField.updateValidator(validator: self.validator(for: documentType)) + }.dispose(in: disposeBag) + self.documentTypeField = documentTypeField + return documentTypeField + } + + private func createNumberField() -> FormRowTextInputView? { + guard showIdDocument == true else { return nil } + + let idDocumentDataPoint = userData.idDocumentDataPoint + let initiallyReadOnly = mode == .updateUser + let placeholder = "collect_user_data.dob.doc_id.placeholder".podLocalized() + var label = "collect_user_data.dob.doc_id.title".podLocalized() + let currentCountry = idDocumentDataPoint.country.value ?? allowedCountries[0] + if let allowedDocumentTypes = allowedDocuments[currentCountry], + (allowedCountries.count == 1 && allowedDocumentTypes.count == 1) { + label.append(" (\(allowedDocumentTypes[0].localizedDescription))") + } + numberField = textInputRow(with: idDocumentDataPoint.documentType.value, + label: label, + placeholder: placeholder, + initiallyReadOnly: initiallyReadOnly, uiConfig: uiConfig) + idDocumentDataPoint.value.bidirectionalBind(to: numberField.bndValue) + validatableRows.append(numberField) + numberTextField = numberField + return numberField + } + + private func validator(for documentType: IdDocumentType) -> DataValidator { + switch documentType { + case .ssn: + return SSNUSFormatValidator(failReasonMessage: "ssn-collector.id-document.invalid".podLocalized()) + case .identityCard: + return NonEmptyTextValidator(failReasonMessage: "ssn-collector.id-card.invalid".podLocalized()) + case .passport: + return NonEmptyTextValidator(failReasonMessage: "ssn-collector.passport.invalid".podLocalized()) + case .driversLicense: + return NonEmptyTextValidator(failReasonMessage: "ssn-collector.driver-license.invalid".podLocalized()) + } + } + + private func accessibility(for documentType: IdDocumentType?) -> String { + guard let documentType = documentType else { return "" } + switch documentType { + case .ssn: + return "Id document Input Field" + case .identityCard: + return "Identity Card Input Field" + case .passport: + return "Passport Input Field" + case .driversLicense: + return "Driver license Input Field" + } + } + + private func textInputRow(with documentType: IdDocumentType?, + label: String, + placeholder: String, + accessibilityLabel: String? = nil, + validator: DataValidator? = nil, + initiallyReadonly: Bool = false, + firstFormField: Bool = false, + lastFormField: Bool = false, + initiallyReadOnly: Bool, + uiConfig: UIConfig) -> FormRowTextInputView? { + let accessibilityLabel = accessibility(for: documentType) + if let documentType = documentType { + let validator = self.validator(for: documentType) + return FormBuilder.standardTextInputRowWith(label: label, + placeholder: placeholder, + value: "", + accessibilityLabel: accessibilityLabel, + validator: validator, + initiallyReadonly: initiallyReadOnly, + uiConfig: uiConfig) + } else { + return FormBuilder.standardTextInputRowWith(label: label, + placeholder: placeholder, + value: "", + accessibilityLabel: accessibilityLabel, + initiallyReadonly: initiallyReadOnly, + uiConfig: uiConfig) + } + } + + private func setUpOptionalIdDocument() -> FormRowCheckView? { + guard showOptionalIdDocument == true else { return nil } + let idDocumentDataPoint = userData.idDocumentDataPoint + let text = "collect_user_data.dob.doc_id.not-specified.title".podLocalized() + let label = ComponentCatalog.formListLabelWith(text: text, + uiConfig: uiConfig) + let documentNotSpecified = FormRowCheckView(label: label, height: 20) + documentNotSpecified.checkIcon.tintColor = uiConfig.uiPrimaryColor + rows.append(documentNotSpecified) + if let notSpecified = idDocumentDataPoint.notSpecified { + documentNotSpecified.bndValue.send(notSpecified) + numberField.bndValue.send(nil) + self.validatableRows = self.validatableRows.compactMap { ($0 == self.numberField) ? nil : $0 } + self.setupStepValidation() + } + documentNotSpecified.bndValue.observeNext { checked in + idDocumentDataPoint.notSpecified = checked + idDocumentDataPoint.country.send(nil) + idDocumentDataPoint.value.send(nil) + idDocumentDataPoint.documentType.send(nil) + self.numberField.isEnabled = !checked + self.countryField?.isEnabled = !checked + self.documentTypeField?.isEnabled = !checked + if checked { + self.numberField.bndValue.send(nil) + self.validatableRows = self.validatableRows.compactMap { + ($0 == self.numberField || $0 == self.countryField || $0 == self.documentTypeField) ? nil : $0 + } + } + else { + self.validatableRows.append(self.numberField) + if let countryField = self.countryField { + self.validatableRows.append(countryField) + } + if let documentTypeField = self.documentTypeField { + self.validatableRows.append(documentTypeField) + } + } + self.setupStepValidation() + }.dispose(in: disposeBag) + return documentNotSpecified + } + +} diff --git a/Pod/Classes/ui/UserKit/UserDataCollectorModule/UserDataCollectorPresenter.swift b/Pod/Classes/ui/UserKit/UserDataCollectorModule/UserDataCollectorPresenter.swift index 387027f..a2edd35 100644 --- a/Pod/Classes/ui/UserKit/UserDataCollectorModule/UserDataCollectorPresenter.swift +++ b/Pod/Classes/ui/UserKit/UserDataCollectorModule/UserDataCollectorPresenter.swift @@ -46,6 +46,7 @@ enum DataCollectorStep: Int { case info case address case birthDaySSN + case ssn } class UserDataCollectorPresenter: UserDataCollectorDataReceiver, UserDataCollectorEventHandler { @@ -311,8 +312,10 @@ private extension UserDataCollectorPresenter { return [.info] case .address: return [.address] - case .birthDate, .idDocument: + case .birthDate: return [.birthDaySSN] + case .idDocument: + return [.ssn] case .financialAccount: fatalError("Unsupported data point type financialAccount") } diff --git a/Pod/Classes/ui/UserKit/UserDataCollectorModule/UserDataCollectorStepFactory.swift b/Pod/Classes/ui/UserKit/UserDataCollectorModule/UserDataCollectorStepFactory.swift index 2e0c54e..c4ef5f8 100644 --- a/Pod/Classes/ui/UserKit/UserDataCollectorModule/UserDataCollectorStepFactory.swift +++ b/Pod/Classes/ui/UserKit/UserDataCollectorModule/UserDataCollectorStepFactory.swift @@ -52,12 +52,19 @@ class UserDataCollectorStepFactory { uiConfig: uiConfig, googleGeocodingApiKey: googleGeocodingAPIKey) case .birthDaySSN: - return BirthdaySSNStep(requiredData: requiredData, + return BirthdayStep(requiredData: requiredData, secondaryCredentialType: secondaryCredentialType, userData: userData, mode: mode, uiConfig: uiConfig, linkHandler: linkHandler) + case .ssn: + return SSNStep(requiredData: requiredData, + secondaryCredentialType: secondaryCredentialType, + userData: userData, + mode: mode, + uiConfig: uiConfig, + linkHandler: linkHandler) } } } diff --git a/Pod/Localization/en.lproj/Localizable.strings b/Pod/Localization/en.lproj/Localizable.strings index 3af5d1d..5841511 100644 --- a/Pod/Localization/en.lproj/Localizable.strings +++ b/Pod/Localization/en.lproj/Localizable.strings @@ -300,7 +300,13 @@ "birthday-collector.id-document.number.placeholder" = "the document number"; -"birthday-collector.id-document.invalid" = "The document number is required and must be a valid format."; +"ssn-collector.id-document.invalid" = "The document number is required and must be a valid format."; + +"ssn-collector.id-card.invalid" = "The ID card number cannot be empty."; + +"ssn-collector.driver-license.invalid" = "The driver license number cannot be empty."; + +"ssn-collector.passport.invalid" = "The Passport cannot be empty."; "birthday-collector.id-document.not-specified.title" = "I don't have a document"; @@ -1179,6 +1185,10 @@ "load_funds.direct_deposit.disclaimer.call_to_action.title" = "Accept"; "load_funds.direct_deposit.disclaimer.cancel_action.title" = "Decline"; "load_funds.direct_deposit.disclaimer.title" = "Legal"; +"load_funds.add_card.onboarding.title" = "Debit Card"; +"load_funds.add_card.onboarding.explanation" = "You can instantly transfer funds from your existing bank debit card to your <> card account. Please note that transfers are reviewed and they can be delayed or declined if we suspect risks."; +"load_funds.add_card.onboarding.explanation_2" = "This transaction will appear in your bank account statement as: <>"; +"load_funds.add_card.onboarding.primary_cta" = "Continue"; /* * PIN verification */ @@ -1593,6 +1603,21 @@ "card_settings.legal.exchange_rates.title" = "Foreign exchange rates"; "card_settings.legal.exchange_rates.description" = "Compare the crossborder fees"; +"card_settings.apple_pay.add_to_wallet.title" = "Add to Apple Wallet"; + +"card_settings.apple_pay.add_to_watch_wallet.title" = "Add to Apple Watch"; + +"load_funds.add_card.error.card_type" = "Card type not supported"; + +"load_funds.add_card.error.number" = "Invalid card number"; + +"load_funds.add_card.error.cvv" = "Invalid CVV"; + +"load_funds.add_card.error.expiration" = "Invalid expiration date"; + +"load_funds.add_card.error.postal_code" = "Invalid postal Code"; + +"load_funds.add_card.error.address" = "This geography is not currently supported. Please contact support for assistance"; /* * Account Settings */ @@ -1879,4 +1904,3 @@ "credit_card_view.wrong_code.ok_action" = "Ok"; "fetch_card.card_not_found" = "Can't find the specified card"; -