diff --git a/SwedbankPaySDK.podspec b/SwedbankPaySDK.podspec index ba949e4..936b095 100644 --- a/SwedbankPaySDK.podspec +++ b/SwedbankPaySDK.podspec @@ -20,7 +20,7 @@ The Swedbank Pay iOS SDK enables simple embedding of Swedbank Pay Checkout to an s.author = 'Swedbank Pay' s.source = { :git => 'https://github.com/SwedbankPay/swedbank-pay-sdk-ios.git', :tag => s.version.to_s } - s.ios.deployment_target = '10.0' + s.ios.deployment_target = '11.0' s.swift_versions = '5.0', '5.1' s.source_files = 'SwedbankPaySDK/Classes/**/*' diff --git a/SwedbankPaySDK.xcodeproj/project.pbxproj b/SwedbankPaySDK.xcodeproj/project.pbxproj index 18a6969..654da11 100644 --- a/SwedbankPaySDK.xcodeproj/project.pbxproj +++ b/SwedbankPaySDK.xcodeproj/project.pbxproj @@ -3,10 +3,56 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ + 4517A24A2C10467A000BB7A8 /* CustomDateDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4517A2492C10467A000BB7A8 /* CustomDateDecoder.swift */; }; + 4517A24B2C104702000BB7A8 /* CustomDateDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4517A2492C10467A000BB7A8 /* CustomDateDecoder.swift */; }; + 4517A24D2C1324AC000BB7A8 /* SCAWebViewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4517A24C2C1324AC000BB7A8 /* SCAWebViewService.swift */; }; + 4517A24E2C132F21000BB7A8 /* SCAWebViewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4517A24C2C1324AC000BB7A8 /* SCAWebViewService.swift */; }; + 4517C6A02BFF6B7E001687E7 /* BeaconEndpointRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4517C69F2BFF6B7E001687E7 /* BeaconEndpointRouter.swift */; }; + 4517C6A12BFF8980001687E7 /* BeaconEndpointRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4517C69F2BFF6B7E001687E7 /* BeaconEndpointRouter.swift */; }; + 4517C6A32BFF928B001687E7 /* BeaconService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4517C6A22BFF928B001687E7 /* BeaconService.swift */; }; + 455849322C945BAD0062A315 /* SwedbankPaymentNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 455849312C945BAD0062A315 /* SwedbankPaymentNetwork.swift */; }; + 455A7B982C205DA3003CF320 /* SwedbankPayPaymentSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 455A7B972C205DA3003CF320 /* SwedbankPayPaymentSession.swift */; }; + 455A7B992C205DA3003CF320 /* SwedbankPayPaymentSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 455A7B972C205DA3003CF320 /* SwedbankPayPaymentSession.swift */; }; + 456900812BFB9AA5009475A5 /* PaymentSessionProblem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 456900802BFB9AA5009475A5 /* PaymentSessionProblem.swift */; }; + 456900822BFB9AA5009475A5 /* PaymentSessionProblem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 456900802BFB9AA5009475A5 /* PaymentSessionProblem.swift */; }; + 456913E62C78C2C60014BD41 /* SwedbankPayAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 456913E52C78C2C60014BD41 /* SwedbankPayAuthorization.swift */; }; + 45B429942C18687100620A0A /* SwedbankPaySCAWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B429932C18687100620A0A /* SwedbankPaySCAWebViewController.swift */; }; + 45B429952C18687100620A0A /* SwedbankPaySCAWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B429932C18687100620A0A /* SwedbankPaySCAWebViewController.swift */; }; + 45B4495B2C05F34F00A1F46D /* BeaconType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B4495A2C05F34F00A1F46D /* BeaconType.swift */; }; + 45B4495D2C05F46200A1F46D /* Beacon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B4495C2C05F46200A1F46D /* Beacon.swift */; }; + 45B4495E2C05F51500A1F46D /* Beacon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B4495C2C05F46200A1F46D /* Beacon.swift */; }; + 45B4495F2C05F51B00A1F46D /* BeaconService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4517C6A22BFF928B001687E7 /* BeaconService.swift */; }; + 45B449602C05F51F00A1F46D /* BeaconType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B4495A2C05F34F00A1F46D /* BeaconType.swift */; }; + 45C100902BF247F300AA3523 /* SwedbankPayAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C1008B2BF247F300AA3523 /* SwedbankPayAPIError.swift */; }; + 45C100912BF247F300AA3523 /* SwedbankPayAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C1008B2BF247F300AA3523 /* SwedbankPayAPIError.swift */; }; + 45C100922BF247F300AA3523 /* OperationOutputModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C1008D2BF247F300AA3523 /* OperationOutputModel.swift */; }; + 45C100932BF247F300AA3523 /* OperationOutputModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C1008D2BF247F300AA3523 /* OperationOutputModel.swift */; }; + 45C100942BF247F300AA3523 /* SwedbankPayAPIEnpointRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C1008E2BF247F300AA3523 /* SwedbankPayAPIEnpointRouter.swift */; }; + 45C100952BF247F300AA3523 /* SwedbankPayAPIEnpointRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C1008E2BF247F300AA3523 /* SwedbankPayAPIEnpointRouter.swift */; }; + 45C100962BF247F300AA3523 /* SwedbankPayAPIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C1008F2BF247F300AA3523 /* SwedbankPayAPIConstants.swift */; }; + 45C100972BF247F300AA3523 /* SwedbankPayAPIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C1008F2BF247F300AA3523 /* SwedbankPayAPIConstants.swift */; }; + 45C1009D2BF2583D00AA3523 /* PaymentSessionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C1009B2BF2583D00AA3523 /* PaymentSessionModel.swift */; }; + 45C1009E2BF2583D00AA3523 /* PaymentSessionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C1009B2BF2583D00AA3523 /* PaymentSessionModel.swift */; }; + 45C1009F2BF2583D00AA3523 /* MethodBaseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C1009C2BF2583D00AA3523 /* MethodBaseModel.swift */; }; + 45C100A02BF2583D00AA3523 /* MethodBaseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C1009C2BF2583D00AA3523 /* MethodBaseModel.swift */; }; + 45C100A22BF2584E00AA3523 /* IntegrationTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C100A12BF2584E00AA3523 /* IntegrationTask.swift */; }; + 45C100A32BF2584E00AA3523 /* IntegrationTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C100A12BF2584E00AA3523 /* IntegrationTask.swift */; }; + 45C100A52BF2597B00AA3523 /* PaymentOutputModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C100A42BF2597B00AA3523 /* PaymentOutputModel.swift */; }; + 45C100A62BF2597B00AA3523 /* PaymentOutputModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C100A42BF2597B00AA3523 /* PaymentOutputModel.swift */; }; + 45C100A82BF2598D00AA3523 /* ProblemDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C100A72BF2598D00AA3523 /* ProblemDetails.swift */; }; + 45C100A92BF2598D00AA3523 /* ProblemDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C100A72BF2598D00AA3523 /* ProblemDetails.swift */; }; + 45C100AD2BF2622E00AA3523 /* NetworkStatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C100AB2BF2622E00AA3523 /* NetworkStatusProvider.swift */; }; + 45C100AE2BF2622E00AA3523 /* NetworkStatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C100AB2BF2622E00AA3523 /* NetworkStatusProvider.swift */; }; + 45C100AF2BF2622E00AA3523 /* TimeZone+OffsetFromGMT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C100AC2BF2622E00AA3523 /* TimeZone+OffsetFromGMT.swift */; }; + 45C100B02BF2622E00AA3523 /* TimeZone+OffsetFromGMT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C100AC2BF2622E00AA3523 /* TimeZone+OffsetFromGMT.swift */; }; + 45C100B32BF34EED00AA3523 /* PaymentAttemptInstrument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C100B22BF34EED00AA3523 /* PaymentAttemptInstrument.swift */; }; + 45C100B42BF34EED00AA3523 /* PaymentAttemptInstrument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C100B22BF34EED00AA3523 /* PaymentAttemptInstrument.swift */; }; + 45FE72A92C2EE74C00ECACB6 /* SwedbankPayConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FE72A82C2EE74C00ECACB6 /* SwedbankPayConfiguration.swift */; }; + 45FE72AA2C2EE77100ECACB6 /* SwedbankPayConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FE72A82C2EE74C00ECACB6 /* SwedbankPayConfiguration.swift */; }; 6367D3EB26F340F700F89F62 /* TestConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6367D3EA26F340F700F89F62 /* TestConfiguration.swift */; }; 637E94A82733F00000879C71 /* SwedbankPaySDKLocalizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 637E94AA2733F00000879C71 /* SwedbankPaySDKLocalizable.strings */; }; 637E94AE2733F5E300879C71 /* SwedbankPaySDKResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 637E94AD2733F5E300879C71 /* SwedbankPaySDKResources.swift */; }; @@ -247,6 +293,30 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 4517A2492C10467A000BB7A8 /* CustomDateDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDateDecoder.swift; sourceTree = ""; }; + 4517A24C2C1324AC000BB7A8 /* SCAWebViewService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCAWebViewService.swift; sourceTree = ""; }; + 4517C69F2BFF6B7E001687E7 /* BeaconEndpointRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeaconEndpointRouter.swift; sourceTree = ""; }; + 4517C6A22BFF928B001687E7 /* BeaconService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeaconService.swift; sourceTree = ""; }; + 455849312C945BAD0062A315 /* SwedbankPaymentNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwedbankPaymentNetwork.swift; sourceTree = ""; }; + 455A7B972C205DA3003CF320 /* SwedbankPayPaymentSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwedbankPayPaymentSession.swift; sourceTree = ""; }; + 456900802BFB9AA5009475A5 /* PaymentSessionProblem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSessionProblem.swift; sourceTree = ""; }; + 456913E52C78C2C60014BD41 /* SwedbankPayAuthorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwedbankPayAuthorization.swift; sourceTree = ""; }; + 45B429932C18687100620A0A /* SwedbankPaySCAWebViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwedbankPaySCAWebViewController.swift; sourceTree = ""; }; + 45B4495A2C05F34F00A1F46D /* BeaconType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeaconType.swift; sourceTree = ""; }; + 45B4495C2C05F46200A1F46D /* Beacon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Beacon.swift; sourceTree = ""; }; + 45C1008B2BF247F300AA3523 /* SwedbankPayAPIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwedbankPayAPIError.swift; sourceTree = ""; }; + 45C1008D2BF247F300AA3523 /* OperationOutputModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationOutputModel.swift; sourceTree = ""; }; + 45C1008E2BF247F300AA3523 /* SwedbankPayAPIEnpointRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwedbankPayAPIEnpointRouter.swift; sourceTree = ""; }; + 45C1008F2BF247F300AA3523 /* SwedbankPayAPIConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwedbankPayAPIConstants.swift; sourceTree = ""; }; + 45C1009B2BF2583D00AA3523 /* PaymentSessionModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentSessionModel.swift; sourceTree = ""; }; + 45C1009C2BF2583D00AA3523 /* MethodBaseModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MethodBaseModel.swift; sourceTree = ""; }; + 45C100A12BF2584E00AA3523 /* IntegrationTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegrationTask.swift; sourceTree = ""; }; + 45C100A42BF2597B00AA3523 /* PaymentOutputModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentOutputModel.swift; sourceTree = ""; }; + 45C100A72BF2598D00AA3523 /* ProblemDetails.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProblemDetails.swift; sourceTree = ""; }; + 45C100AB2BF2622E00AA3523 /* NetworkStatusProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkStatusProvider.swift; sourceTree = ""; }; + 45C100AC2BF2622E00AA3523 /* TimeZone+OffsetFromGMT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TimeZone+OffsetFromGMT.swift"; sourceTree = ""; }; + 45C100B22BF34EED00AA3523 /* PaymentAttemptInstrument.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentAttemptInstrument.swift; sourceTree = ""; }; + 45FE72A82C2EE74C00ECACB6 /* SwedbankPayConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwedbankPayConfiguration.swift; sourceTree = ""; }; 6367D3EA26F340F700F89F62 /* TestConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConfiguration.swift; sourceTree = ""; }; 637E94A92733F00000879C71 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/SwedbankPaySDKLocalizable.strings; sourceTree = ""; }; 637E94AB2733F00300879C71 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/SwedbankPaySDKLocalizable.strings; sourceTree = ""; }; @@ -423,6 +493,62 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4517C69E2BFE34DC001687E7 /* Beacon */ = { + isa = PBXGroup; + children = ( + 45B4495C2C05F46200A1F46D /* Beacon.swift */, + 4517C69F2BFF6B7E001687E7 /* BeaconEndpointRouter.swift */, + 4517C6A22BFF928B001687E7 /* BeaconService.swift */, + 45B4495A2C05F34F00A1F46D /* BeaconType.swift */, + ); + path = Beacon; + sourceTree = ""; + }; + 4558492E2C932FE90062A315 /* ApplePay */ = { + isa = PBXGroup; + children = ( + 456913E52C78C2C60014BD41 /* SwedbankPayAuthorization.swift */, + 455849312C945BAD0062A315 /* SwedbankPaymentNetwork.swift */, + ); + path = ApplePay; + sourceTree = ""; + }; + 45C1008A2BF247F300AA3523 /* Api */ = { + isa = PBXGroup; + children = ( + 45C100AA2BF2622E00AA3523 /* Helpers */, + 45C1008C2BF247F300AA3523 /* Models */, + 45C1008B2BF247F300AA3523 /* SwedbankPayAPIError.swift */, + 45C1008E2BF247F300AA3523 /* SwedbankPayAPIEnpointRouter.swift */, + 45C1008F2BF247F300AA3523 /* SwedbankPayAPIConstants.swift */, + ); + path = Api; + sourceTree = ""; + }; + 45C1008C2BF247F300AA3523 /* Models */ = { + isa = PBXGroup; + children = ( + 45C100A12BF2584E00AA3523 /* IntegrationTask.swift */, + 45C1009C2BF2583D00AA3523 /* MethodBaseModel.swift */, + 45C1008D2BF247F300AA3523 /* OperationOutputModel.swift */, + 45C100B22BF34EED00AA3523 /* PaymentAttemptInstrument.swift */, + 45C100A42BF2597B00AA3523 /* PaymentOutputModel.swift */, + 45C1009B2BF2583D00AA3523 /* PaymentSessionModel.swift */, + 45C100A72BF2598D00AA3523 /* ProblemDetails.swift */, + ); + path = Models; + sourceTree = ""; + }; + 45C100AA2BF2622E00AA3523 /* Helpers */ = { + isa = PBXGroup; + children = ( + 45C100AB2BF2622E00AA3523 /* NetworkStatusProvider.swift */, + 45C100AC2BF2622E00AA3523 /* TimeZone+OffsetFromGMT.swift */, + 4517A2492C10467A000BB7A8 /* CustomDateDecoder.swift */, + ); + path = Helpers; + sourceTree = ""; + }; A502C37525430D90004FBFCB /* Models */ = { isa = PBXGroup; children = ( @@ -552,6 +678,8 @@ A5808B04261C942A00FF7EC1 /* SwedbankPayWebViewControllerBase.swift */, A5808B08261C980000FF7EC1 /* SwedbankPayWebViewControllerDelegate.swift */, A5808B10261CA8B500FF7EC1 /* WKWebViewCanOpen.swift */, + 4517A24C2C1324AC000BB7A8 /* SCAWebViewService.swift */, + 45B429932C18687100620A0A /* SwedbankPaySCAWebViewController.swift */, ); path = WebView; sourceTree = ""; @@ -591,6 +719,7 @@ A59AEE9C25372C9A00255A3A /* Instrument.swift */, A5F3416823FD9293005D65BA /* PaymentOrder.swift */, C50CF6A6237AAF85003F79DF /* ClientProblem.swift */, + 456900802BFB9AA5009475A5 /* PaymentSessionProblem.swift */, C50CF696237AA8ED003F79DF /* Configuration.swift */, C50CF69C237AACC3003F79DF /* Consumer.swift */, C50CF6A8237AAFBF003F79DF /* ServerProblem.swift */, @@ -702,7 +831,12 @@ C585AF27237066EE006C2E16 /* SwedbankPaySDK.swift */, C585AF2A237066EE006C2E16 /* SwedbankPaySDKController.swift */, C585AF28237066EE006C2E16 /* SwedbankPaySDKViewModel.swift */, + 455A7B972C205DA3003CF320 /* SwedbankPayPaymentSession.swift */, + 45FE72A82C2EE74C00ECACB6 /* SwedbankPayConfiguration.swift */, + 45C1008A2BF247F300AA3523 /* Api */, A5808B03261C93FD00FF7EC1 /* WebView */, + 4517C69E2BFE34DC001687E7 /* Beacon */, + 4558492E2C932FE90062A315 /* ApplePay */, A52E0ACA24BCAA6D00770286 /* GoodWebViewRedirects.swift */, ); path = Classes; @@ -1031,34 +1165,58 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 456913E62C78C2C60014BD41 /* SwedbankPayAuthorization.swift in Sources */, + 45C100962BF247F300AA3523 /* SwedbankPayAPIConstants.swift in Sources */, A5808B11261CA8B500FF7EC1 /* WKWebViewCanOpen.swift in Sources */, C50CF6A5237AAF47003F79DF /* SwedbankPaySubProblem.swift in Sources */, A52E0AC124BC9E0100770286 /* WebViewRedirects.swift in Sources */, A504895123C8A2FD00201DEC /* SwedbankPayWebContent.swift in Sources */, C50CF69D237AACC3003F79DF /* Consumer.swift in Sources */, + 45C1009F2BF2583D00AA3523 /* MethodBaseModel.swift in Sources */, + 45C1009D2BF2583D00AA3523 /* PaymentSessionModel.swift in Sources */, + 45FE72A92C2EE74C00ECACB6 /* SwedbankPayConfiguration.swift in Sources */, + 45C100A52BF2597B00AA3523 /* PaymentOutputModel.swift in Sources */, A57170DA25011F8500AC28BE /* FileLines.swift in Sources */, + 45C100B32BF34EED00AA3523 /* PaymentAttemptInstrument.swift in Sources */, 63EECFD0270730C800C37B69 /* CodableUserData.swift in Sources */, A59AEE9D25372C9A00255A3A /* Instrument.swift in Sources */, C585AF2F237066EE006C2E16 /* SwedbankPaySDKController.swift in Sources */, + 455849322C945BAD0062A315 /* SwedbankPaymentNetwork.swift in Sources */, + 45B4495B2C05F34F00A1F46D /* BeaconType.swift in Sources */, + 45C100942BF247F300AA3523 /* SwedbankPayAPIEnpointRouter.swift in Sources */, D50C9B4D27E36EBB00FCE33D /* VersionReporter.swift in Sources */, + 45C100902BF247F300AA3523 /* SwedbankPayAPIError.swift in Sources */, 63F5D69F26F0B23600C1F207 /* ConfigurationAsync.swift in Sources */, 637E94AE2733F5E300879C71 /* SwedbankPaySDKResources.swift in Sources */, C50CF68D237AA3B0003F79DF /* TypeAliases.swift in Sources */, + 4517C6A32BFF928B001687E7 /* BeaconService.swift in Sources */, A5808B0D261CA7A200FF7EC1 /* SwedbankPayExtraWebViewController.swift in Sources */, A5AA1D6F2397B63D008A62CC /* CallbackHandling.swift in Sources */, C50CF6A7237AAF85003F79DF /* ClientProblem.swift in Sources */, C50CF6A9237AAFBF003F79DF /* ServerProblem.swift in Sources */, + 45B4495D2C05F46200A1F46D /* Beacon.swift in Sources */, + 455A7B982C205DA3003CF320 /* SwedbankPayPaymentSession.swift in Sources */, + 45C100A22BF2584E00AA3523 /* IntegrationTask.swift in Sources */, A52E0ACB24BCAA6D00770286 /* GoodWebViewRedirects.swift in Sources */, + 45C100A82BF2598D00AA3523 /* ProblemDetails.swift in Sources */, A5808B09261C980000FF7EC1 /* SwedbankPayWebViewControllerDelegate.swift in Sources */, + 4517A24A2C10467A000BB7A8 /* CustomDateDecoder.swift in Sources */, + 45C100AF2BF2622E00AA3523 /* TimeZone+OffsetFromGMT.swift in Sources */, + 4517C6A02BFF6B7E001687E7 /* BeaconEndpointRouter.swift in Sources */, A5DC2CAA251A2DFA0037C7DA /* ViewPaymentOrderInfo.swift in Sources */, A5DC2CA6251A2D730037C7DA /* ViewConsumerIdentificationInfo.swift in Sources */, C585AF2C237066EE006C2E16 /* SwedbankPaySDK.swift in Sources */, C50CF697237AA8ED003F79DF /* Configuration.swift in Sources */, + 45C100922BF247F300AA3523 /* OperationOutputModel.swift in Sources */, D581EFDF27A92A56001B85A7 /* VersionOptions.swift in Sources */, + 45B429942C18687100620A0A /* SwedbankPaySCAWebViewController.swift in Sources */, D50CA69B27D23EE000FC6007 /* Operation.swift in Sources */, + 4517A24D2C1324AC000BB7A8 /* SCAWebViewService.swift in Sources */, C50CF699237AAC73003F79DF /* WhitelistedDomain.swift in Sources */, + 45C100AD2BF2622E00AA3523 /* NetworkStatusProvider.swift in Sources */, A5C3D01E23A23BC900B45085 /* SwedbankPayWebViewController.swift in Sources */, A5808B05261C942A00FF7EC1 /* SwedbankPayWebViewControllerBase.swift in Sources */, + 456900812BFB9AA5009475A5 /* PaymentSessionProblem.swift in Sources */, A560BE432420C7CE00C1D023 /* TerminalFailure.swift in Sources */, A5F3416923FD9293005D65BA /* PaymentOrder.swift in Sources */, D5AE5DA027E0D4DA00BE5468 /* ExpandResource.swift in Sources */, @@ -1103,23 +1261,33 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 45C100952BF247F300AA3523 /* SwedbankPayAPIEnpointRouter.swift in Sources */, D511EC9B2975599D0059AB05 /* Preconditions.swift in Sources */, D511EC9C2975599D0059AB05 /* PinPublicKeys.swift in Sources */, D511EC9D2975599D0059AB05 /* PaymentOrderExtensions.swift in Sources */, + 45B4495F2C05F51B00A1F46D /* BeaconService.swift in Sources */, D511EC9E2975599D0059AB05 /* PaymentOrdersLink.swift in Sources */, D511EC9F2975599D0059AB05 /* ConsumerSession.swift in Sources */, + 4517A24B2C104702000BB7A8 /* CustomDateDecoder.swift in Sources */, D511ECA02975599D0059AB05 /* DeletePaymentTokenLink.swift in Sources */, + 45C100A02BF2583D00AA3523 /* MethodBaseModel.swift in Sources */, D511ECA12975599D0059AB05 /* RootLink.swift in Sources */, D511ECA22975599D0059AB05 /* Link.swift in Sources */, D511ECA32975599D0059AB05 /* PaymentOrderIn.swift in Sources */, + 45C100972BF247F300AA3523 /* SwedbankPayAPIConstants.swift in Sources */, D511ECA42975599D0059AB05 /* SetInstrumentLink.swift in Sources */, + 45C100A32BF2584E00AA3523 /* IntegrationTask.swift in Sources */, D511ECA52975599D0059AB05 /* BackendOperation.swift in Sources */, D511ECA62975599D0059AB05 /* TopLevelResources.swift in Sources */, + 45B449602C05F51F00A1F46D /* BeaconType.swift in Sources */, + 45C100B42BF34EED00AA3523 /* PaymentAttemptInstrument.swift in Sources */, D511ECA72975599D0059AB05 /* ConsumersLink.swift in Sources */, D511ECA82975599D0059AB05 /* PayerOwnedPaymentTokensResponse.swift in Sources */, D511ECA92975599D0059AB05 /* PayerOwnedPaymentTokens.swift in Sources */, D511ECAA2975599D0059AB05 /* PaymentTokenInfo.swift in Sources */, D511ECAB2975599D0059AB05 /* EmptyJsonResponse.swift in Sources */, + 45C100AE2BF2622E00AA3523 /* NetworkStatusProvider.swift in Sources */, + 45C100A92BF2598D00AA3523 /* ProblemDetails.swift in Sources */, D511ECAC2975599D0059AB05 /* AbortPaymentOperation.swift in Sources */, D511ECAD2975599D0059AB05 /* MerchantBackendConfiguration.swift in Sources */, D511ECAE2975599D0059AB05 /* MerchantBackend.swift in Sources */, @@ -1130,19 +1298,29 @@ D511ECB32975599D0059AB05 /* MerchantBackendApi.swift in Sources */, D511ECB42975599D0059AB05 /* RequestDecorator.swift in Sources */, D511EC702975591B0059AB05 /* WKWebViewCanOpen.swift in Sources */, + 456900822BFB9AA5009475A5 /* PaymentSessionProblem.swift in Sources */, D511EC712975591B0059AB05 /* SwedbankPaySubProblem.swift in Sources */, D511EC722975591B0059AB05 /* WebViewRedirects.swift in Sources */, D511EC732975591B0059AB05 /* SwedbankPayWebContent.swift in Sources */, + 45B4495E2C05F51500A1F46D /* Beacon.swift in Sources */, + 45FE72AA2C2EE77100ECACB6 /* SwedbankPayConfiguration.swift in Sources */, + 45C100912BF247F300AA3523 /* SwedbankPayAPIError.swift in Sources */, D511EC742975591B0059AB05 /* Consumer.swift in Sources */, D511EC752975591B0059AB05 /* FileLines.swift in Sources */, D511EC762975591B0059AB05 /* CodableUserData.swift in Sources */, + 4517C6A12BFF8980001687E7 /* BeaconEndpointRouter.swift in Sources */, D511EC772975591B0059AB05 /* Instrument.swift in Sources */, D511EC782975591B0059AB05 /* SwedbankPaySDKController.swift in Sources */, + 45C1009E2BF2583D00AA3523 /* PaymentSessionModel.swift in Sources */, D511EC792975591B0059AB05 /* VersionReporter.swift in Sources */, D511EC7A2975591B0059AB05 /* ConfigurationAsync.swift in Sources */, + 455A7B992C205DA3003CF320 /* SwedbankPayPaymentSession.swift in Sources */, D511EC7B2975591B0059AB05 /* SwedbankPaySDKResources.swift in Sources */, D511EC7C2975591B0059AB05 /* TypeAliases.swift in Sources */, + 45B429952C18687100620A0A /* SwedbankPaySCAWebViewController.swift in Sources */, + 45C100932BF247F300AA3523 /* OperationOutputModel.swift in Sources */, D511EC7D2975591B0059AB05 /* SwedbankPayExtraWebViewController.swift in Sources */, + 45C100B02BF2622E00AA3523 /* TimeZone+OffsetFromGMT.swift in Sources */, D511EC7E2975591B0059AB05 /* CallbackHandling.swift in Sources */, D511EC7F2975591B0059AB05 /* ClientProblem.swift in Sources */, D511EC802975591B0059AB05 /* ServerProblem.swift in Sources */, @@ -1154,9 +1332,11 @@ D511EC862975591B0059AB05 /* Configuration.swift in Sources */, D511EC872975591B0059AB05 /* VersionOptions.swift in Sources */, D511EC882975591B0059AB05 /* Operation.swift in Sources */, + 45C100A62BF2597B00AA3523 /* PaymentOutputModel.swift in Sources */, D511EC892975591B0059AB05 /* WhitelistedDomain.swift in Sources */, D511EC8A2975591B0059AB05 /* SwedbankPayWebViewController.swift in Sources */, D511EC8B2975591B0059AB05 /* SwedbankPayWebViewControllerBase.swift in Sources */, + 4517A24E2C132F21000BB7A8 /* SCAWebViewService.swift in Sources */, D511EC8C2975591B0059AB05 /* TerminalFailure.swift in Sources */, D511EC8D2975591B0059AB05 /* PaymentOrder.swift in Sources */, D511EC8E2975591B0059AB05 /* ExpandResource.swift in Sources */, @@ -1398,7 +1578,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1457,7 +1637,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -1480,7 +1660,6 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = SwedbankPaySDK/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1505,7 +1684,6 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = SwedbankPaySDK/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/SwedbankPaySDK/Classes/Api/Helpers/CustomDateDecoder.swift b/SwedbankPaySDK/Classes/Api/Helpers/CustomDateDecoder.swift new file mode 100644 index 0000000..539a2c0 --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/Helpers/CustomDateDecoder.swift @@ -0,0 +1,39 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +class CustomDateDecoder: JSONDecoder, @unchecked Sendable { + let dateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.timeZone = TimeZone(identifier: "UTC") + return formatter + }() + + override init() { + super.init() + + dateDecodingStrategy = .custom({ (decoder) -> Date in + let container = try decoder.singleValueContainer() + let dateStr = try container.decode(String.self) + + if let date = self.dateFormatter.date(from: dateStr) { + return date + } + + return Date.distantPast + }) + } +} diff --git a/SwedbankPaySDK/Classes/Api/Helpers/NetworkStatusProvider.swift b/SwedbankPaySDK/Classes/Api/Helpers/NetworkStatusProvider.swift new file mode 100644 index 0000000..fbcf68c --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/Helpers/NetworkStatusProvider.swift @@ -0,0 +1,57 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +struct NetworkStatusProvider { + enum Network: String { + case wifi = "en0" + case cellular = "pdp_ip0" + } + + static func getAddress(for network: NetworkStatusProvider.Network) -> String? { + var address: String? + + // Get list of all interfaces on the local machine: + var ifaddr: UnsafeMutablePointer? + guard getifaddrs(&ifaddr) == 0 else { return nil } + guard let firstAddr = ifaddr else { return nil } + + // For each interface ... + for ifptr in sequence(first: firstAddr, next: { $0.pointee.ifa_next }) { + let interface = ifptr.pointee + + // Check for IPv4 or IPv6 interface: + let addrFamily = interface.ifa_addr.pointee.sa_family + if addrFamily == UInt8(AF_INET) || addrFamily == UInt8(AF_INET6) { + + // Check interface name: + let name = String(cString: interface.ifa_name) + if name == network.rawValue { + + // Convert interface address to a human readable string: + var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len), + &hostname, socklen_t(hostname.count), + nil, socklen_t(0), NI_NUMERICHOST) + address = String(cString: hostname) + } + } + } + freeifaddrs(ifaddr) + + return address + } +} diff --git a/SwedbankPaySDK/Classes/Api/Helpers/TimeZone+OffsetFromGMT.swift b/SwedbankPaySDK/Classes/Api/Helpers/TimeZone+OffsetFromGMT.swift new file mode 100644 index 0000000..4d9f6814 --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/Helpers/TimeZone+OffsetFromGMT.swift @@ -0,0 +1,30 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +extension TimeZone { + func offsetFromGMT() -> String { + let localTimeZoneFormatter = DateFormatter() + localTimeZoneFormatter.timeZone = self + localTimeZoneFormatter.dateFormat = "Z" + return localTimeZoneFormatter.string(from: Date()) + } + + func minutesFromGMT() -> String { + let minutes = (secondsFromGMT() / 60) + return String(minutes) + } +} diff --git a/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift b/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift new file mode 100644 index 0000000..bce012b --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift @@ -0,0 +1,176 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +struct IntegrationTask: Codable, Hashable { + let rel: IntegrationTaskRel? + let href: String? + let method: String? + let contentType: String? + let expects: [ExpectationModel]? +} + +extension Sequence where Iterator.Element == ExpectationModel +{ + func firstExpectation(withName name: String) -> ExpectationModel? { + return first(where: { $0.name == name }) + } + + func value(for name: String) -> String? { + return firstExpectation(withName: name)?.value + } + + func contains(name: String) -> Bool { + return firstExpectation(withName: name) != nil + } +} + +enum IntegrationTaskRel: Codable, Equatable, Hashable { + case scaMethodRequest + case scaRedirect + case launchClientApp + case walletSdk + + case unknown(String) + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let type = try container.decode(String.self) + + switch type { + case Self.scaMethodRequest.rawValue: self = .scaMethodRequest + case Self.scaRedirect.rawValue: self = .scaRedirect + case Self.launchClientApp.rawValue: self = .launchClientApp + case Self.walletSdk.rawValue: self = .walletSdk + default: self = .unknown(type) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } + + var rawValue: String { + switch self { + case .scaMethodRequest: "sca-method-request" + case .scaRedirect: "sca-redirect" + case .launchClientApp: "launch-client-app" + case .walletSdk: "wallet-sdk" + case .unknown(let value): value + } + } +} + +enum ExpectationModel: Codable, Equatable, Hashable { + case string(name: String?, value: String?) + case stringArray(name: String?, value: [String]?) + + case unknown(String) + + var name: String? { + switch self { + case .string(let name, _): + return name + case .stringArray(let name, _): + return name + case .unknown: + return "unknown" + } + } + + var value: String? { + switch self { + case .string(_, let value): + return value + case .stringArray: + return nil + case .unknown: + return nil + } + } + + var stringArray: [String]? { + switch self { + case .string: + return nil + case .stringArray(_, let value): + return value + case .unknown: + return nil + } + } + + + private enum CodingKeys: String, CodingKey { + case name, type, value + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let type = try container.decode(String.self, forKey: .type) + switch type { + case "string": + self = .string( + name: try? container.decode(String?.self, forKey: CodingKeys.name), + value: try? container.decode(String?.self, forKey: CodingKeys.value) + ) + case "string[]": + self = .stringArray( + name: try? container.decode(String?.self, forKey: CodingKeys.name), + value: try? container.decode([String]?.self, forKey: CodingKeys.value) + ) + default: + self = .unknown(type) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .string(let name, let value): + try container.encode(name) + try container.encode(value) + case .stringArray(let name, let value): + try container.encode(name) + try container.encode(value) + case .unknown(let type): + try container.encode(type) + } + } +} + +extension Array where Element == ExpectationModel { + var httpBody: Data? { + return self.compactMap({ + switch $0 { + case .string(let name, let value): + guard let name = name else { + return nil + } + + let value = value?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "" + + return name + "=" + value + default: + return nil + } + }) + .joined(separator: "&") + .data(using: .utf8) + } +} diff --git a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift new file mode 100644 index 0000000..6a01f12 --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift @@ -0,0 +1,167 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +enum MethodBaseModel: Codable, Equatable, Hashable { + case swish(prefills: [SwedbankPaySDK.SwishMethodPrefillModel]?, operations: [OperationOutputModel]?) + case creditCard(prefills: [SwedbankPaySDK.CreditCardMethodPrefillModel]?, operations: [OperationOutputModel]?, cardBrands: [String]?) + case applePay(operations: [OperationOutputModel]?, cardBrands: [String]?) + case webBased(paymentMethod: String) + + private enum CodingKeys: String, CodingKey { + case paymentMethod, prefills, operations, cardBrands + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let paymentMethod = try container.decode(String.self, forKey: .paymentMethod) + switch paymentMethod { + case "Swish": + self = .swish( + prefills: try? container.decode([SwedbankPaySDK.SwishMethodPrefillModel]?.self, forKey: CodingKeys.prefills), + operations: try? container.decode([OperationOutputModel]?.self, forKey: CodingKeys.operations) + ) + case "CreditCard": + self = .creditCard( + prefills: try? container.decode([SwedbankPaySDK.CreditCardMethodPrefillModel].self, forKey: CodingKeys.prefills), + operations: try? container.decode([OperationOutputModel]?.self, forKey: CodingKeys.operations), + cardBrands: try? container.decode([String]?.self, forKey: CodingKeys.cardBrands) + ) + case "ApplePay": + self = .applePay( + operations: try? container.decode([OperationOutputModel]?.self, forKey: CodingKeys.operations), + cardBrands: try? container.decode([String]?.self, forKey: CodingKeys.cardBrands) + ) + default: + self = .webBased(paymentMethod: paymentMethod) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .swish(let prefills, let operations): + try container.encode(prefills) + try container.encode(operations) + case .creditCard(let prefills, let operations, let cardBrands): + try container.encode(prefills) + try container.encode(operations) + try container.encode(cardBrands) + case .applePay(let operations, let cardBrands): + try container.encode(operations) + try container.encode(cardBrands) + case .webBased(let paymentMethod): + try container.encode(paymentMethod) + } + } + + var name: String { + switch self { + case .swish: + return "Swish" + case .creditCard: + return "CreditCard" + case .applePay: + return "ApplePay" + case .webBased(let paymentMethod): + return paymentMethod + } + } + + var operations: [OperationOutputModel]? { + switch self { + case .swish(_, let opertations): + return opertations + case .creditCard(_, let opertations, _): + return opertations + case .applePay(let operations, _): + return operations + case .webBased: + return nil + } + } +} + +extension Sequence where Iterator.Element == MethodBaseModel +{ + func firstMethod(withName name: String) -> MethodBaseModel? { + return first(where: { $0.name == name }) + } +} + +extension SwedbankPaySDK { + /// Avilable instrument for Native Payment. + public enum AvailableInstrument: Codable, Equatable, Hashable { + + /// Swish native payment with a list of prefills + case swish(prefills: [SwishMethodPrefillModel]?) + + case creditCard(prefills: [CreditCardMethodPrefillModel]?) + + case applePay + + case webBased(paymentMethod: String) + + public var paymentMethod: String { + switch self { + case .swish: + return "Swish" + case .creditCard: + return "CreditCard" + case .applePay: + return "ApplePay" + case .webBased(let paymentMethod): + return paymentMethod + } + } + } + + /// Prefill information for Swish payment. + public struct SwishMethodPrefillModel: Codable, Hashable { + public let rank: Int32 + public let msisdn: String + } + + /// Prefill information for Credit Card payment. + public struct CreditCardMethodPrefillModel: Codable, Hashable { + public let rank: Int32 + public let paymentToken: String + public let cardBrand: String + public let maskedPan: String + public let expiryDate: Date + + public var expiryMonth: String { + let formatter = DateFormatter() + formatter.dateFormat = "MM" + formatter.timeZone = TimeZone(identifier: "UTC") + + return formatter.string(from: expiryDate) + } + + public var expiryYear: String { + let formatter = DateFormatter() + formatter.dateFormat = "YY" + formatter.timeZone = TimeZone(identifier: "UTC") + + return formatter.string(from: expiryDate) + } + + public var expiryString: String { + return expiryMonth + "/" + expiryYear + } + } +} diff --git a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift new file mode 100644 index 0000000..be4a6d7 --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift @@ -0,0 +1,117 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +struct OperationOutputModel: Codable, Hashable { + let rel: OperationRel? + let href: String? + let method: String? + let next: Bool? + let tasks: [IntegrationTask]? + let expects: [ExpectationModel]? +} + +extension OperationOutputModel { + func firstTask(withRel rel: IntegrationTaskRel) -> IntegrationTask? { + if let task = tasks?.first(where: { $0.rel == rel }) { + return task + } + + return nil + } +} + +extension Sequence where Iterator.Element == OperationOutputModel +{ + func firstOperation(withRel rel: OperationRel) -> OperationOutputModel? { + return first(where: { $0.rel == rel }) + } + + func containsOperation(withRel rel: OperationRel) -> Bool { + return firstOperation(withRel: rel) != nil + } +} + +enum OperationRel: Codable, Equatable, Hashable { + case expandMethod + case startPaymentAttempt + case createAuthentication + case completeAuthentication + case getPayment + case preparePayment + case redirectPayer + case acknowledgeFailedAttempt + case abortPayment + case eventLogging + case viewPayment + case attemptPayload + case customizePayment + case failPaymentAttempt + + case unknown(String) + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let type = try container.decode(String.self) + + switch type { + case Self.expandMethod.rawValue: self = .expandMethod + case Self.startPaymentAttempt.rawValue: self = .startPaymentAttempt + case Self.createAuthentication.rawValue: self = .createAuthentication + case Self.completeAuthentication.rawValue: self = .completeAuthentication + case Self.getPayment.rawValue: self = .getPayment + case Self.preparePayment.rawValue: self = .preparePayment + case Self.redirectPayer.rawValue: self = .redirectPayer + case Self.acknowledgeFailedAttempt.rawValue: self = .acknowledgeFailedAttempt + case Self.abortPayment.rawValue: self = .abortPayment + case Self.eventLogging.rawValue: self = .eventLogging + case Self.viewPayment.rawValue: self = .viewPayment + case Self.attemptPayload.rawValue: self = .attemptPayload + case Self.customizePayment.rawValue: self = .customizePayment + case Self.failPaymentAttempt.rawValue: self = .failPaymentAttempt + default: self = .unknown(type) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } + + var rawValue: String { + switch self { + case .expandMethod: "expand-method" + case .startPaymentAttempt: "start-payment-attempt" + case .createAuthentication: "create-authentication" + case .completeAuthentication: "complete-authentication" + case .getPayment: "get-payment" + case .preparePayment: "prepare-payment" + case .redirectPayer: "redirect-payer" + case .acknowledgeFailedAttempt: "acknowledge-failed-attempt" + case .abortPayment: "abort-payment" + case .eventLogging: "event-logging" + case .viewPayment: "view-payment" + case .attemptPayload: "attempt-payload" + case .customizePayment: "customize-payment" + case .failPaymentAttempt: "fail-payment-attempt" + case .unknown(let value): value + } + } + + var isUnknown: Bool { + if case .unknown = self { return true } + + return false + } +} diff --git a/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift new file mode 100644 index 0000000..9db75d0 --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift @@ -0,0 +1,45 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +extension SwedbankPaySDK { + /// Instrument with needed values to make a payment attempt. + public enum PaymentAttemptInstrument: Equatable { + case swish(msisdn: String?) + case creditCard(prefill: CreditCardMethodPrefillModel) + case applePay(merchantIdentifier: String) + case newCreditCard(enabledPaymentDetailsConsentCheckbox: Bool) + + var paymentMethod: String { + switch self { + case .swish: + return "Swish" + case .creditCard, + .newCreditCard: + return "CreditCard" + case .applePay: + return "ApplePay" + } + } + + var instrumentModeRequired: Bool { + switch self { + case .newCreditCard: + return true + case .swish, .applePay, .creditCard: + return false + } + } + } +} diff --git a/SwedbankPaySDK/Classes/Api/Models/PaymentOutputModel.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentOutputModel.swift new file mode 100644 index 0000000..95d6e3c --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentOutputModel.swift @@ -0,0 +1,49 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +struct PaymentOutputModel: Codable, Hashable { + let paymentSession: PaymentSessionModel + let operations: [OperationOutputModel]? + let problem: SwedbankPaySDK.ProblemDetails? +} + +extension PaymentOutputModel { + var prioritisedOperations: [OperationOutputModel] { + var operations = operations ?? [] + operations.append(contentsOf: paymentSession.allMethodOperations) + + operations = operations.filter({ $0.rel?.isUnknown == false }) + + if operations.contains(where: { $0.next == true }) { + operations = operations.filter({ $0.next == true }) + } + + return operations + } + + func firstTask(with rel: IntegrationTaskRel) -> IntegrationTask? { + guard let operations = operations else { + return nil + } + + for operation in operations { + if let task = operation.tasks?.first(where: { $0.rel == rel }) { + return task + } + } + + return nil + } +} diff --git a/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift new file mode 100644 index 0000000..2c2ffdd --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift @@ -0,0 +1,70 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +struct PaymentSessionModel: Codable, Hashable { + let culture: String? + let methods: [MethodBaseModel]? + let settings: SettingsModel? + let urls: UrlsModel? + let instrumentModePaymentMethod: String? +} + +extension PaymentSessionModel { + var allMethodOperations: [OperationOutputModel] { + guard let methods = methods else { + return [] + } + + var allOperations = [OperationOutputModel]() + + for method in methods { + if let operations = method.operations { + allOperations.append(contentsOf: operations) + } + } + + return allOperations + } + + var allPaymentMethods: [String] { + return methods?.compactMap({$0.name}) ?? [] + } + + var restrictedToInstruments: [String]? { + guard let methods = methods, let settings = settings else { + return nil + } + + if allPaymentMethods.sorted() == settings.enabledPaymentMethods.sorted() { + return nil + } else { + return allPaymentMethods + } + } +} + +struct UrlsModel: Codable, Hashable { + let completeUrl: URL? + let cancelUrl: URL? + let paymentUrl: URL? + let hostUrls: [URL]? + let termsOfServiceUrl: URL? +} + +struct SettingsModel: Codable, Hashable { + let enabledPaymentMethods: [String] +} diff --git a/SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift b/SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift new file mode 100644 index 0000000..e224b15 --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift @@ -0,0 +1,27 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +public extension SwedbankPaySDK { + /// Problem details returned with `sessionProblemOccurred` + struct ProblemDetails: Codable, Hashable { + public let type: String + public let title: String? + public let status: Int32? + public let detail: String? + public let originalDetail: String? + + let operation: OperationOutputModel? + } +} diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift new file mode 100644 index 0000000..c680243 --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift @@ -0,0 +1,34 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +struct SwedbankPayAPIConstants { + static var commonHeaders: [String: String] = [ + HTTPHeaderField.acceptType.rawValue: ContentType.json.rawValue, + HTTPHeaderField.contentType.rawValue: ContentType.json.rawValue + ] + + static var requestTimeoutInterval = 10.0 + static var sessionTimeoutInterval = 20.0 + static var creditCardTimoutInterval = 30.0 +} + +private enum HTTPHeaderField: String { + case acceptType = "Accept" + case contentType = "Content-Type" +} + +private enum ContentType: String { + case json = "application/json" +} diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift new file mode 100644 index 0000000..51ee0f0 --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -0,0 +1,298 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation +import UIKit +import PassKit + +struct Endpoint { + let router: EnpointRouter? + let href: String? + let method: String? +} + +enum FailPaymentAttemptProblemType: String { + case userCancelled = "UserCancelled" + case technicalError = "TechnicalError" + case clientAppLaunchFailed = "ClientAppLaunchFailed" +} + +enum EnpointRouter { + case expandMethod(instrument: SwedbankPaySDK.PaymentAttemptInstrument) + case startPaymentAttempt(instrument: SwedbankPaySDK.PaymentAttemptInstrument, culture: String?) + case createAuthentication(methodCompletionIndicator: String, notificationUrl: String) + case completeAuthentication(cRes: String) + case getPayment + case preparePayment + case acknowledgeFailedAttempt + case abortPayment + case attemptPayload(paymentPayload: String) + case customizePayment(instrument: SwedbankPaySDK.PaymentAttemptInstrument?, paymentMethod: String?, restrictToPaymentMethods: [String]?) + case failPaymentAttempt(problemType: FailPaymentAttemptProblemType, errorCode: String?) +} + +protocol EndpointRouterProtocol { + var body: [String: Any?]? { get } + var requestTimeoutInterval: TimeInterval { get } + var sessionTimeoutInterval: TimeInterval { get } +} + +struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { + let endpoint: Endpoint + let sessionStartTimestamp: Date + + var body: [String: Any?]? { + switch endpoint.router { + case .expandMethod(instrument: let instrument): + return ["paymentMethod": instrument.paymentMethod] + case .startPaymentAttempt(let instrument, let culture): + switch instrument { + case .swish(let msisdn): + return ["culture": culture, + "msisdn": msisdn, + "client": ["userAgent": SwedbankPaySDK.VersionReporter.userAgent, + "ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? "", + "screenHeight": String(Int32(UIScreen.main.nativeBounds.height)), + "screenWidth": String(Int32(UIScreen.main.nativeBounds.width)), + "screenColorDepth": String(24)] + ] + case .creditCard(let prefill): + return ["culture": culture, + "paymentToken": prefill.paymentToken, + "cardNumber": prefill.maskedPan, + "cardExpiryMonth": prefill.expiryMonth, + "cardExpiryYear": prefill.expiryYear, + "client": ["userAgent": SwedbankPaySDK.VersionReporter.userAgent, + "ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? "", + "screenHeight": String(Int32(UIScreen.main.nativeBounds.height)), + "screenWidth": String(Int32(UIScreen.main.nativeBounds.width)), + "screenColorDepth": String(24)] + ] + case .applePay: + return ["culture": culture, + "client": ["userAgent": SwedbankPaySDK.VersionReporter.userAgent, + "ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? "", + "screenHeight": String(Int32(UIScreen.main.nativeBounds.height)), + "screenWidth": String(Int32(UIScreen.main.nativeBounds.width)), + "screenColorDepth": String(24)] + ] + case .newCreditCard: + return nil + } + case .preparePayment: + return ["integration": "HostedView", + "deviceAcceptedWallets": PKPaymentAuthorizationController.canMakePayments() ? "ApplePay;ClickToPay" : "ClickToPay", + "client": ["userAgent": SwedbankPaySDK.VersionReporter.userAgent, + "ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? "", + "screenHeight": String(Int32(UIScreen.main.nativeBounds.height)), + "screenWidth": String(Int32(UIScreen.main.nativeBounds.width)), + "screenColorDepth": String(24)], + "browser": ["acceptHeader": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "languageHeader": Locale.current.identifier.replacingOccurrences(of: "_", with: "-"), + "timeZoneOffset": TimeZone.current.minutesFromGMT(), + "javascriptEnabled": true], + "service": ["name": "SwedbankPaySDK-iOS", + "version": SwedbankPaySDK.VersionReporter.currentVersion] + ] + case .createAuthentication(let methodCompletionIndicator, let notificationUrl): + return ["methodCompletionIndicator": methodCompletionIndicator, + "notificationUrl": notificationUrl, + "requestWindowSize": "FULLSCREEN", + "client": ["userAgent": SwedbankPaySDK.VersionReporter.userAgent, + "ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? "", + "screenHeight": String(Int32(UIScreen.main.nativeBounds.height)), + "screenWidth": String(Int32(UIScreen.main.nativeBounds.width)), + "screenColorDepth": String(24)], + "browser": ["acceptHeader": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "languageHeader": Locale.current.identifier.replacingOccurrences(of: "_", with: "-"), + "timeZoneOffset": TimeZone.current.minutesFromGMT(), + "javascriptEnabled": true] + ] + case .completeAuthentication(let cRes): + return ["cRes": cRes, + "client": ["userAgent": SwedbankPaySDK.VersionReporter.userAgent, + "ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? ""], + ] + case .attemptPayload(let paymentPayload): + return ["paymentMethod": "ApplePay", + "paymentPayload": paymentPayload] + case .customizePayment(let instrument, let paymentMethod, let restrictToPaymentMethods): + + switch (instrument, paymentMethod, restrictToPaymentMethods) { + case (nil, nil, let restrictToPaymentMethods?): + return ["paymentMethod": nil, + "restrictToPaymentMethods": restrictToPaymentMethods.isEmpty ? nil : restrictToPaymentMethods] + case (.newCreditCard(let enabledPaymentDetailsConsentCheckbox), _, _): + return ["paymentMethod": "CreditCard", + "restrictToPaymentMethods": nil, + "hideStoredPaymentOptions": true, + "showConsentAffirmation" : enabledPaymentDetailsConsentCheckbox, + ] + case (nil, let paymentMethod?, nil): + return ["paymentMethod": paymentMethod, + "restrictToPaymentMethods": nil] + case (let instrument?, nil, nil): + return ["paymentMethod": instrument.paymentMethod, + "restrictToPaymentMethods": nil] + default: + return ["paymentMethod": nil, + "restrictToPaymentMethods": nil] + } + case .failPaymentAttempt(let problemType, let errorCode): + return ["problemType": problemType.rawValue, + "errorCode": errorCode] + default: + return nil + } + } + + var requestTimeoutInterval: TimeInterval { + switch endpoint.router { + case .startPaymentAttempt(let instrument, _): + switch instrument { + case .creditCard: + return SwedbankPayAPIConstants.creditCardTimoutInterval + default: + return SwedbankPayAPIConstants.requestTimeoutInterval + } + case .createAuthentication, + .completeAuthentication: + return SwedbankPayAPIConstants.creditCardTimoutInterval + default: + return SwedbankPayAPIConstants.requestTimeoutInterval + } + } + + var sessionTimeoutInterval: TimeInterval { + switch endpoint.router { + case .startPaymentAttempt(let instrument, _): + switch instrument { + case .creditCard: + return SwedbankPayAPIConstants.creditCardTimoutInterval + default: + return SwedbankPayAPIConstants.sessionTimeoutInterval + } + case .createAuthentication, + .completeAuthentication: + return SwedbankPayAPIConstants.creditCardTimoutInterval + default: + return SwedbankPayAPIConstants.sessionTimeoutInterval + } + } +} + +extension SwedbankPayAPIEnpointRouter { + func makeRequest(handler: @escaping (Result) -> Void) { + let requestStartTimestamp: Date = Date() + + requestWithDataResponse(requestStartTimestamp: requestStartTimestamp) { result in + switch result { + case .success(let data): + do { + let result: PaymentOutputModel = try Self.parseData(data: data) + handler(.success(result)) + } catch { + handler(.success(nil)) + } + case .failure(let error): + handler(.failure(error)) + } + } + } + + + private static func parseData(data: Data) throws -> T { + let decodedData: T + + do { + decodedData = try CustomDateDecoder().decode(T.self, from: data) + } catch { + throw error + } + + return decodedData + } + + private func requestWithDataResponse(requestStartTimestamp: Date, handler: @escaping (Result) -> Void) { + guard let href = endpoint.href, + var components = URLComponents(string: href) else { + handler(.failure(SwedbankPayAPIError.invalidUrl)) + return + } + + if components.scheme == "http" { + components.scheme = "https" + } + + guard let url = components.url else { + handler(.failure(SwedbankPayAPIError.invalidUrl)) + return + } + + var request = URLRequest(url: url) + request.httpMethod = endpoint.method + request.allHTTPHeaderFields = SwedbankPayAPIConstants.commonHeaders + request.timeoutInterval = requestTimeoutInterval + + if let body = body, let jsonData = try? JSONSerialization.data(withJSONObject: body) { + request.httpBody = jsonData + } + + URLSession.shared.dataTask(with: request) { data, response, error in + + var responseStatusCode: Int? + if let response = response as? HTTPURLResponse { + responseStatusCode = response.statusCode + } + + var values: [String: String]? + if let error = error as? NSError { + values = ["errorDescription": error.localizedDescription, + "errorCode": String(error.code), + "errorDomain": error.domain] + } + + BeaconService.shared.log(type: .httpRequest(duration: Int32((Date().timeIntervalSince(requestStartTimestamp) * 1000.0).rounded()), + requestUrl: endpoint.href ?? "", + method: endpoint.method ?? "", + responseStatusCode: responseStatusCode, + values: values)) + + guard let response = response as? HTTPURLResponse, !(500...599 ~= response.statusCode) else { + guard Date().timeIntervalSince(requestStartTimestamp) < requestTimeoutInterval && + Date().timeIntervalSince(sessionStartTimestamp) < sessionTimeoutInterval else { + handler(.failure(error ?? SwedbankPayAPIError.unknown)) + return + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + let requestStartTimestamp: Date = Date() + + requestWithDataResponse(requestStartTimestamp: requestStartTimestamp, handler: handler) + } + + return + } + + guard let data, 200...204 ~= response.statusCode else { + handler(.failure(error ?? SwedbankPayAPIError.unknown)) + + return + } + + handler(.success(data)) + }.resume() + } +} diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIError.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIError.swift new file mode 100644 index 0000000..e7d6b70 --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIError.swift @@ -0,0 +1,32 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +enum SwedbankPayAPIError: Error { + case invalidUrl + case unknown +} + +extension SwedbankPayAPIError: LocalizedError { + public var errorDescription: String? { + switch self { + case .invalidUrl: + return SwedbankPaySDKResources.localizedString(key: "swedbankpaysdk_native_invalid_url") + case .unknown: + return SwedbankPaySDKResources.localizedString(key: "swedbankpaysdk_native_unknown") + } + } +} diff --git a/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift b/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift new file mode 100644 index 0000000..02fbab7 --- /dev/null +++ b/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift @@ -0,0 +1,126 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation +import PassKit + +enum ApplePayError: Error { + case userCancelled +} + +class SwedbankPayAuthorization: NSObject { + static let shared = SwedbankPayAuthorization() + + private var operation: OperationOutputModel? + private var task: IntegrationTask? + private var handler: ((Result) -> Void)? + + private var success: PaymentOutputModel? + private var errors: [Error]? + private var status: PKPaymentAuthorizationStatus? + private var hasAuthorizedPayment = false + + func showApplePay(operation: OperationOutputModel, task: IntegrationTask, merchantIdentifier: String, handler: @escaping (Result) -> Void) { + self.errors = nil + self.status = nil + + self.operation = operation + self.task = task + self.handler = handler + + let paymentRequest = PKPaymentRequest() + + if let totalAmountLabel = task.expects?.first(where: { $0.name == "TotalAmountLabel" })?.value, + let totalAmount = task.expects?.first(where: { $0.name == "TotalAmount" })?.value { + let total = PKPaymentSummaryItem(label: totalAmountLabel, amount: NSDecimalNumber(string: totalAmount), type: .final) + paymentRequest.paymentSummaryItems = [total] + } + + paymentRequest.merchantIdentifier = merchantIdentifier + + if (task.expects?.first(where: { $0.name == "MerchantCapabilities" })?.stringArray?.contains(where: { $0 == "supports3DS" })) != nil { + paymentRequest.merchantCapabilities = .threeDSecure + } + + if let identifier = task.expects?.first(where: { $0.name == "Locale" })?.value, + let countryCode = Locale(identifier: identifier).regionCode { + paymentRequest.countryCode = countryCode + } + + if let currencyCode = task.expects?.first(where: { $0.name == "CurrencyCode" })?.value { + paymentRequest.currencyCode = currencyCode + } + + if let supportedNetworks: [PKPaymentNetwork] = task.expects?.first(where: { $0.name == "SupportedNetworks" })?.stringArray?.compactMap({ string in + return SwedbankPaymentNetwork(rawValue: string)?.pkPaymentNetwork + }) { + paymentRequest.supportedNetworks = supportedNetworks + } + + if let supportedCountries = task.expects?.first(where: { $0.name == "SupportedCountries" })?.stringArray { + paymentRequest.supportedCountries = Set(supportedCountries.map { $0 }) + } + + if let requiredShippingContactFields: [String] = task.expects?.first(where: { $0.name == "RequiredShippingContactFields" })?.stringArray { + paymentRequest.requiredShippingContactFields = Set(requiredShippingContactFields.map { PKContactField(rawValue: $0) }) + } + + let paymentController = PKPaymentAuthorizationController(paymentRequest: paymentRequest) + paymentController.delegate = self + paymentController.present(completion: { (presented: Bool) in + if !presented { + handler(.failure(SwedbankPayAPIError.unknown)) + } + }) + } +} + +extension SwedbankPayAuthorization: PKPaymentAuthorizationControllerDelegate { + func paymentAuthorizationControllerDidFinish(_ controller: PKPaymentAuthorizationController) { + if let handler = self.handler { + if status != nil, let success = success { + handler(.success(success)) + } else { + handler(.failure(ApplePayError.userCancelled)) + } + } + + self.handler = nil + + controller.dismiss() + } + + func paymentAuthorizationController(_ controller: PKPaymentAuthorizationController, didAuthorizePayment payment: PKPayment, handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) { + let paymentPayload = payment.token.paymentData.base64EncodedString() + + let router = EnpointRouter.attemptPayload(paymentPayload: paymentPayload) + + SwedbankPayAPIEnpointRouter(endpoint: Endpoint(router: router, href: operation?.href, method: operation?.method), + sessionStartTimestamp: Date()).makeRequest { result in + switch result { + case .success(let paymentOutputModel): + self.success = paymentOutputModel + self.status = PKPaymentAuthorizationStatus.success + self.errors = [Error]() + case .failure(let error): + self.success = nil + self.status = PKPaymentAuthorizationStatus.failure + self.errors = [error] + } + + completion(PKPaymentAuthorizationResult(status: self.status!, errors: self.errors)) + } + } +} diff --git a/SwedbankPaySDK/Classes/ApplePay/SwedbankPaymentNetwork.swift b/SwedbankPaySDK/Classes/ApplePay/SwedbankPaymentNetwork.swift new file mode 100644 index 0000000..ddef372 --- /dev/null +++ b/SwedbankPaySDK/Classes/ApplePay/SwedbankPaymentNetwork.swift @@ -0,0 +1,87 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation +import PassKit + +enum SwedbankPaymentNetwork: String, Hashable, Equatable { + case amex = "amex" + case carteBancaires = "cartebancaires" + case chinaUnionPay = "chinaunionpay" + case discover = "discover" + case interac = "interac" + case idCredit = "id" + case JCB = "jcb" + case masterCard = "mastercard" + case quicPay = "quicpay" + case suica = "suica" + case visa = "visa" + + var pkPaymentNetwork: PKPaymentNetwork { + switch self { + case .amex: + return .amex + case .carteBancaires: + return .carteBancaires + case .chinaUnionPay: + return .chinaUnionPay + case .discover: + return .discover + case .interac: + return .interac + case .idCredit: + return .idCredit + case .JCB: + return .JCB + case .masterCard: + return .masterCard + case .quicPay: + return .quicPay + case .suica: + return .suica + case .visa: + return .visa + } + } + + init?(pkPaymentNetwork: PKPaymentNetwork) { + switch pkPaymentNetwork { + case .amex: + self = .amex + case .carteBancaires: + self = .carteBancaires + case .chinaUnionPay: + self = .chinaUnionPay + case .discover: + self = .discover + case .interac: + self = .interac + case .idCredit: + self = .idCredit + case .JCB: + self = .JCB + case .masterCard: + self = .masterCard + case .quicPay: + self = .quicPay + case .suica: + self = .suica + case .visa: + self = .visa + default: + return nil + } + } +} diff --git a/SwedbankPaySDK/Classes/Beacon/Beacon.swift b/SwedbankPaySDK/Classes/Beacon/Beacon.swift new file mode 100644 index 0000000..c7e5b01 --- /dev/null +++ b/SwedbankPaySDK/Classes/Beacon/Beacon.swift @@ -0,0 +1,21 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +struct Beacon { + let actionType: BeaconType + let created: Date +} diff --git a/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift b/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift new file mode 100644 index 0000000..a7a4bb3 --- /dev/null +++ b/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift @@ -0,0 +1,138 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation +import UIKit + +protocol BeaconEndpointRouterProtocol { + var body: [String: Any?]? { get } +} + +struct BeaconEndpointRouter: BeaconEndpointRouterProtocol { + let href: String? + let beacon: Beacon + + private let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.calendar = Calendar(identifier: .iso8601) + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" + return formatter + }() + + // MARK: - Body + var body: [String: Any?]? { + var body: [String: Any?] = ["type": "ClientEvent", + "client": ["userAgent": SwedbankPaySDK.VersionReporter.userAgent, + "ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? "", + "screenHeight": String(Int32(UIScreen.main.nativeBounds.height)), + "screenWidth": String(Int32(UIScreen.main.nativeBounds.width)), + "screenColorDepth": String(24)], + "service": ["name": "SwedbankPaySDK-iOS", + "version": SwedbankPaySDK.VersionReporter.currentVersion], + "event": ["created": dateFormatter.string(from: beacon.created), + "action": beacon.actionType.action]] + + switch beacon.actionType { + case .sdkMethodInvoked(name: let name, succeeded: let succeeded, values: let values): + body["method"] = ["name": name, + "sdk": "true", + "succeeded": succeeded] + if let values = values { + body["extensions"] = ["values": values] + } + case .sdkCallbackInvoked(name: let name, succeeded: let succeeded, values: let values): + body["method"] = ["name": name, + "sdk": "true", + "succeeded": succeeded] + if let values = values { + body["extensions"] = ["values": values] + } + case .httpRequest(duration: let duration, requestUrl: let requestUrl, method: let method, responseStatusCode: let responseStatusCode, values: let values): + body["event"] = ["created": dateFormatter.string(from: beacon.created), + "action": beacon.actionType.action, + "duration": duration] + + var http: [String: Any] = ["requestUrl": requestUrl, + "method": method] + if let responseStatusCode = responseStatusCode { + http["responseStatusCode"] = responseStatusCode + } + body["http"] = http + + if let values = values { + body["extensions"] = ["values": values] + } + case .launchClientApp(values: let values): + if let values = values { + body["extensions"] = ["values": values] + } + case .clientAppCallback(values: let values): + if let values = values { + body["extensions"] = ["values": values] + } + } + + return body + } +} + +extension BeaconEndpointRouter { + func makeRequest(handler: @escaping (Result) -> Void) { + requestWithDataResponse { result in + switch result { + case .success: + handler(.success(())) + case .failure(let error): + handler(.failure(error)) + } + } + } + + private func requestWithDataResponse(handler: @escaping (Result) -> Void) { + guard let href = href, + var components = URLComponents(string: href) else { + handler(.failure(SwedbankPayAPIError.invalidUrl)) + return + } + + if components.scheme == "http" { + components.scheme = "https" + } + + guard let url = components.url else { + handler(.failure(SwedbankPayAPIError.invalidUrl)) + return + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.allHTTPHeaderFields = SwedbankPayAPIConstants.commonHeaders + + if let body = body, let jsonData = try? JSONSerialization.data(withJSONObject: body) { + request.httpBody = jsonData + } + + URLSession.shared.dataTask(with: request) { data, response, error in + guard let response = response as? HTTPURLResponse, + response.statusCode == 204 else { + handler(.failure(error ?? SwedbankPayAPIError.unknown)) + + return + } + + handler(.success(())) + }.resume() + } +} diff --git a/SwedbankPaySDK/Classes/Beacon/BeaconService.swift b/SwedbankPaySDK/Classes/Beacon/BeaconService.swift new file mode 100644 index 0000000..fa01110 --- /dev/null +++ b/SwedbankPaySDK/Classes/Beacon/BeaconService.swift @@ -0,0 +1,49 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +class BeaconService { + static let shared = BeaconService() + + var href: String? = nil + + private var beacons: [Beacon] = [] + + func clear() { + beacons = [] + } + + func log(type: BeaconType) { + beacons.append(Beacon(actionType: type, created: Date())) + + var noFailures = false + + while !beacons.isEmpty, !noFailures { + if let beacon = beacons.popLast() { + BeaconEndpointRouter(href: href, beacon: beacon).makeRequest { result in + switch result { + case .success(()): + break + case .failure(_): + self.beacons.append(beacon) + + noFailures = true + } + } + } + } + } +} diff --git a/SwedbankPaySDK/Classes/Beacon/BeaconType.swift b/SwedbankPaySDK/Classes/Beacon/BeaconType.swift new file mode 100644 index 0000000..9b0f1e8 --- /dev/null +++ b/SwedbankPaySDK/Classes/Beacon/BeaconType.swift @@ -0,0 +1,37 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +enum BeaconType { + case sdkMethodInvoked(name: String, succeeded: Bool, values: [String: String?]?) + case sdkCallbackInvoked(name: String, succeeded: Bool, values: [String: String?]?) + case httpRequest(duration: Int32, requestUrl: String, method: String, responseStatusCode: Int?, values: [String: String?]?) + case launchClientApp(values: [String: String?]?) + case clientAppCallback(values: [String: String?]?) + + var action: String { + switch self { + case .sdkMethodInvoked: + return "SDKMethodInvoked" + case .sdkCallbackInvoked: + return "SDKCallbackInvoked" + case .httpRequest: + return "HttpRequest" + case .launchClientApp: + return "LaunchClientApp" + case .clientAppCallback: + return "ClientAppCallback" + } + } +} diff --git a/SwedbankPaySDK/Classes/SwedbankPayConfiguration.swift b/SwedbankPaySDK/Classes/SwedbankPayConfiguration.swift new file mode 100644 index 0000000..9806e91 --- /dev/null +++ b/SwedbankPaySDK/Classes/SwedbankPayConfiguration.swift @@ -0,0 +1,50 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +enum SwedbankPayConfigurationError: Error { + case notImplemented +} + +internal class SwedbankPayConfiguration { + let orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo + + init(isV3: Bool = true, webViewBaseURL: URL?, + viewPaymentLink: URL, completeUrl: URL, cancelUrl: URL?, + paymentUrl: URL? = nil, termsOfServiceUrl: URL? = nil) { + self.orderInfo = SwedbankPaySDK.ViewPaymentOrderInfo( + isV3: isV3, + webViewBaseURL: webViewBaseURL, + viewPaymentLink: viewPaymentLink, + completeUrl: completeUrl, + cancelUrl: cancelUrl, + paymentUrl: paymentUrl, + termsOfServiceUrl: termsOfServiceUrl + ) + } +} + +extension SwedbankPayConfiguration: SwedbankPaySDKConfiguration { + + // This delegate method is not used but required + func postConsumers(consumer: SwedbankPaySDK.Consumer?, userData: Any?, completion: @escaping (Result) -> Void) { + completion(.failure(SwedbankPayConfigurationError.notImplemented)) + } + + func postPaymentorders(paymentOrder: SwedbankPaySDK.PaymentOrder?, userData: Any?, consumerProfileRef: String?, options: SwedbankPaySDK.VersionOptions, completion: @escaping (Result) -> Void) { + completion(.success(orderInfo)) + } +} diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift new file mode 100644 index 0000000..9c35256 --- /dev/null +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -0,0 +1,805 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation +import UIKit +import WebKit + +/// Swedbank Pay SDK protocol, conform to this to get the result of the payment process +public protocol SwedbankPaySDKPaymentSessionDelegate: AnyObject { + /// Called whenever the payment has been completed. + func paymentSessionComplete() + + /// Called whenever the payment has been canceled for any reason. + func paymentSessionCanceled() + + /// Called when an list of available instruments is known. + /// + /// - parameter availableInstruments: List of different instruments that is available to be used for the payment session. + func paymentSessionFetched(availableInstruments: [SwedbankPaySDK.AvailableInstrument]) + + /// Called if there is a session problem with performing the payment. + /// + /// - parameter problem: The problem that caused the failure + func sessionProblemOccurred(problem: SwedbankPaySDK.ProblemDetails) + + /// Called if there is a SDK problem with performing the payment. + /// + /// - parameter problem: The problem that caused the failure + func sdkProblemOccurred(problem: SwedbankPaySDK.PaymentSessionProblem) + + /// Called when a 3D secure view needs to be presented. + /// + /// - parameter viewController: The UIViewController with 3D secure web view. + func show3DSecureViewController(viewController: UIViewController) + + /// Called whenever the 3D secure view can be dismissed. + func dismiss3DSecureViewController() + + func showSwedbankPaySDKController(viewController: SwedbankPaySDKController) +} + +public extension SwedbankPaySDK { + enum SwedbankPayPaymentSessionSDKControllerMode { + case menu(restrictedToInstruments: [SwedbankPaySDK.AvailableInstrument]?) + case instrumentMode(instrument: SwedbankPaySDK.AvailableInstrument) + } + + /// Object that handles payment sessions + class SwedbankPayPaymentSession: CallbackUrlDelegate { + /// Order information that provides `PaymentSession` with callback URLs. + public var orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo? + + /// A delegate to receive callbacks as the native payment changes. + public weak var delegate: SwedbankPaySDKPaymentSessionDelegate? + + private var ongoingModel: PaymentOutputModel? = nil + private var sessionIsOngoing: Bool = false + private var paymentViewSessionIsOngoing: Bool = false + private var instrument: SwedbankPaySDK.PaymentAttemptInstrument? = nil + private var sdkControllerMode: SwedbankPaySDK.SwedbankPayPaymentSessionSDKControllerMode? = nil + private var hasShownAvailableInstruments: Bool = false + private var merchantIdentifier: String? = nil + + private var hasLaunchClientAppURLs: [URL] = [] + private var hasShownProblemDetails: [ProblemDetails] = [] + private var scaMethodRequestDataPerformed: [(name: String, value: String)] = [] + private var scaRedirectDataPerformed: [(name: String, value: String)] = [] + private var notificationUrl: String? = nil + + private var sessionStartTimestamp = Date() + + private var webViewService = SCAWebViewService() + private lazy var webViewController = SwedbankPaySCAWebViewController() + + private var automaticConfiguration: Bool = true + + public init(manualOrderInfo orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo? = nil) { + if let orderInfo { + self.orderInfo = orderInfo + self.automaticConfiguration = false + } + + SwedbankPaySDK.addCallbackUrlDelegate(self) + } + + deinit { + SwedbankPaySDK.removeCallbackUrlDelegate(self) + } + + /// Starts a new native payment session. + /// + /// Calling this when a payment is already started will throw out the old payment. + /// + /// - parameter with sessionURL: Session URL needed to start the native payment session + public func fetchPaymentSession(sessionURL: URL) { + sessionIsOngoing = true + paymentViewSessionIsOngoing = false + instrument = nil + sdkControllerMode = nil + merchantIdentifier = nil + ongoingModel = nil + hasLaunchClientAppURLs = [] + hasShownProblemDetails = [] + scaMethodRequestDataPerformed = [] + scaRedirectDataPerformed = [] + notificationUrl = nil + hasShownAvailableInstruments = false + + if automaticConfiguration { + orderInfo = nil + } + + let model = OperationOutputModel(rel: nil, + href: sessionURL.absoluteString, + method: "GET", + next: nil, + tasks: nil, + expects: nil) + + sessionStartTimestamp = Date() + makeRequest(router: nil, operation: model) + + BeaconService.shared.clear() + BeaconService.shared.log(type: .sdkMethodInvoked(name: "startPaymentSession", + succeeded: true, + values: nil)) + } + + /// Make a payment attempt with a specific instrument. + /// + /// There needs to be an active payment session before an payment attempt can be made. + /// + /// - parameter instrument: Payment attempt instrument + public func makeNativePaymentAttempt(instrument: SwedbankPaySDK.PaymentAttemptInstrument) { + guard let ongoingModel = ongoingModel else { + self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": SwedbankPaySDK.PaymentSessionProblem.internalInconsistencyError.rawValue])) + + return + } + + paymentViewSessionIsOngoing = false + self.instrument = instrument + + switch instrument { + case .applePay(let merchantIdentifier): + self.merchantIdentifier = merchantIdentifier + default: + break + } + + sessionOperationHandling(paymentOutputModel: ongoingModel, culture: ongoingModel.paymentSession.culture) + + switch instrument { + case .swish(let msisdn): + BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", + succeeded: true, + values: ["instrument": instrument.paymentMethod, + "msisdn": msisdn])) + case .creditCard(let prefill): + BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", + succeeded: true, + values: ["instrument": instrument.paymentMethod, + "paymentToken": prefill.paymentToken, + "cardNumber": prefill.maskedPan, + "cardExpiryMonth": prefill.expiryMonth, + "cardExpiryYear": prefill.expiryYear])) + case .applePay: + BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", + succeeded: true, + values: ["instrument": instrument.paymentMethod])) + case .newCreditCard(enabledPaymentDetailsConsentCheckbox: let enabledPaymentDetailsConsentCheckbox): + BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", + succeeded: true, + values: ["instrument": instrument.paymentMethod, + "showConsentAffirmation": enabledPaymentDetailsConsentCheckbox.description])) + } + + } + + /// Creates a SwedbankPaySDKController. + /// + /// There needs to be an active payment session before an payment attempt can be made. + /// + /// - returns:- SwedbankPaySDKController to be shown. + public func createSwedbankPaySDKController(mode: SwedbankPayPaymentSessionSDKControllerMode) { + guard let ongoingModel = ongoingModel else { + self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": SwedbankPaySDK.PaymentSessionProblem.internalInconsistencyError.rawValue])) + + return + } + + let logValues: [String: String?] + + switch mode { + case .instrumentMode(let instrument): + logValues = [ + "mode": "instrumentMode", + "instrument": instrument.paymentMethod + ] + case .menu(let restrictedToInstruments): + logValues = [ + "mode": "menu", + "restrictedToInstruments": restrictedToInstruments?.compactMap({ $0.paymentMethod }).joined(separator: ";") + ] + } + + BeaconService.shared.log(type: .sdkMethodInvoked(name: "createSwedbankPaySDKController", + succeeded: true, + values: logValues)) + + paymentViewSessionIsOngoing = false + sdkControllerMode = mode + + sessionOperationHandling(paymentOutputModel: ongoingModel, culture: ongoingModel.paymentSession.culture) + } + + private func createSwedbankPaySDKController() { + guard let ongoingModel = ongoingModel, + let operation = ongoingModel.operations?.firstOperation(withRel: .viewPayment), + let orderInfo = orderInfo, + let href = operation.href, + let viewPaymentLink = URL(string: href) else { + self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": SwedbankPaySDK.PaymentSessionProblem.internalInconsistencyError.rawValue])) + + return + } + + let configuration = SwedbankPayConfiguration( + isV3: orderInfo.isV3, + webViewBaseURL: automaticConfiguration ? ongoingModel.paymentSession.urls?.hostUrls?.first : orderInfo.webViewBaseURL, + viewPaymentLink: viewPaymentLink, + completeUrl: orderInfo.completeUrl, + cancelUrl: orderInfo.cancelUrl, + paymentUrl: orderInfo.paymentUrl) + + let viewController = SwedbankPaySDKController( + configuration: configuration, + withCheckin: false, + consumer: nil, + paymentOrder: nil, + userData: nil) + + paymentViewSessionIsOngoing = true + + viewController.internalDelegate = self + + delegate?.showSwedbankPaySDKController(viewController: viewController) + } + + /// Abort an active payment session. + /// + /// Does nothing if there isn't an active payment session. + public func abortPaymentSession() { + guard let ongoingModel = ongoingModel else { + self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": SwedbankPaySDK.PaymentSessionProblem.internalInconsistencyError.rawValue])) + + return + } + + var succeeded = false + if let operation = ongoingModel.operations?.firstOperation(withRel: .abortPayment) { + sessionStartTimestamp = Date() + makeRequest(router: .abortPayment, operation: operation) + succeeded = true + } + + BeaconService.shared.log(type: .sdkMethodInvoked(name: "abortPaymentSession", + succeeded: succeeded, + values: nil)) + } + + private func makeRequest(router: EnpointRouter?, operation: OperationOutputModel) { + SwedbankPayAPIEnpointRouter(endpoint: Endpoint(router: router, href: operation.href, method: operation.method), + sessionStartTimestamp: sessionStartTimestamp).makeRequest { result in + switch result { + case .success(let success): + if let paymentOutputModel = success { + if self.automaticConfiguration, router == nil { + guard let urls = paymentOutputModel.paymentSession.urls, urls.completeUrl != nil, urls.hostUrls != nil else { + self.delegate?.sdkProblemOccurred(problem: .automaticConfigurationFailed) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": SwedbankPaySDK.PaymentSessionProblem.automaticConfigurationFailed.rawValue])) + + return + } + + self.orderInfo = SwedbankPaySDK.ViewPaymentOrderInfo(isV3: true, + webViewBaseURL: nil, + viewPaymentLink: URL(string: "https://")!, + completeUrl: urls.completeUrl!, + cancelUrl: urls.cancelUrl, + paymentUrl: urls.paymentUrl, + termsOfServiceUrl: urls.termsOfServiceUrl) + } + + if let eventLogging = paymentOutputModel.operations?.firstOperation(withRel: .eventLogging) { + BeaconService.shared.href = eventLogging.href + } + + self.sessionOperationHandling(paymentOutputModel: paymentOutputModel, culture: paymentOutputModel.paymentSession.culture) + } + case .failure(let failure): + DispatchQueue.main.async { + let problem = SwedbankPaySDK.PaymentSessionProblem.paymentSessionAPIRequestFailed(error: failure, + retry: { + self.sessionStartTimestamp = Date() + self.makeRequest(router: router, operation: operation) + }) + + self.delegate?.sdkProblemOccurred(problem: problem) + + let error = failure as NSError + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": problem.rawValue, + "errorDescription": error.localizedDescription, + "errorCode": String(error.code), + "errorDomain": error.domain])) + } + } + } + } + + private func launchClientApp(task: IntegrationTask, failPaymentAttemptOperation: OperationOutputModel) { + guard let href = task.href, var components = URLComponents(string: href) else { + self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": SwedbankPaySDK.PaymentSessionProblem.internalInconsistencyError.rawValue])) + + return + } + + // If the scheme is `swish` then we need to add a `callbackurl` if it's not already included in the link. + if components.scheme == "swish", + components.queryItems?.contains(where: { $0.name == "callbackurl" }) == false || + components.queryItems?.contains(where: { $0.name == "callbackurl" && ($0.value == nil || $0.value?.isEmpty == true) }) == true { + if let paymentUrl = orderInfo?.paymentUrl?.absoluteString { + components.queryItems?.append(URLQueryItem(name: "callbackurl", value: paymentUrl.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed))) + } + } + + if let url = components.url { + DispatchQueue.main.async { + UIApplication.shared.open(url) { complete in + self.instrument = nil + + if complete { + self.hasLaunchClientAppURLs.append(url) + } else { + self.makeRequest(router: .failPaymentAttempt(problemType: .clientAppLaunchFailed, errorCode: nil), operation: failPaymentAttemptOperation) + } + + BeaconService.shared.log(type: .launchClientApp(values: ["callbackUrl": self.orderInfo?.paymentUrl?.absoluteString ?? "", + "clientAppLaunchUrl": url.absoluteString, + "launchSucceeded": complete.description])) + } + } + } + } + + private func makeApplePayAuthorization(attemptPayloadOperation: OperationOutputModel, failPaymentAttemptOperation: OperationOutputModel, task: IntegrationTask) { + guard let merchantIdentifier = merchantIdentifier else { + self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": SwedbankPaySDK.PaymentSessionProblem.internalInconsistencyError.rawValue])) + + return + } + + SwedbankPayAuthorization.shared.showApplePay(operation: attemptPayloadOperation, task: task, merchantIdentifier: merchantIdentifier) { result in + switch result { + case .success(let paymentOutputModel): + self.sessionOperationHandling(paymentOutputModel: paymentOutputModel, culture: paymentOutputModel.paymentSession.culture) + case .failure(ApplePayError.userCancelled): + self.makeRequest(router: .failPaymentAttempt(problemType: .userCancelled, errorCode: nil), operation: failPaymentAttemptOperation) + case .failure(let error): + self.makeRequest(router: .failPaymentAttempt(problemType: .technicalError, errorCode: error.localizedDescription), operation: failPaymentAttemptOperation) + } + } + } + + private func sessionOperationHandling(paymentOutputModel: PaymentOutputModel, culture: String? = nil) { + ongoingModel = paymentOutputModel + + var hasShowedError = false + + if let modelProblem = paymentOutputModel.problem, + let problemOperation = modelProblem.operation, + problemOperation.rel == .acknowledgeFailedAttempt { + if !hasShownProblemDetails.contains(where: { $0.operation?.href == problemOperation.href }) { + hasShownProblemDetails.append(modelProblem) + hasShowedError = true + + DispatchQueue.main.async { + self.delegate?.sessionProblemOccurred(problem: modelProblem) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sessionProblemOccurred", + succeeded: self.delegate != nil, + values: ["problemTitle": modelProblem.title ?? "", + "problemStatus": String(modelProblem.status ?? 0), + "problemDetail": modelProblem.detail ?? ""])) + } + + makeRequest(router: .acknowledgeFailedAttempt, operation: problemOperation) + } + } + + let operations = paymentOutputModel.prioritisedOperations + + if let preparePayment = operations.firstOperation(withRel: .preparePayment) { + // Initial state of payment session, run preparePayment operation + + makeRequest(router: .preparePayment, operation: preparePayment) + } else if let attemptPayload = operations.firstOperation(withRel: .attemptPayload), + let failPayment = paymentOutputModel.paymentSession.methods?.firstMethod(withName: AvailableInstrument.applePay.paymentMethod)?.operations?.firstOperation(withRel: .failPaymentAttempt), + let walletSdk = attemptPayload.firstTask(withRel: .walletSdk) { + // We have an active walletSdk task, this means we should initiate an Apple Pay Payment Request locally on the device + + makeApplePayAuthorization(attemptPayloadOperation: attemptPayload, failPaymentAttemptOperation: failPayment, task: walletSdk) + } else if let instrument = self.instrument, + (paymentOutputModel.paymentSession.instrumentModePaymentMethod != nil && paymentOutputModel.paymentSession.instrumentModePaymentMethod != instrument.paymentMethod) + || !paymentOutputModel.paymentSession.allPaymentMethods.contains(where: {$0 == instrument.paymentMethod}), + let customizePayment = paymentOutputModel.operations?.firstOperation(withRel: .customizePayment) { + // Resetting Instrument Mode session to Menu Mode (instrumentModePaymentMethod set to nil), if new payment attempt is made with an instrument other than the current Instrument Mode instrument or with an instrument not in the list of methods (restricted menu) + + makeRequest(router: .customizePayment(instrument: nil, paymentMethod: nil, restrictToPaymentMethods: nil), operation: customizePayment) + } else if let instrument = self.instrument, + instrument.instrumentModeRequired, + paymentOutputModel.paymentSession.instrumentModePaymentMethod == nil + || paymentOutputModel.paymentSession.instrumentModePaymentMethod != instrument.paymentMethod, + let customizePayment = paymentOutputModel.operations?.firstOperation(withRel: .customizePayment) { + // Switching to Instrument Mode from Menu Mode session (instrumentModePaymentMethod set to nil) or from Instrument Mode with other instrument, if new payment attempt is made with an Instrument Mode required instrument (newCreditCard) + + makeRequest(router: .customizePayment(instrument: instrument, paymentMethod: nil, restrictToPaymentMethods: nil), operation: customizePayment) + } else if let instrument = self.instrument, + instrument.instrumentModeRequired, + paymentOutputModel.paymentSession.instrumentModePaymentMethod == instrument.paymentMethod { + // Session is in Instrument Mode, and the set instrument is matching payment attempt, time to create a web based view and send to the merchant app + + self.instrument = nil + + DispatchQueue.main.async { + self.createSwedbankPaySDKController() + } + } else if let sdkControllerMode = self.sdkControllerMode, + case .instrumentMode(let instrument) = sdkControllerMode, + paymentOutputModel.paymentSession.instrumentModePaymentMethod == nil + || paymentOutputModel.paymentSession.instrumentModePaymentMethod != instrument.paymentMethod, + let customizePayment = paymentOutputModel.operations?.firstOperation(withRel: .customizePayment) { + // Switching to Instrument Mode from Menu Mode session (instrumentModePaymentMethod set to nil) or from Instrument Mode with other instrument, if a SDK view controller is requiested in instrument mode + + makeRequest(router: .customizePayment(instrument: nil, paymentMethod: instrument.paymentMethod, restrictToPaymentMethods: nil), operation: customizePayment) + } else if let sdkControllerMode = self.sdkControllerMode, + case .instrumentMode(let instrument) = sdkControllerMode, + paymentOutputModel.paymentSession.instrumentModePaymentMethod == instrument.paymentMethod { + // Session is in Instrument Mode, and the set SDK view controller mode is matching the instrument, time to create a web based view and send to the merchant app + + self.sdkControllerMode = nil + + DispatchQueue.main.async { + self.createSwedbankPaySDKController() + } + } else if let sdkControllerMode = self.sdkControllerMode, + case .menu(let restrictedToInstruments) = sdkControllerMode, + paymentOutputModel.paymentSession.instrumentModePaymentMethod != nil + || paymentOutputModel.paymentSession.restrictedToInstruments?.sorted() != restrictedToInstruments?.compactMap({$0.paymentMethod}).sorted(), + let customizePayment = paymentOutputModel.operations?.firstOperation(withRel: .customizePayment) { + // Switching to Menu Mode with potential list of restricted instruments from Instrument Mode or when list of restricted instruments doesn't match (different list of instruments) + + makeRequest(router: .customizePayment(instrument: nil, paymentMethod: nil, restrictToPaymentMethods: restrictedToInstruments?.compactMap({$0.paymentMethod})), operation: customizePayment) + } else if let sdkControllerMode = self.sdkControllerMode, + case .menu(let restrictedToInstruments) = sdkControllerMode, + paymentOutputModel.paymentSession.instrumentModePaymentMethod == nil + && paymentOutputModel.paymentSession.restrictedToInstruments?.sorted() == restrictedToInstruments?.compactMap({$0.paymentMethod}).sorted() { + // Session is in Menu Mode, and the list of restricted instruments match the set SDK view controller mode, time to create a web based view and send to the merchant app + + self.sdkControllerMode = nil + + DispatchQueue.main.async { + self.createSwedbankPaySDKController() + } + } else if operations.containsOperation(withRel: .startPaymentAttempt), + let instrument = instrument, + let startPaymentAttempt = paymentOutputModel.paymentSession.methods? + .firstMethod(withName: instrument.paymentMethod)?.operations? + .firstOperation(withRel: .startPaymentAttempt) { + // We have a startPaymentAttempt and it's matching the set instrument, time to make a payment attempt + + makeRequest(router: .startPaymentAttempt(instrument: instrument, culture: culture), operation: startPaymentAttempt) + self.instrument = nil + } else if let launchClientApp = operations.first(where: { $0.firstTask(withRel: .launchClientApp) != nil }), + let tasks = launchClientApp.firstTask(withRel: .launchClientApp), + let failPayment = paymentOutputModel.paymentSession.methods?.firstMethod(withName: AvailableInstrument.swish(prefills: nil).paymentMethod)?.operations?.firstOperation(withRel: .failPaymentAttempt), + !hasLaunchClientAppURLs.contains(where: { $0.absoluteString.contains(tasks.href ?? "") }) { + // We have an active launchClientApp task, and the contained URL isn't in the list of already launched Client App URLs, launch the external app on the device + + self.launchClientApp(task: tasks, failPaymentAttemptOperation: failPayment) + } else if let scaMethodRequest = operations.first(where: { $0.firstTask(withRel: .scaMethodRequest) != nil }), + let task = scaMethodRequest.firstTask(withRel: .scaMethodRequest), + let href = task.href, + !href.isEmpty, + !scaMethodRequestDataPerformed.contains(where: { $0.name == task.expects?.value(for: "threeDSMethodData") ?? "null" }) { + // We have an active scaMethodRequest task, with a non-empty and non-nil href, and we haven't loaded the Method Request URL before (as identified by threeDSMethodData value as key), load the SCA Method Request in the "invisble web view" + + DispatchQueue.main.async { + self.webViewService.load(task: task) { result in + switch result { + case .success: + self.scaMethodRequestDataPerformed.append((name: task.expects?.value(for: "threeDSMethodData") ?? "null", value: "Y")) + case .failure: + self.scaMethodRequestDataPerformed.append((name: task.expects?.value(for: "threeDSMethodData") ?? "null", value: "N")) + } + + self.sessionOperationHandling(paymentOutputModel: paymentOutputModel, culture: culture) + } + } + } else if let createAuthentication = operations.firstOperation(withRel: .createAuthentication), + let notificationUrl = createAuthentication.expects?.value(for: "NotificationUrl") { + // We have a createAuthentication operation and should move forward with sending one of the Method Completion Indicators + + self.notificationUrl = notificationUrl + + if let task = createAuthentication.firstTask(withRel: .scaMethodRequest), + let scaMethod = scaMethodRequestDataPerformed.first(where: { $0.name == task.expects?.value(for: "threeDSMethodData") ?? "null" }) { + // We have loaded the Method Request URL in the "invisible web view" before (as identified by threeDSMethodData value as key), so we can use the result and run the createAuthentication operation + + makeRequest(router: .createAuthentication(methodCompletionIndicator: scaMethod.value, notificationUrl: notificationUrl), operation: createAuthentication) + } else if let methodCompletionIndicator = createAuthentication.expects?.value(for: "methodCompletionIndicator") { + // The Session API has already provided us with a pre-defined Method Completion Indicator, so we take that and run the createAuthentication operation + + makeRequest(router: .createAuthentication(methodCompletionIndicator: methodCompletionIndicator, notificationUrl: notificationUrl), operation: createAuthentication) + } else { + // We didn't have a result from a loaded Method Request URL, and we didn't get a pre-defined Method Completion Indicator, so we will have to send in the Unkonwn (U) indicator + + makeRequest(router: .createAuthentication(methodCompletionIndicator: "U", notificationUrl: notificationUrl), operation: createAuthentication) + } + } else if let operation = operations.first(where: { $0.firstTask(withRel: .scaRedirect) != nil }), + let task = operation.firstTask(withRel: .scaRedirect), + !scaRedirectDataPerformed.contains(where: { $0.name == task.expects?.value(for: "creq") }) { + // We have an active scaRedirect task, and the 3D secure page hasn't been shown to the user yet (as identified by creq as key), tell the merchant app to show a 3D Secure View Controller + + DispatchQueue.main.async { + self.webViewController.notificationUrl = self.notificationUrl + + self.delegate?.show3DSecureViewController(viewController: self.webViewController) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "show3DSecureViewController", + succeeded: self.delegate != nil, + values: nil)) + + self.scaRedirectDataPerformed(task: task, culture: culture) + } + } else if let completeAuthentication = operations.firstOperation(withRel: .completeAuthentication), + let task = completeAuthentication.tasks?.first(where: { $0.expects?.contains(where: { $0.name == "creq" } ) ?? false } ), + let scaRedirect = scaRedirectDataPerformed.first(where: { $0.name == task.expects?.value(for: "creq") }) { + // We have an active scaRedirect task, and the 3D secure page has been shown to the user (as identified by creq as key), run the completeAuthentication operation with the result + + makeRequest(router: .completeAuthentication(cRes: scaRedirect.value), operation: completeAuthentication) + } else if let redirectPayer = operations.firstOperation(withRel: .redirectPayer) { + // We have a redirectPayer operation, this means the payment session has ended and we can look at the URL to determine the result + + DispatchQueue.main.async { + if redirectPayer.href == self.orderInfo?.cancelUrl?.absoluteString { + // URL matches the cancelUrl, the session has been cancelled + + self.delegate?.paymentSessionCanceled() + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentSessionCanceled", + succeeded: self.delegate != nil, + values: nil)) + } else if redirectPayer.href == self.orderInfo?.completeUrl.absoluteString { + // URL matches the completeUrl, the session has been completed + + self.delegate?.paymentSessionComplete() + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentSessionComplete", + succeeded: self.delegate != nil, + values: nil)) + } else { + // Redirect to an unknown URL, no way to recover from here + + self.delegate?.sdkProblemOccurred(problem: .paymentSessionEndStateReached) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": SwedbankPaySDK.PaymentSessionProblem.paymentSessionEndStateReached.rawValue])) + } + } + sessionIsOngoing = false + hasLaunchClientAppURLs = [] + hasShownProblemDetails = [] + scaMethodRequestDataPerformed = [] + scaRedirectDataPerformed = [] + notificationUrl = nil + hasShownAvailableInstruments = false + } else if let instrument = self.instrument, + let operation = paymentOutputModel.paymentSession.methods? + .firstMethod(withName: instrument.paymentMethod)?.operations? + .first(where: { $0.rel == .expandMethod || $0.rel == .startPaymentAttempt || $0.rel == .getPayment }) { + // We have a method matching the set instrument, and it has one of the three supported method operations (expandMethod, startPaymentAttempt or getPayment) + + sessionStartTimestamp = Date() + + switch operation.rel { + case .expandMethod: + // The current instrument has an expandMethod operation, run that to move to the next step of the process (startPaymentAttempt) + + makeRequest(router: .expandMethod(instrument: instrument), operation: operation) + case .startPaymentAttempt: + // The current instrument has a startPaymentAttempt operation, run that to move to the next step of the process (getPayment, redirectPayer or problem) + + makeRequest(router: .startPaymentAttempt(instrument: instrument, culture: culture), operation: operation) + self.instrument = nil + case .getPayment: + // The current instrument has a getPayment operation, run that so we're polling the session until we can move to the next step of the process (redirectPayer or problem) + + makeRequest(router: .getPayment, operation: operation) + default: + // We already checked the operation in the if statement above, so this code should not be reachable + + fatalError("Operantion rel is not supported for makeNativePaymentAttempt: \(String(describing: operation.rel))") + } + } else if let getPayment = operations.firstOperation(withRel: .getPayment) { + // We're told to simply fetch the session again, wait until polling and fetch the session, running the session operation handling once again + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + self.sessionStartTimestamp = Date() + self.makeRequest(router: .getPayment, operation: getPayment) + } + } else if hasShownAvailableInstruments == false { + // No process has been initiated above, and we haven't sent the available instrumnts to the merchant app for this session yet, so send the list and wait for action + + DispatchQueue.main.async { + let availableInstruments: [AvailableInstrument] = paymentOutputModel.paymentSession.methods?.compactMap({ model in + switch model { + case .swish(let prefills, _): + return AvailableInstrument.swish(prefills: prefills) + case .creditCard(let prefills, _, _): + return AvailableInstrument.creditCard(prefills: prefills) + case .applePay: + return AvailableInstrument.applePay + case .webBased(let paymentMethod): + return AvailableInstrument.webBased(paymentMethod: paymentMethod) + } + }) ?? [] + + self.hasShownAvailableInstruments = true + + self.delegate?.paymentSessionFetched(availableInstruments: availableInstruments) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentSessionFetched", + succeeded: self.delegate != nil, + values: ["instruments": availableInstruments.compactMap({ $0.paymentMethod }).joined(separator: ";")])) + } + } else if !hasShowedError { + // No process has been initiated at all. The session is in a state that this session operation handling logic can't resolve. + + DispatchQueue.main.async { + self.delegate?.sdkProblemOccurred(problem: .paymentSessionEndStateReached) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": SwedbankPaySDK.PaymentSessionProblem.paymentSessionEndStateReached.rawValue])) + } + } + } + + internal func handleCallbackUrl(_ url: URL) -> Bool { + guard url.appendingPathComponent("") == orderInfo?.paymentUrl?.appendingPathComponent(""), + paymentViewSessionIsOngoing == false else { + return false + } + + if let ongoingModel = ongoingModel { + if let operation = ongoingModel.paymentSession.allMethodOperations.firstOperation(withRel: .getPayment) { + sessionStartTimestamp = Date() + makeRequest(router: .getPayment, operation: operation) + } + } + + BeaconService.shared.log(type: .clientAppCallback(values: ["callbackUrl": url.absoluteString])) + + return true + } + + private func scaRedirectDataPerformed(task: IntegrationTask, culture: String?) { + self.webViewController.load(task: task) { result in + switch result { + case .success(let value): + if !self.scaRedirectDataPerformed.contains(where: { $0.value == value }) { + self.scaRedirectDataPerformed.append((name: task.expects!.value(for: "creq")!, value: value)) + + self.delegate?.dismiss3DSecureViewController() + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "dismiss3DSecureViewController", + succeeded: self.delegate != nil, + values: nil)) + + if let model = self.ongoingModel { + self.sessionOperationHandling(paymentOutputModel: model, culture: culture) + } + } + case .failure(let error): + let problem = SwedbankPaySDK.PaymentSessionProblem.paymentSession3DSecureViewControllerLoadFailed(error: error, retry: { + self.scaRedirectDataPerformed(task: task, culture: culture) + }) + + self.delegate?.sdkProblemOccurred(problem: problem) + + let error = error as NSError + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": problem.rawValue, + "errorDescription": error.localizedDescription, + "errorCode": String(error.code), + "errorDomain": error.domain])) + } + } + } + } +} + +extension SwedbankPaySDK.SwedbankPayPaymentSession: SwedbankPaySDKInternalDelegate { + public func updatePaymentOrderFailed(updateInfo: Any, error: any Error) { + let problem = SwedbankPaySDK.PaymentSessionProblem.paymentSessionAPIRequestFailed(error: error, retry: nil) + + self.delegate?.sdkProblemOccurred(problem: problem) + + let error = error as NSError + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": problem.rawValue, + "errorDescription": error.localizedDescription, + "errorCode": String(error.code), + "errorDomain": error.domain])) + } + + public func paymentComplete() { + self.delegate?.paymentSessionComplete() + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentSessionComplete", + succeeded: self.delegate != nil, + values: nil)) + } + + public func paymentCanceled() { + self.delegate?.paymentSessionCanceled() + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentSessionCanceled", + succeeded: self.delegate != nil, + values: nil)) + } + + public func paymentFailed(error: any Error) { + let problem = SwedbankPaySDK.PaymentSessionProblem.paymentControllerPaymentFailed(error: error, retry: nil) + + self.delegate?.sdkProblemOccurred(problem: problem) + + let error = error as NSError + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": problem.rawValue, + "errorDescription": error.localizedDescription, + "errorCode": String(error.code), + "errorDomain": error.domain])) + } +} diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/ConfigurationAsync.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/ConfigurationAsync.swift index 551a38a..480114d 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/ConfigurationAsync.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/ConfigurationAsync.swift @@ -196,7 +196,11 @@ public extension SwedbankPaySDKConfigurationAsync { @available(*, deprecated, message: "no longer maintained") func urlMatchesListOfGoodRedirects(_ url: URL) async -> Bool { return await withUnsafeContinuation { continuation in - urlMatchesListOfGoodRedirects(url, completion: continuation.resume(returning:)) + urlMatchesListOfGoodRedirects(url, completion: { _ in + Task { @MainActor in + continuation.resume(returning:) + } + }) } } @@ -204,7 +208,11 @@ public extension SwedbankPaySDKConfigurationAsync { navigationAction: WKNavigationAction ) async -> SwedbankPaySDK.PaymentMenuRedirectPolicy { return await withUnsafeContinuation { continuation in - decidePolicyForPaymentMenuRedirect(navigationAction: navigationAction, completion: continuation.resume(returning:)) + decidePolicyForPaymentMenuRedirect(navigationAction: navigationAction, completion: { paymentMenuRedirectPolicy in + Task { @MainActor in + continuation.resume(returning: paymentMenuRedirectPolicy) + } + }) } } } diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift new file mode 100644 index 0000000..108d90c --- /dev/null +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift @@ -0,0 +1,39 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +public extension SwedbankPaySDK { + /// Payment session problem returned with `sdkProblemOccurred` + enum PaymentSessionProblem { + case paymentSessionEndStateReached + case paymentSessionAPIRequestFailed(error: Error, retry: (()->Void)?) + case paymentControllerPaymentFailed(error: Error, retry: (()->Void)?) + case paymentSession3DSecureViewControllerLoadFailed(error: Error, retry: (()->Void)?) + case internalInconsistencyError + case automaticConfigurationFailed + + var rawValue: String { + switch self { + case .paymentSessionEndStateReached: "paymentSessionEndStateReached" + case .paymentSessionAPIRequestFailed: "paymentSessionAPIRequestFailed" + case .paymentControllerPaymentFailed: "paymentControllerPaymentFailed" + case .paymentSession3DSecureViewControllerLoadFailed: "paymentSession3DSecureViewControllerLoadFailed" + case .internalInconsistencyError: "internalInconsistencyError" + case .automaticConfigurationFailed: "automaticConfigurationFailed" + } + } + } +} diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift b/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift index 325728a..8bcd2ed 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift @@ -70,6 +70,14 @@ public protocol SwedbankPaySDKDelegate: AnyObject { /// func javaScriptEvent(name: String, arguments: [String: Any]) } + +internal protocol SwedbankPaySDKInternalDelegate: AnyObject { + func updatePaymentOrderFailed(updateInfo: Any, error: Error) + func paymentComplete() + func paymentCanceled() + func paymentFailed(error: Error) +} + public extension SwedbankPaySDKDelegate { func shippingAddressIsKnown() {} func instrumentSelected() {} @@ -168,7 +176,9 @@ open class SwedbankPaySDKController: UIViewController, UIViewControllerRestorati notifyDelegateIfNeeded() } } - + + internal var internalDelegate: SwedbankPaySDKInternalDelegate? + /// Styling for the payment menu /// /// Styling the payment menu requires a separate agreement with Swedbank Pay. @@ -524,12 +534,16 @@ open class SwedbankPaySDKController: UIViewController, UIViewControllerRestorati switch viewModel.state { case .complete: delegate?.paymentComplete() + internalDelegate?.paymentComplete() case .canceled: delegate?.paymentCanceled() + internalDelegate?.paymentCanceled() case .failed(_, let error): delegate?.paymentFailed(error: error) + internalDelegate?.paymentFailed(error: error) case .paying(_, options: _, failedUpdate: let failedUpdate?): delegate?.updatePaymentOrderFailed(updateInfo: failedUpdate.updateInfo, error: failedUpdate.error) + internalDelegate?.updatePaymentOrderFailed(updateInfo: failedUpdate.updateInfo, error: failedUpdate.error) default: break } @@ -751,7 +765,7 @@ extension SwedbankPaySDKController : CallbackUrlDelegate { return false } // See SwedbankPaySDKConfiguration for discussion on why we need to do this. - return url == paymentUrl + return url.appendingPathComponent("") == paymentUrl.appendingPathComponent("") || viewModel?.configuration.url(url, matchesPaymentUrl: paymentUrl) == true } } diff --git a/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift new file mode 100644 index 0000000..fea8dc1 --- /dev/null +++ b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift @@ -0,0 +1,91 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation +@preconcurrency import WebKit + +class SCAWebViewService: NSObject, WKNavigationDelegate { + private var handler: ((Result) -> Void)? + + private var webView: WKWebView? + + func load(task: IntegrationTask, handler: @escaping (Result) -> Void) { + guard let taskHref = task.href, + let url = URL(string: taskHref) else { + handler(.failure(SwedbankPayAPIError.invalidUrl)) + + return + } + + webView?.stopLoading() + webView = nil + + let preferences = WKPreferences() + preferences.javaScriptEnabled = true + let configuration = WKWebViewConfiguration() + configuration.preferences = preferences + webView = WKWebView(frame: .zero, configuration: configuration) + + self.handler = handler + + var request = URLRequest(url: url) + request.httpMethod = task.method + request.allHTTPHeaderFields = ["Content-Type": task.contentType ?? ""] + request.timeoutInterval = SwedbankPayAPIConstants.creditCardTimoutInterval + + if let httpBody = task.expects?.httpBody { + request.httpBody = httpBody + } + + webView?.navigationDelegate = self + webView?.load(request) + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + handler?(.success(())) + handler = nil + + self.webView?.stopLoading() + self.webView = nil + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + handler?(.failure(error)) + handler = nil + + self.webView?.stopLoading() + self.webView = nil + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + handler?(.failure(error)) + handler = nil + + self.webView?.stopLoading() + self.webView = nil + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { + if let response = navigationResponse.response as? HTTPURLResponse { + if 400...599 ~= response.statusCode { + decisionHandler(.cancel) + + return + } + } + + decisionHandler(.allow) + } +} diff --git a/SwedbankPaySDK/Classes/WebView/SwedbankPayExtraWebViewController.swift b/SwedbankPaySDK/Classes/WebView/SwedbankPayExtraWebViewController.swift index f523530..b141377 100644 --- a/SwedbankPaySDK/Classes/WebView/SwedbankPayExtraWebViewController.swift +++ b/SwedbankPaySDK/Classes/WebView/SwedbankPayExtraWebViewController.swift @@ -14,7 +14,7 @@ // limitations under the License. import Foundation -import WebKit +@preconcurrency import WebKit final class SwedbankPayExtraWebViewController: SwedbankPayWebViewControllerBase { override init( diff --git a/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift new file mode 100644 index 0000000..6d8ba2e --- /dev/null +++ b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift @@ -0,0 +1,269 @@ +// +// Copyright 2024 Swedbank AB +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import UIKit +@preconcurrency import WebKit + +class SwedbankPaySCAWebViewController: UIViewController { + internal var lastRootPage: (navigation: WKNavigation?, baseURL: URL?)? + + /// keep track on all webView redirects and the reason for redirection. Set to activate logging (active by default in DEBUG). + var redirectLog: [(url: URL, note: String, date: Date)]? + + /// print urls to log and send them to the navigationLogger + func navigationLog(_ url: URL?, _ note: String) { + guard let url else { return } +#if DEBUG + debugPrint("navigation: \(note) url: \(url.absoluteString)") + if redirectLog == nil { + redirectLog = .init() + } +#endif + redirectLog?.append((url, note, Date())) + } + + var isAtRoot: Bool { + return lastRootPage != nil + } + + var attemptOpenUniversalLink: (URL, @escaping (Bool) -> Void) -> Void = { url, completionHandler in + UIApplication.shared.open(url, options: [.universalLinksOnly: true], completionHandler: completionHandler) + } + + private var handler: ((Result) -> Void)? + + let webView: WKWebView + + let activityView = UIActivityIndicatorView() + + var notificationUrl: String? + + init() { + let preferences = WKPreferences() + preferences.javaScriptEnabled = true + let config = WKWebViewConfiguration() + config.preferences = preferences + webView = WKWebView(frame: .zero, configuration: config) + + super.init(nibName: nil, bundle: nil) + webView.navigationDelegate = self + webView.addSubview(activityView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + self.view = webView + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + activityView.center = webView.center + } + + func load(task: IntegrationTask, handler: @escaping (Result) -> Void) { + guard let taskHref = task.href, + let url = URL(string: taskHref) else { + return + } + + self.activityView.startAnimating() + self.activityView.isHidden = true + + self.handler = handler + + var request = URLRequest(url: url) + request.httpMethod = task.method + request.allHTTPHeaderFields = ["Content-Type": task.contentType ?? ""] + request.timeoutInterval = SwedbankPayAPIConstants.creditCardTimoutInterval + + if let httpBody = task.expects?.httpBody { + request.httpBody = httpBody + } + + let navigation = webView.load(request) + + lastRootPage = (navigation, url) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + if self.activityView.isAnimating { + self.activityView.isHidden = false + } + } + } +} + +extension SwedbankPaySCAWebViewController: WKNavigationDelegate { + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + guard navigation == lastRootPage?.navigation else { + return + } + + activityView.isHidden = true + activityView.stopAnimating() + } + + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + guard navigation == lastRootPage?.navigation else { + return + } + + activityView.isHidden = true + activityView.stopAnimating() + + handler?(.failure(error)) + handler = nil + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + guard navigation == lastRootPage?.navigation else { + return + } + + activityView.isHidden = true + activityView.stopAnimating() + + handler?(.failure(error)) + handler = nil + } + + func webView( + _ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void + ) { + let request = navigationAction.request + + if request.url?.absoluteString == self.notificationUrl, + let httpBody = request.httpBody, + let bodyString = String(data: httpBody, encoding: .utf8), + let urlComponents = URLComponents(string: "https://www.apple.com?\(bodyString)"), + let cRes = urlComponents.queryItems?.first(where: { $0.name == "cres" })?.value { + navigationLog(request.url, "Link CRes") + self.handler?(.success(cRes)) + self.handler = nil + decisionHandler(.allow) + } else if isBaseUrlNavigation(navigationAction: navigationAction) { + navigationLog(request.url, "Link isBaseUrlNavigation") + decisionHandler(.allow) + } else if let url = request.url { + // If targetFrame is nil, this is a new window navigation; + // handle like a main frame navigation. + if navigationAction.targetFrame?.isMainFrame != false { + decidePolicyFor(navigationAction: navigationAction, url: url, decisionHandler: decisionHandler) + } else { + let canOpen = WKWebView.canOpen(url: url) + navigationLog(url, "New window navigation, \(canOpen ? "allowed" : "cancelled")") + decisionHandler(canOpen ? .allow : .cancel) + if canOpen == false { + + // if link has been cancelled due to not beeing able to open, we need to open it as an external app. + self.navigationLog(url, "External link opened in browser or app") + UIApplication.shared.open(url, options: [.universalLinksOnly: false], completionHandler: nil) + } + } + } else { + decisionHandler(.cancel) + } + } + + internal func decidePolicyFor( + navigationAction: WKNavigationAction, + url: URL, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void + ) { + attemptOpenUniversalLink(url) { opened in + if opened { + self.navigationLog(url, "Universal link opened in browser") + decisionHandler(.cancel) + } else { + self.decidePolicyForNormalLink( + navigationAction: navigationAction, + url: url, + decisionHandler: decisionHandler + ) + } + } + } + + internal func decidePolicyForNormalLink( + navigationAction: WKNavigationAction, + url: URL, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void + ) { + if WKWebView.canOpen(url: url) { + if navigationAction.targetFrame == nil { + navigationLog(url, "Link with no targetFrame, opened in webview") + decisionHandler(.allow) // always allow new frame navigations + } else { + self.navigationLog(url, "Link opened in webview") + decisionHandler(.allow) + } + } else { + // A custom-scheme url. Must let another app take care of it. + navigationLog(url, "Custom-scheme url opened in another app") + UIApplication.shared.open(url, options: [:]) + decisionHandler(.cancel) + } + } + + internal func continueNavigationInBrowser(url: URL) { + // Naively, one would think that opening the original navigation + // target here would work. However, testing has shown that not + // to be the case. Without expending time to work out the exact + // problem, it can be assumed that the Swedbank Pay page that + // redirects to the payment instrument issuer page sets up + // the browser environment in some way that some issuer pages + // depend on. Therefore the approach is that when we encounter + // a navigation to a page outside the goodlist, we reopen the + // _current_ page in the browser. This works for the Swedbank Pay + // "PrepareAcsChallenge" page, and it can be assumed that it will + // continue to work for that page. Whether it works if any previously + // tested flow is changed to navigate to previously unknown pages + // is anyone's guess, but even in those cases it is the best we can + // do, since attempting to restart the whole flow by opening the + // "originating" Swedbank Pay page will, in general not work + // (this has been tested). In any case, it is important to + // keep testing the SDK against different issuers and keep + // the goodlist up-to-date. + let target = isAtRoot ? url : (webView.url ?? url) + UIApplication.shared.open(target, options: [:]) + } + + internal func ensurePath(url: URL) -> URL { + return url.path.isEmpty ? URL(string: "/", relativeTo: url)!.absoluteURL : url.absoluteURL + } + + internal func isBaseUrlNavigation(navigationAction: WKNavigationAction) -> Bool { + if let lastRootPage = lastRootPage, navigationAction.targetFrame?.isMainFrame == true { + let url = navigationAction.request.url + if let baseUrl = lastRootPage.baseURL { + // WKWebView silently turns https://foo.bar to https://foo.bar/ + // So append a path to baseURL if needed + let baseUrlWithPath = ensurePath(url: baseUrl) + return navigationAction.request.url?.absoluteURL == baseUrlWithPath + } else { + // A nil baseURL results in WKWebView using about:blank as the page url instead + return url?.absoluteString == "about:blank" + } + } else { + return false + } + } +} diff --git a/SwedbankPaySDK/Classes/WebView/SwedbankPayWebViewController.swift b/SwedbankPaySDK/Classes/WebView/SwedbankPayWebViewController.swift index 7e987b6..c4f7ed3 100644 --- a/SwedbankPaySDK/Classes/WebView/SwedbankPayWebViewController.swift +++ b/SwedbankPaySDK/Classes/WebView/SwedbankPayWebViewController.swift @@ -14,7 +14,7 @@ // limitations under the License. import UIKit -import WebKit +@preconcurrency import WebKit class SwedbankPayWebViewController: SwedbankPayWebViewControllerBase { internal static let maybeStuckNoteMinimumIntervalFromDidBecomeActive = 3.0 diff --git a/SwedbankPaySDK/Classes/WebView/SwedbankPayWebViewControllerBase.swift b/SwedbankPaySDK/Classes/WebView/SwedbankPayWebViewControllerBase.swift index 0fe02c1..73afb92 100644 --- a/SwedbankPaySDK/Classes/WebView/SwedbankPayWebViewControllerBase.swift +++ b/SwedbankPaySDK/Classes/WebView/SwedbankPayWebViewControllerBase.swift @@ -14,7 +14,7 @@ // limitations under the License. import UIKit -import WebKit +@preconcurrency import WebKit class SwedbankPayWebViewControllerBase: UIViewController { weak var delegate: SwedbankPayWebViewControllerDelegate? diff --git a/SwedbankPaySDK/Resources/en.lproj/SwedbankPaySDKLocalizable.strings b/SwedbankPaySDK/Resources/en.lproj/SwedbankPaySDKLocalizable.strings index 88cd6ff..8003335 100644 --- a/SwedbankPaySDK/Resources/en.lproj/SwedbankPaySDKLocalizable.strings +++ b/SwedbankPaySDK/Resources/en.lproj/SwedbankPaySDKLocalizable.strings @@ -8,3 +8,6 @@ "maybeStuckAlertBody" = "It looks like the payment has not progressed for a while. Do you want to wait, or retry the payment in compatibility mode?"; "maybeStuckAlertWait" = "Wait"; "maybeStuckAlertRetry" = "Retry"; + +"swedbankpaysdk_native_invalid_url" = "Invalid URL was provided."; +"swedbankpaysdk_native_unknown" = "Something went wrong."; diff --git a/SwedbankPaySDK/Resources/nb.lproj/SwedbankPaySDKLocalizable.strings b/SwedbankPaySDK/Resources/nb.lproj/SwedbankPaySDKLocalizable.strings index f3b4ee1..4c66f0b 100644 --- a/SwedbankPaySDK/Resources/nb.lproj/SwedbankPaySDKLocalizable.strings +++ b/SwedbankPaySDK/Resources/nb.lproj/SwedbankPaySDKLocalizable.strings @@ -28,3 +28,6 @@ "swedbankpaysdk_problem_system_error" = "Intern feil på tjenesten. Vennligst prøv igjen."; "swedbankpaysdk_problem_configuration_error" = "Oppsettet er ugyldig."; "swedbankpaysdk_problem_unknown" = "Det skjedde en uventet feil"; + +"swedbankpaysdk_native_invalid_url" = "Ugyldig URL ble oppgitt."; +"swedbankpaysdk_native_unknown" = "Det skjedde en uventet feil."; diff --git a/SwedbankPaySDK/Resources/sv.lproj/SwedbankPaySDKLocalizable.strings b/SwedbankPaySDK/Resources/sv.lproj/SwedbankPaySDKLocalizable.strings index 48a3a55..f5771f6 100644 --- a/SwedbankPaySDK/Resources/sv.lproj/SwedbankPaySDKLocalizable.strings +++ b/SwedbankPaySDK/Resources/sv.lproj/SwedbankPaySDKLocalizable.strings @@ -28,3 +28,6 @@ "swedbankpaysdk_problem_system_error" = "Internt fel på tjänsten. Försök igen."; "swedbankpaysdk_problem_configuration_error" = "Felaktig konfiguration."; "swedbankpaysdk_problem_unknown" = "Oväntat fel."; + +"swedbankpaysdk_native_invalid_url" = "Ogiltig URL har angetts."; +"swedbankpaysdk_native_unknown" = "Oväntat fel."; diff --git a/SwedbankPaySDKMerchantBackend.podspec b/SwedbankPaySDKMerchantBackend.podspec index 2b1a6df..2c1ffe7 100644 --- a/SwedbankPaySDKMerchantBackend.podspec +++ b/SwedbankPaySDKMerchantBackend.podspec @@ -13,7 +13,7 @@ a backend server that implements the Merchant Backend API. s.author = 'Swedbank Pay' s.source = { :git => 'https://github.com/SwedbankPay/swedbank-pay-sdk-ios.git', :tag => s.version.to_s } - s.ios.deployment_target = '10.0' + s.ios.deployment_target = '11.0' s.swift_versions = '5.0', '5.1' s.dependency 'SwedbankPaySDK', s.version.to_s diff --git a/SwedbankPaySDKMerchantBackend/Classes/MerchantBackendApi.swift b/SwedbankPaySDKMerchantBackend/Classes/MerchantBackendApi.swift index 611c43e..fc5443f 100644 --- a/SwedbankPaySDKMerchantBackend/Classes/MerchantBackendApi.swift +++ b/SwedbankPaySDKMerchantBackend/Classes/MerchantBackendApi.swift @@ -66,7 +66,7 @@ struct MerchantBackendApi { let interceptor = requestDecorator.map { RequestDecoratorInterceptor.init( requestDecorator: $0, - decoratorCall: decoratorCall + decoratorCallHolder: .init(decoratorCall: decoratorCall) ) } return session.request( @@ -95,10 +95,14 @@ struct MerchantBackendApi { return false } + struct DecoratorCallHolder { + let decoratorCall: DecoratorCall + } + internal struct RequestDecoratorInterceptor: RequestInterceptor { let requestDecorator: SwedbankPaySDKRequestDecorator - let decoratorCall: DecoratorCall - + let decoratorCallHolder: DecoratorCallHolder + func adapt( _ urlRequest: URLRequest, for session: Session, @@ -107,7 +111,7 @@ struct MerchantBackendApi { var request = urlRequest DispatchQueue.main.async { self.requestDecorator.decorateAny(request: &request) - self.decoratorCall(self.requestDecorator, &request) + self.decoratorCallHolder.decoratorCall(self.requestDecorator, &request) completion(.success(request)) } } diff --git a/SwedbankPaySDKMerchantBackend/Classes/RequestDecorator.swift b/SwedbankPaySDKMerchantBackend/Classes/RequestDecorator.swift index 0c95b73..b556ea8 100644 --- a/SwedbankPaySDKMerchantBackend/Classes/RequestDecorator.swift +++ b/SwedbankPaySDKMerchantBackend/Classes/RequestDecorator.swift @@ -31,7 +31,7 @@ public extension SwedbankPaySDK { } } -public protocol SwedbankPaySDKRequestDecorator { +public protocol SwedbankPaySDKRequestDecorator: Sendable { func decorateAny(request: inout URLRequest) func decorateGetTopLevelResources(request: inout URLRequest)