From 62d16a41f92d1f0eae4bc560a5d1a558b80cfe4e Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Mon, 13 May 2024 15:05:36 +0200 Subject: [PATCH 01/75] SP-8 REST API klient --- SwedbankPaySDK.xcodeproj/project.pbxproj | 42 +++++++++- .../Api/Models/OperationOutputModel.swift | 19 +++++ .../Classes/Api/SwedbankPayAPIConstants.swift | 30 +++++++ .../Api/SwedbankPayAPIEnpointRouter.swift | 80 +++++++++++++++++++ .../Classes/Api/SwedbankPayAPIError.swift | 32 ++++++++ .../SwedbankPaySDKLocalizable.strings | 3 + .../SwedbankPaySDKLocalizable.strings | 3 + .../SwedbankPaySDKLocalizable.strings | 3 + 8 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift create mode 100644 SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift create mode 100644 SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift create mode 100644 SwedbankPaySDK/Classes/Api/SwedbankPayAPIError.swift diff --git a/SwedbankPaySDK.xcodeproj/project.pbxproj b/SwedbankPaySDK.xcodeproj/project.pbxproj index 18a6969..78f0408 100644 --- a/SwedbankPaySDK.xcodeproj/project.pbxproj +++ b/SwedbankPaySDK.xcodeproj/project.pbxproj @@ -3,10 +3,18 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ + 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 */; }; 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 +255,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 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 = ""; }; 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 +435,25 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 45C1008A2BF247F300AA3523 /* Api */ = { + isa = PBXGroup; + children = ( + 45C1008C2BF247F300AA3523 /* Models */, + 45C1008B2BF247F300AA3523 /* SwedbankPayAPIError.swift */, + 45C1008E2BF247F300AA3523 /* SwedbankPayAPIEnpointRouter.swift */, + 45C1008F2BF247F300AA3523 /* SwedbankPayAPIConstants.swift */, + ); + path = Api; + sourceTree = ""; + }; + 45C1008C2BF247F300AA3523 /* Models */ = { + isa = PBXGroup; + children = ( + 45C1008D2BF247F300AA3523 /* OperationOutputModel.swift */, + ); + path = Models; + sourceTree = ""; + }; A502C37525430D90004FBFCB /* Models */ = { isa = PBXGroup; children = ( @@ -702,6 +733,7 @@ C585AF27237066EE006C2E16 /* SwedbankPaySDK.swift */, C585AF2A237066EE006C2E16 /* SwedbankPaySDKController.swift */, C585AF28237066EE006C2E16 /* SwedbankPaySDKViewModel.swift */, + 45C1008A2BF247F300AA3523 /* Api */, A5808B03261C93FD00FF7EC1 /* WebView */, A52E0ACA24BCAA6D00770286 /* GoodWebViewRedirects.swift */, ); @@ -1031,6 +1063,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 45C100962BF247F300AA3523 /* SwedbankPayAPIConstants.swift in Sources */, A5808B11261CA8B500FF7EC1 /* WKWebViewCanOpen.swift in Sources */, C50CF6A5237AAF47003F79DF /* SwedbankPaySubProblem.swift in Sources */, A52E0AC124BC9E0100770286 /* WebViewRedirects.swift in Sources */, @@ -1040,7 +1073,9 @@ 63EECFD0270730C800C37B69 /* CodableUserData.swift in Sources */, A59AEE9D25372C9A00255A3A /* Instrument.swift in Sources */, C585AF2F237066EE006C2E16 /* SwedbankPaySDKController.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 */, @@ -1054,6 +1089,7 @@ 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 */, D50CA69B27D23EE000FC6007 /* Operation.swift in Sources */, C50CF699237AAC73003F79DF /* WhitelistedDomain.swift in Sources */, @@ -1103,6 +1139,7 @@ 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 */, @@ -1112,6 +1149,7 @@ 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 */, D511ECA52975599D0059AB05 /* BackendOperation.swift in Sources */, D511ECA62975599D0059AB05 /* TopLevelResources.swift in Sources */, @@ -1133,6 +1171,7 @@ D511EC712975591B0059AB05 /* SwedbankPaySubProblem.swift in Sources */, D511EC722975591B0059AB05 /* WebViewRedirects.swift in Sources */, D511EC732975591B0059AB05 /* SwedbankPayWebContent.swift in Sources */, + 45C100912BF247F300AA3523 /* SwedbankPayAPIError.swift in Sources */, D511EC742975591B0059AB05 /* Consumer.swift in Sources */, D511EC752975591B0059AB05 /* FileLines.swift in Sources */, D511EC762975591B0059AB05 /* CodableUserData.swift in Sources */, @@ -1142,6 +1181,7 @@ D511EC7A2975591B0059AB05 /* ConfigurationAsync.swift in Sources */, D511EC7B2975591B0059AB05 /* SwedbankPaySDKResources.swift in Sources */, D511EC7C2975591B0059AB05 /* TypeAliases.swift in Sources */, + 45C100932BF247F300AA3523 /* OperationOutputModel.swift in Sources */, D511EC7D2975591B0059AB05 /* SwedbankPayExtraWebViewController.swift in Sources */, D511EC7E2975591B0059AB05 /* CallbackHandling.swift in Sources */, D511EC7F2975591B0059AB05 /* ClientProblem.swift in Sources */, diff --git a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift new file mode 100644 index 0000000..b031ef1 --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift @@ -0,0 +1,19 @@ +// +// 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 href: String? + let method: String? +} diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift new file mode 100644 index 0000000..32ab83d --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.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. + +struct SwedbankPayAPIConstants { + static var commonHeaders: [String: String] = [ + HTTPHeaderField.acceptType.rawValue: ContentType.json.rawValue, + HTTPHeaderField.contentType.rawValue: ContentType.json.rawValue + ] +} + +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..605e110 --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -0,0 +1,80 @@ +// +// 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 + +protocol EndpointRouterProtocol { + var body: [String: Any?]? { get } +} + +struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { + let model: OperationOutputModel + + var body: [String: Any?]? { + return nil + } +} + +extension SwedbankPayAPIEnpointRouter { + 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 = model.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 = model.method + 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, 200...299 ~= response.statusCode else { + handler(.failure(error ?? SwedbankPayAPIError.unknown)) + return + } + + guard let data 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/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."; From d4e8189a9609edca2cd189e1a159949d0476108d Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Mon, 13 May 2024 15:45:32 +0200 Subject: [PATCH 02/75] SP-19 Start native payment session SDK method --- SwedbankPaySDK.xcodeproj/project.pbxproj | 6 ++ .../NativePayment.swift | 55 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift diff --git a/SwedbankPaySDK.xcodeproj/project.pbxproj b/SwedbankPaySDK.xcodeproj/project.pbxproj index 78f0408..78fa462 100644 --- a/SwedbankPaySDK.xcodeproj/project.pbxproj +++ b/SwedbankPaySDK.xcodeproj/project.pbxproj @@ -15,6 +15,8 @@ 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 */; }; + 45C100992BF24E0D00AA3523 /* NativePayment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C100982BF24E0D00AA3523 /* NativePayment.swift */; }; + 45C1009A2BF24E0D00AA3523 /* NativePayment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C100982BF24E0D00AA3523 /* NativePayment.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 */; }; @@ -259,6 +261,7 @@ 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 = ""; }; + 45C100982BF24E0D00AA3523 /* NativePayment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativePayment.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 = ""; }; @@ -637,6 +640,7 @@ 63EECFCF270730C800C37B69 /* CodableUserData.swift */, D581EFDE27A92A56001B85A7 /* VersionOptions.swift */, D5AE5D9F27E0D4DA00BE5468 /* ExpandResource.swift */, + 45C100982BF24E0D00AA3523 /* NativePayment.swift */, ); path = "SwedbankPaySDK+Extensions"; sourceTree = ""; @@ -1069,6 +1073,7 @@ A52E0AC124BC9E0100770286 /* WebViewRedirects.swift in Sources */, A504895123C8A2FD00201DEC /* SwedbankPayWebContent.swift in Sources */, C50CF69D237AACC3003F79DF /* Consumer.swift in Sources */, + 45C100992BF24E0D00AA3523 /* NativePayment.swift in Sources */, A57170DA25011F8500AC28BE /* FileLines.swift in Sources */, 63EECFD0270730C800C37B69 /* CodableUserData.swift in Sources */, A59AEE9D25372C9A00255A3A /* Instrument.swift in Sources */, @@ -1158,6 +1163,7 @@ D511ECA92975599D0059AB05 /* PayerOwnedPaymentTokens.swift in Sources */, D511ECAA2975599D0059AB05 /* PaymentTokenInfo.swift in Sources */, D511ECAB2975599D0059AB05 /* EmptyJsonResponse.swift in Sources */, + 45C1009A2BF24E0D00AA3523 /* NativePayment.swift in Sources */, D511ECAC2975599D0059AB05 /* AbortPaymentOperation.swift in Sources */, D511ECAD2975599D0059AB05 /* MerchantBackendConfiguration.swift in Sources */, D511ECAE2975599D0059AB05 /* MerchantBackend.swift in Sources */, diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift new file mode 100644 index 0000000..d0c2d01 --- /dev/null +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -0,0 +1,55 @@ +// +// 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 { + class NativePayment { + /// The `SwedbankPaySDKConfiguration` used by this `NativePayment`. + /// + /// Note that `NativePayment` accesses this property only once during initialization, + /// and will use the returned value thereafter. Hence, you cannot change the configuration "in-flight" + /// by changing the value returned from here. + open var configuration: SwedbankPaySDKConfiguration + + /// A delegate to receive callbacks as the state of SwedbankPaySDKController changes. + public weak var delegate: SwedbankPaySDKDelegate? + + private var sessionIsOngoing: Bool = false + + public init(configuration: SwedbankPaySDKConfiguration) { + self.configuration = configuration + } + + public func startPaymentSession(with sessionApi: String) { + sessionIsOngoing = true + + let model = OperationOutputModel(href: sessionApi, + method: "GET") + + makeRequest(model: model) + } + + private func makeRequest(model: OperationOutputModel) { + SwedbankPayAPIEnpointRouter(model: model).makeRequest { result in + switch result { + case .success: + break + case .failure(let failure): + self.delegate?.paymentFailed(error: failure) + self.sessionIsOngoing = false + } + } + } + } +} From 1ef22d12929ee501f951d13459651bfd06e69623 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Mon, 13 May 2024 16:25:38 +0200 Subject: [PATCH 03/75] SP-9 Get Session request --- SwedbankPaySDK.xcodeproj/project.pbxproj | 30 ++++++ .../Classes/Api/Models/IntegrationTask.swift | 61 ++++++++++++ .../Classes/Api/Models/MethodBaseModel.swift | 98 +++++++++++++++++++ .../Api/Models/OperationOutputModel.swift | 73 +++++++++++++- .../Api/Models/PaymentOutputModel.swift | 36 +++++++ .../Api/Models/PaymentSessionModel.swift | 37 +++++++ .../Classes/Api/Models/ProblemDetails.swift | 24 +++++ .../Api/SwedbankPayAPIEnpointRouter.swift | 24 ++++- .../NativePayment.swift | 7 +- 9 files changed, 384 insertions(+), 6 deletions(-) create mode 100644 SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift create mode 100644 SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift create mode 100644 SwedbankPaySDK/Classes/Api/Models/PaymentOutputModel.swift create mode 100644 SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift create mode 100644 SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift diff --git a/SwedbankPaySDK.xcodeproj/project.pbxproj b/SwedbankPaySDK.xcodeproj/project.pbxproj index 78fa462..5fc7607 100644 --- a/SwedbankPaySDK.xcodeproj/project.pbxproj +++ b/SwedbankPaySDK.xcodeproj/project.pbxproj @@ -17,6 +17,16 @@ 45C100972BF247F300AA3523 /* SwedbankPayAPIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C1008F2BF247F300AA3523 /* SwedbankPayAPIConstants.swift */; }; 45C100992BF24E0D00AA3523 /* NativePayment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C100982BF24E0D00AA3523 /* NativePayment.swift */; }; 45C1009A2BF24E0D00AA3523 /* NativePayment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C100982BF24E0D00AA3523 /* NativePayment.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 */; }; 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 */; }; @@ -262,6 +272,11 @@ 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 = ""; }; 45C100982BF24E0D00AA3523 /* NativePayment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativePayment.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 = ""; }; 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 = ""; }; @@ -452,7 +467,12 @@ 45C1008C2BF247F300AA3523 /* Models */ = { isa = PBXGroup; children = ( + 45C100A12BF2584E00AA3523 /* IntegrationTask.swift */, + 45C1009C2BF2583D00AA3523 /* MethodBaseModel.swift */, 45C1008D2BF247F300AA3523 /* OperationOutputModel.swift */, + 45C100A42BF2597B00AA3523 /* PaymentOutputModel.swift */, + 45C1009B2BF2583D00AA3523 /* PaymentSessionModel.swift */, + 45C100A72BF2598D00AA3523 /* ProblemDetails.swift */, ); path = Models; sourceTree = ""; @@ -1074,6 +1094,9 @@ A504895123C8A2FD00201DEC /* SwedbankPayWebContent.swift in Sources */, C50CF69D237AACC3003F79DF /* Consumer.swift in Sources */, 45C100992BF24E0D00AA3523 /* NativePayment.swift in Sources */, + 45C1009F2BF2583D00AA3523 /* MethodBaseModel.swift in Sources */, + 45C1009D2BF2583D00AA3523 /* PaymentSessionModel.swift in Sources */, + 45C100A52BF2597B00AA3523 /* PaymentOutputModel.swift in Sources */, A57170DA25011F8500AC28BE /* FileLines.swift in Sources */, 63EECFD0270730C800C37B69 /* CodableUserData.swift in Sources */, A59AEE9D25372C9A00255A3A /* Instrument.swift in Sources */, @@ -1088,7 +1111,9 @@ A5AA1D6F2397B63D008A62CC /* CallbackHandling.swift in Sources */, C50CF6A7237AAF85003F79DF /* ClientProblem.swift in Sources */, C50CF6A9237AAFBF003F79DF /* ServerProblem.swift in Sources */, + 45C100A22BF2584E00AA3523 /* IntegrationTask.swift in Sources */, A52E0ACB24BCAA6D00770286 /* GoodWebViewRedirects.swift in Sources */, + 45C100A82BF2598D00AA3523 /* ProblemDetails.swift in Sources */, A5808B09261C980000FF7EC1 /* SwedbankPayWebViewControllerDelegate.swift in Sources */, A5DC2CAA251A2DFA0037C7DA /* ViewPaymentOrderInfo.swift in Sources */, A5DC2CA6251A2D730037C7DA /* ViewConsumerIdentificationInfo.swift in Sources */, @@ -1151,11 +1176,13 @@ D511EC9E2975599D0059AB05 /* PaymentOrdersLink.swift in Sources */, D511EC9F2975599D0059AB05 /* ConsumerSession.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 */, D511ECA72975599D0059AB05 /* ConsumersLink.swift in Sources */, @@ -1164,6 +1191,7 @@ D511ECAA2975599D0059AB05 /* PaymentTokenInfo.swift in Sources */, D511ECAB2975599D0059AB05 /* EmptyJsonResponse.swift in Sources */, 45C1009A2BF24E0D00AA3523 /* NativePayment.swift in Sources */, + 45C100A92BF2598D00AA3523 /* ProblemDetails.swift in Sources */, D511ECAC2975599D0059AB05 /* AbortPaymentOperation.swift in Sources */, D511ECAD2975599D0059AB05 /* MerchantBackendConfiguration.swift in Sources */, D511ECAE2975599D0059AB05 /* MerchantBackend.swift in Sources */, @@ -1183,6 +1211,7 @@ D511EC762975591B0059AB05 /* CodableUserData.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 */, D511EC7B2975591B0059AB05 /* SwedbankPaySDKResources.swift in Sources */, @@ -1200,6 +1229,7 @@ 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 */, diff --git a/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift b/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift new file mode 100644 index 0000000..cf90b3b --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift @@ -0,0 +1,61 @@ +// +// 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 IntegrationTask: Codable, Hashable { + let rel: IntegrationTaskRel? + let href: String? + let method: String? + let contentType: String? + let expects: [ExpectationModel]? +} + +enum IntegrationTaskRel: Codable, Equatable, Hashable { + case scaMethodRequest + case scaRedirect + case launchClientApp + + 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 + 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 .unknown(let value): value + } + } +} + +struct ExpectationModel: Codable, Hashable { + let name: String? + let value: String? +} diff --git a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift new file mode 100644 index 0000000..0f9261f --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift @@ -0,0 +1,98 @@ +// +// 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 { + public enum MethodBaseModel: Codable, Equatable, Hashable { + case swish(prefills: [[SwishMethodPrefillModel]]?, operations: [OperationOutputModel]?) + case creditCard(prefills: [[CreditCardMethodPrefillModel]]?, operations: [OperationOutputModel]?, cardBrands: [String]?) + + case unknown(String) + + private enum CodingKeys: String, CodingKey { + case instrument, prefills, operations, cardBrands + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let type = try container.decode(String.self, forKey: .instrument) + switch type { + case "Swish": + self = .swish( + prefills: try? container.decode([[SwishMethodPrefillModel]]?.self, forKey: CodingKeys.prefills), + operations: try? container.decode([OperationOutputModel]?.self, forKey: CodingKeys.operations) + ) + case "CreditCard": + self = .creditCard( + prefills: try? container.decode([[CreditCardMethodPrefillModel]].self, forKey: CodingKeys.prefills), + operations: try? container.decode([OperationOutputModel]?.self, forKey: CodingKeys.operations), + cardBrands: try? container.decode([String]?.self, forKey: CodingKeys.cardBrands) + ) + default: + self = .unknown(type) + } + } + + public 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 .unknown(let type): + try container.encode(type) + } + } + + var name: String { + switch self { + case .swish: + return "swish" + case .creditCard: + return "creditCard" + case .unknown: + return "unknown" + } + } + + var operations: [OperationOutputModel]? { + switch self { + case .swish(_, let opertations): + return opertations + case .creditCard(_, let opertations, _): + return opertations + case .unknown: + return nil + } + } + } + + public struct SwishMethodPrefillModel: Codable, Hashable { + let rank: Int32? + public let msisdn: String? + } + + public struct CreditCardMethodPrefillModel: Codable, Hashable { + let rank: Int32? + let paymentToken: String? + let cardBrand: String? + let maskedPan: String? + let expiryDate: String? + } +} diff --git a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift index b031ef1..e2f83ab 100644 --- a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift @@ -13,7 +13,78 @@ // See the License for the specific language governing permissions and // limitations under the License. -struct OperationOutputModel: Codable, Hashable { +public struct OperationOutputModel: Codable, Hashable { + let rel: OperationRel? let href: String? let method: String? + let next: Bool? + let tasks: [IntegrationTask]? +} + +extension OperationOutputModel { + func firstTask(with rel: IntegrationTaskRel) -> IntegrationTask? { + if let task = tasks?.first(where: { $0.rel == rel }) { + return task + } + + return 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 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 + 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 .unknown(let value): value + } + } + + var isUnknown: Bool { + if case .unknown = self { return true } + + return false + } } diff --git a/SwedbankPaySDK/Classes/Api/Models/PaymentOutputModel.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentOutputModel.swift new file mode 100644 index 0000000..0d003fc --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentOutputModel.swift @@ -0,0 +1,36 @@ +// +// 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 { + 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..2f8642b --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.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. + +struct PaymentSessionModel: Codable, Hashable { + let culture: String? + let methods: [SwedbankPaySDK.MethodBaseModel]? +} + +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 + } +} diff --git a/SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift b/SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift new file mode 100644 index 0000000..70393eb --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift @@ -0,0 +1,24 @@ +// +// 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 { + struct ProblemDetails: Codable, Hashable { + public let title: String? + public let status: Int32? + public let detail: String? + + let operation: OperationOutputModel? + } +} diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index 605e110..f928942 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -28,17 +28,35 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { } extension SwedbankPayAPIEnpointRouter { - func makeRequest(handler: @escaping (Result) -> Void) { + func makeRequest(handler: @escaping (Result) -> Void) { requestWithDataResponse { result in switch result { - case .success: - handler(.success(())) + 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 JSONDecoder().decode(T.self, from: data) + } catch { + throw error + } + + return decodedData + } + private func requestWithDataResponse(handler: @escaping (Result) -> Void) { guard let href = model.href, var components = URLComponents(string: href) else { diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index d0c2d01..2811fca 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -34,8 +34,11 @@ public extension SwedbankPaySDK { public func startPaymentSession(with sessionApi: String) { sessionIsOngoing = true - let model = OperationOutputModel(href: sessionApi, - method: "GET") + let model = OperationOutputModel(rel: nil, + href: sessionApi, + method: "GET", + next: nil, + tasks: nil) makeRequest(model: model) } From b0d6ffc8a0068f9310d09f58835d9feb2819721f Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Wed, 15 May 2024 10:56:57 +0200 Subject: [PATCH 04/75] Fixed array for prefills --- SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift index 0f9261f..f98d101 100644 --- a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift @@ -15,8 +15,8 @@ extension SwedbankPaySDK { public enum MethodBaseModel: Codable, Equatable, Hashable { - case swish(prefills: [[SwishMethodPrefillModel]]?, operations: [OperationOutputModel]?) - case creditCard(prefills: [[CreditCardMethodPrefillModel]]?, operations: [OperationOutputModel]?, cardBrands: [String]?) + case swish(prefills: [SwishMethodPrefillModel]?, operations: [OperationOutputModel]?) + case creditCard(prefills: [CreditCardMethodPrefillModel]?, operations: [OperationOutputModel]?, cardBrands: [String]?) case unknown(String) @@ -31,12 +31,12 @@ extension SwedbankPaySDK { switch type { case "Swish": self = .swish( - prefills: try? container.decode([[SwishMethodPrefillModel]]?.self, forKey: CodingKeys.prefills), + prefills: try? container.decode([SwishMethodPrefillModel]?.self, forKey: CodingKeys.prefills), operations: try? container.decode([OperationOutputModel]?.self, forKey: CodingKeys.operations) ) case "CreditCard": self = .creditCard( - prefills: try? container.decode([[CreditCardMethodPrefillModel]].self, forKey: CodingKeys.prefills), + prefills: try? container.decode([CreditCardMethodPrefillModel].self, forKey: CodingKeys.prefills), operations: try? container.decode([OperationOutputModel]?.self, forKey: CodingKeys.operations), cardBrands: try? container.decode([String]?.self, forKey: CodingKeys.cardBrands) ) From 41e281a841437b57de6fe16ed92171f108f44bd2 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Mon, 13 May 2024 17:01:55 +0200 Subject: [PATCH 05/75] SP-10 Post Integrations request --- SwedbankPaySDK.xcodeproj/project.pbxproj | 20 +++++++ .../Api/Helpers/NetworkStatusProvider.swift | 57 +++++++++++++++++++ .../Api/Helpers/TimeZone+OffsetFromGMT.swift | 25 ++++++++ .../Api/SwedbankPayAPIEnpointRouter.swift | 22 ++++++- 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 SwedbankPaySDK/Classes/Api/Helpers/NetworkStatusProvider.swift create mode 100644 SwedbankPaySDK/Classes/Api/Helpers/TimeZone+OffsetFromGMT.swift diff --git a/SwedbankPaySDK.xcodeproj/project.pbxproj b/SwedbankPaySDK.xcodeproj/project.pbxproj index 5fc7607..9b2ecfa 100644 --- a/SwedbankPaySDK.xcodeproj/project.pbxproj +++ b/SwedbankPaySDK.xcodeproj/project.pbxproj @@ -27,6 +27,10 @@ 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 */; }; 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 */; }; @@ -277,6 +281,8 @@ 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 = ""; }; 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 = ""; }; @@ -456,6 +462,7 @@ 45C1008A2BF247F300AA3523 /* Api */ = { isa = PBXGroup; children = ( + 45C100AA2BF2622E00AA3523 /* Helpers */, 45C1008C2BF247F300AA3523 /* Models */, 45C1008B2BF247F300AA3523 /* SwedbankPayAPIError.swift */, 45C1008E2BF247F300AA3523 /* SwedbankPayAPIEnpointRouter.swift */, @@ -477,6 +484,15 @@ path = Models; sourceTree = ""; }; + 45C100AA2BF2622E00AA3523 /* Helpers */ = { + isa = PBXGroup; + children = ( + 45C100AB2BF2622E00AA3523 /* NetworkStatusProvider.swift */, + 45C100AC2BF2622E00AA3523 /* TimeZone+OffsetFromGMT.swift */, + ); + path = Helpers; + sourceTree = ""; + }; A502C37525430D90004FBFCB /* Models */ = { isa = PBXGroup; children = ( @@ -1115,6 +1131,7 @@ A52E0ACB24BCAA6D00770286 /* GoodWebViewRedirects.swift in Sources */, 45C100A82BF2598D00AA3523 /* ProblemDetails.swift in Sources */, A5808B09261C980000FF7EC1 /* SwedbankPayWebViewControllerDelegate.swift in Sources */, + 45C100AF2BF2622E00AA3523 /* TimeZone+OffsetFromGMT.swift in Sources */, A5DC2CAA251A2DFA0037C7DA /* ViewPaymentOrderInfo.swift in Sources */, A5DC2CA6251A2D730037C7DA /* ViewConsumerIdentificationInfo.swift in Sources */, C585AF2C237066EE006C2E16 /* SwedbankPaySDK.swift in Sources */, @@ -1123,6 +1140,7 @@ D581EFDF27A92A56001B85A7 /* VersionOptions.swift in Sources */, D50CA69B27D23EE000FC6007 /* Operation.swift in Sources */, C50CF699237AAC73003F79DF /* WhitelistedDomain.swift in Sources */, + 45C100AD2BF2622E00AA3523 /* NetworkStatusProvider.swift in Sources */, A5C3D01E23A23BC900B45085 /* SwedbankPayWebViewController.swift in Sources */, A5808B05261C942A00FF7EC1 /* SwedbankPayWebViewControllerBase.swift in Sources */, A560BE432420C7CE00C1D023 /* TerminalFailure.swift in Sources */, @@ -1190,6 +1208,7 @@ D511ECA92975599D0059AB05 /* PayerOwnedPaymentTokens.swift in Sources */, D511ECAA2975599D0059AB05 /* PaymentTokenInfo.swift in Sources */, D511ECAB2975599D0059AB05 /* EmptyJsonResponse.swift in Sources */, + 45C100AE2BF2622E00AA3523 /* NetworkStatusProvider.swift in Sources */, 45C1009A2BF24E0D00AA3523 /* NativePayment.swift in Sources */, 45C100A92BF2598D00AA3523 /* ProblemDetails.swift in Sources */, D511ECAC2975599D0059AB05 /* AbortPaymentOperation.swift in Sources */, @@ -1218,6 +1237,7 @@ D511EC7C2975591B0059AB05 /* TypeAliases.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 */, 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..254de26 --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/Helpers/TimeZone+OffsetFromGMT.swift @@ -0,0 +1,25 @@ +// +// 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()) + } +} diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index f928942..879bb89 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -14,6 +14,7 @@ // limitations under the License. import Foundation +import UIKit protocol EndpointRouterProtocol { var body: [String: Any?]? { get } @@ -23,7 +24,26 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { let model: OperationOutputModel var body: [String: Any?]? { - return nil + switch model.rel { + case .preparePayment: + let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String + + return ["initiatingSystem": "swedbank-pay-sdk-ios", + "initiatingSystemVersion": appVersion, + "integration": "HostedView", + "deviceAcceptedWallets": "", + "client": ["userAgent": "swedbank-pay-sdk-ios/\(appVersion)", + "ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? ""], + "browser": ["acceptHeader": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "languageHeader": Locale.current.identifier, + "timeZoneOffset": TimeZone.current.offsetFromGMT(), + "screenHeight": String(Int32(UIScreen.main.nativeBounds.height)), + "screenWidth": String(Int32(UIScreen.main.nativeBounds.width)), + "colorDepth": String(24)] + ] + default: + return nil + } } } From 6e95f39ab68250d67588cbeb0d54ac789ce68822 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Wed, 15 May 2024 10:43:11 +0200 Subject: [PATCH 06/75] Updated body for preparePayment --- .../Api/SwedbankPayAPIEnpointRouter.swift | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index 879bb89..255d0e6 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -26,20 +26,19 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { var body: [String: Any?]? { switch model.rel { case .preparePayment: - let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String - - return ["initiatingSystem": "swedbank-pay-sdk-ios", - "initiatingSystemVersion": appVersion, - "integration": "HostedView", + return ["integration": "HostedView", "deviceAcceptedWallets": "", - "client": ["userAgent": "swedbank-pay-sdk-ios/\(appVersion)", - "ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? ""], + "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, "timeZoneOffset": TimeZone.current.offsetFromGMT(), - "screenHeight": String(Int32(UIScreen.main.nativeBounds.height)), - "screenWidth": String(Int32(UIScreen.main.nativeBounds.width)), - "colorDepth": String(24)] + "javascriptEnabled": true], + "service": ["name": "SwedbankPaySDK-iOS", + "version": SwedbankPaySDK.VersionReporter.currentVersion] ] default: return nil From 9a7a7f9504169c51d3d2cbe8213b98f31e88e474 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Tue, 14 May 2024 10:57:21 +0200 Subject: [PATCH 07/75] SP-11 Post Swish Instrument Views and Attempt requests --- SwedbankPaySDK.xcodeproj/project.pbxproj | 6 ++ .../Api/Models/PaymentAttemptInstrument.swift | 30 ++++++++++ .../Api/SwedbankPayAPIEnpointRouter.swift | 18 ++++++ .../NativePayment.swift | 57 +++++++++++++++---- 4 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift diff --git a/SwedbankPaySDK.xcodeproj/project.pbxproj b/SwedbankPaySDK.xcodeproj/project.pbxproj index 9b2ecfa..31e4550 100644 --- a/SwedbankPaySDK.xcodeproj/project.pbxproj +++ b/SwedbankPaySDK.xcodeproj/project.pbxproj @@ -31,6 +31,8 @@ 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 */; }; 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 */; }; @@ -283,6 +285,7 @@ 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 = ""; }; 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 = ""; }; @@ -477,6 +480,7 @@ 45C100A12BF2584E00AA3523 /* IntegrationTask.swift */, 45C1009C2BF2583D00AA3523 /* MethodBaseModel.swift */, 45C1008D2BF247F300AA3523 /* OperationOutputModel.swift */, + 45C100B22BF34EED00AA3523 /* PaymentAttemptInstrument.swift */, 45C100A42BF2597B00AA3523 /* PaymentOutputModel.swift */, 45C1009B2BF2583D00AA3523 /* PaymentSessionModel.swift */, 45C100A72BF2598D00AA3523 /* ProblemDetails.swift */, @@ -1114,6 +1118,7 @@ 45C1009D2BF2583D00AA3523 /* PaymentSessionModel.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 */, @@ -1203,6 +1208,7 @@ 45C100A32BF2584E00AA3523 /* IntegrationTask.swift in Sources */, D511ECA52975599D0059AB05 /* BackendOperation.swift in Sources */, D511ECA62975599D0059AB05 /* TopLevelResources.swift in Sources */, + 45C100B42BF34EED00AA3523 /* PaymentAttemptInstrument.swift in Sources */, D511ECA72975599D0059AB05 /* ConsumersLink.swift in Sources */, D511ECA82975599D0059AB05 /* PayerOwnedPaymentTokensResponse.swift in Sources */, D511ECA92975599D0059AB05 /* PayerOwnedPaymentTokens.swift in Sources */, diff --git a/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift new file mode 100644 index 0000000..3924882 --- /dev/null +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.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. + +extension SwedbankPaySDK { + public enum PaymentAttemptInstrument { + case swish(msisdn: String?) + case creditCard(paymentToken: String) + + var name: String { + switch self { + case .swish: + return "swish" + case .creditCard: + return "creditCard" + } + } + } +} diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index 255d0e6..fc976db 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -22,9 +22,27 @@ protocol EndpointRouterProtocol { struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { let model: OperationOutputModel + let culture: String? + let instrument: SwedbankPaySDK.PaymentAttemptInstrument? var body: [String: Any?]? { switch model.rel { + case .expandMethod: + return ["instrumentName": instrument?.name] + case .startPaymentAttempt: + switch instrument { + case .swish(let msisdn): + return ["culture": culture, + "msisdn": msisdn, + "client": ["ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? ""]] + case .creditCard(let paymentToken): + return ["culture": culture, + "paymentToken": paymentToken, + "client": ["ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? ""]] + case .none: + return ["culture": culture, + "client": ["ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? ""]] + } case .preparePayment: return ["integration": "HostedView", "deviceAcceptedWallets": "", diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index 2811fca..a0666f2 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -13,26 +13,28 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Foundation +import UIKit + public extension SwedbankPaySDK { class NativePayment { - /// The `SwedbankPaySDKConfiguration` used by this `NativePayment`. - /// - /// Note that `NativePayment` accesses this property only once during initialization, - /// and will use the returned value thereafter. Hence, you cannot change the configuration "in-flight" - /// by changing the value returned from here. - open var configuration: SwedbankPaySDKConfiguration + /// Order information that provides `NativePayment` with callback URLs. + public var orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo /// A delegate to receive callbacks as the state of SwedbankPaySDKController changes. public weak var delegate: SwedbankPaySDKDelegate? + private var ongoingModel: PaymentOutputModel? = nil private var sessionIsOngoing: Bool = false + private var instrument: SwedbankPaySDK.PaymentAttemptInstrument? = nil - public init(configuration: SwedbankPaySDKConfiguration) { - self.configuration = configuration + public init(orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo) { + self.orderInfo = orderInfo } public func startPaymentSession(with sessionApi: String) { sessionIsOngoing = true + instrument = nil let model = OperationOutputModel(rel: nil, href: sessionApi, @@ -43,8 +45,22 @@ public extension SwedbankPaySDK { makeRequest(model: model) } - private func makeRequest(model: OperationOutputModel) { - SwedbankPayAPIEnpointRouter(model: model).makeRequest { result in + public func makePaymentAttempt(with instrument: SwedbankPaySDK.PaymentAttemptInstrument) { + guard let ongoingModel = ongoingModel else { + return + } + + self.instrument = instrument + + if let operation = ongoingModel.paymentSession.methods? + .first(where: { $0.name == instrument.name })?.operations? + .first(where: { $0.rel == .expandMethod || $0.rel == .startPaymentAttempt || $0.rel == .getPayment }) { + makeRequest(model: operation, culture: ongoingModel.paymentSession.culture) + } + } + + private func makeRequest(model: OperationOutputModel, culture: String? = nil) { + SwedbankPayAPIEnpointRouter(model: model, culture: culture, instrument: instrument).makeRequest { result in switch result { case .success: break @@ -54,5 +70,26 @@ public extension SwedbankPaySDK { } } } + + private func launchClientApp(task: IntegrationTask) { + guard let href = task.href, var components = URLComponents(string: href) else { + 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) + } + } + } } } From 3ea624f663b3f6415617f106c9936ce42686887f Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Wed, 15 May 2024 11:17:21 +0200 Subject: [PATCH 08/75] Updated body for startPaymentAttempt --- .../Api/SwedbankPayAPIEnpointRouter.swift | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index fc976db..3feb525 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -34,14 +34,29 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { case .swish(let msisdn): return ["culture": culture, "msisdn": msisdn, - "client": ["ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? ""]] + "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 paymentToken): return ["culture": culture, "paymentToken": paymentToken, - "client": ["ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? ""]] + "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 .none: return ["culture": culture, - "client": ["ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? ""]] + "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 .preparePayment: return ["integration": "HostedView", From 4804604658ff11e85e9b15affe0af4f438c96cdc Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Tue, 14 May 2024 14:15:41 +0200 Subject: [PATCH 09/75] SP-17 Session operation handling --- .../NativePayment.swift | 52 ++++++++++++++++++- .../Classes/SwedbankPaySDKController.swift | 7 ++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index a0666f2..0068edb 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -28,6 +28,8 @@ public extension SwedbankPaySDK { private var sessionIsOngoing: Bool = false private var instrument: SwedbankPaySDK.PaymentAttemptInstrument? = nil + private var hasLaunchClientApp: [URL] = [] + public init(orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo) { self.orderInfo = orderInfo } @@ -35,6 +37,8 @@ public extension SwedbankPaySDK { public func startPaymentSession(with sessionApi: String) { sessionIsOngoing = true instrument = nil + ongoingModel = nil + hasLaunchClientApp = [] let model = OperationOutputModel(rel: nil, href: sessionApi, @@ -62,8 +66,10 @@ public extension SwedbankPaySDK { private func makeRequest(model: OperationOutputModel, culture: String? = nil) { SwedbankPayAPIEnpointRouter(model: model, culture: culture, instrument: instrument).makeRequest { result in switch result { - case .success: - break + case .success(let success): + if let model = success { + self.sessionOperationHandling(model: model, culture: model.paymentSession.culture) + } case .failure(let failure): self.delegate?.paymentFailed(error: failure) self.sessionIsOngoing = false @@ -87,9 +93,51 @@ public extension SwedbankPaySDK { if let url = components.url { DispatchQueue.main.async { + self.hasLaunchClientApp.append(url) UIApplication.shared.open(url) } } } + + private func sessionOperationHandling(model: PaymentOutputModel, culture: String? = nil) { + ongoingModel = model + + var operations = model.operations ?? [] + operations.append(contentsOf: model.paymentSession.allMethodOperations) + + operations = operations.filter({ $0.rel?.isUnknown == false }) + + if operations.contains(where: { $0.next == true }) { + operations = operations.filter({ $0.next == true }) + } + + if let preparePayment = operations.first(where: { $0.rel == .preparePayment }) { + makeRequest(model: preparePayment, culture: culture) + } else if let startPaymentAttempt = operations.first(where: { $0.rel == .startPaymentAttempt }) { + if instrument != nil { + makeRequest(model: startPaymentAttempt, culture: culture) + instrument = nil + } else { + delegate?.availableInstrumentsFetched(model.paymentSession.methods ?? []) + } + } else if let launchClientApp = operations.first(where: { $0.firstTask(with: .launchClientApp) != nil }), + let tasks = launchClientApp.firstTask(with: .launchClientApp), + !hasLaunchClientApp.contains(where: { $0.absoluteString == tasks.href }) { + self.launchClientApp(task: launchClientApp.firstTask(with: .launchClientApp)!) + } else if let redirectPayer = operations.first(where: { $0.rel == .redirectPayer }) { + if redirectPayer.href == orderInfo.cancelUrl?.absoluteString { + delegate?.paymentCanceled() + } else if redirectPayer.href == orderInfo.completeUrl.absoluteString { + delegate?.paymentComplete() + } + sessionIsOngoing = false + } else if let _ = operations.first(where: { $0.rel == .expandMethod }) { + delegate?.availableInstrumentsFetched(model.paymentSession.methods ?? []) + } else if let getPayment = operations.first(where: { $0.rel == .getPayment }) { + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + self.makeRequest(model: getPayment, culture: culture) + } + } + } } } diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift b/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift index 325728a..b8976ee 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift @@ -43,7 +43,12 @@ public protocol SwedbankPaySDKDelegate: AnyObject { func paymentComplete() func paymentCanceled() - + + /// Called when an list of available instruments is known. + /// + /// - parameter availableInstruments: List of different instruments + func availableInstrumentsFetched(_ availableInstruments: [SwedbankPaySDK.MethodBaseModel]) + /// Called if there is an error in performing the payment. /// The error may be SwedbankPaySDKController.WebContentError, /// or any error reported by your SwedbankPaySDKConfiguration. From d5b5c019f70bdbf50db915abea0ba83b76136b67 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Wed, 15 May 2024 09:39:20 +0200 Subject: [PATCH 10/75] SP-18 External app callback handling --- .../NativePayment.swift | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index 0068edb..67fa480 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -17,7 +17,7 @@ import Foundation import UIKit public extension SwedbankPaySDK { - class NativePayment { + class NativePayment: CallbackUrlDelegate { /// Order information that provides `NativePayment` with callback URLs. public var orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo @@ -32,6 +32,12 @@ public extension SwedbankPaySDK { public init(orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo) { self.orderInfo = orderInfo + + SwedbankPaySDK.addCallbackUrlDelegate(self) + } + + deinit { + SwedbankPaySDK.removeCallbackUrlDelegate(self) } public func startPaymentSession(with sessionApi: String) { @@ -122,7 +128,7 @@ public extension SwedbankPaySDK { } } else if let launchClientApp = operations.first(where: { $0.firstTask(with: .launchClientApp) != nil }), let tasks = launchClientApp.firstTask(with: .launchClientApp), - !hasLaunchClientApp.contains(where: { $0.absoluteString == tasks.href }) { + !hasLaunchClientApp.contains(where: { $0.absoluteString.contains(tasks.href ?? "") }) { self.launchClientApp(task: launchClientApp.firstTask(with: .launchClientApp)!) } else if let redirectPayer = operations.first(where: { $0.rel == .redirectPayer }) { if redirectPayer.href == orderInfo.cancelUrl?.absoluteString { @@ -139,5 +145,20 @@ public extension SwedbankPaySDK { } } } + + func handleCallbackUrl(_ url: URL) -> Bool { + guard url == orderInfo.paymentUrl else { + return false + } + + if let ongoingModel = ongoingModel { + if let operation = ongoingModel.paymentSession.allMethodOperations + .first(where: { $0.rel == .getPayment }) { + makeRequest(model: operation, culture: ongoingModel.paymentSession.culture) + } + } + + return true + } } } From 8b1dc700031c52aa70741d5e3013d17cffb57aa7 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Wed, 15 May 2024 13:08:23 +0200 Subject: [PATCH 11/75] SP-27 Payment attempt problem handling --- .../Api/Models/PaymentOutputModel.swift | 18 ++++++++++++++++++ .../NativePayment.swift | 15 ++++++--------- .../Classes/SwedbankPaySDKController.swift | 7 ++++++- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/Models/PaymentOutputModel.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentOutputModel.swift index 0d003fc..133dd9d 100644 --- a/SwedbankPaySDK/Classes/Api/Models/PaymentOutputModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentOutputModel.swift @@ -20,6 +20,24 @@ struct PaymentOutputModel: Codable, Hashable { } extension PaymentOutputModel { + var prioritisedOperations: [OperationOutputModel] { + if let problemOperation = problem?.operation, + problemOperation.rel?.isUnknown == false { + return [problemOperation] + } + + 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 diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index 67fa480..66cff6c 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -108,16 +108,13 @@ public extension SwedbankPaySDK { private func sessionOperationHandling(model: PaymentOutputModel, culture: String? = nil) { ongoingModel = model - var operations = model.operations ?? [] - operations.append(contentsOf: model.paymentSession.allMethodOperations) + let operations = model.prioritisedOperations - operations = operations.filter({ $0.rel?.isUnknown == false }) - - if operations.contains(where: { $0.next == true }) { - operations = operations.filter({ $0.next == true }) - } - - if let preparePayment = operations.first(where: { $0.rel == .preparePayment }) { + if let acknowledgeFailedAttempt = operations.first(where: { $0.rel == .acknowledgeFailedAttempt }), + let problem = model.problem { + delegate?.paymentFailed(problem: problem) + makeRequest(model: acknowledgeFailedAttempt, culture: culture) + } else if let preparePayment = operations.first(where: { $0.rel == .preparePayment }) { makeRequest(model: preparePayment, culture: culture) } else if let startPaymentAttempt = operations.first(where: { $0.rel == .startPaymentAttempt }) { if instrument != nil { diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift b/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift index b8976ee..8f25082 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift @@ -60,7 +60,12 @@ public protocol SwedbankPaySDKDelegate: AnyObject { /// /// - parameter error: The error that caused the failure func paymentFailed(error: Error) - + + /// Called if there is a problem with performing the payment. + /// + /// - parameter problem: The problem that caused the failure + func paymentFailed(problem: SwedbankPaySDK.ProblemDetails) + /// Called when the user taps on the Terms of Service Link /// in the Payment Menu. /// From f08cd64148ad445ed41b236b00fb9bffcb7284f9 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Wed, 15 May 2024 13:17:49 +0200 Subject: [PATCH 12/75] SP-29 Abort Payment request --- .../SwedbankPaySDK+Extensions/NativePayment.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index 66cff6c..1fc4db3 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -69,6 +69,17 @@ public extension SwedbankPaySDK { } } + public func abortPaymentSession() { + guard let ongoingModel = ongoingModel else { + return + } + + if let operation = ongoingModel.operations? + .first(where: { $0.rel == .abortPayment }) { + makeRequest(model: operation, culture: ongoingModel.paymentSession.culture) + } + } + private func makeRequest(model: OperationOutputModel, culture: String? = nil) { SwedbankPayAPIEnpointRouter(model: model, culture: culture, instrument: instrument).makeRequest { result in switch result { From 1e21991c414a0f7ebcf36982a81cd2d2ee1181ab Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Fri, 17 May 2024 11:13:24 +0200 Subject: [PATCH 13/75] SP-31 State desync handling --- .../NativePayment.swift | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index 1fc4db3..c68618b 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -28,7 +28,8 @@ public extension SwedbankPaySDK { private var sessionIsOngoing: Bool = false private var instrument: SwedbankPaySDK.PaymentAttemptInstrument? = nil - private var hasLaunchClientApp: [URL] = [] + private var hasLaunchClientAppURLs: [URL] = [] + private var hasShownProblemDetails: [ProblemDetails] = [] public init(orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo) { self.orderInfo = orderInfo @@ -44,7 +45,8 @@ public extension SwedbankPaySDK { sessionIsOngoing = true instrument = nil ongoingModel = nil - hasLaunchClientApp = [] + hasLaunchClientAppURLs = [] + hasShownProblemDetails = [] let model = OperationOutputModel(rel: nil, href: sessionApi, @@ -89,7 +91,6 @@ public extension SwedbankPaySDK { } case .failure(let failure): self.delegate?.paymentFailed(error: failure) - self.sessionIsOngoing = false } } } @@ -110,7 +111,7 @@ public extension SwedbankPaySDK { if let url = components.url { DispatchQueue.main.async { - self.hasLaunchClientApp.append(url) + self.hasLaunchClientAppURLs.append(url) UIApplication.shared.open(url) } } @@ -123,7 +124,11 @@ public extension SwedbankPaySDK { if let acknowledgeFailedAttempt = operations.first(where: { $0.rel == .acknowledgeFailedAttempt }), let problem = model.problem { - delegate?.paymentFailed(problem: problem) + if !hasShownProblemDetails.contains(where: { $0.operation?.href == problem.operation?.href }) { + hasShownProblemDetails.append(problem) + delegate?.paymentFailed(problem: problem) + } + makeRequest(model: acknowledgeFailedAttempt, culture: culture) } else if let preparePayment = operations.first(where: { $0.rel == .preparePayment }) { makeRequest(model: preparePayment, culture: culture) @@ -136,7 +141,7 @@ public extension SwedbankPaySDK { } } else if let launchClientApp = operations.first(where: { $0.firstTask(with: .launchClientApp) != nil }), let tasks = launchClientApp.firstTask(with: .launchClientApp), - !hasLaunchClientApp.contains(where: { $0.absoluteString.contains(tasks.href ?? "") }) { + !hasLaunchClientAppURLs.contains(where: { $0.absoluteString.contains(tasks.href ?? "") }) { self.launchClientApp(task: launchClientApp.firstTask(with: .launchClientApp)!) } else if let redirectPayer = operations.first(where: { $0.rel == .redirectPayer }) { if redirectPayer.href == orderInfo.cancelUrl?.absoluteString { @@ -145,6 +150,8 @@ public extension SwedbankPaySDK { delegate?.paymentComplete() } sessionIsOngoing = false + hasLaunchClientAppURLs = [] + hasShownProblemDetails = [] } else if let _ = operations.first(where: { $0.rel == .expandMethod }) { delegate?.availableInstrumentsFetched(model.paymentSession.methods ?? []) } else if let getPayment = operations.first(where: { $0.rel == .getPayment }) { From a3b16c1bd79506c2dcc7c970f061e9f1dae40795 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Fri, 17 May 2024 16:42:30 +0200 Subject: [PATCH 14/75] SP-36 Network instability handling --- .../Classes/Api/SwedbankPayAPIConstants.swift | 3 +++ .../Api/SwedbankPayAPIEnpointRouter.swift | 23 ++++++++++++------- .../NativePayment.swift | 9 +++++++- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift index 32ab83d..91c899e 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift @@ -18,6 +18,9 @@ struct SwedbankPayAPIConstants { HTTPHeaderField.acceptType.rawValue: ContentType.json.rawValue, HTTPHeaderField.contentType.rawValue: ContentType.json.rawValue ] + + static var requestTimeoutInterval = 10.0 + static var sessionTimeoutInterval = 20.0 } private enum HTTPHeaderField: String { diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index 3feb525..c4e78dd 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -24,6 +24,7 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { let model: OperationOutputModel let culture: String? let instrument: SwedbankPaySDK.PaymentAttemptInstrument? + let sessionStartTimestamp: Date var body: [String: Any?]? { switch model.rel { @@ -81,7 +82,9 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { extension SwedbankPayAPIEnpointRouter { func makeRequest(handler: @escaping (Result) -> Void) { - requestWithDataResponse { result in + let requestStartTimestamp: Date = Date() + + requestWithDataResponse(requestStartTimestamp: requestStartTimestamp) { result in switch result { case .success(let data): do { @@ -109,7 +112,7 @@ extension SwedbankPayAPIEnpointRouter { return decodedData } - private func requestWithDataResponse(handler: @escaping (Result) -> Void) { + private func requestWithDataResponse(requestStartTimestamp: Date, handler: @escaping (Result) -> Void) { guard let href = model.href, var components = URLComponents(string: href) else { handler(.failure(SwedbankPayAPIError.invalidUrl)) @@ -134,13 +137,17 @@ extension SwedbankPayAPIEnpointRouter { } URLSession.shared.dataTask(with: request) { data, response, error in - guard let response = response as? HTTPURLResponse, 200...299 ~= response.statusCode else { - handler(.failure(error ?? SwedbankPayAPIError.unknown)) - return - } + guard let data, let response = response as? HTTPURLResponse, !(500...599 ~= response.statusCode) else { + guard Date().timeIntervalSince(requestStartTimestamp) < SwedbankPayAPIConstants.requestTimeoutInterval && + Date().timeIntervalSince(sessionStartTimestamp) < SwedbankPayAPIConstants.sessionTimeoutInterval else { + handler(.failure(error ?? SwedbankPayAPIError.unknown)) + return + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + requestWithDataResponse(requestStartTimestamp: requestStartTimestamp, handler: handler) + } - guard let data else { - handler(.failure(error ?? SwedbankPayAPIError.unknown)) return } diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index c68618b..cc3ce14 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -31,6 +31,8 @@ public extension SwedbankPaySDK { private var hasLaunchClientAppURLs: [URL] = [] private var hasShownProblemDetails: [ProblemDetails] = [] + private var sessionStartTimestamp = Date() + public init(orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo) { self.orderInfo = orderInfo @@ -54,6 +56,7 @@ public extension SwedbankPaySDK { next: nil, tasks: nil) + sessionStartTimestamp = Date() makeRequest(model: model) } @@ -67,6 +70,7 @@ public extension SwedbankPaySDK { if let operation = ongoingModel.paymentSession.methods? .first(where: { $0.name == instrument.name })?.operations? .first(where: { $0.rel == .expandMethod || $0.rel == .startPaymentAttempt || $0.rel == .getPayment }) { + sessionStartTimestamp = Date() makeRequest(model: operation, culture: ongoingModel.paymentSession.culture) } } @@ -78,12 +82,13 @@ public extension SwedbankPaySDK { if let operation = ongoingModel.operations? .first(where: { $0.rel == .abortPayment }) { + sessionStartTimestamp = Date() makeRequest(model: operation, culture: ongoingModel.paymentSession.culture) } } private func makeRequest(model: OperationOutputModel, culture: String? = nil) { - SwedbankPayAPIEnpointRouter(model: model, culture: culture, instrument: instrument).makeRequest { result in + SwedbankPayAPIEnpointRouter(model: model, culture: culture, instrument: instrument, sessionStartTimestamp: sessionStartTimestamp).makeRequest { result in switch result { case .success(let success): if let model = success { @@ -156,6 +161,7 @@ public extension SwedbankPaySDK { delegate?.availableInstrumentsFetched(model.paymentSession.methods ?? []) } else if let getPayment = operations.first(where: { $0.rel == .getPayment }) { DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + self.sessionStartTimestamp = Date() self.makeRequest(model: getPayment, culture: culture) } } @@ -169,6 +175,7 @@ public extension SwedbankPaySDK { if let ongoingModel = ongoingModel { if let operation = ongoingModel.paymentSession.allMethodOperations .first(where: { $0.rel == .getPayment }) { + sessionStartTimestamp = Date() makeRequest(model: operation, culture: ongoingModel.paymentSession.culture) } } From cebcae44a4cd4d671033a750fbbae509ab7a3b33 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Tue, 21 May 2024 09:52:34 +0200 Subject: [PATCH 15/75] SP-35 Local problem handling --- SwedbankPaySDK.xcodeproj/project.pbxproj | 6 ++ .../Api/Models/PaymentOutputModel.swift | 5 -- .../NativePayment.swift | 65 +++++++++++++------ .../NativePaymentProblem.swift | 24 +++++++ .../Classes/SwedbankPaySDKController.swift | 9 ++- 5 files changed, 83 insertions(+), 26 deletions(-) create mode 100644 SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift diff --git a/SwedbankPaySDK.xcodeproj/project.pbxproj b/SwedbankPaySDK.xcodeproj/project.pbxproj index 31e4550..4733219 100644 --- a/SwedbankPaySDK.xcodeproj/project.pbxproj +++ b/SwedbankPaySDK.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 456900812BFB9AA5009475A5 /* NativePaymentProblem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 456900802BFB9AA5009475A5 /* NativePaymentProblem.swift */; }; + 456900822BFB9AA5009475A5 /* NativePaymentProblem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 456900802BFB9AA5009475A5 /* NativePaymentProblem.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 */; }; @@ -273,6 +275,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 456900802BFB9AA5009475A5 /* NativePaymentProblem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePaymentProblem.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 = ""; }; @@ -665,6 +668,7 @@ A59AEE9C25372C9A00255A3A /* Instrument.swift */, A5F3416823FD9293005D65BA /* PaymentOrder.swift */, C50CF6A6237AAF85003F79DF /* ClientProblem.swift */, + 456900802BFB9AA5009475A5 /* NativePaymentProblem.swift */, C50CF696237AA8ED003F79DF /* Configuration.swift */, C50CF69C237AACC3003F79DF /* Consumer.swift */, C50CF6A8237AAFBF003F79DF /* ServerProblem.swift */, @@ -1148,6 +1152,7 @@ 45C100AD2BF2622E00AA3523 /* NetworkStatusProvider.swift in Sources */, A5C3D01E23A23BC900B45085 /* SwedbankPayWebViewController.swift in Sources */, A5808B05261C942A00FF7EC1 /* SwedbankPayWebViewControllerBase.swift in Sources */, + 456900812BFB9AA5009475A5 /* NativePaymentProblem.swift in Sources */, A560BE432420C7CE00C1D023 /* TerminalFailure.swift in Sources */, A5F3416923FD9293005D65BA /* PaymentOrder.swift in Sources */, D5AE5DA027E0D4DA00BE5468 /* ExpandResource.swift in Sources */, @@ -1227,6 +1232,7 @@ D511ECB32975599D0059AB05 /* MerchantBackendApi.swift in Sources */, D511ECB42975599D0059AB05 /* RequestDecorator.swift in Sources */, D511EC702975591B0059AB05 /* WKWebViewCanOpen.swift in Sources */, + 456900822BFB9AA5009475A5 /* NativePaymentProblem.swift in Sources */, D511EC712975591B0059AB05 /* SwedbankPaySubProblem.swift in Sources */, D511EC722975591B0059AB05 /* WebViewRedirects.swift in Sources */, D511EC732975591B0059AB05 /* SwedbankPayWebContent.swift in Sources */, diff --git a/SwedbankPaySDK/Classes/Api/Models/PaymentOutputModel.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentOutputModel.swift index 133dd9d..95d6e3c 100644 --- a/SwedbankPaySDK/Classes/Api/Models/PaymentOutputModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentOutputModel.swift @@ -21,11 +21,6 @@ struct PaymentOutputModel: Codable, Hashable { extension PaymentOutputModel { var prioritisedOperations: [OperationOutputModel] { - if let problemOperation = problem?.operation, - problemOperation.rel?.isUnknown == false { - return [problemOperation] - } - var operations = operations ?? [] operations.append(contentsOf: paymentSession.allMethodOperations) diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index cc3ce14..64f61d8 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -95,7 +95,13 @@ public extension SwedbankPaySDK { self.sessionOperationHandling(model: model, culture: model.paymentSession.culture) } case .failure(let failure): - self.delegate?.paymentFailed(error: failure) + DispatchQueue.main.async { + self.delegate?.sdkProblemOccurred(problem: .paymentSessionAPIRequestFailed(error: failure, + retry: { + self.sessionStartTimestamp = Date() + self.makeRequest(model: model, culture: culture) + })) + } } } } @@ -116,54 +122,75 @@ public extension SwedbankPaySDK { if let url = components.url { DispatchQueue.main.async { - self.hasLaunchClientAppURLs.append(url) - UIApplication.shared.open(url) + UIApplication.shared.open(url) { complete in + if complete { + self.hasLaunchClientAppURLs.append(url) + self.instrument = nil + } else { + self.delegate?.sdkProblemOccurred(problem: .clientAppLaunchFailed) + } + } } } } private func sessionOperationHandling(model: PaymentOutputModel, culture: String? = nil) { ongoingModel = model + + if let modelProblem = model.problem, + let problemOperation = modelProblem.operation, + problemOperation.rel == .acknowledgeFailedAttempt { + if !hasShownProblemDetails.contains(where: { $0.operation?.href == problemOperation.href }) { + hasShownProblemDetails.append(modelProblem) + DispatchQueue.main.async { + self.delegate?.sessionProblemOccurred(problem: modelProblem) + } + } + + makeRequest(model: problemOperation, culture: culture) + } let operations = model.prioritisedOperations - - if let acknowledgeFailedAttempt = operations.first(where: { $0.rel == .acknowledgeFailedAttempt }), - let problem = model.problem { - if !hasShownProblemDetails.contains(where: { $0.operation?.href == problem.operation?.href }) { - hasShownProblemDetails.append(problem) - delegate?.paymentFailed(problem: problem) - } - makeRequest(model: acknowledgeFailedAttempt, culture: culture) - } else if let preparePayment = operations.first(where: { $0.rel == .preparePayment }) { + if let preparePayment = operations.first(where: { $0.rel == .preparePayment }) { makeRequest(model: preparePayment, culture: culture) } else if let startPaymentAttempt = operations.first(where: { $0.rel == .startPaymentAttempt }) { if instrument != nil { makeRequest(model: startPaymentAttempt, culture: culture) - instrument = nil + self.instrument = nil } else { - delegate?.availableInstrumentsFetched(model.paymentSession.methods ?? []) + DispatchQueue.main.async { + self.delegate?.availableInstrumentsFetched(model.paymentSession.methods ?? []) + } } } else if let launchClientApp = operations.first(where: { $0.firstTask(with: .launchClientApp) != nil }), let tasks = launchClientApp.firstTask(with: .launchClientApp), !hasLaunchClientAppURLs.contains(where: { $0.absoluteString.contains(tasks.href ?? "") }) { self.launchClientApp(task: launchClientApp.firstTask(with: .launchClientApp)!) } else if let redirectPayer = operations.first(where: { $0.rel == .redirectPayer }) { - if redirectPayer.href == orderInfo.cancelUrl?.absoluteString { - delegate?.paymentCanceled() - } else if redirectPayer.href == orderInfo.completeUrl.absoluteString { - delegate?.paymentComplete() + DispatchQueue.main.async { + if redirectPayer.href == self.orderInfo.cancelUrl?.absoluteString { + self.delegate?.paymentCanceled() + } else if redirectPayer.href == self.orderInfo.completeUrl.absoluteString { + self.delegate?.paymentComplete() + } } sessionIsOngoing = false hasLaunchClientAppURLs = [] hasShownProblemDetails = [] } else if let _ = operations.first(where: { $0.rel == .expandMethod }) { - delegate?.availableInstrumentsFetched(model.paymentSession.methods ?? []) + DispatchQueue.main.async { + self.delegate?.availableInstrumentsFetched(model.paymentSession.methods ?? []) + } } else if let getPayment = operations.first(where: { $0.rel == .getPayment }) { DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { self.sessionStartTimestamp = Date() self.makeRequest(model: getPayment, culture: culture) } + } else { + DispatchQueue.main.async { + self.delegate?.sdkProblemOccurred(problem: .paymentSessionEndStateReached) + } } } diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift new file mode 100644 index 0000000..c554ca4 --- /dev/null +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift @@ -0,0 +1,24 @@ +// +// 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 { + enum NativePaymentProblem { + case paymentSessionEndStateReached + case paymentSessionAPIRequestFailed(error: Error, retry: ()->Void) + case clientAppLaunchFailed + } +} diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift b/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift index 8f25082..eefef15 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift @@ -61,10 +61,15 @@ public protocol SwedbankPaySDKDelegate: AnyObject { /// - parameter error: The error that caused the failure func paymentFailed(error: Error) - /// Called if there is a problem with performing the payment. + /// Called if there is a session problem with performing the payment. /// /// - parameter problem: The problem that caused the failure - func paymentFailed(problem: SwedbankPaySDK.ProblemDetails) + 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.NativePaymentProblem) /// Called when the user taps on the Terms of Service Link /// in the Payment Menu. From 751c4dc5a2138bc8dbe7c9c420b8cb9c68d65f65 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Thu, 30 May 2024 11:06:02 +0200 Subject: [PATCH 16/75] Added internalInconsistencyError --- .../Classes/SwedbankPaySDK+Extensions/NativePayment.swift | 6 ++++++ .../SwedbankPaySDK+Extensions/NativePaymentProblem.swift | 1 + 2 files changed, 7 insertions(+) diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index 64f61d8..7421cb7 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -62,6 +62,8 @@ public extension SwedbankPaySDK { public func makePaymentAttempt(with instrument: SwedbankPaySDK.PaymentAttemptInstrument) { guard let ongoingModel = ongoingModel else { + self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) + return } @@ -77,6 +79,8 @@ public extension SwedbankPaySDK { public func abortPaymentSession() { guard let ongoingModel = ongoingModel else { + self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) + return } @@ -108,6 +112,8 @@ public extension SwedbankPaySDK { private func launchClientApp(task: IntegrationTask) { guard let href = task.href, var components = URLComponents(string: href) else { + self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) + return } diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift index c554ca4..879d0cf 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift @@ -20,5 +20,6 @@ public extension SwedbankPaySDK { case paymentSessionEndStateReached case paymentSessionAPIRequestFailed(error: Error, retry: ()->Void) case clientAppLaunchFailed + case internalInconsistencyError } } From 840202c5e33d54e34469f0c774635b5876010b0a Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Tue, 28 May 2024 13:28:26 +0200 Subject: [PATCH 17/75] SP-33 Event logging --- SwedbankPaySDK.xcodeproj/project.pbxproj | 32 ++++ .../Classes/Api/Models/MethodBaseModel.swift | 12 +- .../Api/Models/OperationOutputModel.swift | 3 + .../Api/Models/PaymentAttemptInstrument.swift | 4 +- .../Api/SwedbankPayAPIEnpointRouter.swift | 21 +++ SwedbankPaySDK/Classes/Beacon/Beacon.swift | 21 +++ .../Classes/Beacon/BeaconEndpointRouter.swift | 139 ++++++++++++++++++ .../Classes/Beacon/BeaconService.swift | 49 ++++++ .../Classes/Beacon/BeaconType.swift | 37 +++++ .../NativePayment.swift | 93 ++++++++++-- 10 files changed, 393 insertions(+), 18 deletions(-) create mode 100644 SwedbankPaySDK/Classes/Beacon/Beacon.swift create mode 100644 SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift create mode 100644 SwedbankPaySDK/Classes/Beacon/BeaconService.swift create mode 100644 SwedbankPaySDK/Classes/Beacon/BeaconType.swift diff --git a/SwedbankPaySDK.xcodeproj/project.pbxproj b/SwedbankPaySDK.xcodeproj/project.pbxproj index 4733219..aa6d3be 100644 --- a/SwedbankPaySDK.xcodeproj/project.pbxproj +++ b/SwedbankPaySDK.xcodeproj/project.pbxproj @@ -7,8 +7,16 @@ objects = { /* Begin PBXBuildFile section */ + 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 */; }; 456900812BFB9AA5009475A5 /* NativePaymentProblem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 456900802BFB9AA5009475A5 /* NativePaymentProblem.swift */; }; 456900822BFB9AA5009475A5 /* NativePaymentProblem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 456900802BFB9AA5009475A5 /* NativePaymentProblem.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 */; }; @@ -275,7 +283,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 4517C69F2BFF6B7E001687E7 /* BeaconEndpointRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeaconEndpointRouter.swift; sourceTree = ""; }; + 4517C6A22BFF928B001687E7 /* BeaconService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeaconService.swift; sourceTree = ""; }; 456900802BFB9AA5009475A5 /* NativePaymentProblem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePaymentProblem.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 = ""; }; @@ -465,6 +477,17 @@ /* 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 = ""; + }; 45C1008A2BF247F300AA3523 /* Api */ = { isa = PBXGroup; children = ( @@ -783,6 +806,7 @@ C585AF28237066EE006C2E16 /* SwedbankPaySDKViewModel.swift */, 45C1008A2BF247F300AA3523 /* Api */, A5808B03261C93FD00FF7EC1 /* WebView */, + 4517C69E2BFE34DC001687E7 /* Beacon */, A52E0ACA24BCAA6D00770286 /* GoodWebViewRedirects.swift */, ); path = Classes; @@ -1126,21 +1150,25 @@ 63EECFD0270730C800C37B69 /* CodableUserData.swift in Sources */, A59AEE9D25372C9A00255A3A /* Instrument.swift in Sources */, C585AF2F237066EE006C2E16 /* SwedbankPaySDKController.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 */, 45C100A22BF2584E00AA3523 /* IntegrationTask.swift in Sources */, A52E0ACB24BCAA6D00770286 /* GoodWebViewRedirects.swift in Sources */, 45C100A82BF2598D00AA3523 /* ProblemDetails.swift in Sources */, A5808B09261C980000FF7EC1 /* SwedbankPayWebViewControllerDelegate.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 */, @@ -1201,6 +1229,7 @@ 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 */, D511ECA02975599D0059AB05 /* DeletePaymentTokenLink.swift in Sources */, @@ -1213,6 +1242,7 @@ 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 */, @@ -1236,10 +1266,12 @@ D511EC712975591B0059AB05 /* SwedbankPaySubProblem.swift in Sources */, D511EC722975591B0059AB05 /* WebViewRedirects.swift in Sources */, D511EC732975591B0059AB05 /* SwedbankPayWebContent.swift in Sources */, + 45B4495E2C05F51500A1F46D /* Beacon.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 */, diff --git a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift index f98d101..33711c3 100644 --- a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift @@ -63,11 +63,11 @@ extension SwedbankPaySDK { var name: String { switch self { case .swish: - return "swish" + return "Swish" case .creditCard: - return "creditCard" + return "CreditCard" case .unknown: - return "unknown" + return "Unknown" } } @@ -81,6 +81,12 @@ extension SwedbankPaySDK { return nil } } + + var isUnknown: Bool { + if case .unknown = self { return true } + + return false + } } public struct SwishMethodPrefillModel: Codable, Hashable { diff --git a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift index e2f83ab..54935fc 100644 --- a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift @@ -41,6 +41,7 @@ enum OperationRel: Codable, Equatable, Hashable { case redirectPayer case acknowledgeFailedAttempt case abortPayment + case eventLogging case unknown(String) @@ -58,6 +59,7 @@ enum OperationRel: Codable, Equatable, Hashable { case Self.redirectPayer.rawValue: self = .redirectPayer case Self.acknowledgeFailedAttempt.rawValue: self = .acknowledgeFailedAttempt case Self.abortPayment.rawValue: self = .abortPayment + case Self.eventLogging.rawValue: self = .eventLogging default: self = .unknown(type) } } @@ -78,6 +80,7 @@ enum OperationRel: Codable, Equatable, Hashable { case .redirectPayer: "redirect-payer" case .acknowledgeFailedAttempt: "acknowledge-failed-attempt" case .abortPayment: "abort-payment" + case .eventLogging: "event-logging" case .unknown(let value): value } } diff --git a/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift index 3924882..ffed1c2 100644 --- a/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift @@ -21,9 +21,9 @@ extension SwedbankPaySDK { var name: String { switch self { case .swish: - return "swish" + return "Swish" case .creditCard: - return "creditCard" + return "CreditCard" } } } diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index c4e78dd..b3bb871 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -137,6 +137,25 @@ extension SwedbankPayAPIEnpointRouter { } URLSession.shared.dataTask(with: request) { data, response, error in + + var responseStatusCode: Int? + if let response = response as? HTTPURLResponse { + responseStatusCode = response.statusCode + } + + var values: [String: Any]? + if let error = error as? NSError { + values = ["errorDescription": error.localizedDescription, + "errorCode": error.code, + "errorDomain": error.domain] + } + + BeaconService.shared.log(type: .httpRequest(duration: Int32((Date().timeIntervalSince(requestStartTimestamp) * 1000.0).rounded()), + requestUrl: model.href ?? "", + method: model.method ?? "", + responseStatusCode: responseStatusCode, + values: values)) + guard let data, let response = response as? HTTPURLResponse, !(500...599 ~= response.statusCode) else { guard Date().timeIntervalSince(requestStartTimestamp) < SwedbankPayAPIConstants.requestTimeoutInterval && Date().timeIntervalSince(sessionStartTimestamp) < SwedbankPayAPIConstants.sessionTimeoutInterval else { @@ -145,6 +164,8 @@ extension SwedbankPayAPIEnpointRouter { } DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + let requestStartTimestamp: Date = Date() + requestWithDataResponse(requestStartTimestamp: requestStartTimestamp, handler: handler) } 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..fe6686a --- /dev/null +++ b/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift @@ -0,0 +1,139 @@ +// +// 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: EndpointRouterProtocol { + 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.SSS'Z'Z" + return formatter + }() + + // MARK: - Body + var body: [String: Any?]? { + var body: [String: Any?] = ["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) { + let requestStartTimestamp: Date = Date() + + requestWithDataResponse { result in + switch result { + case .success(let data): + 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..27431fd --- /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: Any?]?) + case sdkCallbackInvoked(name: String, succeeded: Bool, values: [String: Any?]?) + case httpRequest(duration: Int32, requestUrl: String, method: String, responseStatusCode: Int?, values: [String: Any?]?) + case launchClientApp(values: [String: Any?]?) + case clientAppCallback(values: [String: Any?]?) + + 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/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index 7421cb7..062bafe 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -58,6 +58,11 @@ public extension SwedbankPaySDK { sessionStartTimestamp = Date() makeRequest(model: model) + + BeaconService.shared.clear() + BeaconService.shared.log(type: .sdkMethodInvoked(name: "startPaymentSession", + succeeded: true, + values: nil)) } public func makePaymentAttempt(with instrument: SwedbankPaySDK.PaymentAttemptInstrument) { @@ -69,12 +74,28 @@ public extension SwedbankPaySDK { self.instrument = instrument + var succeeded = false if let operation = ongoingModel.paymentSession.methods? .first(where: { $0.name == instrument.name })?.operations? .first(where: { $0.rel == .expandMethod || $0.rel == .startPaymentAttempt || $0.rel == .getPayment }) { sessionStartTimestamp = Date() makeRequest(model: operation, culture: ongoingModel.paymentSession.culture) + var succeeded = true + } + + switch instrument { + case .swish(let msisdn): + BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", + succeeded: succeeded, + values: ["instrument": "Swish", + "msisdn": msisdn])) + case .creditCard(let paymentToken): + BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", + succeeded: succeeded, + values: ["instrument": "CreditCard", + "paymentToken": paymentToken])) } + } public func abortPaymentSession() { @@ -84,11 +105,17 @@ public extension SwedbankPaySDK { return } + var succeeded = false if let operation = ongoingModel.operations? .first(where: { $0.rel == .abortPayment }) { sessionStartTimestamp = Date() makeRequest(model: operation, culture: ongoingModel.paymentSession.culture) + succeeded = true } + + BeaconService.shared.log(type: .sdkMethodInvoked(name: "abortPaymentSession", + succeeded: succeeded, + values: nil)) } private func makeRequest(model: OperationOutputModel, culture: String? = nil) { @@ -96,6 +123,10 @@ public extension SwedbankPaySDK { switch result { case .success(let success): if let model = success { + if let eventLogging = model.operations?.first(where: { $0.rel == .eventLogging }) { + BeaconService.shared.href = eventLogging.href + } + self.sessionOperationHandling(model: model, culture: model.paymentSession.culture) } case .failure(let failure): @@ -105,6 +136,15 @@ public extension SwedbankPaySDK { self.sessionStartTimestamp = Date() self.makeRequest(model: model, culture: culture) })) + + let error = failure as NSError + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": "paymentSessionAPIRequestFailed", + "errorDescription": error.localizedDescription, + "errorCode": error.code, + "errorDomain": error.domain])) } } } @@ -134,7 +174,15 @@ public extension SwedbankPaySDK { self.instrument = nil } else { self.delegate?.sdkProblemOccurred(problem: .clientAppLaunchFailed) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": "clientAppLaunchFailed"])) } + + BeaconService.shared.log(type: .launchClientApp(values: ["callbackUrl": self.orderInfo.paymentUrl?.absoluteString ?? "", + "clientAppLaunchUrl": url.absoluteString, + "launchSucceeded": complete])) } } } @@ -142,33 +190,34 @@ public extension SwedbankPaySDK { private func sessionOperationHandling(model: PaymentOutputModel, culture: String? = nil) { ongoingModel = model - + if let modelProblem = model.problem, let problemOperation = modelProblem.operation, - problemOperation.rel == .acknowledgeFailedAttempt { + problemOperation.rel == .acknowledgeFailedAttempt { if !hasShownProblemDetails.contains(where: { $0.operation?.href == problemOperation.href }) { hasShownProblemDetails.append(modelProblem) DispatchQueue.main.async { self.delegate?.sessionProblemOccurred(problem: modelProblem) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sessionProblemOccurred", + succeeded: self.delegate != nil, + values: ["problemTitle": modelProblem.title, + "problemStatus": modelProblem.status, + "problemDetail": modelProblem.detail])) } } - + makeRequest(model: problemOperation, culture: culture) } let operations = model.prioritisedOperations - + if let preparePayment = operations.first(where: { $0.rel == .preparePayment }) { makeRequest(model: preparePayment, culture: culture) - } else if let startPaymentAttempt = operations.first(where: { $0.rel == .startPaymentAttempt }) { - if instrument != nil { - makeRequest(model: startPaymentAttempt, culture: culture) - self.instrument = nil - } else { - DispatchQueue.main.async { - self.delegate?.availableInstrumentsFetched(model.paymentSession.methods ?? []) - } - } + } else if let startPaymentAttempt = operations.first(where: { $0.rel == .startPaymentAttempt }), + instrument != nil { + makeRequest(model: startPaymentAttempt, culture: culture) + self.instrument = nil } else if let launchClientApp = operations.first(where: { $0.firstTask(with: .launchClientApp) != nil }), let tasks = launchClientApp.firstTask(with: .launchClientApp), !hasLaunchClientAppURLs.contains(where: { $0.absoluteString.contains(tasks.href ?? "") }) { @@ -177,8 +226,16 @@ public extension SwedbankPaySDK { DispatchQueue.main.async { if redirectPayer.href == self.orderInfo.cancelUrl?.absoluteString { self.delegate?.paymentCanceled() + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentCanceled", + succeeded: self.delegate != nil, + values: nil)) } else if redirectPayer.href == self.orderInfo.completeUrl.absoluteString { self.delegate?.paymentComplete() + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentComplete", + succeeded: self.delegate != nil, + values: nil)) } } sessionIsOngoing = false @@ -187,6 +244,10 @@ public extension SwedbankPaySDK { } else if let _ = operations.first(where: { $0.rel == .expandMethod }) { DispatchQueue.main.async { self.delegate?.availableInstrumentsFetched(model.paymentSession.methods ?? []) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "availableInstrumentsFetched", + succeeded: self.delegate != nil, + values: ["instruments": model.paymentSession.methods?.filter({ !$0.isUnknown }).compactMap({ $0.name }).joined(separator: ";")])) } } else if let getPayment = operations.first(where: { $0.rel == .getPayment }) { DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { @@ -196,6 +257,10 @@ public extension SwedbankPaySDK { } else { DispatchQueue.main.async { self.delegate?.sdkProblemOccurred(problem: .paymentSessionEndStateReached) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": "paymentSessionEndStateReached"])) } } } @@ -213,6 +278,8 @@ public extension SwedbankPaySDK { } } + BeaconService.shared.log(type: .clientAppCallback(values: ["callbackUrl": url.absoluteString])) + return true } } From 5978da77771e50b4d48bb2336a332db3cbe35dfa Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Tue, 28 May 2024 15:08:44 +0200 Subject: [PATCH 18/75] Fixed AvailableInstrument object --- .../Classes/Api/Models/MethodBaseModel.swift | 129 ++++++++++-------- .../Api/Models/PaymentSessionModel.swift | 2 +- .../NativePayment.swift | 17 ++- .../Classes/SwedbankPaySDKController.swift | 2 +- 4 files changed, 86 insertions(+), 64 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift index 33711c3..9f028aa 100644 --- a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift @@ -13,80 +13,91 @@ // See the License for the specific language governing permissions and // limitations under the License. -extension SwedbankPaySDK { - public enum MethodBaseModel: Codable, Equatable, Hashable { - case swish(prefills: [SwishMethodPrefillModel]?, operations: [OperationOutputModel]?) - case creditCard(prefills: [CreditCardMethodPrefillModel]?, operations: [OperationOutputModel]?, cardBrands: [String]?) +public enum MethodBaseModel: Codable, Equatable, Hashable { + case swish(prefills: [SwedbankPaySDK.SwishMethodPrefillModel]?, operations: [OperationOutputModel]?) + case creditCard(prefills: [SwedbankPaySDK.CreditCardMethodPrefillModel]?, operations: [OperationOutputModel]?, cardBrands: [String]?) + + case unknown(String) + + private enum CodingKeys: String, CodingKey { + case instrument, prefills, operations, cardBrands + } - case unknown(String) + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) - private enum CodingKeys: String, CodingKey { - case instrument, prefills, operations, cardBrands + let type = try container.decode(String.self, forKey: .instrument) + switch type { + 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) + ) + default: + self = .unknown(type) } + } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) + public 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 .unknown(let type): + try container.encode(type) + } + } - let type = try container.decode(String.self, forKey: .instrument) - switch type { - case "Swish": - self = .swish( - prefills: try? container.decode([SwishMethodPrefillModel]?.self, forKey: CodingKeys.prefills), - operations: try? container.decode([OperationOutputModel]?.self, forKey: CodingKeys.operations) - ) - case "CreditCard": - self = .creditCard( - prefills: try? container.decode([CreditCardMethodPrefillModel].self, forKey: CodingKeys.prefills), - operations: try? container.decode([OperationOutputModel]?.self, forKey: CodingKeys.operations), - cardBrands: try? container.decode([String]?.self, forKey: CodingKeys.cardBrands) - ) - default: - self = .unknown(type) - } + var name: String { + switch self { + case .swish: + return "Swish" + case .creditCard: + return "CreditCard" + case .unknown: + return "Unknown" } + } - public 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 .unknown(let type): - try container.encode(type) - } + var operations: [OperationOutputModel]? { + switch self { + case .swish(_, let opertations): + return opertations + case .creditCard(_, let opertations, _): + return opertations + case .unknown: + return nil } + } + + var isUnknown: Bool { + if case .unknown = self { return true } + + return false + } +} + +extension SwedbankPaySDK { + public enum AvailableInstrument: Codable, Equatable, Hashable { + case swish(prefills: [SwishMethodPrefillModel]?) var name: String { switch self { case .swish: return "Swish" - case .creditCard: - return "CreditCard" - case .unknown: - return "Unknown" - } - } - - var operations: [OperationOutputModel]? { - switch self { - case .swish(_, let opertations): - return opertations - case .creditCard(_, let opertations, _): - return opertations - case .unknown: - return nil } } - - var isUnknown: Bool { - if case .unknown = self { return true } - - return false - } } public struct SwishMethodPrefillModel: Codable, Hashable { diff --git a/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift index 2f8642b..d9f0280 100644 --- a/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift @@ -15,7 +15,7 @@ struct PaymentSessionModel: Codable, Hashable { let culture: String? - let methods: [SwedbankPaySDK.MethodBaseModel]? + let methods: [MethodBaseModel]? } extension PaymentSessionModel { diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index 062bafe..b3923c4 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -80,7 +80,7 @@ public extension SwedbankPaySDK { .first(where: { $0.rel == .expandMethod || $0.rel == .startPaymentAttempt || $0.rel == .getPayment }) { sessionStartTimestamp = Date() makeRequest(model: operation, culture: ongoingModel.paymentSession.culture) - var succeeded = true + succeeded = true } switch instrument { @@ -243,11 +243,22 @@ public extension SwedbankPaySDK { hasShownProblemDetails = [] } else if let _ = operations.first(where: { $0.rel == .expandMethod }) { DispatchQueue.main.async { - self.delegate?.availableInstrumentsFetched(model.paymentSession.methods ?? []) + let availableInstruments: [AvailableInstrument] = model.paymentSession.methods?.compactMap({ model in + switch model { + case .swish(let prefills, _): + return AvailableInstrument.swish(prefills: prefills) + case .creditCard(_, _, _): + return nil + case .unknown(_): + return nil + } + }) ?? [] + + self.delegate?.availableInstrumentsFetched(availableInstruments) BeaconService.shared.log(type: .sdkCallbackInvoked(name: "availableInstrumentsFetched", succeeded: self.delegate != nil, - values: ["instruments": model.paymentSession.methods?.filter({ !$0.isUnknown }).compactMap({ $0.name }).joined(separator: ";")])) + values: ["instruments": availableInstruments.compactMap({ $0.name }).joined(separator: ";")])) } } else if let getPayment = operations.first(where: { $0.rel == .getPayment }) { DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift b/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift index eefef15..d02a5a2 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift @@ -47,7 +47,7 @@ public protocol SwedbankPaySDKDelegate: AnyObject { /// Called when an list of available instruments is known. /// /// - parameter availableInstruments: List of different instruments - func availableInstrumentsFetched(_ availableInstruments: [SwedbankPaySDK.MethodBaseModel]) + func availableInstrumentsFetched(_ availableInstruments: [SwedbankPaySDK.AvailableInstrument]) /// Called if there is an error in performing the payment. /// The error may be SwedbankPaySDKController.WebContentError, From 614bfeb0a764a99e65fef9b8feef8b6cbf8d2dd9 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Wed, 29 May 2024 13:18:09 +0200 Subject: [PATCH 19/75] Added "type" to Beacon body --- SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift b/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift index fe6686a..15bde2c 100644 --- a/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift +++ b/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift @@ -33,7 +33,8 @@ struct BeaconEndpointRouter: EndpointRouterProtocol { // MARK: - Body var body: [String: Any?]? { - var body: [String: Any?] = ["client": ["userAgent": SwedbankPaySDK.VersionReporter.userAgent, + var body: [String: Any?] = ["type": 0, + "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)), From e48031a134dd23bb5a22d6b13153ed8bafc4b440 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Thu, 30 May 2024 11:08:17 +0200 Subject: [PATCH 20/75] Logging for internalInconsistencyError --- .../SwedbankPaySDK+Extensions/NativePayment.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index b3923c4..6dd419b 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -69,6 +69,10 @@ public extension SwedbankPaySDK { guard let ongoingModel = ongoingModel else { self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": "internalInconsistencyError"])) + return } @@ -102,6 +106,10 @@ public extension SwedbankPaySDK { guard let ongoingModel = ongoingModel else { self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": "internalInconsistencyError"])) + return } @@ -154,6 +162,10 @@ public extension SwedbankPaySDK { 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": "internalInconsistencyError"])) + return } From 944913a57219cec0d476ca67e73d63b43e9a8835 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Wed, 29 May 2024 14:09:33 +0200 Subject: [PATCH 21/75] SP-39 Updated logic for availableInstruments callback --- .../Classes/SwedbankPaySDK+Extensions/NativePayment.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index 6dd419b..f72d177 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -27,6 +27,7 @@ public extension SwedbankPaySDK { private var ongoingModel: PaymentOutputModel? = nil private var sessionIsOngoing: Bool = false private var instrument: SwedbankPaySDK.PaymentAttemptInstrument? = nil + private var hasShownAvailableInstruments: Bool = false private var hasLaunchClientAppURLs: [URL] = [] private var hasShownProblemDetails: [ProblemDetails] = [] @@ -49,6 +50,7 @@ public extension SwedbankPaySDK { ongoingModel = nil hasLaunchClientAppURLs = [] hasShownProblemDetails = [] + hasShownAvailableInstruments = false let model = OperationOutputModel(rel: nil, href: sessionApi, @@ -253,7 +255,9 @@ public extension SwedbankPaySDK { sessionIsOngoing = false hasLaunchClientAppURLs = [] hasShownProblemDetails = [] - } else if let _ = operations.first(where: { $0.rel == .expandMethod }) { + hasShownAvailableInstruments = false + } else if (operations.contains(where: { $0.rel == .expandMethod }) || operations.contains(where: { $0.rel == .startPaymentAttempt })) && + hasShownAvailableInstruments == false { DispatchQueue.main.async { let availableInstruments: [AvailableInstrument] = model.paymentSession.methods?.compactMap({ model in switch model { @@ -266,6 +270,8 @@ public extension SwedbankPaySDK { } }) ?? [] + self.hasShownAvailableInstruments = true + self.delegate?.availableInstrumentsFetched(availableInstruments) BeaconService.shared.log(type: .sdkCallbackInvoked(name: "availableInstrumentsFetched", From 83535e8b3c306e660b41a210ab13c371a362de70 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Wed, 29 May 2024 16:15:41 +0200 Subject: [PATCH 22/75] Added extra check when doing start-payment-attempt --- .../Classes/SwedbankPaySDK+Extensions/NativePayment.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index f72d177..dad6093 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -228,8 +228,12 @@ public extension SwedbankPaySDK { if let preparePayment = operations.first(where: { $0.rel == .preparePayment }) { makeRequest(model: preparePayment, culture: culture) - } else if let startPaymentAttempt = operations.first(where: { $0.rel == .startPaymentAttempt }), - instrument != nil { + } else if operations.contains(where: { $0.rel == .startPaymentAttempt }), + let instrument = instrument, + let startPaymentAttempt = ongoingModel?.paymentSession.methods? + .first(where: { $0.name == instrument.name })?.operations? + .first(where: { $0.rel == .startPaymentAttempt }) { + makeRequest(model: startPaymentAttempt, culture: culture) self.instrument = nil } else if let launchClientApp = operations.first(where: { $0.firstTask(with: .launchClientApp) != nil }), From 774ee9c23a1dbc9512559cca61009229429b3b97 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Thu, 30 May 2024 11:02:24 +0200 Subject: [PATCH 23/75] SP-41 Add public API documentation --- .../Classes/Api/Models/MethodBaseModel.swift | 4 ++++ .../Api/Models/PaymentAttemptInstrument.swift | 1 + .../Classes/Api/Models/ProblemDetails.swift | 1 + .../NativePayment.swift | 18 ++++++++++++++++-- .../NativePaymentProblem.swift | 1 + .../Classes/SwedbankPaySDKController.swift | 5 ++++- 6 files changed, 27 insertions(+), 3 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift index 9f028aa..05abc94 100644 --- a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift @@ -89,7 +89,10 @@ public enum MethodBaseModel: Codable, Equatable, Hashable { } 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]?) var name: String { @@ -100,6 +103,7 @@ extension SwedbankPaySDK { } } + /// Prefill information for Swish payment. public struct SwishMethodPrefillModel: Codable, Hashable { let rank: Int32? public let msisdn: String? diff --git a/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift index ffed1c2..aefcaed 100644 --- a/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift @@ -14,6 +14,7 @@ // limitations under the License. extension SwedbankPaySDK { + /// Instrument with needed values to make a payment attempt. public enum PaymentAttemptInstrument { case swish(msisdn: String?) case creditCard(paymentToken: String) diff --git a/SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift b/SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift index 70393eb..7abad5b 100644 --- a/SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift +++ b/SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift @@ -14,6 +14,7 @@ // limitations under the License. public extension SwedbankPaySDK { + /// Problem details returned with `sessionProblemOccurred` struct ProblemDetails: Codable, Hashable { public let title: String? public let status: Int32? diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index dad6093..2a4ac4e 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -17,6 +17,7 @@ import Foundation import UIKit public extension SwedbankPaySDK { + /// Object that handles native payments class NativePayment: CallbackUrlDelegate { /// Order information that provides `NativePayment` with callback URLs. public var orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo @@ -44,7 +45,12 @@ public extension SwedbankPaySDK { SwedbankPaySDK.removeCallbackUrlDelegate(self) } - public func startPaymentSession(with sessionApi: String) { + /// 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 startPaymentSession(with sessionURL: String) { sessionIsOngoing = true instrument = nil ongoingModel = nil @@ -53,7 +59,7 @@ public extension SwedbankPaySDK { hasShownAvailableInstruments = false let model = OperationOutputModel(rel: nil, - href: sessionApi, + href: sessionURL, method: "GET", next: nil, tasks: nil) @@ -67,6 +73,11 @@ public extension SwedbankPaySDK { 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 with instrument: Payment attempt instrument public func makePaymentAttempt(with instrument: SwedbankPaySDK.PaymentAttemptInstrument) { guard let ongoingModel = ongoingModel else { self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) @@ -104,6 +115,9 @@ public extension SwedbankPaySDK { } + /// 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) diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift index 879d0cf..b0cdc74 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift @@ -16,6 +16,7 @@ import Foundation public extension SwedbankPaySDK { + /// Native payment problem returned with `sdkProblemOccurred` enum NativePaymentProblem { case paymentSessionEndStateReached case paymentSessionAPIRequestFailed(error: Error, retry: ()->Void) diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift b/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift index d02a5a2..eebba97 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift @@ -41,12 +41,15 @@ public protocol SwedbankPaySDKDelegate: AnyObject { error: Error ) + /// Called whenever the payment has been completed. func paymentComplete() + + /// Called whenever the payment has been canceled for any reason. func paymentCanceled() /// Called when an list of available instruments is known. /// - /// - parameter availableInstruments: List of different instruments + /// - parameter availableInstruments: List of different instruments that is available to be used for the payment session. func availableInstrumentsFetched(_ availableInstruments: [SwedbankPaySDK.AvailableInstrument]) /// Called if there is an error in performing the payment. From 068b1d7f2cdcff989cf48c3cfa40bc00c2954849 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Fri, 31 May 2024 09:57:30 +0200 Subject: [PATCH 24/75] Code Review Fixes --- SwedbankPaySDK.xcodeproj/project.pbxproj | 6 ++++ .../Classes/Api/Models/MethodBaseModel.swift | 4 +-- .../Classes/Api/Models/ProblemDetails.swift | 1 + .../NativePayment.swift | 36 ++++++++++--------- .../NativePaymentProblem.swift | 9 +++++ .../Classes/SwedbankPaySDKController.swift | 22 ++---------- .../SwedbankPaySDKNativePaymentDelegate.swift | 31 ++++++++++++++++ 7 files changed, 70 insertions(+), 39 deletions(-) create mode 100644 SwedbankPaySDK/Classes/SwedbankPaySDKNativePaymentDelegate.swift diff --git a/SwedbankPaySDK.xcodeproj/project.pbxproj b/SwedbankPaySDK.xcodeproj/project.pbxproj index aa6d3be..db2b2bd 100644 --- a/SwedbankPaySDK.xcodeproj/project.pbxproj +++ b/SwedbankPaySDK.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ 4517C6A32BFF928B001687E7 /* BeaconService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4517C6A22BFF928B001687E7 /* BeaconService.swift */; }; 456900812BFB9AA5009475A5 /* NativePaymentProblem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 456900802BFB9AA5009475A5 /* NativePaymentProblem.swift */; }; 456900822BFB9AA5009475A5 /* NativePaymentProblem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 456900802BFB9AA5009475A5 /* NativePaymentProblem.swift */; }; + 457C9C5F2C08C27C00420B4F /* SwedbankPaySDKNativePaymentDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKNativePaymentDelegate.swift */; }; + 457C9C602C08C2F900420B4F /* SwedbankPaySDKNativePaymentDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKNativePaymentDelegate.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 */; }; @@ -286,6 +288,7 @@ 4517C69F2BFF6B7E001687E7 /* BeaconEndpointRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeaconEndpointRouter.swift; sourceTree = ""; }; 4517C6A22BFF928B001687E7 /* BeaconService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeaconService.swift; sourceTree = ""; }; 456900802BFB9AA5009475A5 /* NativePaymentProblem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePaymentProblem.swift; sourceTree = ""; }; + 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKNativePaymentDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwedbankPaySDKNativePaymentDelegate.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 = ""; }; @@ -803,6 +806,7 @@ C50CF6D2237AC76A003F79DF /* Utils */, C585AF27237066EE006C2E16 /* SwedbankPaySDK.swift */, C585AF2A237066EE006C2E16 /* SwedbankPaySDKController.swift */, + 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKNativePaymentDelegate.swift */, C585AF28237066EE006C2E16 /* SwedbankPaySDKViewModel.swift */, 45C1008A2BF247F300AA3523 /* Api */, A5808B03261C93FD00FF7EC1 /* WebView */, @@ -1151,6 +1155,7 @@ A59AEE9D25372C9A00255A3A /* Instrument.swift in Sources */, C585AF2F237066EE006C2E16 /* SwedbankPaySDKController.swift in Sources */, 45B4495B2C05F34F00A1F46D /* BeaconType.swift in Sources */, + 457C9C5F2C08C27C00420B4F /* SwedbankPaySDKNativePaymentDelegate.swift in Sources */, 45C100942BF247F300AA3523 /* SwedbankPayAPIEnpointRouter.swift in Sources */, D50C9B4D27E36EBB00FCE33D /* VersionReporter.swift in Sources */, 45C100902BF247F300AA3523 /* SwedbankPayAPIError.swift in Sources */, @@ -1279,6 +1284,7 @@ D511EC7A2975591B0059AB05 /* ConfigurationAsync.swift in Sources */, D511EC7B2975591B0059AB05 /* SwedbankPaySDKResources.swift in Sources */, D511EC7C2975591B0059AB05 /* TypeAliases.swift in Sources */, + 457C9C602C08C2F900420B4F /* SwedbankPaySDKNativePaymentDelegate.swift in Sources */, 45C100932BF247F300AA3523 /* OperationOutputModel.swift in Sources */, D511EC7D2975591B0059AB05 /* SwedbankPayExtraWebViewController.swift in Sources */, 45C100B02BF2622E00AA3523 /* TimeZone+OffsetFromGMT.swift in Sources */, diff --git a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift index 05abc94..3e00083 100644 --- a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift @@ -105,8 +105,8 @@ extension SwedbankPaySDK { /// Prefill information for Swish payment. public struct SwishMethodPrefillModel: Codable, Hashable { - let rank: Int32? - public let msisdn: String? + public let rank: Int32 + public let msisdn: String } public struct CreditCardMethodPrefillModel: Codable, Hashable { diff --git a/SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift b/SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift index 7abad5b..a7c3d7b 100644 --- a/SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift +++ b/SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift @@ -16,6 +16,7 @@ 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? diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index 2a4ac4e..dd85361 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -22,8 +22,8 @@ public extension SwedbankPaySDK { /// Order information that provides `NativePayment` with callback URLs. public var orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo - /// A delegate to receive callbacks as the state of SwedbankPaySDKController changes. - public weak var delegate: SwedbankPaySDKDelegate? + /// A delegate to receive callbacks as the native payment changes. + public weak var delegate: SwedbankPaySDKNativePaymentDelegate? private var ongoingModel: PaymentOutputModel? = nil private var sessionIsOngoing: Bool = false @@ -50,7 +50,7 @@ public extension SwedbankPaySDK { /// 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 startPaymentSession(with sessionURL: String) { + public func startPaymentSession(sessionURL: URL) { sessionIsOngoing = true instrument = nil ongoingModel = nil @@ -59,7 +59,7 @@ public extension SwedbankPaySDK { hasShownAvailableInstruments = false let model = OperationOutputModel(rel: nil, - href: sessionURL, + href: sessionURL.absoluteString, method: "GET", next: nil, tasks: nil) @@ -77,14 +77,14 @@ public extension SwedbankPaySDK { /// /// There needs to be an active payment session before an payment attempt can be made. /// - /// - parameter with instrument: Payment attempt instrument - public func makePaymentAttempt(with instrument: SwedbankPaySDK.PaymentAttemptInstrument) { + /// - parameter instrument: Payment attempt instrument + public func makePaymentAttempt(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": "internalInconsistencyError"])) + values: ["problem": SwedbankPaySDK.NativePaymentProblem.internalInconsistencyError.rawValue])) return } @@ -104,12 +104,12 @@ public extension SwedbankPaySDK { case .swish(let msisdn): BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", succeeded: succeeded, - values: ["instrument": "Swish", + values: ["instrument": instrument.name, "msisdn": msisdn])) case .creditCard(let paymentToken): BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", succeeded: succeeded, - values: ["instrument": "CreditCard", + values: ["instrument": instrument.name, "paymentToken": paymentToken])) } @@ -124,7 +124,7 @@ public extension SwedbankPaySDK { BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", succeeded: self.delegate != nil, - values: ["problem": "internalInconsistencyError"])) + values: ["problem": SwedbankPaySDK.NativePaymentProblem.internalInconsistencyError.rawValue])) return } @@ -155,17 +155,19 @@ public extension SwedbankPaySDK { } case .failure(let failure): DispatchQueue.main.async { - self.delegate?.sdkProblemOccurred(problem: .paymentSessionAPIRequestFailed(error: failure, - retry: { + let problem = SwedbankPaySDK.NativePaymentProblem.paymentSessionAPIRequestFailed(error: failure, + retry: { self.sessionStartTimestamp = Date() self.makeRequest(model: model, culture: culture) - })) + }) + + self.delegate?.sdkProblemOccurred(problem: problem) let error = failure as NSError BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", succeeded: self.delegate != nil, - values: ["problem": "paymentSessionAPIRequestFailed", + values: ["problem": problem.rawValue, "errorDescription": error.localizedDescription, "errorCode": error.code, "errorDomain": error.domain])) @@ -180,7 +182,7 @@ public extension SwedbankPaySDK { BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", succeeded: self.delegate != nil, - values: ["problem": "internalInconsistencyError"])) + values: ["problem": SwedbankPaySDK.NativePaymentProblem.internalInconsistencyError.rawValue])) return } @@ -205,7 +207,7 @@ public extension SwedbankPaySDK { BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", succeeded: self.delegate != nil, - values: ["problem": "clientAppLaunchFailed"])) + values: ["problem": SwedbankPaySDK.NativePaymentProblem.clientAppLaunchFailed.rawValue])) } BeaconService.shared.log(type: .launchClientApp(values: ["callbackUrl": self.orderInfo.paymentUrl?.absoluteString ?? "", @@ -307,7 +309,7 @@ public extension SwedbankPaySDK { BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", succeeded: self.delegate != nil, - values: ["problem": "paymentSessionEndStateReached"])) + values: ["problem": SwedbankPaySDK.NativePaymentProblem.paymentSessionEndStateReached.rawValue])) } } } diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift index b0cdc74..4ce1206 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift @@ -22,5 +22,14 @@ public extension SwedbankPaySDK { case paymentSessionAPIRequestFailed(error: Error, retry: ()->Void) case clientAppLaunchFailed case internalInconsistencyError + + var rawValue: String { + switch self { + case .paymentSessionEndStateReached: "paymentSessionEndStateReached" + case .paymentSessionAPIRequestFailed: "paymentSessionAPIRequestFailed" + case .clientAppLaunchFailed: "clientAppLaunchFailed" + case .internalInconsistencyError: "internalInconsistencyError" + } + } } } diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift b/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift index eebba97..325728a 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift @@ -41,17 +41,9 @@ public protocol SwedbankPaySDKDelegate: AnyObject { error: Error ) - /// Called whenever the payment has been completed. func paymentComplete() - - /// Called whenever the payment has been canceled for any reason. func paymentCanceled() - - /// 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 availableInstrumentsFetched(_ availableInstruments: [SwedbankPaySDK.AvailableInstrument]) - + /// Called if there is an error in performing the payment. /// The error may be SwedbankPaySDKController.WebContentError, /// or any error reported by your SwedbankPaySDKConfiguration. @@ -63,17 +55,7 @@ public protocol SwedbankPaySDKDelegate: AnyObject { /// /// - parameter error: The error that caused the failure func paymentFailed(error: Error) - - /// 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.NativePaymentProblem) - + /// Called when the user taps on the Terms of Service Link /// in the Payment Menu. /// diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDKNativePaymentDelegate.swift b/SwedbankPaySDK/Classes/SwedbankPaySDKNativePaymentDelegate.swift new file mode 100644 index 0000000..de94ddd --- /dev/null +++ b/SwedbankPaySDK/Classes/SwedbankPaySDKNativePaymentDelegate.swift @@ -0,0 +1,31 @@ +// +// SwedbankPaySDKNativePaymentDelegate.swift +// SwedbankPaySDK +// +// Created by Michael Balsiger on 2024-05-30. +// Copyright © 2024 Swedbank. All rights reserved. +// + +/// Swedbank Pay SDK protocol, conform to this to get the result of the payment process +public protocol SwedbankPaySDKNativePaymentDelegate: AnyObject { + /// Called whenever the payment has been completed. + func paymentComplete() + + /// Called whenever the payment has been canceled for any reason. + func paymentCanceled() + + /// 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 availableInstrumentsFetched(_ 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.NativePaymentProblem) +} From 6fde7346800bc36fe188f1e0aa338dc6d7f18335 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Tue, 4 Jun 2024 13:43:37 +0200 Subject: [PATCH 25/75] SP-43 Example app improvements --- .../SwedbankPaySDK+Extensions/NativePayment.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index dd85361..f049f7d 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -221,11 +221,15 @@ public extension SwedbankPaySDK { private func sessionOperationHandling(model: PaymentOutputModel, culture: String? = nil) { ongoingModel = model + var hasShowedError = false + if let modelProblem = model.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) @@ -270,6 +274,12 @@ public extension SwedbankPaySDK { BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentComplete", succeeded: self.delegate != nil, values: nil)) + } else { + self.delegate?.sdkProblemOccurred(problem: .paymentSessionEndStateReached) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": SwedbankPaySDK.NativePaymentProblem.paymentSessionEndStateReached.rawValue])) } } sessionIsOngoing = false @@ -303,7 +313,7 @@ public extension SwedbankPaySDK { self.sessionStartTimestamp = Date() self.makeRequest(model: getPayment, culture: culture) } - } else { + } else if !hasShowedError { DispatchQueue.main.async { self.delegate?.sdkProblemOccurred(problem: .paymentSessionEndStateReached) From ee6c7b3cdd72d0fd443254080bb8d0fa08eebd6e Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Wed, 5 Jun 2024 13:38:07 +0200 Subject: [PATCH 26/75] SP-45 CreditCard prefills and payment attempt --- SwedbankPaySDK.xcodeproj/project.pbxproj | 6 +++ .../Api/Helpers/CustomDateDecoder.swift | 39 ++++++++++++++ .../Classes/Api/Models/MethodBaseModel.swift | 37 ++++++++++++-- .../Api/Models/PaymentAttemptInstrument.swift | 2 +- .../Classes/Api/SwedbankPayAPIConstants.swift | 1 + .../Api/SwedbankPayAPIEnpointRouter.swift | 51 +++++++++++++++---- .../Classes/Beacon/BeaconEndpointRouter.swift | 2 +- .../NativePayment.swift | 11 ++-- 8 files changed, 129 insertions(+), 20 deletions(-) create mode 100644 SwedbankPaySDK/Classes/Api/Helpers/CustomDateDecoder.swift diff --git a/SwedbankPaySDK.xcodeproj/project.pbxproj b/SwedbankPaySDK.xcodeproj/project.pbxproj index db2b2bd..679ae00 100644 --- a/SwedbankPaySDK.xcodeproj/project.pbxproj +++ b/SwedbankPaySDK.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ 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 */; }; 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 */; }; @@ -285,6 +287,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 4517A2492C10467A000BB7A8 /* CustomDateDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDateDecoder.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 = ""; }; 456900802BFB9AA5009475A5 /* NativePaymentProblem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePaymentProblem.swift; sourceTree = ""; }; @@ -522,6 +525,7 @@ children = ( 45C100AB2BF2622E00AA3523 /* NetworkStatusProvider.swift */, 45C100AC2BF2622E00AA3523 /* TimeZone+OffsetFromGMT.swift */, + 4517A2492C10467A000BB7A8 /* CustomDateDecoder.swift */, ); path = Helpers; sourceTree = ""; @@ -1172,6 +1176,7 @@ 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 */, @@ -1237,6 +1242,7 @@ 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 */, diff --git a/SwedbankPaySDK/Classes/Api/Helpers/CustomDateDecoder.swift b/SwedbankPaySDK/Classes/Api/Helpers/CustomDateDecoder.swift new file mode 100644 index 0000000..dd884b6 --- /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 { + 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/Models/MethodBaseModel.swift b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift index 3e00083..0e5a250 100644 --- a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Foundation + public enum MethodBaseModel: Codable, Equatable, Hashable { case swish(prefills: [SwedbankPaySDK.SwishMethodPrefillModel]?, operations: [OperationOutputModel]?) case creditCard(prefills: [SwedbankPaySDK.CreditCardMethodPrefillModel]?, operations: [OperationOutputModel]?, cardBrands: [String]?) @@ -95,10 +97,14 @@ extension SwedbankPaySDK { /// Swish native payment with a list of prefills case swish(prefills: [SwishMethodPrefillModel]?) + case creditCard(prefills: [CreditCardMethodPrefillModel]?) + var name: String { switch self { case .swish: return "Swish" + case .creditCard: + return "CreditCard" } } } @@ -109,11 +115,32 @@ extension SwedbankPaySDK { public let msisdn: String } + /// Prefill information for Credit Card payment. public struct CreditCardMethodPrefillModel: Codable, Hashable { - let rank: Int32? - let paymentToken: String? - let cardBrand: String? - let maskedPan: String? - let expiryDate: String? + 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/PaymentAttemptInstrument.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift index aefcaed..437981b 100644 --- a/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift @@ -17,7 +17,7 @@ extension SwedbankPaySDK { /// Instrument with needed values to make a payment attempt. public enum PaymentAttemptInstrument { case swish(msisdn: String?) - case creditCard(paymentToken: String) + case creditCard(prefill: CreditCardMethodPrefillModel) var name: String { switch self { diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift index 91c899e..c680243 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift @@ -21,6 +21,7 @@ struct SwedbankPayAPIConstants { static var requestTimeoutInterval = 10.0 static var sessionTimeoutInterval = 20.0 + static var creditCardTimoutInterval = 30.0 } private enum HTTPHeaderField: String { diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index b3bb871..b09e180 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -18,6 +18,8 @@ import UIKit protocol EndpointRouterProtocol { var body: [String: Any?]? { get } + var requestTimeoutInterval: TimeInterval { get } + var sessionTimeoutInterval: TimeInterval { get } } struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { @@ -41,9 +43,12 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { "screenWidth": String(Int32(UIScreen.main.nativeBounds.width)), "screenColorDepth": String(24)] ] - case .creditCard(let paymentToken): + case .creditCard(let prefill): return ["culture": culture, - "paymentToken": paymentToken, + "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)), @@ -78,6 +83,34 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { return nil } } + + var requestTimeoutInterval: TimeInterval { + switch model.rel { + case .startPaymentAttempt: + switch instrument { + case .creditCard: + return SwedbankPayAPIConstants.creditCardTimoutInterval + default: + return SwedbankPayAPIConstants.requestTimeoutInterval + } + default: + return SwedbankPayAPIConstants.requestTimeoutInterval + } + } + + var sessionTimeoutInterval: TimeInterval { + switch model.rel { + case .startPaymentAttempt: + switch instrument { + case .creditCard: + return SwedbankPayAPIConstants.creditCardTimoutInterval + default: + return SwedbankPayAPIConstants.sessionTimeoutInterval + } + default: + return SwedbankPayAPIConstants.sessionTimeoutInterval + } + } } extension SwedbankPayAPIEnpointRouter { @@ -104,7 +137,7 @@ extension SwedbankPayAPIEnpointRouter { let decodedData: T do { - decodedData = try JSONDecoder().decode(T.self, from: data) + decodedData = try CustomDateDecoder().decode(T.self, from: data) } catch { throw error } @@ -151,14 +184,14 @@ extension SwedbankPayAPIEnpointRouter { } BeaconService.shared.log(type: .httpRequest(duration: Int32((Date().timeIntervalSince(requestStartTimestamp) * 1000.0).rounded()), - requestUrl: model.href ?? "", - method: model.method ?? "", - responseStatusCode: responseStatusCode, - values: values)) + requestUrl: model.href ?? "", + method: model.method ?? "", + responseStatusCode: responseStatusCode, + values: values)) guard let data, let response = response as? HTTPURLResponse, !(500...599 ~= response.statusCode) else { - guard Date().timeIntervalSince(requestStartTimestamp) < SwedbankPayAPIConstants.requestTimeoutInterval && - Date().timeIntervalSince(sessionStartTimestamp) < SwedbankPayAPIConstants.sessionTimeoutInterval else { + guard Date().timeIntervalSince(requestStartTimestamp) < requestTimeoutInterval && + Date().timeIntervalSince(sessionStartTimestamp) < sessionTimeoutInterval else { handler(.failure(error ?? SwedbankPayAPIError.unknown)) return } diff --git a/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift b/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift index 15bde2c..a3f0822 100644 --- a/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift +++ b/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift @@ -20,7 +20,7 @@ protocol BeaconEndpointRouterProtocol { var body: [String: Any?]? { get } } -struct BeaconEndpointRouter: EndpointRouterProtocol { +struct BeaconEndpointRouter: BeaconEndpointRouterProtocol { let href: String? let beacon: Beacon diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index f049f7d..b31fb72 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -106,11 +106,14 @@ public extension SwedbankPaySDK { succeeded: succeeded, values: ["instrument": instrument.name, "msisdn": msisdn])) - case .creditCard(let paymentToken): + case .creditCard(let prefill): BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", succeeded: succeeded, values: ["instrument": instrument.name, - "paymentToken": paymentToken])) + "paymentToken": prefill.paymentToken, + "cardNumber": prefill.maskedPan, + "cardExpiryMonth": prefill.expiryMonth, + "cardExpiryYear": prefill.expiryYear])) } } @@ -293,8 +296,8 @@ public extension SwedbankPaySDK { switch model { case .swish(let prefills, _): return AvailableInstrument.swish(prefills: prefills) - case .creditCard(_, _, _): - return nil + case .creditCard(let prefills, _, _): + return AvailableInstrument.creditCard(prefills: prefills) case .unknown(_): return nil } From 1bd46468576bf2a7e2cb1c92da936a449fe28490 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Wed, 5 Jun 2024 16:37:45 +0200 Subject: [PATCH 27/75] SP-47 CreditCard authentication request --- .../Api/SwedbankPayAPIEnpointRouter.swift | 19 +++++++++++++++++++ .../NativePayment.swift | 2 ++ 2 files changed, 21 insertions(+) diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index b09e180..543236b 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -79,6 +79,20 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { "service": ["name": "SwedbankPaySDK-iOS", "version": SwedbankPaySDK.VersionReporter.currentVersion] ] + case .createAuthentication: + return ["methodCompletionIndicator": "Y", + "notificationUrl": "https://fake.payex.com/notification", + "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, + "timeZoneOffset": TimeZone.current.offsetFromGMT(), + "javascriptEnabled": true] + ] default: return nil } @@ -93,6 +107,8 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { default: return SwedbankPayAPIConstants.requestTimeoutInterval } + case .createAuthentication: + return SwedbankPayAPIConstants.creditCardTimoutInterval default: return SwedbankPayAPIConstants.requestTimeoutInterval } @@ -107,6 +123,8 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { default: return SwedbankPayAPIConstants.sessionTimeoutInterval } + case .createAuthentication: + return SwedbankPayAPIConstants.creditCardTimoutInterval default: return SwedbankPayAPIConstants.sessionTimeoutInterval } @@ -164,6 +182,7 @@ extension SwedbankPayAPIEnpointRouter { var request = URLRequest(url: url) request.httpMethod = model.method request.allHTTPHeaderFields = SwedbankPayAPIConstants.commonHeaders + request.timeoutInterval = requestTimeoutInterval if let body = body, let jsonData = try? JSONSerialization.data(withJSONObject: body) { request.httpBody = jsonData diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index b31fb72..e775c86 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -263,6 +263,8 @@ public extension SwedbankPaySDK { let tasks = launchClientApp.firstTask(with: .launchClientApp), !hasLaunchClientAppURLs.contains(where: { $0.absoluteString.contains(tasks.href ?? "") }) { self.launchClientApp(task: launchClientApp.firstTask(with: .launchClientApp)!) + } else if let createAuthentication = operations.first(where: { $0.rel == .createAuthentication }) { + makeRequest(model: createAuthentication, culture: culture) } else if let redirectPayer = operations.first(where: { $0.rel == .redirectPayer }) { DispatchQueue.main.async { if redirectPayer.href == self.orderInfo.cancelUrl?.absoluteString { From 4f9e7190a82671a0cf97eb9487ad2d48cc90d534 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Fri, 7 Jun 2024 14:14:12 +0200 Subject: [PATCH 28/75] SP-49 SCA Method Request --- SwedbankPaySDK.xcodeproj/project.pbxproj | 6 ++ .../NativePayment.swift | 13 +++++ .../Classes/WebView/SCAWebViewService.swift | 58 +++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift diff --git a/SwedbankPaySDK.xcodeproj/project.pbxproj b/SwedbankPaySDK.xcodeproj/project.pbxproj index 679ae00..9c3971b 100644 --- a/SwedbankPaySDK.xcodeproj/project.pbxproj +++ b/SwedbankPaySDK.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* 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 */; }; @@ -288,6 +290,7 @@ /* 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 = ""; }; 456900802BFB9AA5009475A5 /* NativePaymentProblem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePaymentProblem.swift; sourceTree = ""; }; @@ -659,6 +662,7 @@ A5808B04261C942A00FF7EC1 /* SwedbankPayWebViewControllerBase.swift */, A5808B08261C980000FF7EC1 /* SwedbankPayWebViewControllerDelegate.swift */, A5808B10261CA8B500FF7EC1 /* WKWebViewCanOpen.swift */, + 4517A24C2C1324AC000BB7A8 /* SCAWebViewService.swift */, ); path = WebView; sourceTree = ""; @@ -1186,6 +1190,7 @@ 45C100922BF247F300AA3523 /* OperationOutputModel.swift in Sources */, D581EFDF27A92A56001B85A7 /* VersionOptions.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 */, @@ -1309,6 +1314,7 @@ 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 */, diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index e775c86..e5f7832 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -32,6 +32,7 @@ public extension SwedbankPaySDK { private var hasLaunchClientAppURLs: [URL] = [] private var hasShownProblemDetails: [ProblemDetails] = [] + private var hasSCAMethodRequest: [URL] = [] private var sessionStartTimestamp = Date() @@ -263,6 +264,18 @@ public extension SwedbankPaySDK { let tasks = launchClientApp.firstTask(with: .launchClientApp), !hasLaunchClientAppURLs.contains(where: { $0.absoluteString.contains(tasks.href ?? "") }) { self.launchClientApp(task: launchClientApp.firstTask(with: .launchClientApp)!) + } else if let launchClientApp = operations.first(where: { $0.firstTask(with: .scaMethodRequest) != nil }), + let task = launchClientApp.firstTask(with: .scaMethodRequest), + !hasSCAMethodRequest.contains(where: { $0.absoluteString.contains(task.href ?? "") }) { // Should not be URL + let webViewService = SCAWebViewService() + webViewService.load(task: task) { result in + switch result { + case .success: + print("success") + case .failure(let error): + print("error \(error)") + } + } } else if let createAuthentication = operations.first(where: { $0.rel == .createAuthentication }) { makeRequest(model: createAuthentication, culture: culture) } else if let redirectPayer = operations.first(where: { $0.rel == .redirectPayer }) { diff --git a/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift new file mode 100644 index 0000000..cd5bfbb --- /dev/null +++ b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift @@ -0,0 +1,58 @@ +// +// SCAWebViewService.swift +// SwedbankPaySDK +// +// Created by Michael Balsiger on 2024-06-07. +// Copyright © 2024 Swedbank. All rights reserved. +// + +import Foundation +import WebKit + +class SCAWebViewService: NSObject, WKNavigationDelegate { + let webView: WKWebView + + var handler: ((Result) -> Void)? + + override init() { + self.webView = WKWebView() + + super.init() + + self.webView.navigationDelegate = self + } + + func load(task: IntegrationTask, handler: @escaping (Result) -> Void) { + self.webView.stopLoading() + self.handler = handler + + var request = URLRequest(url: URL(string: task.href!)!) + request.httpMethod = task.method + request.allHTTPHeaderFields = ["Content-Type": task.contentType ?? ""] + request.timeoutInterval = 30 + + var body: [String: Any?] = [:] + + if let expects = task.expects { + for expect in expects { + if let name = expect.name { + body[name] = expect.value + } + } + } + + if let jsonData = try? JSONSerialization.data(withJSONObject: body) { + request.httpBody = jsonData + } + + self.webView.load(request) + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + self.handler?(.success(())) + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + self.handler?(.failure(error)) + } +} From d0c4254e50553e7a1dd48d7014e9720d02a1c3ed Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Tue, 11 Jun 2024 11:33:26 +0200 Subject: [PATCH 29/75] WKNavigationResponse --- .../Classes/Api/Models/IntegrationTask.swift | 1 + .../Api/SwedbankPayAPIEnpointRouter.swift | 4 +- .../NativePayment.swift | 49 ++++++++++----- .../Classes/WebView/SCAWebViewService.swift | 60 ++++++++++++++----- 4 files changed, 83 insertions(+), 31 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift b/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift index cf90b3b..82a927f 100644 --- a/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift +++ b/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift @@ -57,5 +57,6 @@ enum IntegrationTaskRel: Codable, Equatable, Hashable { struct ExpectationModel: Codable, Hashable { let name: String? + let type: String? let value: String? } diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index 543236b..381998b 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -26,6 +26,8 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { let model: OperationOutputModel let culture: String? let instrument: SwedbankPaySDK.PaymentAttemptInstrument? + let methodCompletionIndicator: String? + let sessionStartTimestamp: Date var body: [String: Any?]? { @@ -80,7 +82,7 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { "version": SwedbankPaySDK.VersionReporter.currentVersion] ] case .createAuthentication: - return ["methodCompletionIndicator": "Y", + return ["methodCompletionIndicator": methodCompletionIndicator ?? "N", "notificationUrl": "https://fake.payex.com/notification", "requestWindowSize": "FULLSCREEN", "client": ["userAgent": SwedbankPaySDK.VersionReporter.userAgent, diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index e5f7832..7dcaf8a 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -17,6 +17,12 @@ import Foundation import UIKit public extension SwedbankPaySDK { + + struct SCAMethod { + var value: String + var result: String + } + /// Object that handles native payments class NativePayment: CallbackUrlDelegate { /// Order information that provides `NativePayment` with callback URLs. @@ -32,10 +38,12 @@ public extension SwedbankPaySDK { private var hasLaunchClientAppURLs: [URL] = [] private var hasShownProblemDetails: [ProblemDetails] = [] - private var hasSCAMethodRequest: [URL] = [] + private var scaMethodRequestDataPerformed: [SCAMethod] = [] private var sessionStartTimestamp = Date() + private var webViewService = SCAWebViewService() + public init(orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo) { self.orderInfo = orderInfo @@ -146,8 +154,12 @@ public extension SwedbankPaySDK { values: nil)) } - private func makeRequest(model: OperationOutputModel, culture: String? = nil) { - SwedbankPayAPIEnpointRouter(model: model, culture: culture, instrument: instrument, sessionStartTimestamp: sessionStartTimestamp).makeRequest { result in + private func makeRequest(model: OperationOutputModel, culture: String? = nil, methodCompletionIndicator: String? = nil) { + SwedbankPayAPIEnpointRouter(model: model, + culture: culture, + instrument: instrument, + methodCompletionIndicator: methodCompletionIndicator, + sessionStartTimestamp: sessionStartTimestamp).makeRequest { result in switch result { case .success(let success): if let model = success { @@ -264,20 +276,27 @@ public extension SwedbankPaySDK { let tasks = launchClientApp.firstTask(with: .launchClientApp), !hasLaunchClientAppURLs.contains(where: { $0.absoluteString.contains(tasks.href ?? "") }) { self.launchClientApp(task: launchClientApp.firstTask(with: .launchClientApp)!) - } else if let launchClientApp = operations.first(where: { $0.firstTask(with: .scaMethodRequest) != nil }), - let task = launchClientApp.firstTask(with: .scaMethodRequest), - !hasSCAMethodRequest.contains(where: { $0.absoluteString.contains(task.href ?? "") }) { // Should not be URL - let webViewService = SCAWebViewService() - webViewService.load(task: task) { result in - switch result { - case .success: - print("success") - case .failure(let error): - print("error \(error)") + } else if let scaMethodRequest = operations.first(where: { $0.firstTask(with: .scaMethodRequest) != nil }), + let task = scaMethodRequest.firstTask(with: .scaMethodRequest), + !scaMethodRequestDataPerformed.contains(where: { $0.value == task.expects?.first(where: { $0.name == "ThreeDsMethodData" })?.value }) { + DispatchQueue.main.async { + self.webViewService.load(task: task) { result in + switch result { + case .success: + self.scaMethodRequestDataPerformed.append(SCAMethod(value: task.expects?.first(where: { $0.name == "ThreeDsMethodData" })?.value ?? "", result: "Y")) + case .failure(let error): + self.scaMethodRequestDataPerformed.append(SCAMethod(value: task.expects?.first(where: { $0.name == "ThreeDsMethodData" })?.value ?? "", result: "N")) + } + + if let model = self.ongoingModel { + self.sessionOperationHandling(model: model, culture: culture) + } } } - } else if let createAuthentication = operations.first(where: { $0.rel == .createAuthentication }) { - makeRequest(model: createAuthentication, culture: culture) + } else if let createAuthentication = operations.first(where: { $0.rel == .createAuthentication }), + let task = createAuthentication.firstTask(with: .scaMethodRequest), + let scaMethod = scaMethodRequestDataPerformed.first(where: { $0.value == task.expects?.first(where: { $0.name == "ThreeDsMethodData" })?.value }) { + makeRequest(model: createAuthentication, culture: culture, methodCompletionIndicator: scaMethod.result) } else if let redirectPayer = operations.first(where: { $0.rel == .redirectPayer }) { DispatchQueue.main.async { if redirectPayer.href == self.orderInfo.cancelUrl?.absoluteString { diff --git a/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift index cd5bfbb..ea7eba7 100644 --- a/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift +++ b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift @@ -10,32 +10,39 @@ import Foundation import WebKit class SCAWebViewService: NSObject, WKNavigationDelegate { - let webView: WKWebView - var handler: ((Result) -> Void)? - override init() { - self.webView = WKWebView() + private var webView: WKWebView? - super.init() + func load(task: IntegrationTask, handler: @escaping (Result) -> Void) { + guard let taskHref = task.href, + let url = URL(string: taskHref) else { + handler(.failure(SwedbankPayAPIError.invalidUrl)) - self.webView.navigationDelegate = self - } + return + } + + webView?.stopLoading() + webView = nil + + let preferences = WKPreferences() + preferences.javaScriptEnabled = true + let configuration = WKWebViewConfiguration() + configuration.preferences = preferences + webView = WKWebView(frame: .zero, configuration: configuration) - func load(task: IntegrationTask, handler: @escaping (Result) -> Void) { - self.webView.stopLoading() self.handler = handler - var request = URLRequest(url: URL(string: task.href!)!) + var request = URLRequest(url: url) request.httpMethod = task.method request.allHTTPHeaderFields = ["Content-Type": task.contentType ?? ""] - request.timeoutInterval = 30 + request.timeoutInterval = 5 var body: [String: Any?] = [:] if let expects = task.expects { for expect in expects { - if let name = expect.name { + if expect.type == "string", let name = expect.name { body[name] = expect.value } } @@ -45,14 +52,37 @@ class SCAWebViewService: NSObject, WKNavigationDelegate { request.httpBody = jsonData } - self.webView.load(request) + webView?.navigationDelegate = self + webView?.load(request) } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - self.handler?(.success(())) + handler?(.success(())) + self.webView?.stopLoading() + self.webView = nil } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - self.handler?(.failure(error)) + handler?(.failure(error)) + self.webView?.stopLoading() + self.webView = nil + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + handler?(.failure(error)) + 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) } } From 2f02b0200f1ed58da988e0c7c0e46fdf3ecf317b Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Thu, 13 Jun 2024 14:34:17 +0200 Subject: [PATCH 30/75] WIP: SwedbankPaySCAWebViewController --- SwedbankPaySDK.xcodeproj/project.pbxproj | 6 + .../Classes/Api/SwedbankPayAPIConstants.swift | 2 + .../Api/SwedbankPayAPIEnpointRouter.swift | 22 +- .../NativePayment.swift | 74 ++++-- .../SwedbankPaySDKNativePaymentDelegate.swift | 6 + .../Classes/WebView/SCAWebViewService.swift | 2 +- .../SwedbankPaySCAWebViewController.swift | 224 ++++++++++++++++++ 7 files changed, 317 insertions(+), 19 deletions(-) create mode 100644 SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift diff --git a/SwedbankPaySDK.xcodeproj/project.pbxproj b/SwedbankPaySDK.xcodeproj/project.pbxproj index 9c3971b..a4dbeca 100644 --- a/SwedbankPaySDK.xcodeproj/project.pbxproj +++ b/SwedbankPaySDK.xcodeproj/project.pbxproj @@ -18,6 +18,8 @@ 456900822BFB9AA5009475A5 /* NativePaymentProblem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 456900802BFB9AA5009475A5 /* NativePaymentProblem.swift */; }; 457C9C5F2C08C27C00420B4F /* SwedbankPaySDKNativePaymentDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKNativePaymentDelegate.swift */; }; 457C9C602C08C2F900420B4F /* SwedbankPaySDKNativePaymentDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKNativePaymentDelegate.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 */; }; @@ -295,6 +297,7 @@ 4517C6A22BFF928B001687E7 /* BeaconService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeaconService.swift; sourceTree = ""; }; 456900802BFB9AA5009475A5 /* NativePaymentProblem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePaymentProblem.swift; sourceTree = ""; }; 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKNativePaymentDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwedbankPaySDKNativePaymentDelegate.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 = ""; }; @@ -663,6 +666,7 @@ A5808B08261C980000FF7EC1 /* SwedbankPayWebViewControllerDelegate.swift */, A5808B10261CA8B500FF7EC1 /* WKWebViewCanOpen.swift */, 4517A24C2C1324AC000BB7A8 /* SCAWebViewService.swift */, + 45B429932C18687100620A0A /* SwedbankPaySCAWebViewController.swift */, ); path = WebView; sourceTree = ""; @@ -1189,6 +1193,7 @@ 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 */, @@ -1296,6 +1301,7 @@ D511EC7B2975591B0059AB05 /* SwedbankPaySDKResources.swift in Sources */, D511EC7C2975591B0059AB05 /* TypeAliases.swift in Sources */, 457C9C602C08C2F900420B4F /* SwedbankPaySDKNativePaymentDelegate.swift in Sources */, + 45B429952C18687100620A0A /* SwedbankPaySCAWebViewController.swift in Sources */, 45C100932BF247F300AA3523 /* OperationOutputModel.swift in Sources */, D511EC7D2975591B0059AB05 /* SwedbankPayExtraWebViewController.swift in Sources */, 45C100B02BF2622E00AA3523 /* TimeZone+OffsetFromGMT.swift in Sources */, diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift index c680243..db0cb3a 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift @@ -22,6 +22,8 @@ struct SwedbankPayAPIConstants { static var requestTimeoutInterval = 10.0 static var sessionTimeoutInterval = 20.0 static var creditCardTimoutInterval = 30.0 + + static var notificationUrl = "https://fake.payex.com/notification" } private enum HTTPHeaderField: String { diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index 381998b..c5f7521 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -27,6 +27,7 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { let culture: String? let instrument: SwedbankPaySDK.PaymentAttemptInstrument? let methodCompletionIndicator: String? + let cRes: String? let sessionStartTimestamp: Date @@ -83,7 +84,7 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { ] case .createAuthentication: return ["methodCompletionIndicator": methodCompletionIndicator ?? "N", - "notificationUrl": "https://fake.payex.com/notification", + "notificationUrl": SwedbankPayAPIConstants.notificationUrl, "requestWindowSize": "FULLSCREEN", "client": ["userAgent": SwedbankPaySDK.VersionReporter.userAgent, "ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? "", @@ -95,6 +96,11 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { "timeZoneOffset": TimeZone.current.offsetFromGMT(), "javascriptEnabled": true] ] + case .completeAuthentication: + return ["cRes": cRes ?? "", + "client": ["userAgent": SwedbankPaySDK.VersionReporter.userAgent, + "ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? ""], + ] default: return nil } @@ -109,7 +115,8 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { default: return SwedbankPayAPIConstants.requestTimeoutInterval } - case .createAuthentication: + case .createAuthentication, + .completeAuthentication: return SwedbankPayAPIConstants.creditCardTimoutInterval default: return SwedbankPayAPIConstants.requestTimeoutInterval @@ -125,7 +132,8 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { default: return SwedbankPayAPIConstants.sessionTimeoutInterval } - case .createAuthentication: + case .createAuthentication, + .completeAuthentication: return SwedbankPayAPIConstants.creditCardTimoutInterval default: return SwedbankPayAPIConstants.sessionTimeoutInterval @@ -210,7 +218,7 @@ extension SwedbankPayAPIEnpointRouter { responseStatusCode: responseStatusCode, values: values)) - guard let data, let response = response as? HTTPURLResponse, !(500...599 ~= response.statusCode) else { + 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)) @@ -226,6 +234,12 @@ extension SwedbankPayAPIEnpointRouter { return } + guard let data, 200...204 ~= response.statusCode else { + handler(.failure(error ?? SwedbankPayAPIError.unknown)) + + return + } + handler(.success(data)) }.resume() } diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index 7dcaf8a..30ff80d 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -15,14 +15,9 @@ import Foundation import UIKit +import WebKit public extension SwedbankPaySDK { - - struct SCAMethod { - var value: String - var result: String - } - /// Object that handles native payments class NativePayment: CallbackUrlDelegate { /// Order information that provides `NativePayment` with callback URLs. @@ -38,11 +33,13 @@ public extension SwedbankPaySDK { private var hasLaunchClientAppURLs: [URL] = [] private var hasShownProblemDetails: [ProblemDetails] = [] - private var scaMethodRequestDataPerformed: [SCAMethod] = [] + private var scaMethodRequestDataPerformed: [ExpectationModel] = [] + private var scaRedirectDataPerformed: [ExpectationModel] = [] private var sessionStartTimestamp = Date() private var webViewService = SCAWebViewService() + private lazy var webViewController = SwedbankPaySCAWebViewController() public init(orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo) { self.orderInfo = orderInfo @@ -65,6 +62,8 @@ public extension SwedbankPaySDK { ongoingModel = nil hasLaunchClientAppURLs = [] hasShownProblemDetails = [] + scaMethodRequestDataPerformed = [] + scaRedirectDataPerformed = [] hasShownAvailableInstruments = false let model = OperationOutputModel(rel: nil, @@ -107,6 +106,16 @@ public extension SwedbankPaySDK { sessionStartTimestamp = Date() makeRequest(model: operation, culture: ongoingModel.paymentSession.culture) succeeded = true + } else { + DispatchQueue.main.async { + self.delegate?.sdkProblemOccurred(problem: .paymentSessionEndStateReached) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": SwedbankPaySDK.NativePaymentProblem.paymentSessionEndStateReached.rawValue])) + } + + return } switch instrument { @@ -154,11 +163,12 @@ public extension SwedbankPaySDK { values: nil)) } - private func makeRequest(model: OperationOutputModel, culture: String? = nil, methodCompletionIndicator: String? = nil) { + private func makeRequest(model: OperationOutputModel, culture: String? = nil, methodCompletionIndicator: String? = nil, cRes: String? = nil) { SwedbankPayAPIEnpointRouter(model: model, culture: culture, instrument: instrument, methodCompletionIndicator: methodCompletionIndicator, + cRes: cRes, sessionStartTimestamp: sessionStartTimestamp).makeRequest { result in switch result { case .success(let success): @@ -174,7 +184,11 @@ public extension SwedbankPaySDK { let problem = SwedbankPaySDK.NativePaymentProblem.paymentSessionAPIRequestFailed(error: failure, retry: { self.sessionStartTimestamp = Date() - self.makeRequest(model: model, culture: culture) + self.makeRequest(model: model, + culture: culture, + methodCompletionIndicator: + methodCompletionIndicator, + cRes: cRes) }) self.delegate?.sdkProblemOccurred(problem: problem) @@ -278,14 +292,14 @@ public extension SwedbankPaySDK { self.launchClientApp(task: launchClientApp.firstTask(with: .launchClientApp)!) } else if let scaMethodRequest = operations.first(where: { $0.firstTask(with: .scaMethodRequest) != nil }), let task = scaMethodRequest.firstTask(with: .scaMethodRequest), - !scaMethodRequestDataPerformed.contains(where: { $0.value == task.expects?.first(where: { $0.name == "ThreeDsMethodData" })?.value }) { + !scaMethodRequestDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "ThreeDsMethodData" })?.value }) { DispatchQueue.main.async { self.webViewService.load(task: task) { result in switch result { case .success: - self.scaMethodRequestDataPerformed.append(SCAMethod(value: task.expects?.first(where: { $0.name == "ThreeDsMethodData" })?.value ?? "", result: "Y")) + self.scaMethodRequestDataPerformed.append(ExpectationModel(name: task.expects?.first(where: { $0.name == "ThreeDsMethodData" })?.value ?? "", type: "string", value: "Y")) case .failure(let error): - self.scaMethodRequestDataPerformed.append(SCAMethod(value: task.expects?.first(where: { $0.name == "ThreeDsMethodData" })?.value ?? "", result: "N")) + self.scaMethodRequestDataPerformed.append(ExpectationModel(name: task.expects?.first(where: { $0.name == "ThreeDsMethodData" })?.value ?? "", type: "string", value: "N")) } if let model = self.ongoingModel { @@ -295,8 +309,38 @@ public extension SwedbankPaySDK { } } else if let createAuthentication = operations.first(where: { $0.rel == .createAuthentication }), let task = createAuthentication.firstTask(with: .scaMethodRequest), - let scaMethod = scaMethodRequestDataPerformed.first(where: { $0.value == task.expects?.first(where: { $0.name == "ThreeDsMethodData" })?.value }) { - makeRequest(model: createAuthentication, culture: culture, methodCompletionIndicator: scaMethod.result) + let scaMethod = scaMethodRequestDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "ThreeDsMethodData" })?.value }) { + makeRequest(model: createAuthentication, culture: culture, methodCompletionIndicator: scaMethod.value) + } else if let operation = operations.first(where: { $0.firstTask(with: .scaRedirect) != nil }), + let task = operation.firstTask(with: .scaRedirect), + !scaRedirectDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "CReq" })?.value }) { + DispatchQueue.main.async { + self.delegate?.showViewController(viewController: self.webViewController) + + self.webViewController.load(task: task) { result in + switch result { + case .success(let value): + if !self.scaRedirectDataPerformed.contains(where: { $0.value == value }) { + print("success \(value)") + self.scaRedirectDataPerformed.append(ExpectationModel(name: task.expects!.first(where: { $0.name == "CReq" })!.value!, type: "string", value: value)) + + self.delegate?.finishedWithViewController() + } else { + print("success old") + } + case .failure(let error): + print("success \(error)") + } + + if let model = self.ongoingModel { + self.sessionOperationHandling(model: model, culture: culture) + } + } + } + } else if let completeAuthentication = operations.first(where: { $0.rel == .completeAuthentication }), + let task = completeAuthentication.tasks?.first(where: { $0.expects?.contains(where: { $0.name == "CReq" } ) ?? false } ), + let scaRedirect = scaRedirectDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "CReq" })?.value }) { + makeRequest(model: completeAuthentication, culture: culture, cRes: scaRedirect.value) } else if let redirectPayer = operations.first(where: { $0.rel == .redirectPayer }) { DispatchQueue.main.async { if redirectPayer.href == self.orderInfo.cancelUrl?.absoluteString { @@ -322,6 +366,8 @@ public extension SwedbankPaySDK { sessionIsOngoing = false hasLaunchClientAppURLs = [] hasShownProblemDetails = [] + scaMethodRequestDataPerformed = [] + scaRedirectDataPerformed = [] hasShownAvailableInstruments = false } else if (operations.contains(where: { $0.rel == .expandMethod }) || operations.contains(where: { $0.rel == .startPaymentAttempt })) && hasShownAvailableInstruments == false { diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDKNativePaymentDelegate.swift b/SwedbankPaySDK/Classes/SwedbankPaySDKNativePaymentDelegate.swift index de94ddd..6797382 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDKNativePaymentDelegate.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDKNativePaymentDelegate.swift @@ -6,6 +6,8 @@ // Copyright © 2024 Swedbank. All rights reserved. // +import UIKit + /// Swedbank Pay SDK protocol, conform to this to get the result of the payment process public protocol SwedbankPaySDKNativePaymentDelegate: AnyObject { /// Called whenever the payment has been completed. @@ -28,4 +30,8 @@ public protocol SwedbankPaySDKNativePaymentDelegate: AnyObject { /// /// - parameter problem: The problem that caused the failure func sdkProblemOccurred(problem: SwedbankPaySDK.NativePaymentProblem) + + func showViewController(viewController: UIViewController) + + func finishedWithViewController() } diff --git a/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift index ea7eba7..353e2c8 100644 --- a/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift +++ b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift @@ -10,7 +10,7 @@ import Foundation import WebKit class SCAWebViewService: NSObject, WKNavigationDelegate { - var handler: ((Result) -> Void)? + private var handler: ((Result) -> Void)? private var webView: WKWebView? diff --git a/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift new file mode 100644 index 0000000..fa60282 --- /dev/null +++ b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift @@ -0,0 +1,224 @@ +// +// 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 +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 + + 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 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + self.view = webView + } + + func load(task: IntegrationTask, handler: @escaping (Result) -> Void) { + guard let taskHref = task.href, + let url = URL(string: "https://firebasestorage.googleapis.com/v0/b/consid-beta.appspot.com/o/fake-3ds.html?alt=media") else { + return + } + + self.handler = handler + + var request = URLRequest(url: url) +// request.httpMethod = task.method +// request.allHTTPHeaderFields = ["Content-Type": task.contentType ?? ""] +// +// var body: [String: Any?] = [:] +// +// if let expects = task.expects { +// for expect in expects { +// if expect.type == "string", let name = expect.name { +// body[name] = expect.value +// } +// } +// } + +// if let jsonData = try? JSONSerialization.data(withJSONObject: body) { +// request.httpBody = jsonData +// } + + let navigation = webView.load(request) + + lastRootPage = (navigation, url) + } +} + +extension SwedbankPaySCAWebViewController: WKNavigationDelegate { + + func webView( + _ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void + ) { + let request = navigationAction.request + + if request.url?.absoluteString == SwedbankPayAPIConstants.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)) + 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 + } + } +} From 8effc8f64b6d1cad1e3a6783ef6fbe489a1bc11d Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Thu, 20 Jun 2024 13:05:30 +0200 Subject: [PATCH 31/75] WIP: Fixed httpBody --- .../NativePayment.swift | 16 ++++---- .../Classes/WebView/SCAWebViewService.swift | 21 +++++----- .../SwedbankPaySCAWebViewController.swift | 38 ++++++++++--------- 3 files changed, 39 insertions(+), 36 deletions(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index 30ff80d..e28e52e 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -292,14 +292,14 @@ public extension SwedbankPaySDK { self.launchClientApp(task: launchClientApp.firstTask(with: .launchClientApp)!) } else if let scaMethodRequest = operations.first(where: { $0.firstTask(with: .scaMethodRequest) != nil }), let task = scaMethodRequest.firstTask(with: .scaMethodRequest), - !scaMethodRequestDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "ThreeDsMethodData" })?.value }) { + !scaMethodRequestDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value }) { DispatchQueue.main.async { self.webViewService.load(task: task) { result in switch result { case .success: - self.scaMethodRequestDataPerformed.append(ExpectationModel(name: task.expects?.first(where: { $0.name == "ThreeDsMethodData" })?.value ?? "", type: "string", value: "Y")) + self.scaMethodRequestDataPerformed.append(ExpectationModel(name: task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "", type: "string", value: "Y")) case .failure(let error): - self.scaMethodRequestDataPerformed.append(ExpectationModel(name: task.expects?.first(where: { $0.name == "ThreeDsMethodData" })?.value ?? "", type: "string", value: "N")) + self.scaMethodRequestDataPerformed.append(ExpectationModel(name: task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "", type: "string", value: "N")) } if let model = self.ongoingModel { @@ -309,11 +309,11 @@ public extension SwedbankPaySDK { } } else if let createAuthentication = operations.first(where: { $0.rel == .createAuthentication }), let task = createAuthentication.firstTask(with: .scaMethodRequest), - let scaMethod = scaMethodRequestDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "ThreeDsMethodData" })?.value }) { + let scaMethod = scaMethodRequestDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value }) { makeRequest(model: createAuthentication, culture: culture, methodCompletionIndicator: scaMethod.value) } else if let operation = operations.first(where: { $0.firstTask(with: .scaRedirect) != nil }), let task = operation.firstTask(with: .scaRedirect), - !scaRedirectDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "CReq" })?.value }) { + !scaRedirectDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "creq" })?.value }) { DispatchQueue.main.async { self.delegate?.showViewController(viewController: self.webViewController) @@ -322,7 +322,7 @@ public extension SwedbankPaySDK { case .success(let value): if !self.scaRedirectDataPerformed.contains(where: { $0.value == value }) { print("success \(value)") - self.scaRedirectDataPerformed.append(ExpectationModel(name: task.expects!.first(where: { $0.name == "CReq" })!.value!, type: "string", value: value)) + self.scaRedirectDataPerformed.append(ExpectationModel(name: task.expects!.first(where: { $0.name == "creq" })!.value!, type: "string", value: value)) self.delegate?.finishedWithViewController() } else { @@ -338,8 +338,8 @@ public extension SwedbankPaySDK { } } } else if let completeAuthentication = operations.first(where: { $0.rel == .completeAuthentication }), - let task = completeAuthentication.tasks?.first(where: { $0.expects?.contains(where: { $0.name == "CReq" } ) ?? false } ), - let scaRedirect = scaRedirectDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "CReq" })?.value }) { + let task = completeAuthentication.tasks?.first(where: { $0.expects?.contains(where: { $0.name == "creq" } ) ?? false } ), + let scaRedirect = scaRedirectDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "creq" })?.value }) { makeRequest(model: completeAuthentication, culture: culture, cRes: scaRedirect.value) } else if let redirectPayer = operations.first(where: { $0.rel == .redirectPayer }) { DispatchQueue.main.async { diff --git a/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift index 353e2c8..e7cf107 100644 --- a/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift +++ b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift @@ -38,18 +38,19 @@ class SCAWebViewService: NSObject, WKNavigationDelegate { request.allHTTPHeaderFields = ["Content-Type": task.contentType ?? ""] request.timeoutInterval = 5 - var body: [String: Any?] = [:] - - if let expects = task.expects { - for expect in expects { - if expect.type == "string", let name = expect.name { - body[name] = expect.value + if let bodyString = task.expects? + .filter({ $0.type == "string" }) + .compactMap({ + guard let name = $0.name, + let value = $0.value?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { + return nil } - } - } - if let jsonData = try? JSONSerialization.data(withJSONObject: body) { - request.httpBody = jsonData + return name + "=" + value + }) + .joined(separator: "&") { + + request.httpBody = bodyString.data(using: .utf8) } webView?.navigationDelegate = self diff --git a/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift index fa60282..5ebbcd2 100644 --- a/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift +++ b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift @@ -67,29 +67,31 @@ class SwedbankPaySCAWebViewController: UIViewController { func load(task: IntegrationTask, handler: @escaping (Result) -> Void) { guard let taskHref = task.href, - let url = URL(string: "https://firebasestorage.googleapis.com/v0/b/consid-beta.appspot.com/o/fake-3ds.html?alt=media") else { + let url = URL(string: taskHref) else { return } self.handler = handler var request = URLRequest(url: url) -// request.httpMethod = task.method -// request.allHTTPHeaderFields = ["Content-Type": task.contentType ?? ""] -// -// var body: [String: Any?] = [:] -// -// if let expects = task.expects { -// for expect in expects { -// if expect.type == "string", let name = expect.name { -// body[name] = expect.value -// } -// } -// } - -// if let jsonData = try? JSONSerialization.data(withJSONObject: body) { -// request.httpBody = jsonData -// } + request.httpMethod = task.method + request.allHTTPHeaderFields = ["Content-Type": task.contentType ?? ""] + request.timeoutInterval = 5 + + if let bodyString = task.expects? + .filter({ $0.type == "string" }) + .compactMap({ + guard let name = $0.name, + let value = $0.value?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { + return nil + } + + return name + "=" + value + }) + .joined(separator: "&") { + + request.httpBody = bodyString.data(using: .utf8) + } let navigation = webView.load(request) @@ -110,7 +112,7 @@ extension SwedbankPaySCAWebViewController: WKNavigationDelegate { 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 { + let cRes = urlComponents.queryItems?.first(where: { $0.name == "cres" })?.value { navigationLog(request.url, "Link CRes") self.handler?(.success(cRes)) decisionHandler(.allow) From bf224b7d8b6cb3934254599736d3beb9e045c398 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Mon, 24 Jun 2024 15:49:38 +0200 Subject: [PATCH 32/75] Clean up and error handling --- .../NativePayment.swift | 33 ++++++++++++------- .../SwedbankPaySCAWebViewController.swift | 8 +++++ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index e28e52e..978198e 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -33,8 +33,8 @@ public extension SwedbankPaySDK { private var hasLaunchClientAppURLs: [URL] = [] private var hasShownProblemDetails: [ProblemDetails] = [] - private var scaMethodRequestDataPerformed: [ExpectationModel] = [] - private var scaRedirectDataPerformed: [ExpectationModel] = [] + private var scaMethodRequestDataPerformed: [(name: String, value: String)] = [] + private var scaRedirectDataPerformed: [(name: String, value: String)] = [] private var sessionStartTimestamp = Date() @@ -297,9 +297,9 @@ public extension SwedbankPaySDK { self.webViewService.load(task: task) { result in switch result { case .success: - self.scaMethodRequestDataPerformed.append(ExpectationModel(name: task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "", type: "string", value: "Y")) + self.scaMethodRequestDataPerformed.append((name: task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "", value: "Y")) case .failure(let error): - self.scaMethodRequestDataPerformed.append(ExpectationModel(name: task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "", type: "string", value: "N")) + self.scaMethodRequestDataPerformed.append((name: task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "", value: "N")) } if let model = self.ongoingModel { @@ -317,23 +317,32 @@ public extension SwedbankPaySDK { DispatchQueue.main.async { self.delegate?.showViewController(viewController: self.webViewController) + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "show3dSecure", + succeeded: self.delegate != nil, + values: nil)) + self.webViewController.load(task: task) { result in switch result { case .success(let value): if !self.scaRedirectDataPerformed.contains(where: { $0.value == value }) { - print("success \(value)") - self.scaRedirectDataPerformed.append(ExpectationModel(name: task.expects!.first(where: { $0.name == "creq" })!.value!, type: "string", value: value)) + self.scaRedirectDataPerformed.append((name: task.expects!.first(where: { $0.name == "creq" })!.value!, value: value)) self.delegate?.finishedWithViewController() - } else { - print("success old") + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "dismiss3dSecure", + succeeded: self.delegate != nil, + values: nil)) + } + + if let model = self.ongoingModel { + self.sessionOperationHandling(model: model, culture: culture) } case .failure(let error): - print("success \(error)") - } + self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) - if let model = self.ongoingModel { - self.sessionOperationHandling(model: model, culture: culture) + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": SwedbankPaySDK.NativePaymentProblem.internalInconsistencyError.rawValue])) } } } diff --git a/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift index 5ebbcd2..9e16ee8 100644 --- a/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift +++ b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift @@ -101,6 +101,14 @@ class SwedbankPaySCAWebViewController: UIViewController { extension SwedbankPaySCAWebViewController: WKNavigationDelegate { + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + handler?(.failure(error)) + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + handler?(.failure(error)) + } + func webView( _ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, From 54f5a783ac4f5590d76f8700bcfd7cefe9323c88 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Wed, 26 Jun 2024 10:05:58 +0200 Subject: [PATCH 33/75] WIP: sdkProblemWithViewController --- .../NativePayment.swift | 76 +++++++++++-------- .../SwedbankPaySDKNativePaymentDelegate.swift | 2 + .../Classes/WebView/SCAWebViewService.swift | 6 ++ .../SwedbankPaySCAWebViewController.swift | 3 + 4 files changed, 55 insertions(+), 32 deletions(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift index 978198e..808c8bd 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift @@ -103,8 +103,14 @@ public extension SwedbankPaySDK { if let operation = ongoingModel.paymentSession.methods? .first(where: { $0.name == instrument.name })?.operations? .first(where: { $0.rel == .expandMethod || $0.rel == .startPaymentAttempt || $0.rel == .getPayment }) { + sessionStartTimestamp = Date() makeRequest(model: operation, culture: ongoingModel.paymentSession.culture) + + if operation.rel == .startPaymentAttempt { + self.instrument = nil + } + succeeded = true } else { DispatchQueue.main.async { @@ -314,38 +320,7 @@ public extension SwedbankPaySDK { } else if let operation = operations.first(where: { $0.firstTask(with: .scaRedirect) != nil }), let task = operation.firstTask(with: .scaRedirect), !scaRedirectDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "creq" })?.value }) { - DispatchQueue.main.async { - self.delegate?.showViewController(viewController: self.webViewController) - - BeaconService.shared.log(type: .sdkCallbackInvoked(name: "show3dSecure", - succeeded: self.delegate != nil, - values: nil)) - - 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!.first(where: { $0.name == "creq" })!.value!, value: value)) - - self.delegate?.finishedWithViewController() - - BeaconService.shared.log(type: .sdkCallbackInvoked(name: "dismiss3dSecure", - succeeded: self.delegate != nil, - values: nil)) - } - - if let model = self.ongoingModel { - self.sessionOperationHandling(model: model, culture: culture) - } - case .failure(let error): - self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) - - BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", - succeeded: self.delegate != nil, - values: ["problem": SwedbankPaySDK.NativePaymentProblem.internalInconsistencyError.rawValue])) - } - } - } + scaRedirectDataPerformed(task: task, culture: culture) } else if let completeAuthentication = operations.first(where: { $0.rel == .completeAuthentication }), let task = completeAuthentication.tasks?.first(where: { $0.expects?.contains(where: { $0.name == "creq" } ) ?? false } ), let scaRedirect = scaRedirectDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "creq" })?.value }) { @@ -433,5 +408,42 @@ public extension SwedbankPaySDK { return true } + + func scaRedirectDataPerformed(task: IntegrationTask, culture: String?) { + DispatchQueue.main.async { + self.delegate?.showViewController(viewController: self.webViewController) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "show3dSecure", + succeeded: self.delegate != nil, + values: nil)) + + 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!.first(where: { $0.name == "creq" })!.value!, value: value)) + + self.delegate?.finishedWithViewController() + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "dismiss3dSecure", + succeeded: self.delegate != nil, + values: nil)) + + if let model = self.ongoingModel { + self.sessionOperationHandling(model: model, culture: culture) + } + } + case .failure(let error): + self.delegate?.sdkProblemWithViewController(problem: .paymentSessionAPIRequestFailed(error: error, retry: { + self.scaRedirectDataPerformed(task: task, culture: culture) + })) + +// BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemWithViewController", +// succeeded: self.delegate != nil, +// values: ["problem": SwedbankPaySDK.PaymentSessionProblem.paymentSessionEndStateReached.rawValue])) + } + } + } + } } } diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDKNativePaymentDelegate.swift b/SwedbankPaySDK/Classes/SwedbankPaySDKNativePaymentDelegate.swift index 6797382..0f68377 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDKNativePaymentDelegate.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDKNativePaymentDelegate.swift @@ -34,4 +34,6 @@ public protocol SwedbankPaySDKNativePaymentDelegate: AnyObject { func showViewController(viewController: UIViewController) func finishedWithViewController() + + func sdkProblemWithViewController(problem: SwedbankPaySDK.NativePaymentProblem) } diff --git a/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift index e7cf107..f715749 100644 --- a/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift +++ b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift @@ -59,18 +59,24 @@ class SCAWebViewService: NSObject, WKNavigationDelegate { 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 } diff --git a/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift index 9e16ee8..ba24035 100644 --- a/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift +++ b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift @@ -103,10 +103,12 @@ extension SwedbankPaySCAWebViewController: WKNavigationDelegate { func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { handler?(.failure(error)) + handler = nil } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { handler?(.failure(error)) + handler = nil } func webView( @@ -123,6 +125,7 @@ extension SwedbankPaySCAWebViewController: WKNavigationDelegate { 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") From f8bbdb550cab25850501dd3080d39ce7f11451f2 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Mon, 17 Jun 2024 15:54:18 +0200 Subject: [PATCH 34/75] SP-55 Automatic configuration --- SwedbankPaySDK.xcodeproj/project.pbxproj | 36 +++--- .../Api/Models/PaymentSessionModel.swift | 11 ++ ...tivePayment.swift => PaymentSession.swift} | 121 +++++++++++------- ...blem.swift => PaymentSessionProblem.swift} | 6 +- ...wedbankPaySDKPaymentSessionDelegate.swift} | 23 ++-- 5 files changed, 124 insertions(+), 73 deletions(-) rename SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/{NativePayment.swift => PaymentSession.swift} (79%) rename SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/{NativePaymentProblem.swift => PaymentSessionProblem.swift} (84%) rename SwedbankPaySDK/Classes/{SwedbankPaySDKNativePaymentDelegate.swift => SwedbankPaySDKPaymentSessionDelegate.swift} (51%) diff --git a/SwedbankPaySDK.xcodeproj/project.pbxproj b/SwedbankPaySDK.xcodeproj/project.pbxproj index a4dbeca..5be8be2 100644 --- a/SwedbankPaySDK.xcodeproj/project.pbxproj +++ b/SwedbankPaySDK.xcodeproj/project.pbxproj @@ -14,10 +14,12 @@ 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 */; }; - 456900812BFB9AA5009475A5 /* NativePaymentProblem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 456900802BFB9AA5009475A5 /* NativePaymentProblem.swift */; }; - 456900822BFB9AA5009475A5 /* NativePaymentProblem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 456900802BFB9AA5009475A5 /* NativePaymentProblem.swift */; }; - 457C9C5F2C08C27C00420B4F /* SwedbankPaySDKNativePaymentDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKNativePaymentDelegate.swift */; }; - 457C9C602C08C2F900420B4F /* SwedbankPaySDKNativePaymentDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKNativePaymentDelegate.swift */; }; + 455A7B982C205DA3003CF320 /* PaymentSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 455A7B972C205DA3003CF320 /* PaymentSession.swift */; }; + 455A7B992C205DA3003CF320 /* PaymentSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 455A7B972C205DA3003CF320 /* PaymentSession.swift */; }; + 456900812BFB9AA5009475A5 /* PaymentSessionProblem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 456900802BFB9AA5009475A5 /* PaymentSessionProblem.swift */; }; + 456900822BFB9AA5009475A5 /* PaymentSessionProblem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 456900802BFB9AA5009475A5 /* PaymentSessionProblem.swift */; }; + 457C9C5F2C08C27C00420B4F /* SwedbankPaySDKPaymentSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKPaymentSessionDelegate.swift */; }; + 457C9C602C08C2F900420B4F /* SwedbankPaySDKPaymentSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKPaymentSessionDelegate.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 */; }; @@ -33,8 +35,6 @@ 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 */; }; - 45C100992BF24E0D00AA3523 /* NativePayment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C100982BF24E0D00AA3523 /* NativePayment.swift */; }; - 45C1009A2BF24E0D00AA3523 /* NativePayment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C100982BF24E0D00AA3523 /* NativePayment.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 */; }; @@ -295,8 +295,9 @@ 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 = ""; }; - 456900802BFB9AA5009475A5 /* NativePaymentProblem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePaymentProblem.swift; sourceTree = ""; }; - 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKNativePaymentDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwedbankPaySDKNativePaymentDelegate.swift; sourceTree = ""; }; + 455A7B972C205DA3003CF320 /* PaymentSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentSession.swift; sourceTree = ""; }; + 456900802BFB9AA5009475A5 /* PaymentSessionProblem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSessionProblem.swift; sourceTree = ""; }; + 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKPaymentSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwedbankPaySDKPaymentSessionDelegate.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 = ""; }; @@ -304,7 +305,6 @@ 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 = ""; }; - 45C100982BF24E0D00AA3523 /* NativePayment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativePayment.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 = ""; }; @@ -706,7 +706,7 @@ A59AEE9C25372C9A00255A3A /* Instrument.swift */, A5F3416823FD9293005D65BA /* PaymentOrder.swift */, C50CF6A6237AAF85003F79DF /* ClientProblem.swift */, - 456900802BFB9AA5009475A5 /* NativePaymentProblem.swift */, + 456900802BFB9AA5009475A5 /* PaymentSessionProblem.swift */, C50CF696237AA8ED003F79DF /* Configuration.swift */, C50CF69C237AACC3003F79DF /* Consumer.swift */, C50CF6A8237AAFBF003F79DF /* ServerProblem.swift */, @@ -722,7 +722,7 @@ 63EECFCF270730C800C37B69 /* CodableUserData.swift */, D581EFDE27A92A56001B85A7 /* VersionOptions.swift */, D5AE5D9F27E0D4DA00BE5468 /* ExpandResource.swift */, - 45C100982BF24E0D00AA3523 /* NativePayment.swift */, + 455A7B972C205DA3003CF320 /* PaymentSession.swift */, ); path = "SwedbankPaySDK+Extensions"; sourceTree = ""; @@ -818,7 +818,7 @@ C50CF6D2237AC76A003F79DF /* Utils */, C585AF27237066EE006C2E16 /* SwedbankPaySDK.swift */, C585AF2A237066EE006C2E16 /* SwedbankPaySDKController.swift */, - 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKNativePaymentDelegate.swift */, + 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKPaymentSessionDelegate.swift */, C585AF28237066EE006C2E16 /* SwedbankPaySDKViewModel.swift */, 45C1008A2BF247F300AA3523 /* Api */, A5808B03261C93FD00FF7EC1 /* WebView */, @@ -1157,7 +1157,6 @@ A52E0AC124BC9E0100770286 /* WebViewRedirects.swift in Sources */, A504895123C8A2FD00201DEC /* SwedbankPayWebContent.swift in Sources */, C50CF69D237AACC3003F79DF /* Consumer.swift in Sources */, - 45C100992BF24E0D00AA3523 /* NativePayment.swift in Sources */, 45C1009F2BF2583D00AA3523 /* MethodBaseModel.swift in Sources */, 45C1009D2BF2583D00AA3523 /* PaymentSessionModel.swift in Sources */, 45C100A52BF2597B00AA3523 /* PaymentOutputModel.swift in Sources */, @@ -1167,7 +1166,7 @@ A59AEE9D25372C9A00255A3A /* Instrument.swift in Sources */, C585AF2F237066EE006C2E16 /* SwedbankPaySDKController.swift in Sources */, 45B4495B2C05F34F00A1F46D /* BeaconType.swift in Sources */, - 457C9C5F2C08C27C00420B4F /* SwedbankPaySDKNativePaymentDelegate.swift in Sources */, + 457C9C5F2C08C27C00420B4F /* SwedbankPaySDKPaymentSessionDelegate.swift in Sources */, 45C100942BF247F300AA3523 /* SwedbankPayAPIEnpointRouter.swift in Sources */, D50C9B4D27E36EBB00FCE33D /* VersionReporter.swift in Sources */, 45C100902BF247F300AA3523 /* SwedbankPayAPIError.swift in Sources */, @@ -1180,6 +1179,7 @@ C50CF6A7237AAF85003F79DF /* ClientProblem.swift in Sources */, C50CF6A9237AAFBF003F79DF /* ServerProblem.swift in Sources */, 45B4495D2C05F46200A1F46D /* Beacon.swift in Sources */, + 455A7B982C205DA3003CF320 /* PaymentSession.swift in Sources */, 45C100A22BF2584E00AA3523 /* IntegrationTask.swift in Sources */, A52E0ACB24BCAA6D00770286 /* GoodWebViewRedirects.swift in Sources */, 45C100A82BF2598D00AA3523 /* ProblemDetails.swift in Sources */, @@ -1200,7 +1200,7 @@ 45C100AD2BF2622E00AA3523 /* NetworkStatusProvider.swift in Sources */, A5C3D01E23A23BC900B45085 /* SwedbankPayWebViewController.swift in Sources */, A5808B05261C942A00FF7EC1 /* SwedbankPayWebViewControllerBase.swift in Sources */, - 456900812BFB9AA5009475A5 /* NativePaymentProblem.swift in Sources */, + 456900812BFB9AA5009475A5 /* PaymentSessionProblem.swift in Sources */, A560BE432420C7CE00C1D023 /* TerminalFailure.swift in Sources */, A5F3416923FD9293005D65BA /* PaymentOrder.swift in Sources */, D5AE5DA027E0D4DA00BE5468 /* ExpandResource.swift in Sources */, @@ -1271,7 +1271,6 @@ D511ECAA2975599D0059AB05 /* PaymentTokenInfo.swift in Sources */, D511ECAB2975599D0059AB05 /* EmptyJsonResponse.swift in Sources */, 45C100AE2BF2622E00AA3523 /* NetworkStatusProvider.swift in Sources */, - 45C1009A2BF24E0D00AA3523 /* NativePayment.swift in Sources */, 45C100A92BF2598D00AA3523 /* ProblemDetails.swift in Sources */, D511ECAC2975599D0059AB05 /* AbortPaymentOperation.swift in Sources */, D511ECAD2975599D0059AB05 /* MerchantBackendConfiguration.swift in Sources */, @@ -1283,7 +1282,7 @@ D511ECB32975599D0059AB05 /* MerchantBackendApi.swift in Sources */, D511ECB42975599D0059AB05 /* RequestDecorator.swift in Sources */, D511EC702975591B0059AB05 /* WKWebViewCanOpen.swift in Sources */, - 456900822BFB9AA5009475A5 /* NativePaymentProblem.swift in Sources */, + 456900822BFB9AA5009475A5 /* PaymentSessionProblem.swift in Sources */, D511EC712975591B0059AB05 /* SwedbankPaySubProblem.swift in Sources */, D511EC722975591B0059AB05 /* WebViewRedirects.swift in Sources */, D511EC732975591B0059AB05 /* SwedbankPayWebContent.swift in Sources */, @@ -1298,9 +1297,10 @@ 45C1009E2BF2583D00AA3523 /* PaymentSessionModel.swift in Sources */, D511EC792975591B0059AB05 /* VersionReporter.swift in Sources */, D511EC7A2975591B0059AB05 /* ConfigurationAsync.swift in Sources */, + 455A7B992C205DA3003CF320 /* PaymentSession.swift in Sources */, D511EC7B2975591B0059AB05 /* SwedbankPaySDKResources.swift in Sources */, D511EC7C2975591B0059AB05 /* TypeAliases.swift in Sources */, - 457C9C602C08C2F900420B4F /* SwedbankPaySDKNativePaymentDelegate.swift in Sources */, + 457C9C602C08C2F900420B4F /* SwedbankPaySDKPaymentSessionDelegate.swift in Sources */, 45B429952C18687100620A0A /* SwedbankPaySCAWebViewController.swift in Sources */, 45C100932BF247F300AA3523 /* OperationOutputModel.swift in Sources */, D511EC7D2975591B0059AB05 /* SwedbankPayExtraWebViewController.swift in Sources */, diff --git a/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift index d9f0280..c6d286b 100644 --- a/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift @@ -13,9 +13,12 @@ // 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 urls: UrlsModel? } extension PaymentSessionModel { @@ -35,3 +38,11 @@ extension PaymentSessionModel { return allOperations } } + +struct UrlsModel: Codable, Hashable { + let completeUrl: URL? + let cancelUrl: URL? + let paymentUrl: URL? + let hostUrls: [URL]? + let termsOfServiceUrl: URL? +} diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSession.swift similarity index 79% rename from SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift rename to SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSession.swift index 808c8bd..26ab7db 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePayment.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSession.swift @@ -18,13 +18,13 @@ import UIKit import WebKit public extension SwedbankPaySDK { - /// Object that handles native payments - class NativePayment: CallbackUrlDelegate { - /// Order information that provides `NativePayment` with callback URLs. - public var orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo + /// Object that handles payment sessions + class PaymentSession: 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: SwedbankPaySDKNativePaymentDelegate? + public weak var delegate: SwedbankPaySDKPaymentSessionDelegate? private var ongoingModel: PaymentOutputModel? = nil private var sessionIsOngoing: Bool = false @@ -41,8 +41,13 @@ public extension SwedbankPaySDK { private var webViewService = SCAWebViewService() private lazy var webViewController = SwedbankPaySCAWebViewController() - public init(orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo) { - self.orderInfo = orderInfo + private var automaticConfiguration: Bool = true + + public init(orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo? = nil) { + if let orderInfo { + self.orderInfo = orderInfo + self.automaticConfiguration = false + } SwedbankPaySDK.addCallbackUrlDelegate(self) } @@ -66,6 +71,10 @@ public extension SwedbankPaySDK { scaRedirectDataPerformed = [] hasShownAvailableInstruments = false + if automaticConfiguration { + orderInfo = nil + } + let model = OperationOutputModel(rel: nil, href: sessionURL.absoluteString, method: "GET", @@ -73,7 +82,7 @@ public extension SwedbankPaySDK { tasks: nil) sessionStartTimestamp = Date() - makeRequest(model: model) + makeRequest(operationOutputModel: model) BeaconService.shared.clear() BeaconService.shared.log(type: .sdkMethodInvoked(name: "startPaymentSession", @@ -92,7 +101,7 @@ public extension SwedbankPaySDK { BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", succeeded: self.delegate != nil, - values: ["problem": SwedbankPaySDK.NativePaymentProblem.internalInconsistencyError.rawValue])) + values: ["problem": SwedbankPaySDK.PaymentSessionProblem.internalInconsistencyError.rawValue])) return } @@ -105,7 +114,7 @@ public extension SwedbankPaySDK { .first(where: { $0.rel == .expandMethod || $0.rel == .startPaymentAttempt || $0.rel == .getPayment }) { sessionStartTimestamp = Date() - makeRequest(model: operation, culture: ongoingModel.paymentSession.culture) + makeRequest(operationOutputModel: operation, culture: ongoingModel.paymentSession.culture) if operation.rel == .startPaymentAttempt { self.instrument = nil @@ -118,7 +127,7 @@ public extension SwedbankPaySDK { BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", succeeded: self.delegate != nil, - values: ["problem": SwedbankPaySDK.NativePaymentProblem.paymentSessionEndStateReached.rawValue])) + values: ["problem": SwedbankPaySDK.PaymentSessionProblem.paymentSessionEndStateReached.rawValue])) } return @@ -151,7 +160,7 @@ public extension SwedbankPaySDK { BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", succeeded: self.delegate != nil, - values: ["problem": SwedbankPaySDK.NativePaymentProblem.internalInconsistencyError.rawValue])) + values: ["problem": SwedbankPaySDK.PaymentSessionProblem.internalInconsistencyError.rawValue])) return } @@ -160,7 +169,7 @@ public extension SwedbankPaySDK { if let operation = ongoingModel.operations? .first(where: { $0.rel == .abortPayment }) { sessionStartTimestamp = Date() - makeRequest(model: operation, culture: ongoingModel.paymentSession.culture) + makeRequest(operationOutputModel: operation, culture: ongoingModel.paymentSession.culture) succeeded = true } @@ -169,8 +178,8 @@ public extension SwedbankPaySDK { values: nil)) } - private func makeRequest(model: OperationOutputModel, culture: String? = nil, methodCompletionIndicator: String? = nil, cRes: String? = nil) { - SwedbankPayAPIEnpointRouter(model: model, + private func makeRequest(operationOutputModel: OperationOutputModel, culture: String? = nil, methodCompletionIndicator: String? = nil, cRes: String? = nil) { + SwedbankPayAPIEnpointRouter(model: operationOutputModel, culture: culture, instrument: instrument, methodCompletionIndicator: methodCompletionIndicator, @@ -178,19 +187,39 @@ public extension SwedbankPaySDK { sessionStartTimestamp: sessionStartTimestamp).makeRequest { result in switch result { case .success(let success): - if let model = success { - if let eventLogging = model.operations?.first(where: { $0.rel == .eventLogging }) { + if let paymentOutputModel = success { + if self.automaticConfiguration, operationOutputModel.rel == 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?.first(where: { $0.rel == .eventLogging }) { BeaconService.shared.href = eventLogging.href } - self.sessionOperationHandling(model: model, culture: model.paymentSession.culture) + self.sessionOperationHandling(paymentOutputModel: paymentOutputModel, culture: paymentOutputModel.paymentSession.culture) } case .failure(let failure): DispatchQueue.main.async { - let problem = SwedbankPaySDK.NativePaymentProblem.paymentSessionAPIRequestFailed(error: failure, + let problem = SwedbankPaySDK.PaymentSessionProblem.paymentSessionAPIRequestFailed(error: failure, retry: { self.sessionStartTimestamp = Date() - self.makeRequest(model: model, + self.makeRequest(operationOutputModel: operationOutputModel, culture: culture, methodCompletionIndicator: methodCompletionIndicator, @@ -218,7 +247,7 @@ public extension SwedbankPaySDK { BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", succeeded: self.delegate != nil, - values: ["problem": SwedbankPaySDK.NativePaymentProblem.internalInconsistencyError.rawValue])) + values: ["problem": SwedbankPaySDK.PaymentSessionProblem.internalInconsistencyError.rawValue])) return } @@ -227,7 +256,7 @@ public extension SwedbankPaySDK { 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 { + if let paymentUrl = orderInfo?.paymentUrl?.absoluteString { components.queryItems?.append(URLQueryItem(name: "callbackurl", value: paymentUrl.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed))) } } @@ -243,10 +272,10 @@ public extension SwedbankPaySDK { BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", succeeded: self.delegate != nil, - values: ["problem": SwedbankPaySDK.NativePaymentProblem.clientAppLaunchFailed.rawValue])) + values: ["problem": SwedbankPaySDK.PaymentSessionProblem.clientAppLaunchFailed.rawValue])) } - BeaconService.shared.log(type: .launchClientApp(values: ["callbackUrl": self.orderInfo.paymentUrl?.absoluteString ?? "", + BeaconService.shared.log(type: .launchClientApp(values: ["callbackUrl": self.orderInfo?.paymentUrl?.absoluteString ?? "", "clientAppLaunchUrl": url.absoluteString, "launchSucceeded": complete])) } @@ -254,12 +283,12 @@ public extension SwedbankPaySDK { } } - private func sessionOperationHandling(model: PaymentOutputModel, culture: String? = nil) { - ongoingModel = model + private func sessionOperationHandling(paymentOutputModel: PaymentOutputModel, culture: String? = nil) { + ongoingModel = paymentOutputModel var hasShowedError = false - if let modelProblem = model.problem, + if let modelProblem = paymentOutputModel.problem, let problemOperation = modelProblem.operation, problemOperation.rel == .acknowledgeFailedAttempt { if !hasShownProblemDetails.contains(where: { $0.operation?.href == problemOperation.href }) { @@ -277,20 +306,22 @@ public extension SwedbankPaySDK { } } - makeRequest(model: problemOperation, culture: culture) + makeRequest(operationOutputModel: problemOperation, culture: culture) } - let operations = model.prioritisedOperations + let operations = paymentOutputModel.prioritisedOperations + + print("\(operations.compactMap({ $0.rel }))") if let preparePayment = operations.first(where: { $0.rel == .preparePayment }) { - makeRequest(model: preparePayment, culture: culture) + makeRequest(operationOutputModel: preparePayment, culture: culture) } else if operations.contains(where: { $0.rel == .startPaymentAttempt }), let instrument = instrument, let startPaymentAttempt = ongoingModel?.paymentSession.methods? .first(where: { $0.name == instrument.name })?.operations? .first(where: { $0.rel == .startPaymentAttempt }) { - makeRequest(model: startPaymentAttempt, culture: culture) + makeRequest(operationOutputModel: startPaymentAttempt, culture: culture) self.instrument = nil } else if let launchClientApp = operations.first(where: { $0.firstTask(with: .launchClientApp) != nil }), let tasks = launchClientApp.firstTask(with: .launchClientApp), @@ -309,14 +340,14 @@ public extension SwedbankPaySDK { } if let model = self.ongoingModel { - self.sessionOperationHandling(model: model, culture: culture) + self.sessionOperationHandling(paymentOutputModel: model, culture: culture) } } } } else if let createAuthentication = operations.first(where: { $0.rel == .createAuthentication }), let task = createAuthentication.firstTask(with: .scaMethodRequest), let scaMethod = scaMethodRequestDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value }) { - makeRequest(model: createAuthentication, culture: culture, methodCompletionIndicator: scaMethod.value) + makeRequest(operationOutputModel: createAuthentication, culture: culture, methodCompletionIndicator: scaMethod.value) } else if let operation = operations.first(where: { $0.firstTask(with: .scaRedirect) != nil }), let task = operation.firstTask(with: .scaRedirect), !scaRedirectDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "creq" })?.value }) { @@ -324,16 +355,16 @@ public extension SwedbankPaySDK { } else if let completeAuthentication = operations.first(where: { $0.rel == .completeAuthentication }), let task = completeAuthentication.tasks?.first(where: { $0.expects?.contains(where: { $0.name == "creq" } ) ?? false } ), let scaRedirect = scaRedirectDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "creq" })?.value }) { - makeRequest(model: completeAuthentication, culture: culture, cRes: scaRedirect.value) + makeRequest(operationOutputModel: completeAuthentication, culture: culture, cRes: scaRedirect.value) } else if let redirectPayer = operations.first(where: { $0.rel == .redirectPayer }) { DispatchQueue.main.async { - if redirectPayer.href == self.orderInfo.cancelUrl?.absoluteString { + if redirectPayer.href == self.orderInfo?.cancelUrl?.absoluteString { self.delegate?.paymentCanceled() BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentCanceled", succeeded: self.delegate != nil, values: nil)) - } else if redirectPayer.href == self.orderInfo.completeUrl.absoluteString { + } else if redirectPayer.href == self.orderInfo?.completeUrl.absoluteString { self.delegate?.paymentComplete() BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentComplete", @@ -344,7 +375,7 @@ public extension SwedbankPaySDK { BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", succeeded: self.delegate != nil, - values: ["problem": SwedbankPaySDK.NativePaymentProblem.paymentSessionEndStateReached.rawValue])) + values: ["problem": SwedbankPaySDK.PaymentSessionProblem.paymentSessionEndStateReached.rawValue])) } } sessionIsOngoing = false @@ -356,7 +387,7 @@ public extension SwedbankPaySDK { } else if (operations.contains(where: { $0.rel == .expandMethod }) || operations.contains(where: { $0.rel == .startPaymentAttempt })) && hasShownAvailableInstruments == false { DispatchQueue.main.async { - let availableInstruments: [AvailableInstrument] = model.paymentSession.methods?.compactMap({ model in + let availableInstruments: [AvailableInstrument] = paymentOutputModel.paymentSession.methods?.compactMap({ model in switch model { case .swish(let prefills, _): return AvailableInstrument.swish(prefills: prefills) @@ -369,16 +400,16 @@ public extension SwedbankPaySDK { self.hasShownAvailableInstruments = true - self.delegate?.availableInstrumentsFetched(availableInstruments) + self.delegate?.paymentSessionFetched(availableInstruments: availableInstruments) - BeaconService.shared.log(type: .sdkCallbackInvoked(name: "availableInstrumentsFetched", + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentSessionFetched", succeeded: self.delegate != nil, values: ["instruments": availableInstruments.compactMap({ $0.name }).joined(separator: ";")])) } } else if let getPayment = operations.first(where: { $0.rel == .getPayment }) { DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { self.sessionStartTimestamp = Date() - self.makeRequest(model: getPayment, culture: culture) + self.makeRequest(operationOutputModel: getPayment, culture: culture) } } else if !hasShowedError { DispatchQueue.main.async { @@ -386,13 +417,13 @@ public extension SwedbankPaySDK { BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", succeeded: self.delegate != nil, - values: ["problem": SwedbankPaySDK.NativePaymentProblem.paymentSessionEndStateReached.rawValue])) + values: ["problem": SwedbankPaySDK.PaymentSessionProblem.paymentSessionEndStateReached.rawValue])) } } } func handleCallbackUrl(_ url: URL) -> Bool { - guard url == orderInfo.paymentUrl else { + guard url == orderInfo?.paymentUrl else { return false } @@ -400,7 +431,7 @@ public extension SwedbankPaySDK { if let operation = ongoingModel.paymentSession.allMethodOperations .first(where: { $0.rel == .getPayment }) { sessionStartTimestamp = Date() - makeRequest(model: operation, culture: ongoingModel.paymentSession.culture) + makeRequest(operationOutputModel: operation, culture: ongoingModel.paymentSession.culture) } } @@ -430,7 +461,7 @@ public extension SwedbankPaySDK { values: nil)) if let model = self.ongoingModel { - self.sessionOperationHandling(model: model, culture: culture) + self.sessionOperationHandling(paymentOutputModel: model, culture: culture) } } case .failure(let error): diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift similarity index 84% rename from SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift rename to SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift index 4ce1206..cd30540 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/NativePaymentProblem.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift @@ -16,12 +16,13 @@ import Foundation public extension SwedbankPaySDK { - /// Native payment problem returned with `sdkProblemOccurred` - enum NativePaymentProblem { + /// Payment session problem returned with `sdkProblemOccurred` + enum PaymentSessionProblem { case paymentSessionEndStateReached case paymentSessionAPIRequestFailed(error: Error, retry: ()->Void) case clientAppLaunchFailed case internalInconsistencyError + case automaticConfigurationFailed var rawValue: String { switch self { @@ -29,6 +30,7 @@ public extension SwedbankPaySDK { case .paymentSessionAPIRequestFailed: "paymentSessionAPIRequestFailed" case .clientAppLaunchFailed: "clientAppLaunchFailed" case .internalInconsistencyError: "internalInconsistencyError" + case .automaticConfigurationFailed: "automaticConfigurationFailed" } } } diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDKNativePaymentDelegate.swift b/SwedbankPaySDK/Classes/SwedbankPaySDKPaymentSessionDelegate.swift similarity index 51% rename from SwedbankPaySDK/Classes/SwedbankPaySDKNativePaymentDelegate.swift rename to SwedbankPaySDK/Classes/SwedbankPaySDKPaymentSessionDelegate.swift index 0f68377..f2b07ca 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDKNativePaymentDelegate.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDKPaymentSessionDelegate.swift @@ -1,15 +1,22 @@ // -// SwedbankPaySDKNativePaymentDelegate.swift -// SwedbankPaySDK +// Copyright 2024 Swedbank AB // -// Created by Michael Balsiger on 2024-05-30. -// Copyright © 2024 Swedbank. All rights reserved. +// 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 /// Swedbank Pay SDK protocol, conform to this to get the result of the payment process -public protocol SwedbankPaySDKNativePaymentDelegate: AnyObject { +public protocol SwedbankPaySDKPaymentSessionDelegate: AnyObject { /// Called whenever the payment has been completed. func paymentComplete() @@ -19,7 +26,7 @@ public protocol SwedbankPaySDKNativePaymentDelegate: AnyObject { /// 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 availableInstrumentsFetched(_ availableInstruments: [SwedbankPaySDK.AvailableInstrument]) + func paymentSessionFetched(availableInstruments: [SwedbankPaySDK.AvailableInstrument]) /// Called if there is a session problem with performing the payment. /// @@ -29,11 +36,11 @@ public protocol SwedbankPaySDKNativePaymentDelegate: AnyObject { /// Called if there is a SDK problem with performing the payment. /// /// - parameter problem: The problem that caused the failure - func sdkProblemOccurred(problem: SwedbankPaySDK.NativePaymentProblem) + func sdkProblemOccurred(problem: SwedbankPaySDK.PaymentSessionProblem) func showViewController(viewController: UIViewController) func finishedWithViewController() - func sdkProblemWithViewController(problem: SwedbankPaySDK.NativePaymentProblem) + func sdkProblemWithViewController(problem: SwedbankPaySDK.PaymentSessionProblem) } From 612600562514626d9227dcb0f9f232434f89e042 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Wed, 26 Jun 2024 16:57:50 +0200 Subject: [PATCH 35/75] WIP: Loading and alert in webview --- .../PaymentSession.swift | 56 +++++++++---------- ...SwedbankPaySDKPaymentSessionDelegate.swift | 2 +- .../SwedbankPaySCAWebViewController.swift | 41 ++++++++++++++ 3 files changed, 70 insertions(+), 29 deletions(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSession.swift index 26ab7db..718ae49 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSession.swift @@ -351,7 +351,15 @@ public extension SwedbankPaySDK { } else if let operation = operations.first(where: { $0.firstTask(with: .scaRedirect) != nil }), let task = operation.firstTask(with: .scaRedirect), !scaRedirectDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "creq" })?.value }) { - scaRedirectDataPerformed(task: task, culture: culture) + DispatchQueue.main.async { + self.delegate?.showViewController(viewController: self.webViewController) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "show3dSecure", + succeeded: self.delegate != nil, + values: nil)) + + self.scaRedirectDataPerformed(task: task, culture: culture) + } } else if let completeAuthentication = operations.first(where: { $0.rel == .completeAuthentication }), let task = completeAuthentication.tasks?.first(where: { $0.expects?.contains(where: { $0.name == "creq" } ) ?? false } ), let scaRedirect = scaRedirectDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "creq" })?.value }) { @@ -441,38 +449,30 @@ public extension SwedbankPaySDK { } func scaRedirectDataPerformed(task: IntegrationTask, culture: String?) { - DispatchQueue.main.async { - self.delegate?.showViewController(viewController: self.webViewController) - - BeaconService.shared.log(type: .sdkCallbackInvoked(name: "show3dSecure", - succeeded: self.delegate != nil, - values: nil)) - - 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!.first(where: { $0.name == "creq" })!.value!, value: value)) + 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!.first(where: { $0.name == "creq" })!.value!, value: value)) - self.delegate?.finishedWithViewController() + self.delegate?.finishedWithViewController() - BeaconService.shared.log(type: .sdkCallbackInvoked(name: "dismiss3dSecure", - succeeded: self.delegate != nil, - values: nil)) + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "dismiss3dSecure", + succeeded: self.delegate != nil, + values: nil)) - if let model = self.ongoingModel { - self.sessionOperationHandling(paymentOutputModel: model, culture: culture) - } + if let model = self.ongoingModel { + self.sessionOperationHandling(paymentOutputModel: model, culture: culture) } - case .failure(let error): - self.delegate?.sdkProblemWithViewController(problem: .paymentSessionAPIRequestFailed(error: error, retry: { - self.scaRedirectDataPerformed(task: task, culture: culture) - })) - -// BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemWithViewController", -// succeeded: self.delegate != nil, -// values: ["problem": SwedbankPaySDK.PaymentSessionProblem.paymentSessionEndStateReached.rawValue])) } + case .failure(let error): + self.delegate?.sdkProblemWithViewController(error: error, retry: { + self.scaRedirectDataPerformed(task: task, culture: culture) + }) + +// BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemWithViewController", +// succeeded: self.delegate != nil, +// values: ["problem": SwedbankPaySDK.PaymentSessionProblem.paymentSessionEndStateReached.rawValue])) } } } diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDKPaymentSessionDelegate.swift b/SwedbankPaySDK/Classes/SwedbankPaySDKPaymentSessionDelegate.swift index f2b07ca..13a487b 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDKPaymentSessionDelegate.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDKPaymentSessionDelegate.swift @@ -42,5 +42,5 @@ public protocol SwedbankPaySDKPaymentSessionDelegate: AnyObject { func finishedWithViewController() - func sdkProblemWithViewController(problem: SwedbankPaySDK.PaymentSessionProblem) + func sdkProblemWithViewController(error: Error, retry: @escaping ()->Void) } diff --git a/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift index ba24035..6933256 100644 --- a/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift +++ b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift @@ -46,6 +46,8 @@ class SwedbankPaySCAWebViewController: UIViewController { let webView: WKWebView + let activityView = UIActivityIndicatorView() + init() { let preferences = WKPreferences() preferences.javaScriptEnabled = true @@ -55,6 +57,7 @@ class SwedbankPaySCAWebViewController: UIViewController { super.init(nibName: nil, bundle: nil) webView.navigationDelegate = self + webView.addSubview(activityView) } required init?(coder: NSCoder) { @@ -65,12 +68,21 @@ class SwedbankPaySCAWebViewController: UIViewController { 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) @@ -96,17 +108,46 @@ class SwedbankPaySCAWebViewController: UIViewController { 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 } From 5617c870669d1585d5ffb20c3b082fe0174d2d67 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Thu, 27 Jun 2024 11:43:15 +0200 Subject: [PATCH 36/75] WIP: Renames --- SwedbankPaySDK.xcodeproj/project.pbxproj | 18 ++--- .../Classes/Api/Models/MethodBaseModel.swift | 2 +- .../Api/Models/PaymentAttemptInstrument.swift | 2 +- .../Api/SwedbankPayAPIEnpointRouter.swift | 2 +- ....swift => SwedbankPayPaymentSession.swift} | 78 ++++++++++++++----- ...SwedbankPaySDKPaymentSessionDelegate.swift | 46 ----------- 6 files changed, 67 insertions(+), 81 deletions(-) rename SwedbankPaySDK/Classes/{SwedbankPaySDK+Extensions/PaymentSession.swift => SwedbankPayPaymentSession.swift} (89%) delete mode 100644 SwedbankPaySDK/Classes/SwedbankPaySDKPaymentSessionDelegate.swift diff --git a/SwedbankPaySDK.xcodeproj/project.pbxproj b/SwedbankPaySDK.xcodeproj/project.pbxproj index 5be8be2..1f4dbf9 100644 --- a/SwedbankPaySDK.xcodeproj/project.pbxproj +++ b/SwedbankPaySDK.xcodeproj/project.pbxproj @@ -14,12 +14,10 @@ 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 */; }; - 455A7B982C205DA3003CF320 /* PaymentSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 455A7B972C205DA3003CF320 /* PaymentSession.swift */; }; - 455A7B992C205DA3003CF320 /* PaymentSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 455A7B972C205DA3003CF320 /* PaymentSession.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 */; }; - 457C9C5F2C08C27C00420B4F /* SwedbankPaySDKPaymentSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKPaymentSessionDelegate.swift */; }; - 457C9C602C08C2F900420B4F /* SwedbankPaySDKPaymentSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKPaymentSessionDelegate.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 */; }; @@ -295,9 +293,8 @@ 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 = ""; }; - 455A7B972C205DA3003CF320 /* PaymentSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentSession.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 = ""; }; - 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKPaymentSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwedbankPaySDKPaymentSessionDelegate.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 = ""; }; @@ -722,7 +719,6 @@ 63EECFCF270730C800C37B69 /* CodableUserData.swift */, D581EFDE27A92A56001B85A7 /* VersionOptions.swift */, D5AE5D9F27E0D4DA00BE5468 /* ExpandResource.swift */, - 455A7B972C205DA3003CF320 /* PaymentSession.swift */, ); path = "SwedbankPaySDK+Extensions"; sourceTree = ""; @@ -818,8 +814,8 @@ C50CF6D2237AC76A003F79DF /* Utils */, C585AF27237066EE006C2E16 /* SwedbankPaySDK.swift */, C585AF2A237066EE006C2E16 /* SwedbankPaySDKController.swift */, - 457C9C5E2C08C27C00420B4F /* SwedbankPaySDKPaymentSessionDelegate.swift */, C585AF28237066EE006C2E16 /* SwedbankPaySDKViewModel.swift */, + 455A7B972C205DA3003CF320 /* SwedbankPayPaymentSession.swift */, 45C1008A2BF247F300AA3523 /* Api */, A5808B03261C93FD00FF7EC1 /* WebView */, 4517C69E2BFE34DC001687E7 /* Beacon */, @@ -1166,7 +1162,6 @@ A59AEE9D25372C9A00255A3A /* Instrument.swift in Sources */, C585AF2F237066EE006C2E16 /* SwedbankPaySDKController.swift in Sources */, 45B4495B2C05F34F00A1F46D /* BeaconType.swift in Sources */, - 457C9C5F2C08C27C00420B4F /* SwedbankPaySDKPaymentSessionDelegate.swift in Sources */, 45C100942BF247F300AA3523 /* SwedbankPayAPIEnpointRouter.swift in Sources */, D50C9B4D27E36EBB00FCE33D /* VersionReporter.swift in Sources */, 45C100902BF247F300AA3523 /* SwedbankPayAPIError.swift in Sources */, @@ -1179,7 +1174,7 @@ C50CF6A7237AAF85003F79DF /* ClientProblem.swift in Sources */, C50CF6A9237AAFBF003F79DF /* ServerProblem.swift in Sources */, 45B4495D2C05F46200A1F46D /* Beacon.swift in Sources */, - 455A7B982C205DA3003CF320 /* PaymentSession.swift in Sources */, + 455A7B982C205DA3003CF320 /* SwedbankPayPaymentSession.swift in Sources */, 45C100A22BF2584E00AA3523 /* IntegrationTask.swift in Sources */, A52E0ACB24BCAA6D00770286 /* GoodWebViewRedirects.swift in Sources */, 45C100A82BF2598D00AA3523 /* ProblemDetails.swift in Sources */, @@ -1297,10 +1292,9 @@ 45C1009E2BF2583D00AA3523 /* PaymentSessionModel.swift in Sources */, D511EC792975591B0059AB05 /* VersionReporter.swift in Sources */, D511EC7A2975591B0059AB05 /* ConfigurationAsync.swift in Sources */, - 455A7B992C205DA3003CF320 /* PaymentSession.swift in Sources */, + 455A7B992C205DA3003CF320 /* SwedbankPayPaymentSession.swift in Sources */, D511EC7B2975591B0059AB05 /* SwedbankPaySDKResources.swift in Sources */, D511EC7C2975591B0059AB05 /* TypeAliases.swift in Sources */, - 457C9C602C08C2F900420B4F /* SwedbankPaySDKPaymentSessionDelegate.swift in Sources */, 45B429952C18687100620A0A /* SwedbankPaySCAWebViewController.swift in Sources */, 45C100932BF247F300AA3523 /* OperationOutputModel.swift in Sources */, D511EC7D2975591B0059AB05 /* SwedbankPayExtraWebViewController.swift in Sources */, diff --git a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift index 0e5a250..3963d85 100644 --- a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift @@ -99,7 +99,7 @@ extension SwedbankPaySDK { case creditCard(prefills: [CreditCardMethodPrefillModel]?) - var name: String { + var identifier: String { switch self { case .swish: return "Swish" diff --git a/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift index 437981b..ecf97ac 100644 --- a/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift @@ -19,7 +19,7 @@ extension SwedbankPaySDK { case swish(msisdn: String?) case creditCard(prefill: CreditCardMethodPrefillModel) - var name: String { + var identifier: String { switch self { case .swish: return "Swish" diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index c5f7521..c4a8003 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -34,7 +34,7 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { var body: [String: Any?]? { switch model.rel { case .expandMethod: - return ["instrumentName": instrument?.name] + return ["instrumentName": instrument?.identifier] case .startPaymentAttempt: switch instrument { case .swish(let msisdn): diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift similarity index 89% rename from SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSession.swift rename to SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 718ae49..8dc91f3 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -17,9 +17,39 @@ 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) + + func show3DSecureViewController(viewController: UIViewController) + + func dismiss3DSecureViewController() + + func paymentSession3DSecureViewControllerLoadFailed(error: Error, retry: @escaping ()->Void) +} + public extension SwedbankPaySDK { /// Object that handles payment sessions - class PaymentSession: CallbackUrlDelegate { + class SwedbankPayPaymentSession: CallbackUrlDelegate { /// Order information that provides `PaymentSession` with callback URLs. public var orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo? @@ -43,7 +73,7 @@ public extension SwedbankPaySDK { private var automaticConfiguration: Bool = true - public init(orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo? = nil) { + public init(manualOrderInfo orderInfo: SwedbankPaySDK.ViewPaymentOrderInfo? = nil) { if let orderInfo { self.orderInfo = orderInfo self.automaticConfiguration = false @@ -61,7 +91,7 @@ public extension SwedbankPaySDK { /// 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 startPaymentSession(sessionURL: URL) { + public func fetchPaymentSession(sessionURL: URL) { sessionIsOngoing = true instrument = nil ongoingModel = nil @@ -95,7 +125,7 @@ public extension SwedbankPaySDK { /// There needs to be an active payment session before an payment attempt can be made. /// /// - parameter instrument: Payment attempt instrument - public func makePaymentAttempt(instrument: SwedbankPaySDK.PaymentAttemptInstrument) { + public func makeNativePaymentAttempt(instrument: SwedbankPaySDK.PaymentAttemptInstrument) { guard let ongoingModel = ongoingModel else { self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) @@ -110,7 +140,7 @@ public extension SwedbankPaySDK { var succeeded = false if let operation = ongoingModel.paymentSession.methods? - .first(where: { $0.name == instrument.name })?.operations? + .first(where: { $0.name == instrument.identifier })?.operations? .first(where: { $0.rel == .expandMethod || $0.rel == .startPaymentAttempt || $0.rel == .getPayment }) { sessionStartTimestamp = Date() @@ -137,12 +167,12 @@ public extension SwedbankPaySDK { case .swish(let msisdn): BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", succeeded: succeeded, - values: ["instrument": instrument.name, + values: ["instrument": instrument.identifier, "msisdn": msisdn])) case .creditCard(let prefill): BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", succeeded: succeeded, - values: ["instrument": instrument.name, + values: ["instrument": instrument.identifier, "paymentToken": prefill.paymentToken, "cardNumber": prefill.maskedPan, "cardExpiryMonth": prefill.expiryMonth, @@ -151,6 +181,10 @@ public extension SwedbankPaySDK { } + public func createSwedbankPaySDKController() { + + } + /// Abort an active payment session. /// /// Does nothing if there isn't an active payment session. @@ -318,7 +352,7 @@ public extension SwedbankPaySDK { } else if operations.contains(where: { $0.rel == .startPaymentAttempt }), let instrument = instrument, let startPaymentAttempt = ongoingModel?.paymentSession.methods? - .first(where: { $0.name == instrument.name })?.operations? + .first(where: { $0.name == instrument.identifier })?.operations? .first(where: { $0.rel == .startPaymentAttempt }) { makeRequest(operationOutputModel: startPaymentAttempt, culture: culture) @@ -352,7 +386,7 @@ public extension SwedbankPaySDK { let task = operation.firstTask(with: .scaRedirect), !scaRedirectDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "creq" })?.value }) { DispatchQueue.main.async { - self.delegate?.showViewController(viewController: self.webViewController) + self.delegate?.show3DSecureViewController(viewController: self.webViewController) BeaconService.shared.log(type: .sdkCallbackInvoked(name: "show3dSecure", succeeded: self.delegate != nil, @@ -367,15 +401,15 @@ public extension SwedbankPaySDK { } else if let redirectPayer = operations.first(where: { $0.rel == .redirectPayer }) { DispatchQueue.main.async { if redirectPayer.href == self.orderInfo?.cancelUrl?.absoluteString { - self.delegate?.paymentCanceled() + self.delegate?.paymentSessionCanceled() - BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentCanceled", + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentSessionCanceled", succeeded: self.delegate != nil, values: nil)) } else if redirectPayer.href == self.orderInfo?.completeUrl.absoluteString { - self.delegate?.paymentComplete() + self.delegate?.paymentSessionComplete() - BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentComplete", + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentSessionComplete", succeeded: self.delegate != nil, values: nil)) } else { @@ -401,7 +435,7 @@ public extension SwedbankPaySDK { return AvailableInstrument.swish(prefills: prefills) case .creditCard(let prefills, _, _): return AvailableInstrument.creditCard(prefills: prefills) - case .unknown(_): + case .unknown(let identifier): return nil } }) ?? [] @@ -412,7 +446,7 @@ public extension SwedbankPaySDK { BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentSessionFetched", succeeded: self.delegate != nil, - values: ["instruments": availableInstruments.compactMap({ $0.name }).joined(separator: ";")])) + values: ["instruments": availableInstruments.compactMap({ $0.identifier }).joined(separator: ";")])) } } else if let getPayment = operations.first(where: { $0.rel == .getPayment }) { DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { @@ -455,7 +489,7 @@ public extension SwedbankPaySDK { if !self.scaRedirectDataPerformed.contains(where: { $0.value == value }) { self.scaRedirectDataPerformed.append((name: task.expects!.first(where: { $0.name == "creq" })!.value!, value: value)) - self.delegate?.finishedWithViewController() + self.delegate?.dismiss3DSecureViewController() BeaconService.shared.log(type: .sdkCallbackInvoked(name: "dismiss3dSecure", succeeded: self.delegate != nil, @@ -466,13 +500,17 @@ public extension SwedbankPaySDK { } } case .failure(let error): - self.delegate?.sdkProblemWithViewController(error: error, retry: { + self.delegate?.paymentSession3DSecureViewControllerLoadFailed(error: error, retry: { self.scaRedirectDataPerformed(task: task, culture: culture) }) -// BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemWithViewController", -// succeeded: self.delegate != nil, -// values: ["problem": SwedbankPaySDK.PaymentSessionProblem.paymentSessionEndStateReached.rawValue])) + let error = error as NSError + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentSession3DSecureViewControllerLoadFailed", + succeeded: self.delegate != nil, + values: ["errorDescription": error.localizedDescription, + "errorCode": error.code, + "errorDomain": error.domain])) } } } diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDKPaymentSessionDelegate.swift b/SwedbankPaySDK/Classes/SwedbankPaySDKPaymentSessionDelegate.swift deleted file mode 100644 index 13a487b..0000000 --- a/SwedbankPaySDK/Classes/SwedbankPaySDKPaymentSessionDelegate.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// 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 - -/// 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 paymentComplete() - - /// Called whenever the payment has been canceled for any reason. - func paymentCanceled() - - /// 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) - - func showViewController(viewController: UIViewController) - - func finishedWithViewController() - - func sdkProblemWithViewController(error: Error, retry: @escaping ()->Void) -} From 750afee54be65a95db87297c4b40e5addb3ba0a6 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Fri, 28 Jun 2024 14:53:04 +0200 Subject: [PATCH 37/75] =?UTF-8?q?SP-59=20St=C3=B6d=20f=C3=B6r=20att=20visa?= =?UTF-8?q?=20betalmenyn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SwedbankPaySDK.xcodeproj/project.pbxproj | 6 +++ .../Classes/Api/Models/MethodBaseModel.swift | 4 ++ .../Api/Models/OperationOutputModel.swift | 3 ++ .../Classes/SwedbankPayConfiguration.swift | 50 +++++++++++++++++++ .../Classes/SwedbankPayPaymentSession.swift | 43 +++++++++++++++- 5 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 SwedbankPaySDK/Classes/SwedbankPayConfiguration.swift diff --git a/SwedbankPaySDK.xcodeproj/project.pbxproj b/SwedbankPaySDK.xcodeproj/project.pbxproj index 1f4dbf9..6e15887 100644 --- a/SwedbankPaySDK.xcodeproj/project.pbxproj +++ b/SwedbankPaySDK.xcodeproj/project.pbxproj @@ -49,6 +49,8 @@ 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 */; }; @@ -310,6 +312,7 @@ 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 = ""; }; @@ -816,6 +819,7 @@ C585AF2A237066EE006C2E16 /* SwedbankPaySDKController.swift */, C585AF28237066EE006C2E16 /* SwedbankPaySDKViewModel.swift */, 455A7B972C205DA3003CF320 /* SwedbankPayPaymentSession.swift */, + 45FE72A82C2EE74C00ECACB6 /* SwedbankPayConfiguration.swift */, 45C1008A2BF247F300AA3523 /* Api */, A5808B03261C93FD00FF7EC1 /* WebView */, 4517C69E2BFE34DC001687E7 /* Beacon */, @@ -1155,6 +1159,7 @@ 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 */, @@ -1282,6 +1287,7 @@ 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 */, diff --git a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift index 3963d85..4f89a88 100644 --- a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift @@ -99,12 +99,16 @@ extension SwedbankPaySDK { case creditCard(prefills: [CreditCardMethodPrefillModel]?) + case webBased(identifier: String) + var identifier: String { switch self { case .swish: return "Swish" case .creditCard: return "CreditCard" + case .webBased(identifier: let identifier): + return identifier } } } diff --git a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift index 54935fc..d86877e 100644 --- a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift @@ -42,6 +42,7 @@ enum OperationRel: Codable, Equatable, Hashable { case acknowledgeFailedAttempt case abortPayment case eventLogging + case viewPayment case unknown(String) @@ -60,6 +61,7 @@ enum OperationRel: Codable, Equatable, Hashable { case Self.acknowledgeFailedAttempt.rawValue: self = .acknowledgeFailedAttempt case Self.abortPayment.rawValue: self = .abortPayment case Self.eventLogging.rawValue: self = .eventLogging + case Self.viewPayment.rawValue: self = .viewPayment default: self = .unknown(type) } } @@ -81,6 +83,7 @@ enum OperationRel: Codable, Equatable, Hashable { case .acknowledgeFailedAttempt: "acknowledge-failed-attempt" case .abortPayment: "abort-payment" case .eventLogging: "event-logging" + case .viewPayment: "view-payment" case .unknown(let value): value } } 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 index 8dc91f3..aae42df 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -181,8 +181,47 @@ public extension SwedbankPaySDK { } - public func createSwedbankPaySDKController() { + public func createSwedbankPaySDKController(manualOrderInfo: SwedbankPaySDK.ViewPaymentOrderInfo? = nil) -> SwedbankPaySDKController? { + guard let ongoingModel = ongoingModel, + let operation = ongoingModel.operations?.first(where: { $0.rel == .viewPayment }), + let orderInfo = orderInfo else { + self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": SwedbankPaySDK.PaymentSessionProblem.internalInconsistencyError.rawValue])) + + return nil + } + + let configuration: SwedbankPayConfiguration + + if let manualOrderInfo { + configuration = SwedbankPayConfiguration( + isV3: manualOrderInfo.isV3, + webViewBaseURL: manualOrderInfo.webViewBaseURL, + viewPaymentLink: URL(string: operation.href!)!, + completeUrl: manualOrderInfo.completeUrl, + cancelUrl: manualOrderInfo.cancelUrl, + paymentUrl: manualOrderInfo.paymentUrl) + } else { + configuration = SwedbankPayConfiguration( + isV3: orderInfo.isV3, + webViewBaseURL: ongoingModel.paymentSession.urls?.hostUrls?.first, + viewPaymentLink: URL(string: operation.href!)!, + completeUrl: orderInfo.completeUrl, + cancelUrl: orderInfo.cancelUrl, + paymentUrl: orderInfo.paymentUrl) + } + + let viewController = SwedbankPaySDKController( + configuration: configuration, + withCheckin: false, + consumer: nil, + paymentOrder: nil, + userData: nil) + return viewController } /// Abort an active payment session. @@ -436,7 +475,7 @@ public extension SwedbankPaySDK { case .creditCard(let prefills, _, _): return AvailableInstrument.creditCard(prefills: prefills) case .unknown(let identifier): - return nil + return AvailableInstrument.webBased(identifier: identifier) } }) ?? [] From 339871c97868b03e133fe9898aeaa0a0af1ee113 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Fri, 28 Jun 2024 15:33:49 +0200 Subject: [PATCH 38/75] Code Review fixes --- .../Classes/WebView/SCAWebViewService.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift index f715749..3cfb0fd 100644 --- a/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift +++ b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift @@ -1,10 +1,17 @@ // -// SCAWebViewService.swift -// SwedbankPaySDK +// Copyright 2024 Swedbank AB // -// Created by Michael Balsiger on 2024-06-07. -// Copyright © 2024 Swedbank. All rights reserved. +// 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 WebKit From 2558b96720f07ea1098781521640ee1c5e6c85eb Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Mon, 1 Jul 2024 14:15:50 +0200 Subject: [PATCH 39/75] Clean up --- .../Classes/Api/Models/MethodBaseModel.swift | 6 +-- .../Api/Models/OperationOutputModel.swift | 2 +- .../Classes/SwedbankPayPaymentSession.swift | 53 +++++++++++-------- .../Classes/WebView/SCAWebViewService.swift | 2 +- .../SwedbankPaySCAWebViewController.swift | 2 +- 5 files changed, 36 insertions(+), 29 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift index 4f89a88..07877a0 100644 --- a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift @@ -15,7 +15,7 @@ import Foundation -public enum MethodBaseModel: Codable, Equatable, Hashable { +enum MethodBaseModel: Codable, Equatable, Hashable { case swish(prefills: [SwedbankPaySDK.SwishMethodPrefillModel]?, operations: [OperationOutputModel]?) case creditCard(prefills: [SwedbankPaySDK.CreditCardMethodPrefillModel]?, operations: [OperationOutputModel]?, cardBrands: [String]?) @@ -25,7 +25,7 @@ public enum MethodBaseModel: Codable, Equatable, Hashable { case instrument, prefills, operations, cardBrands } - public init(from decoder: Decoder) throws { + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(String.self, forKey: .instrument) @@ -46,7 +46,7 @@ public enum MethodBaseModel: Codable, Equatable, Hashable { } } - public func encode(to encoder: Encoder) throws { + func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch self { case .swish(let prefills, let operations): diff --git a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift index d86877e..081683e 100644 --- a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -public struct OperationOutputModel: Codable, Hashable { +struct OperationOutputModel: Codable, Hashable { let rel: OperationRel? let href: String? let method: String? diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index aae42df..d590d27 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -40,10 +40,18 @@ public protocol SwedbankPaySDKPaymentSessionDelegate: AnyObject { /// - 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() + /// Called if the 3D secure view loading failed. + /// + /// - parameter error: The error that caused the failure + /// - parameter retry: A block that can be called to retry. func paymentSession3DSecureViewControllerLoadFailed(error: Error, retry: @escaping ()->Void) } @@ -181,10 +189,17 @@ public extension SwedbankPaySDK { } - public func createSwedbankPaySDKController(manualOrderInfo: SwedbankPaySDK.ViewPaymentOrderInfo? = nil) -> SwedbankPaySDKController? { + /// 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() -> SwedbankPaySDKController? { guard let ongoingModel = ongoingModel, let operation = ongoingModel.operations?.first(where: { $0.rel == .viewPayment }), - let orderInfo = orderInfo else { + 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", @@ -194,25 +209,13 @@ public extension SwedbankPaySDK { return nil } - let configuration: SwedbankPayConfiguration - - if let manualOrderInfo { - configuration = SwedbankPayConfiguration( - isV3: manualOrderInfo.isV3, - webViewBaseURL: manualOrderInfo.webViewBaseURL, - viewPaymentLink: URL(string: operation.href!)!, - completeUrl: manualOrderInfo.completeUrl, - cancelUrl: manualOrderInfo.cancelUrl, - paymentUrl: manualOrderInfo.paymentUrl) - } else { - configuration = SwedbankPayConfiguration( - isV3: orderInfo.isV3, - webViewBaseURL: ongoingModel.paymentSession.urls?.hostUrls?.first, - viewPaymentLink: URL(string: operation.href!)!, - completeUrl: orderInfo.completeUrl, - cancelUrl: orderInfo.cancelUrl, - paymentUrl: orderInfo.paymentUrl) - } + 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, @@ -220,6 +223,10 @@ public extension SwedbankPaySDK { consumer: nil, paymentOrder: nil, userData: nil) + + BeaconService.shared.log(type: .sdkMethodInvoked(name: "createSwedbankPaySDKController", + succeeded: true, + values: nil)) return viewController } @@ -427,7 +434,7 @@ public extension SwedbankPaySDK { DispatchQueue.main.async { self.delegate?.show3DSecureViewController(viewController: self.webViewController) - BeaconService.shared.log(type: .sdkCallbackInvoked(name: "show3dSecure", + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "show3DSecureViewController", succeeded: self.delegate != nil, values: nil)) @@ -530,7 +537,7 @@ public extension SwedbankPaySDK { self.delegate?.dismiss3DSecureViewController() - BeaconService.shared.log(type: .sdkCallbackInvoked(name: "dismiss3dSecure", + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "dismiss3DSecureViewController", succeeded: self.delegate != nil, values: nil)) diff --git a/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift index 3cfb0fd..356fa11 100644 --- a/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift +++ b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift @@ -43,7 +43,7 @@ class SCAWebViewService: NSObject, WKNavigationDelegate { var request = URLRequest(url: url) request.httpMethod = task.method request.allHTTPHeaderFields = ["Content-Type": task.contentType ?? ""] - request.timeoutInterval = 5 + request.timeoutInterval = SwedbankPayAPIConstants.creditCardTimoutInterval if let bodyString = task.expects? .filter({ $0.type == "string" }) diff --git a/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift index 6933256..c366619 100644 --- a/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift +++ b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift @@ -88,7 +88,7 @@ class SwedbankPaySCAWebViewController: UIViewController { var request = URLRequest(url: url) request.httpMethod = task.method request.allHTTPHeaderFields = ["Content-Type": task.contentType ?? ""] - request.timeoutInterval = 5 + request.timeoutInterval = SwedbankPayAPIConstants.creditCardTimoutInterval if let bodyString = task.expects? .filter({ $0.type == "string" }) From c4e22214d920b33e06a69d0700c112578248d0ce Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Wed, 3 Jul 2024 13:47:25 +0200 Subject: [PATCH 40/75] WIP: Updating EndpointRouter to not take an OperationOutputModel --- .../Api/SwedbankPayAPIEnpointRouter.swift | 64 ++++++++++--------- .../Classes/SwedbankPayPaymentSession.swift | 52 +++++++-------- 2 files changed, 60 insertions(+), 56 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index c4a8003..a1c80da 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -16,6 +16,23 @@ import Foundation import UIKit +struct Endpoint { + let router: EnpointRouter? + let href: String? + let method: String? +} + +enum EnpointRouter { + case expandMethod(instrument: SwedbankPaySDK.PaymentAttemptInstrument) + case startPaymentAttempt(instrument: SwedbankPaySDK.PaymentAttemptInstrument, culture: String?) + case createAuthentication(methodCompletionIndicator: String) + case completeAuthentication(cRes: String) + case getPayment + case preparePayment + case acknowledgeFailedAttempt + case abortPayment +} + protocol EndpointRouterProtocol { var body: [String: Any?]? { get } var requestTimeoutInterval: TimeInterval { get } @@ -23,19 +40,14 @@ protocol EndpointRouterProtocol { } struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { - let model: OperationOutputModel - let culture: String? - let instrument: SwedbankPaySDK.PaymentAttemptInstrument? - let methodCompletionIndicator: String? - let cRes: String? - + let endpoint: Endpoint let sessionStartTimestamp: Date var body: [String: Any?]? { - switch model.rel { - case .expandMethod: - return ["instrumentName": instrument?.identifier] - case .startPaymentAttempt: + switch endpoint.router { + case .expandMethod(instrument: let instrument): + return ["instrumentName": instrument.identifier] + case .startPaymentAttempt(let instrument, let culture): switch instrument { case .swish(let msisdn): return ["culture": culture, @@ -58,14 +70,6 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { "screenWidth": String(Int32(UIScreen.main.nativeBounds.width)), "screenColorDepth": String(24)] ] - case .none: - 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 .preparePayment: return ["integration": "HostedView", @@ -82,8 +86,8 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { "service": ["name": "SwedbankPaySDK-iOS", "version": SwedbankPaySDK.VersionReporter.currentVersion] ] - case .createAuthentication: - return ["methodCompletionIndicator": methodCompletionIndicator ?? "N", + case .createAuthentication(let methodCompletionIndicator): + return ["methodCompletionIndicator": methodCompletionIndicator, "notificationUrl": SwedbankPayAPIConstants.notificationUrl, "requestWindowSize": "FULLSCREEN", "client": ["userAgent": SwedbankPaySDK.VersionReporter.userAgent, @@ -96,8 +100,8 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { "timeZoneOffset": TimeZone.current.offsetFromGMT(), "javascriptEnabled": true] ] - case .completeAuthentication: - return ["cRes": cRes ?? "", + case .completeAuthentication(let cRes): + return ["cRes": cRes, "client": ["userAgent": SwedbankPaySDK.VersionReporter.userAgent, "ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? ""], ] @@ -107,8 +111,8 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { } var requestTimeoutInterval: TimeInterval { - switch model.rel { - case .startPaymentAttempt: + switch endpoint.router { + case .startPaymentAttempt(let instrument, _): switch instrument { case .creditCard: return SwedbankPayAPIConstants.creditCardTimoutInterval @@ -124,8 +128,8 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { } var sessionTimeoutInterval: TimeInterval { - switch model.rel { - case .startPaymentAttempt: + switch endpoint.router { + case .startPaymentAttempt(let instrument, _): switch instrument { case .creditCard: return SwedbankPayAPIConstants.creditCardTimoutInterval @@ -174,7 +178,7 @@ extension SwedbankPayAPIEnpointRouter { } private func requestWithDataResponse(requestStartTimestamp: Date, handler: @escaping (Result) -> Void) { - guard let href = model.href, + guard let href = endpoint.href, var components = URLComponents(string: href) else { handler(.failure(SwedbankPayAPIError.invalidUrl)) return @@ -190,7 +194,7 @@ extension SwedbankPayAPIEnpointRouter { } var request = URLRequest(url: url) - request.httpMethod = model.method + request.httpMethod = endpoint.method request.allHTTPHeaderFields = SwedbankPayAPIConstants.commonHeaders request.timeoutInterval = requestTimeoutInterval @@ -213,8 +217,8 @@ extension SwedbankPayAPIEnpointRouter { } BeaconService.shared.log(type: .httpRequest(duration: Int32((Date().timeIntervalSince(requestStartTimestamp) * 1000.0).rounded()), - requestUrl: model.href ?? "", - method: model.method ?? "", + requestUrl: endpoint.href ?? "", + method: endpoint.method ?? "", responseStatusCode: responseStatusCode, values: values)) diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index d590d27..ee4e242 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -120,7 +120,7 @@ public extension SwedbankPaySDK { tasks: nil) sessionStartTimestamp = Date() - makeRequest(operationOutputModel: model) + makeRequest(router: nil, operation: model) BeaconService.shared.clear() BeaconService.shared.log(type: .sdkMethodInvoked(name: "startPaymentSession", @@ -152,10 +152,17 @@ public extension SwedbankPaySDK { .first(where: { $0.rel == .expandMethod || $0.rel == .startPaymentAttempt || $0.rel == .getPayment }) { sessionStartTimestamp = Date() - makeRequest(operationOutputModel: operation, culture: ongoingModel.paymentSession.culture) - - if operation.rel == .startPaymentAttempt { + + switch operation.rel { + case .expandMethod: + makeRequest(router: .expandMethod(instrument: instrument), operation: operation) + case .startPaymentAttempt: + makeRequest(router: .startPaymentAttempt(instrument: instrument, culture: ongoingModel.paymentSession.culture), operation: operation) self.instrument = nil + case .getPayment: + makeRequest(router: .getPayment, operation: operation) + default: + fatalError("Operantion rel is not supported for makeNativePaymentAttempt: \(String(describing: operation.rel))") } succeeded = true @@ -249,7 +256,7 @@ public extension SwedbankPaySDK { if let operation = ongoingModel.operations? .first(where: { $0.rel == .abortPayment }) { sessionStartTimestamp = Date() - makeRequest(operationOutputModel: operation, culture: ongoingModel.paymentSession.culture) + makeRequest(router: .abortPayment, operation: operation) succeeded = true } @@ -258,17 +265,13 @@ public extension SwedbankPaySDK { values: nil)) } - private func makeRequest(operationOutputModel: OperationOutputModel, culture: String? = nil, methodCompletionIndicator: String? = nil, cRes: String? = nil) { - SwedbankPayAPIEnpointRouter(model: operationOutputModel, - culture: culture, - instrument: instrument, - methodCompletionIndicator: methodCompletionIndicator, - cRes: cRes, + 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, operationOutputModel.rel == nil { + if self.automaticConfiguration, router == nil { guard let urls = paymentOutputModel.paymentSession.urls, urls.completeUrl != nil, urls.hostUrls != nil else { self.delegate?.sdkProblemOccurred(problem: .automaticConfigurationFailed) @@ -299,11 +302,7 @@ public extension SwedbankPaySDK { let problem = SwedbankPaySDK.PaymentSessionProblem.paymentSessionAPIRequestFailed(error: failure, retry: { self.sessionStartTimestamp = Date() - self.makeRequest(operationOutputModel: operationOutputModel, - culture: culture, - methodCompletionIndicator: - methodCompletionIndicator, - cRes: cRes) + self.makeRequest(router: router, operation: operation) }) self.delegate?.sdkProblemOccurred(problem: problem) @@ -386,7 +385,8 @@ public extension SwedbankPaySDK { } } - makeRequest(operationOutputModel: problemOperation, culture: culture) + makeRequest(router: .acknowledgeFailedAttempt, operation: problemOperation) + } let operations = paymentOutputModel.prioritisedOperations @@ -394,14 +394,14 @@ public extension SwedbankPaySDK { print("\(operations.compactMap({ $0.rel }))") if let preparePayment = operations.first(where: { $0.rel == .preparePayment }) { - makeRequest(operationOutputModel: preparePayment, culture: culture) + makeRequest(router: .preparePayment, operation: preparePayment) } else if operations.contains(where: { $0.rel == .startPaymentAttempt }), let instrument = instrument, let startPaymentAttempt = ongoingModel?.paymentSession.methods? .first(where: { $0.name == instrument.identifier })?.operations? .first(where: { $0.rel == .startPaymentAttempt }) { - makeRequest(operationOutputModel: startPaymentAttempt, culture: culture) + makeRequest(router: .startPaymentAttempt(instrument: instrument, culture: culture), operation: startPaymentAttempt) self.instrument = nil } else if let launchClientApp = operations.first(where: { $0.firstTask(with: .launchClientApp) != nil }), let tasks = launchClientApp.firstTask(with: .launchClientApp), @@ -427,7 +427,7 @@ public extension SwedbankPaySDK { } else if let createAuthentication = operations.first(where: { $0.rel == .createAuthentication }), let task = createAuthentication.firstTask(with: .scaMethodRequest), let scaMethod = scaMethodRequestDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value }) { - makeRequest(operationOutputModel: createAuthentication, culture: culture, methodCompletionIndicator: scaMethod.value) + makeRequest(router: .createAuthentication(methodCompletionIndicator: scaMethod.value), operation: createAuthentication) } else if let operation = operations.first(where: { $0.firstTask(with: .scaRedirect) != nil }), let task = operation.firstTask(with: .scaRedirect), !scaRedirectDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "creq" })?.value }) { @@ -443,7 +443,7 @@ public extension SwedbankPaySDK { } else if let completeAuthentication = operations.first(where: { $0.rel == .completeAuthentication }), let task = completeAuthentication.tasks?.first(where: { $0.expects?.contains(where: { $0.name == "creq" } ) ?? false } ), let scaRedirect = scaRedirectDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "creq" })?.value }) { - makeRequest(operationOutputModel: completeAuthentication, culture: culture, cRes: scaRedirect.value) + makeRequest(router: .completeAuthentication(cRes: scaRedirect.value), operation: completeAuthentication) } else if let redirectPayer = operations.first(where: { $0.rel == .redirectPayer }) { DispatchQueue.main.async { if redirectPayer.href == self.orderInfo?.cancelUrl?.absoluteString { @@ -497,7 +497,7 @@ public extension SwedbankPaySDK { } else if let getPayment = operations.first(where: { $0.rel == .getPayment }) { DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { self.sessionStartTimestamp = Date() - self.makeRequest(operationOutputModel: getPayment, culture: culture) + self.makeRequest(router: .getPayment, operation: getPayment) } } else if !hasShowedError { DispatchQueue.main.async { @@ -510,7 +510,7 @@ public extension SwedbankPaySDK { } } - func handleCallbackUrl(_ url: URL) -> Bool { + internal func handleCallbackUrl(_ url: URL) -> Bool { guard url == orderInfo?.paymentUrl else { return false } @@ -519,7 +519,7 @@ public extension SwedbankPaySDK { if let operation = ongoingModel.paymentSession.allMethodOperations .first(where: { $0.rel == .getPayment }) { sessionStartTimestamp = Date() - makeRequest(operationOutputModel: operation, culture: ongoingModel.paymentSession.culture) + makeRequest(router: .getPayment, operation: operation) } } @@ -528,7 +528,7 @@ public extension SwedbankPaySDK { return true } - func scaRedirectDataPerformed(task: IntegrationTask, culture: String?) { + private func scaRedirectDataPerformed(task: IntegrationTask, culture: String?) { self.webViewController.load(task: task) { result in switch result { case .success(let value): From 9d92103bb63931638509355557ad3217041669bf Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Wed, 4 Sep 2024 14:56:31 +0200 Subject: [PATCH 41/75] =?UTF-8?q?SP-62=20St=C3=B6d=20f=C3=B6r=20null=20i?= =?UTF-8?q?=20parametrar=20f=C3=B6r=20SCA=20Method=20Request?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Classes/Api/Models/IntegrationTask.swift | 19 +++++++++++++++++++ .../Classes/SwedbankPayPaymentSession.swift | 4 ++-- .../Classes/WebView/SCAWebViewService.swift | 15 ++------------- .../SwedbankPaySCAWebViewController.swift | 15 ++------------- 4 files changed, 25 insertions(+), 28 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift b/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift index 82a927f..c54927b 100644 --- a/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift +++ b/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift @@ -13,6 +13,8 @@ // 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? @@ -60,3 +62,20 @@ struct ExpectationModel: Codable, Hashable { let type: String? let value: String? } + +extension Array where Element == ExpectationModel { + var httpBody: Data? { + return self.filter({ $0.type == "string" }) + .compactMap({ + guard let name = $0.name else { + return nil + } + + let value = $0.value?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "" + + return name + "=" + value + }) + .joined(separator: "&") + .data(using: .utf8) + } +} diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index ee4e242..f3618f6 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -409,7 +409,7 @@ public extension SwedbankPaySDK { self.launchClientApp(task: launchClientApp.firstTask(with: .launchClientApp)!) } else if let scaMethodRequest = operations.first(where: { $0.firstTask(with: .scaMethodRequest) != nil }), let task = scaMethodRequest.firstTask(with: .scaMethodRequest), - !scaMethodRequestDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value }) { + !scaMethodRequestDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "" }) { DispatchQueue.main.async { self.webViewService.load(task: task) { result in switch result { @@ -426,7 +426,7 @@ public extension SwedbankPaySDK { } } else if let createAuthentication = operations.first(where: { $0.rel == .createAuthentication }), let task = createAuthentication.firstTask(with: .scaMethodRequest), - let scaMethod = scaMethodRequestDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value }) { + let scaMethod = scaMethodRequestDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "" }) { makeRequest(router: .createAuthentication(methodCompletionIndicator: scaMethod.value), operation: createAuthentication) } else if let operation = operations.first(where: { $0.firstTask(with: .scaRedirect) != nil }), let task = operation.firstTask(with: .scaRedirect), diff --git a/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift index 356fa11..e933ab3 100644 --- a/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift +++ b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift @@ -45,19 +45,8 @@ class SCAWebViewService: NSObject, WKNavigationDelegate { request.allHTTPHeaderFields = ["Content-Type": task.contentType ?? ""] request.timeoutInterval = SwedbankPayAPIConstants.creditCardTimoutInterval - if let bodyString = task.expects? - .filter({ $0.type == "string" }) - .compactMap({ - guard let name = $0.name, - let value = $0.value?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { - return nil - } - - return name + "=" + value - }) - .joined(separator: "&") { - - request.httpBody = bodyString.data(using: .utf8) + if let httpBody = task.expects?.httpBody { + request.httpBody = httpBody } webView?.navigationDelegate = self diff --git a/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift index c366619..85740d0 100644 --- a/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift +++ b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift @@ -90,19 +90,8 @@ class SwedbankPaySCAWebViewController: UIViewController { request.allHTTPHeaderFields = ["Content-Type": task.contentType ?? ""] request.timeoutInterval = SwedbankPayAPIConstants.creditCardTimoutInterval - if let bodyString = task.expects? - .filter({ $0.type == "string" }) - .compactMap({ - guard let name = $0.name, - let value = $0.value?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { - return nil - } - - return name + "=" + value - }) - .joined(separator: "&") { - - request.httpBody = bodyString.data(using: .utf8) + if let httpBody = task.expects?.httpBody { + request.httpBody = httpBody } let navigation = webView.load(request) From 2bcf6a773bf52a996a7a4334948f025048c73881 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Fri, 6 Sep 2024 10:31:04 +0200 Subject: [PATCH 42/75] =?UTF-8?q?SP-64=20Sluta=20anv=C3=A4nda=20h=C3=A5rdk?= =?UTF-8?q?odad=20NotificationURL=20till=20CreditCard=20authentication=20r?= =?UTF-8?q?equest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Api/Models/OperationOutputModel.swift | 1 + .../Classes/Api/Models/ProblemDetails.swift | 1 + .../Classes/Api/SwedbankPayAPIConstants.swift | 2 -- .../Api/SwedbankPayAPIEnpointRouter.swift | 10 +++---- .../Classes/SwedbankPayPaymentSession.swift | 26 ++++++++++++++++--- .../SwedbankPaySCAWebViewController.swift | 4 ++- 6 files changed, 32 insertions(+), 12 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift index 081683e..0397133 100644 --- a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift @@ -19,6 +19,7 @@ struct OperationOutputModel: Codable, Hashable { let method: String? let next: Bool? let tasks: [IntegrationTask]? + let expects: [ExpectationModel]? } extension OperationOutputModel { diff --git a/SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift b/SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift index a7c3d7b..e224b15 100644 --- a/SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift +++ b/SwedbankPaySDK/Classes/Api/Models/ProblemDetails.swift @@ -20,6 +20,7 @@ public extension SwedbankPaySDK { 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 index db0cb3a..c680243 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIConstants.swift @@ -22,8 +22,6 @@ struct SwedbankPayAPIConstants { static var requestTimeoutInterval = 10.0 static var sessionTimeoutInterval = 20.0 static var creditCardTimoutInterval = 30.0 - - static var notificationUrl = "https://fake.payex.com/notification" } private enum HTTPHeaderField: String { diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index a1c80da..093216e 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -25,7 +25,7 @@ struct Endpoint { enum EnpointRouter { case expandMethod(instrument: SwedbankPaySDK.PaymentAttemptInstrument) case startPaymentAttempt(instrument: SwedbankPaySDK.PaymentAttemptInstrument, culture: String?) - case createAuthentication(methodCompletionIndicator: String) + case createAuthentication(methodCompletionIndicator: String, notificationUrl: String) case completeAuthentication(cRes: String) case getPayment case preparePayment @@ -80,15 +80,15 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { "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, + "languageHeader": Locale.current.identifier.replacingOccurrences(of: "_", with: "-"), "timeZoneOffset": TimeZone.current.offsetFromGMT(), "javascriptEnabled": true], "service": ["name": "SwedbankPaySDK-iOS", "version": SwedbankPaySDK.VersionReporter.currentVersion] ] - case .createAuthentication(let methodCompletionIndicator): + case .createAuthentication(let methodCompletionIndicator, let notificationUrl): return ["methodCompletionIndicator": methodCompletionIndicator, - "notificationUrl": SwedbankPayAPIConstants.notificationUrl, + "notificationUrl": notificationUrl, "requestWindowSize": "FULLSCREEN", "client": ["userAgent": SwedbankPaySDK.VersionReporter.userAgent, "ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? "", @@ -96,7 +96,7 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { "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, + "languageHeader": Locale.current.identifier.replacingOccurrences(of: "_", with: "-"), "timeZoneOffset": TimeZone.current.offsetFromGMT(), "javascriptEnabled": true] ] diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index f3618f6..2eda89e 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -73,6 +73,7 @@ public extension SwedbankPaySDK { 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() @@ -107,6 +108,7 @@ public extension SwedbankPaySDK { hasShownProblemDetails = [] scaMethodRequestDataPerformed = [] scaRedirectDataPerformed = [] + notificationUrl = nil hasShownAvailableInstruments = false if automaticConfiguration { @@ -117,7 +119,8 @@ public extension SwedbankPaySDK { href: sessionURL.absoluteString, method: "GET", next: nil, - tasks: nil) + tasks: nil, + expects: nil) sessionStartTimestamp = Date() makeRequest(router: nil, operation: model) @@ -425,13 +428,27 @@ public extension SwedbankPaySDK { } } } else if let createAuthentication = operations.first(where: { $0.rel == .createAuthentication }), - let task = createAuthentication.firstTask(with: .scaMethodRequest), - let scaMethod = scaMethodRequestDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "" }) { - makeRequest(router: .createAuthentication(methodCompletionIndicator: scaMethod.value), operation: createAuthentication) + let notificationUrl = createAuthentication.expects?.first(where: { $0.name == "NotificationUrl" })?.value { + self.notificationUrl = notificationUrl + + if let task = createAuthentication.firstTask(with: .scaMethodRequest), + let scaMethod = scaMethodRequestDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "" }) { + makeRequest(router: .createAuthentication(methodCompletionIndicator: scaMethod.value, notificationUrl: notificationUrl), operation: createAuthentication) + } else if let methodCompletionIndicator = createAuthentication.expects?.first(where: { $0.name == "methodCompletionIndicator" })?.value { + makeRequest(router: .createAuthentication(methodCompletionIndicator: methodCompletionIndicator, notificationUrl: notificationUrl), operation: createAuthentication) + } else { + self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) + + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", + succeeded: self.delegate != nil, + values: ["problem": SwedbankPaySDK.PaymentSessionProblem.internalInconsistencyError.rawValue])) + } } else if let operation = operations.first(where: { $0.firstTask(with: .scaRedirect) != nil }), let task = operation.firstTask(with: .scaRedirect), !scaRedirectDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "creq" })?.value }) { DispatchQueue.main.async { + self.webViewController.notificationUrl = notificationUrl + self.delegate?.show3DSecureViewController(viewController: self.webViewController) BeaconService.shared.log(type: .sdkCallbackInvoked(name: "show3DSecureViewController", @@ -471,6 +488,7 @@ public extension SwedbankPaySDK { hasShownProblemDetails = [] scaMethodRequestDataPerformed = [] scaRedirectDataPerformed = [] + notificationUrl = nil hasShownAvailableInstruments = false } else if (operations.contains(where: { $0.rel == .expandMethod }) || operations.contains(where: { $0.rel == .startPaymentAttempt })) && hasShownAvailableInstruments == false { diff --git a/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift index 85740d0..768ecab 100644 --- a/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift +++ b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift @@ -48,6 +48,8 @@ class SwedbankPaySCAWebViewController: UIViewController { let activityView = UIActivityIndicatorView() + var notificationUrl: String? + init() { let preferences = WKPreferences() preferences.javaScriptEnabled = true @@ -148,7 +150,7 @@ extension SwedbankPaySCAWebViewController: WKNavigationDelegate { ) { let request = navigationAction.request - if request.url?.absoluteString == SwedbankPayAPIConstants.notificationUrl, + 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)"), From 66eb484a74b61a56abc68236136fdd0ece04fe26 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Fri, 6 Sep 2024 14:50:28 +0200 Subject: [PATCH 43/75] self.notificationUrl fix --- SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 2eda89e..f5d9341 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -447,7 +447,7 @@ public extension SwedbankPaySDK { let task = operation.firstTask(with: .scaRedirect), !scaRedirectDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "creq" })?.value }) { DispatchQueue.main.async { - self.webViewController.notificationUrl = notificationUrl + self.webViewController.notificationUrl = self.notificationUrl self.delegate?.show3DSecureViewController(viewController: self.webViewController) From dbc97a900a78972d34d5d11a755ea36e4b086bee Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Fri, 6 Sep 2024 14:51:34 +0200 Subject: [PATCH 44/75] =?UTF-8?q?SP-70=20=C3=84ndring=20av=20format=20f?= =?UTF-8?q?=C3=B6r=20timeZoneOffset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Classes/Api/Helpers/TimeZone+OffsetFromGMT.swift | 5 +++++ SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/Helpers/TimeZone+OffsetFromGMT.swift b/SwedbankPaySDK/Classes/Api/Helpers/TimeZone+OffsetFromGMT.swift index 254de26..4d9f6814 100644 --- a/SwedbankPaySDK/Classes/Api/Helpers/TimeZone+OffsetFromGMT.swift +++ b/SwedbankPaySDK/Classes/Api/Helpers/TimeZone+OffsetFromGMT.swift @@ -22,4 +22,9 @@ extension TimeZone { localTimeZoneFormatter.dateFormat = "Z" return localTimeZoneFormatter.string(from: Date()) } + + func minutesFromGMT() -> String { + let minutes = (secondsFromGMT() / 60) + return String(minutes) + } } diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index 093216e..d422c0d 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -81,7 +81,7 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { "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.offsetFromGMT(), + "timeZoneOffset": TimeZone.current.minutesFromGMT(), "javascriptEnabled": true], "service": ["name": "SwedbankPaySDK-iOS", "version": SwedbankPaySDK.VersionReporter.currentVersion] @@ -97,7 +97,7 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { "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.offsetFromGMT(), + "timeZoneOffset": TimeZone.current.minutesFromGMT(), "javascriptEnabled": true] ] case .completeAuthentication(let cRes): From 810742932fdfc2da331d588569607f405d617e78 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Fri, 6 Sep 2024 15:43:35 +0200 Subject: [PATCH 45/75] Code Review Fixes --- .../Classes/SwedbankPayPaymentSession.swift | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index f5d9341..5dddd2d 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -412,14 +412,15 @@ public extension SwedbankPaySDK { self.launchClientApp(task: launchClientApp.firstTask(with: .launchClientApp)!) } else if let scaMethodRequest = operations.first(where: { $0.firstTask(with: .scaMethodRequest) != nil }), let task = scaMethodRequest.firstTask(with: .scaMethodRequest), - !scaMethodRequestDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "" }) { + task.href != nil, + !scaMethodRequestDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "null" }) { DispatchQueue.main.async { self.webViewService.load(task: task) { result in switch result { case .success: - self.scaMethodRequestDataPerformed.append((name: task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "", value: "Y")) + self.scaMethodRequestDataPerformed.append((name: task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "null", value: "Y")) case .failure(let error): - self.scaMethodRequestDataPerformed.append((name: task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "", value: "N")) + self.scaMethodRequestDataPerformed.append((name: task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "null", value: "N")) } if let model = self.ongoingModel { @@ -432,16 +433,12 @@ public extension SwedbankPaySDK { self.notificationUrl = notificationUrl if let task = createAuthentication.firstTask(with: .scaMethodRequest), - let scaMethod = scaMethodRequestDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "" }) { + let scaMethod = scaMethodRequestDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "null" }) { makeRequest(router: .createAuthentication(methodCompletionIndicator: scaMethod.value, notificationUrl: notificationUrl), operation: createAuthentication) } else if let methodCompletionIndicator = createAuthentication.expects?.first(where: { $0.name == "methodCompletionIndicator" })?.value { makeRequest(router: .createAuthentication(methodCompletionIndicator: methodCompletionIndicator, notificationUrl: notificationUrl), operation: createAuthentication) } else { - self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) - - BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", - succeeded: self.delegate != nil, - values: ["problem": SwedbankPaySDK.PaymentSessionProblem.internalInconsistencyError.rawValue])) + makeRequest(router: .createAuthentication(methodCompletionIndicator: "U", notificationUrl: notificationUrl), operation: createAuthentication) } } else if let operation = operations.first(where: { $0.firstTask(with: .scaRedirect) != nil }), let task = operation.firstTask(with: .scaRedirect), From 3fe9c0d366d42d2f44362147ec067366066b7d37 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Fri, 13 Sep 2024 13:46:42 +0200 Subject: [PATCH 46/75] =?UTF-8?q?WIP:=20SP-72=20Grundfunktionalitet=20f?= =?UTF-8?q?=C3=B6r=20Apple=20Pay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SwedbankPaySDK.xcodeproj/project.pbxproj | 20 +++ .../Classes/Api/Models/IntegrationTask.swift | 102 +++++++++-- .../Classes/Api/Models/MethodBaseModel.swift | 17 ++ .../Api/Models/PaymentAttemptInstrument.swift | 3 + .../Api/SwedbankPayAPIEnpointRouter.swift | 11 +- .../ApplePay/ApplePayEndpointRouter.swift | 93 ++++++++++ .../ApplePay/SwedbankPayAuthorization.swift | 167 ++++++++++++++++++ .../ApplePay/SwedbankPaymentNetwork.swift | 87 +++++++++ .../Classes/Beacon/BeaconEndpointRouter.swift | 2 +- .../Classes/SwedbankPayPaymentSession.swift | 35 +++- .../PaymentSessionProblem.swift | 2 + 11 files changed, 524 insertions(+), 15 deletions(-) create mode 100644 SwedbankPaySDK/Classes/ApplePay/ApplePayEndpointRouter.swift create mode 100644 SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift create mode 100644 SwedbankPaySDK/Classes/ApplePay/SwedbankPaymentNetwork.swift diff --git a/SwedbankPaySDK.xcodeproj/project.pbxproj b/SwedbankPaySDK.xcodeproj/project.pbxproj index 6e15887..05cd65d 100644 --- a/SwedbankPaySDK.xcodeproj/project.pbxproj +++ b/SwedbankPaySDK.xcodeproj/project.pbxproj @@ -14,10 +14,13 @@ 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 */; }; + 455849302C93305C0062A315 /* ApplePayEndpointRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4558492F2C93305C0062A315 /* ApplePayEndpointRouter.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 */; }; @@ -295,8 +298,11 @@ 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 = ""; }; + 4558492F2C93305C0062A315 /* ApplePayEndpointRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePayEndpointRouter.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 = ""; }; @@ -500,6 +506,16 @@ path = Beacon; sourceTree = ""; }; + 4558492E2C932FE90062A315 /* ApplePay */ = { + isa = PBXGroup; + children = ( + 456913E52C78C2C60014BD41 /* SwedbankPayAuthorization.swift */, + 455849312C945BAD0062A315 /* SwedbankPaymentNetwork.swift */, + 4558492F2C93305C0062A315 /* ApplePayEndpointRouter.swift */, + ); + path = ApplePay; + sourceTree = ""; + }; 45C1008A2BF247F300AA3523 /* Api */ = { isa = PBXGroup; children = ( @@ -823,6 +839,7 @@ 45C1008A2BF247F300AA3523 /* Api */, A5808B03261C93FD00FF7EC1 /* WebView */, 4517C69E2BFE34DC001687E7 /* Beacon */, + 4558492E2C932FE90062A315 /* ApplePay */, A52E0ACA24BCAA6D00770286 /* GoodWebViewRedirects.swift */, ); path = Classes; @@ -1151,10 +1168,12 @@ 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 */, + 455849302C93305C0062A315 /* ApplePayEndpointRouter.swift in Sources */, A504895123C8A2FD00201DEC /* SwedbankPayWebContent.swift in Sources */, C50CF69D237AACC3003F79DF /* Consumer.swift in Sources */, 45C1009F2BF2583D00AA3523 /* MethodBaseModel.swift in Sources */, @@ -1166,6 +1185,7 @@ 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 */, diff --git a/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift b/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift index c54927b..ab97c38 100644 --- a/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift +++ b/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift @@ -27,6 +27,7 @@ enum IntegrationTaskRel: Codable, Equatable, Hashable { case scaMethodRequest case scaRedirect case launchClientApp + case walletSdk case unknown(String) @@ -38,6 +39,7 @@ enum IntegrationTaskRel: Codable, Equatable, Hashable { 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) } } @@ -52,30 +54,108 @@ enum IntegrationTaskRel: Codable, Equatable, Hashable { case .scaMethodRequest: "sca-method-request" case .scaRedirect: "sca-redirect" case .launchClientApp: "launch-client-app" + case .walletSdk: "wallet-sdk" case .unknown(let value): value } } } -struct ExpectationModel: Codable, Hashable { - let name: String? - let type: String? - let value: String? +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.filter({ $0.type == "string" }) - .compactMap({ - guard let name = $0.name else { + return self.compactMap({ + switch $0 { + case .string(let name, let value): + guard let name = name else { return nil } - let value = $0.value?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "" + let value = value?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "" return name + "=" + value - }) - .joined(separator: "&") - .data(using: .utf8) + default: + return nil + } + }) + .joined(separator: "&") + .data(using: .utf8) } } diff --git a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift index 07877a0..fbca225 100644 --- a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift @@ -18,6 +18,7 @@ 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 unknown(String) @@ -41,6 +42,11 @@ enum MethodBaseModel: Codable, Equatable, Hashable { 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 = .unknown(type) } @@ -56,6 +62,9 @@ enum MethodBaseModel: Codable, Equatable, Hashable { 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 .unknown(let type): try container.encode(type) } @@ -67,6 +76,8 @@ enum MethodBaseModel: Codable, Equatable, Hashable { return "Swish" case .creditCard: return "CreditCard" + case .applePay: + return "ApplePay" case .unknown: return "Unknown" } @@ -78,6 +89,8 @@ enum MethodBaseModel: Codable, Equatable, Hashable { return opertations case .creditCard(_, let opertations, _): return opertations + case .applePay(let operations, _): + return operations case .unknown: return nil } @@ -99,6 +112,8 @@ extension SwedbankPaySDK { case creditCard(prefills: [CreditCardMethodPrefillModel]?) + case applePay + case webBased(identifier: String) var identifier: String { @@ -107,6 +122,8 @@ extension SwedbankPaySDK { return "Swish" case .creditCard: return "CreditCard" + case .applePay: + return "ApplePay" case .webBased(identifier: let identifier): return identifier } diff --git a/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift index ecf97ac..aa1332f 100644 --- a/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift @@ -18,6 +18,7 @@ extension SwedbankPaySDK { public enum PaymentAttemptInstrument { case swish(msisdn: String?) case creditCard(prefill: CreditCardMethodPrefillModel) + case applePay var identifier: String { switch self { @@ -25,6 +26,8 @@ extension SwedbankPaySDK { return "Swish" case .creditCard: return "CreditCard" + case .applePay: + return "ApplePay" } } } diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index d422c0d..89db792 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -15,6 +15,7 @@ import Foundation import UIKit +import PassKit struct Endpoint { let router: EnpointRouter? @@ -70,10 +71,18 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { "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 .preparePayment: return ["integration": "HostedView", - "deviceAcceptedWallets": "", + "deviceAcceptedWallets": PKPaymentAuthorizationController.canMakePayments() ? "ApplePay" : "", "client": ["userAgent": SwedbankPaySDK.VersionReporter.userAgent, "ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? "", "screenHeight": String(Int32(UIScreen.main.nativeBounds.height)), diff --git a/SwedbankPaySDK/Classes/ApplePay/ApplePayEndpointRouter.swift b/SwedbankPaySDK/Classes/ApplePay/ApplePayEndpointRouter.swift new file mode 100644 index 0000000..a5ada73 --- /dev/null +++ b/SwedbankPaySDK/Classes/ApplePay/ApplePayEndpointRouter.swift @@ -0,0 +1,93 @@ +// +// 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 ApplePayEndpointRouterProtocol { + var body: [String: Any?]? { get } +} + +struct ApplePayEndpointRouter: ApplePayEndpointRouterProtocol { + var cardNetwork: String + var paymentPayload: String + var transactionIdentifier: String + var shippingAddress: [String: String] + + // MARK: - Body + var body: [String: Any?]? { + var body: [String: Any?] = ["Instrument": "ApplePay", + "CardNetwork": cardNetwork, + "PaymentPayload": paymentPayload, + "TransactionIdentifier": transactionIdentifier, + "ShippingAddress": shippingAddress] + + return body + } +} + +extension ApplePayEndpointRouter { + func makeRequest(handler: @escaping (Result) -> Void) { + let requestStartTimestamp: Date = Date() + + requestWithDataResponse { result in + switch result { + case .success(let data): + handler(.success(())) + case .failure(let error): + handler(.failure(error)) + } + } + } + + private func requestWithDataResponse(handler: @escaping (Result) -> Void) { + handler(.success(())) + +// let href = "https://stuff" +// +// guard 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/ApplePay/SwedbankPayAuthorization.swift b/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift new file mode 100644 index 0000000..474443d --- /dev/null +++ b/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.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 +import PassKit + +class SwedbankPayAuthorization: NSObject { + static let shared = SwedbankPayAuthorization() + + private var handler: ((Result) -> Void)? + private var errors: [Error]? + private var status: PKPaymentAuthorizationStatus? + + func testApplePay(task: IntegrationTask, handler: @escaping (Result) -> Void) { + self.errors = nil + self.status = nil + + 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 = "merchant.com.swedbankpay.exampleapp" + + if let merchantCapabilities = task.expects?.first(where: { $0.name == "MerchantCapabilities" })?.stringArray?.contains(where: { $0 == "supports3DS" }) { + 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 { + debugPrint("Presented payment controller") + } else { + debugPrint("Failed to present payment controller") + } + }) + } +} + +extension SwedbankPayAuthorization: PKPaymentAuthorizationControllerDelegate { + func paymentAuthorizationControllerDidFinish(_ controller: PKPaymentAuthorizationController) { + if let handler = self.handler { + if let status = status { + handler(.success(())) + } else { + handler(.failure(self.errors?.first ?? SwedbankPayAPIError.unknown)) + } + } + + self.handler = nil + + debugPrint("Payment Authorization Controller Did Finish") + controller.dismiss() + } + + func paymentAuthorizationController(_ controller: PKPaymentAuthorizationController, didAuthorizePayment payment: PKPayment, handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) { + if let pkPaymentNetwork = payment.token.paymentMethod.network, + let cardNetwork = SwedbankPaymentNetwork(pkPaymentNetwork: pkPaymentNetwork) { + let paymentPayload = payment.token.paymentData.base64EncodedString() + let transactionIdentifier = payment.token.transactionIdentifier + + var shippingAddress: [String: String] = [:] + + if let postalAddress = payment.shippingContact?.postalAddress { + shippingAddress["postalAddress"] = postalAddress.postalCode + } + + if let name = payment.shippingContact?.name { + if #available(iOS 15.0, *) { + shippingAddress["name"] = name.formatted() + } else { + var nameArray: [String] = [] + + if let givenName = name.givenName { + nameArray.append(givenName) + } + + if let middleName = name.middleName { + nameArray.append(middleName) + } + + if let familyName = name.familyName { + nameArray.append(familyName) + } + + shippingAddress["name"] = nameArray.joined(separator: " ") + } + } + + if let phoneNumber = payment.shippingContact?.phoneNumber { + shippingAddress["phone"] = phoneNumber.stringValue + } + + if let emailAddress = payment.shippingContact?.emailAddress { + shippingAddress["email"] = emailAddress + } + + ApplePayEndpointRouter(cardNetwork: cardNetwork.rawValue, + paymentPayload: paymentPayload, + transactionIdentifier: transactionIdentifier, + shippingAddress: shippingAddress).makeRequest { result in + + switch result { + case .success: + self.status = PKPaymentAuthorizationStatus.success + self.errors = [Error]() + case .failure(let error): + self.status = PKPaymentAuthorizationStatus.failure + self.errors = [error] + } + + debugPrint("Payment Authorization Controller Did Authorize Payment", payment) + completion(PKPaymentAuthorizationResult(status: self.status!, errors: self.errors)) + } + } + } + +// func paymentAuthorizationController(_ controller: PKPaymentAuthorizationController, didSelectPaymentMethod paymentMethod: PKPaymentMethod, handler completion: @escaping (PKPaymentRequestPaymentMethodUpdate) -> Void) { +// debugPrint("paymentAuthorizationController didSelectPaymentMethod", paymentMethod) +// completion(PKPaymentRequestPaymentMethodUpdate()) +// } +// +// func paymentAuthorizationControllerWillAuthorizePayment(_ controller: PKPaymentAuthorizationController) { +// debugPrint("paymentAuthorizationControllerWillAuthorizePayment") +// } +} 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/BeaconEndpointRouter.swift b/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift index a3f0822..86bc744 100644 --- a/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift +++ b/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift @@ -33,7 +33,7 @@ struct BeaconEndpointRouter: BeaconEndpointRouterProtocol { // MARK: - Body var body: [String: Any?]? { - var body: [String: Any?] = ["type": 0, + 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)), diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 5dddd2d..de57a18 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -195,6 +195,10 @@ public extension SwedbankPaySDK { "cardNumber": prefill.maskedPan, "cardExpiryMonth": prefill.expiryMonth, "cardExpiryYear": prefill.expiryYear])) + case .applePay: + BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", + succeeded: succeeded, + values: ["instrument": instrument.identifier])) } } @@ -394,10 +398,35 @@ public extension SwedbankPaySDK { let operations = paymentOutputModel.prioritisedOperations - print("\(operations.compactMap({ $0.rel }))") - if let preparePayment = operations.first(where: { $0.rel == .preparePayment }) { makeRequest(router: .preparePayment, operation: preparePayment) + } else if let walletSdk = operations.first(where: { $0.firstTask(with: .walletSdk) != nil }), + let task = walletSdk.firstTask(with: .walletSdk) { + SwedbankPayAuthorization.shared.testApplePay(task: task) { result in + switch result { + case .success: + DispatchQueue.main.async { + if let model = self.ongoingModel { + self.sessionOperationHandling(paymentOutputModel: model, culture: culture) + } + } + case .failure(let failure): + DispatchQueue.main.async { + let problem = SwedbankPaySDK.PaymentSessionProblem.applePayFailed(error: failure) + + 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": error.code, + "errorDomain": error.domain])) + } + } + } } else if operations.contains(where: { $0.rel == .startPaymentAttempt }), let instrument = instrument, let startPaymentAttempt = ongoingModel?.paymentSession.methods? @@ -496,6 +525,8 @@ public extension SwedbankPaySDK { return AvailableInstrument.swish(prefills: prefills) case .creditCard(let prefills, _, _): return AvailableInstrument.creditCard(prefills: prefills) + case .applePay: + return AvailableInstrument.applePay case .unknown(let identifier): return AvailableInstrument.webBased(identifier: identifier) } diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift index cd30540..16d25f4 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift @@ -23,6 +23,7 @@ public extension SwedbankPaySDK { case clientAppLaunchFailed case internalInconsistencyError case automaticConfigurationFailed + case applePayFailed(error: Error) var rawValue: String { switch self { @@ -31,6 +32,7 @@ public extension SwedbankPaySDK { case .clientAppLaunchFailed: "clientAppLaunchFailed" case .internalInconsistencyError: "internalInconsistencyError" case .automaticConfigurationFailed: "automaticConfigurationFailed" + case .applePayFailed: "applePayFailed" } } } From 001a3afa7b137740acf36c55f7a3ba0ce81762d2 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Wed, 25 Sep 2024 15:49:15 +0200 Subject: [PATCH 47/75] =?UTF-8?q?WIP:=20SP-72=20Grundfunktionalitet=20f?= =?UTF-8?q?=C3=B6r=20Apple=20Pay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SwedbankPaySDK.xcodeproj/project.pbxproj | 4 - .../Api/Models/PaymentAttemptInstrument.swift | 2 +- .../Api/SwedbankPayAPIEnpointRouter.swift | 7 ++ .../ApplePay/ApplePayEndpointRouter.swift | 93 ------------------- .../ApplePay/SwedbankPayAuthorization.swift | 29 +++--- .../Classes/SwedbankPayPaymentSession.swift | 11 ++- 6 files changed, 30 insertions(+), 116 deletions(-) delete mode 100644 SwedbankPaySDK/Classes/ApplePay/ApplePayEndpointRouter.swift diff --git a/SwedbankPaySDK.xcodeproj/project.pbxproj b/SwedbankPaySDK.xcodeproj/project.pbxproj index 05cd65d..0675999 100644 --- a/SwedbankPaySDK.xcodeproj/project.pbxproj +++ b/SwedbankPaySDK.xcodeproj/project.pbxproj @@ -14,7 +14,6 @@ 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 */; }; - 455849302C93305C0062A315 /* ApplePayEndpointRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4558492F2C93305C0062A315 /* ApplePayEndpointRouter.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 */; }; @@ -298,7 +297,6 @@ 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 = ""; }; - 4558492F2C93305C0062A315 /* ApplePayEndpointRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplePayEndpointRouter.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 = ""; }; @@ -511,7 +509,6 @@ children = ( 456913E52C78C2C60014BD41 /* SwedbankPayAuthorization.swift */, 455849312C945BAD0062A315 /* SwedbankPaymentNetwork.swift */, - 4558492F2C93305C0062A315 /* ApplePayEndpointRouter.swift */, ); path = ApplePay; sourceTree = ""; @@ -1173,7 +1170,6 @@ A5808B11261CA8B500FF7EC1 /* WKWebViewCanOpen.swift in Sources */, C50CF6A5237AAF47003F79DF /* SwedbankPaySubProblem.swift in Sources */, A52E0AC124BC9E0100770286 /* WebViewRedirects.swift in Sources */, - 455849302C93305C0062A315 /* ApplePayEndpointRouter.swift in Sources */, A504895123C8A2FD00201DEC /* SwedbankPayWebContent.swift in Sources */, C50CF69D237AACC3003F79DF /* Consumer.swift in Sources */, 45C1009F2BF2583D00AA3523 /* MethodBaseModel.swift in Sources */, diff --git a/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift index aa1332f..3af3365 100644 --- a/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift @@ -18,7 +18,7 @@ extension SwedbankPaySDK { public enum PaymentAttemptInstrument { case swish(msisdn: String?) case creditCard(prefill: CreditCardMethodPrefillModel) - case applePay + case applePay(merchantIdentifier: String) var identifier: String { switch self { diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index 89db792..8f9936f 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -32,6 +32,7 @@ enum EnpointRouter { case preparePayment case acknowledgeFailedAttempt case abortPayment + case applePay(cardNetwork: String, paymentPayload: String, transactionIdentifier: String, shippingAddress: [String: String]) } protocol EndpointRouterProtocol { @@ -114,6 +115,12 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { "client": ["userAgent": SwedbankPaySDK.VersionReporter.userAgent, "ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? ""], ] + case .applePay(let cardNetwork, let paymentPayload, let transactionIdentifier, let shippingAddress): + return ["instrument": "ApplePay", + "cardNetwork": cardNetwork, + "paymentPayload": paymentPayload, + "transactionIdentifier": transactionIdentifier, + "shippingAddress": shippingAddress] default: return nil } diff --git a/SwedbankPaySDK/Classes/ApplePay/ApplePayEndpointRouter.swift b/SwedbankPaySDK/Classes/ApplePay/ApplePayEndpointRouter.swift deleted file mode 100644 index a5ada73..0000000 --- a/SwedbankPaySDK/Classes/ApplePay/ApplePayEndpointRouter.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// 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 ApplePayEndpointRouterProtocol { - var body: [String: Any?]? { get } -} - -struct ApplePayEndpointRouter: ApplePayEndpointRouterProtocol { - var cardNetwork: String - var paymentPayload: String - var transactionIdentifier: String - var shippingAddress: [String: String] - - // MARK: - Body - var body: [String: Any?]? { - var body: [String: Any?] = ["Instrument": "ApplePay", - "CardNetwork": cardNetwork, - "PaymentPayload": paymentPayload, - "TransactionIdentifier": transactionIdentifier, - "ShippingAddress": shippingAddress] - - return body - } -} - -extension ApplePayEndpointRouter { - func makeRequest(handler: @escaping (Result) -> Void) { - let requestStartTimestamp: Date = Date() - - requestWithDataResponse { result in - switch result { - case .success(let data): - handler(.success(())) - case .failure(let error): - handler(.failure(error)) - } - } - } - - private func requestWithDataResponse(handler: @escaping (Result) -> Void) { - handler(.success(())) - -// let href = "https://stuff" -// -// guard 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/ApplePay/SwedbankPayAuthorization.swift b/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift index 474443d..bf1302c 100644 --- a/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift +++ b/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift @@ -19,14 +19,17 @@ import PassKit class SwedbankPayAuthorization: NSObject { static let shared = SwedbankPayAuthorization() + private var task: IntegrationTask? private var handler: ((Result) -> Void)? + private var errors: [Error]? private var status: PKPaymentAuthorizationStatus? - func testApplePay(task: IntegrationTask, handler: @escaping (Result) -> Void) { + func makeApplePayTransaction(task: IntegrationTask, merchantIdentifier: String?, handler: @escaping (Result) -> Void) { self.errors = nil self.status = nil + self.task = task self.handler = handler let paymentRequest = PKPaymentRequest() @@ -37,7 +40,9 @@ class SwedbankPayAuthorization: NSObject { paymentRequest.paymentSummaryItems = [total] } - paymentRequest.merchantIdentifier = "merchant.com.swedbankpay.exampleapp" + if let merchantIdentifier = merchantIdentifier { + paymentRequest.merchantIdentifier = merchantIdentifier + } if let merchantCapabilities = task.expects?.first(where: { $0.name == "MerchantCapabilities" })?.stringArray?.contains(where: { $0 == "supports3DS" }) { paymentRequest.merchantCapabilities = .threeDSecure @@ -90,7 +95,6 @@ extension SwedbankPayAuthorization: PKPaymentAuthorizationControllerDelegate { self.handler = nil - debugPrint("Payment Authorization Controller Did Finish") controller.dismiss() } @@ -136,11 +140,12 @@ extension SwedbankPayAuthorization: PKPaymentAuthorizationControllerDelegate { shippingAddress["email"] = emailAddress } - ApplePayEndpointRouter(cardNetwork: cardNetwork.rawValue, - paymentPayload: paymentPayload, - transactionIdentifier: transactionIdentifier, - shippingAddress: shippingAddress).makeRequest { result in + let router = EnpointRouter.applePay(cardNetwork: cardNetwork.rawValue, + paymentPayload: paymentPayload, + transactionIdentifier: transactionIdentifier, + shippingAddress: shippingAddress) + SwedbankPayAPIEnpointRouter(endpoint: Endpoint(router: router, href: task?.href, method: task?.method), sessionStartTimestamp: Date()).makeRequest { result in switch result { case .success: self.status = PKPaymentAuthorizationStatus.success @@ -150,18 +155,8 @@ extension SwedbankPayAuthorization: PKPaymentAuthorizationControllerDelegate { self.errors = [error] } - debugPrint("Payment Authorization Controller Did Authorize Payment", payment) completion(PKPaymentAuthorizationResult(status: self.status!, errors: self.errors)) } } } - -// func paymentAuthorizationController(_ controller: PKPaymentAuthorizationController, didSelectPaymentMethod paymentMethod: PKPaymentMethod, handler completion: @escaping (PKPaymentRequestPaymentMethodUpdate) -> Void) { -// debugPrint("paymentAuthorizationController didSelectPaymentMethod", paymentMethod) -// completion(PKPaymentRequestPaymentMethodUpdate()) -// } -// -// func paymentAuthorizationControllerWillAuthorizePayment(_ controller: PKPaymentAuthorizationController) { -// debugPrint("paymentAuthorizationControllerWillAuthorizePayment") -// } } diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index de57a18..e518bd7 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -68,6 +68,7 @@ public extension SwedbankPaySDK { private var sessionIsOngoing: Bool = false private var instrument: SwedbankPaySDK.PaymentAttemptInstrument? = nil private var hasShownAvailableInstruments: Bool = false + private var merchantIdentifier: String? = nil private var hasLaunchClientAppURLs: [URL] = [] private var hasShownProblemDetails: [ProblemDetails] = [] @@ -103,6 +104,7 @@ public extension SwedbankPaySDK { public func fetchPaymentSession(sessionURL: URL) { sessionIsOngoing = true instrument = nil + merchantIdentifier = nil ongoingModel = nil hasLaunchClientAppURLs = [] hasShownProblemDetails = [] @@ -149,6 +151,13 @@ public extension SwedbankPaySDK { self.instrument = instrument + switch instrument { + case .applePay(let merchantIdentifier): + self.merchantIdentifier = merchantIdentifier + default: + break + } + var succeeded = false if let operation = ongoingModel.paymentSession.methods? .first(where: { $0.name == instrument.identifier })?.operations? @@ -402,7 +411,7 @@ public extension SwedbankPaySDK { makeRequest(router: .preparePayment, operation: preparePayment) } else if let walletSdk = operations.first(where: { $0.firstTask(with: .walletSdk) != nil }), let task = walletSdk.firstTask(with: .walletSdk) { - SwedbankPayAuthorization.shared.testApplePay(task: task) { result in + SwedbankPayAuthorization.shared.makeApplePayTransaction(task: task, merchantIdentifier: merchantIdentifier) { result in switch result { case .success: DispatchQueue.main.async { From f09e912c462811c3ac601bf58d250585f0d805a8 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Thu, 26 Sep 2024 11:16:46 +0200 Subject: [PATCH 48/75] Added support for operation "attempt-payload" --- .../Api/Models/OperationOutputModel.swift | 3 +++ .../ApplePay/SwedbankPayAuthorization.swift | 16 +++++++++++----- .../Classes/SwedbankPayPaymentSession.swift | 14 ++++++-------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift index 0397133..8bee347 100644 --- a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift @@ -44,6 +44,7 @@ enum OperationRel: Codable, Equatable, Hashable { case abortPayment case eventLogging case viewPayment + case attemptPayload case unknown(String) @@ -63,6 +64,7 @@ enum OperationRel: Codable, Equatable, Hashable { case Self.abortPayment.rawValue: self = .abortPayment case Self.eventLogging.rawValue: self = .eventLogging case Self.viewPayment.rawValue: self = .viewPayment + case Self.attemptPayload.rawValue: self = .attemptPayload default: self = .unknown(type) } } @@ -85,6 +87,7 @@ enum OperationRel: Codable, Equatable, Hashable { case .abortPayment: "abort-payment" case .eventLogging: "event-logging" case .viewPayment: "view-payment" + case .attemptPayload: "attempt-payload" case .unknown(let value): value } } diff --git a/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift b/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift index bf1302c..c5416b3 100644 --- a/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift +++ b/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift @@ -19,16 +19,19 @@ import PassKit class SwedbankPayAuthorization: NSObject { static let shared = SwedbankPayAuthorization() + private var operation: OperationOutputModel? private var task: IntegrationTask? - private var handler: ((Result) -> Void)? + private var handler: ((Result) -> Void)? + private var success: PaymentOutputModel? private var errors: [Error]? private var status: PKPaymentAuthorizationStatus? - func makeApplePayTransaction(task: IntegrationTask, merchantIdentifier: String?, handler: @escaping (Result) -> Void) { + func makeApplePayTransaction(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 @@ -87,7 +90,7 @@ extension SwedbankPayAuthorization: PKPaymentAuthorizationControllerDelegate { func paymentAuthorizationControllerDidFinish(_ controller: PKPaymentAuthorizationController) { if let handler = self.handler { if let status = status { - handler(.success(())) + handler(.success((success))) } else { handler(.failure(self.errors?.first ?? SwedbankPayAPIError.unknown)) } @@ -145,12 +148,15 @@ extension SwedbankPayAuthorization: PKPaymentAuthorizationControllerDelegate { transactionIdentifier: transactionIdentifier, shippingAddress: shippingAddress) - SwedbankPayAPIEnpointRouter(endpoint: Endpoint(router: router, href: task?.href, method: task?.method), sessionStartTimestamp: Date()).makeRequest { result in + SwedbankPayAPIEnpointRouter(endpoint: Endpoint(router: router, href: operation?.href, method: operation?.method), + sessionStartTimestamp: Date()).makeRequest { result in switch result { - case .success: + case .success(let success): + self.success = success self.status = PKPaymentAuthorizationStatus.success self.errors = [Error]() case .failure(let error): + self.success = nil self.status = PKPaymentAuthorizationStatus.failure self.errors = [error] } diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index e518bd7..a861670 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -409,15 +409,13 @@ public extension SwedbankPaySDK { if let preparePayment = operations.first(where: { $0.rel == .preparePayment }) { makeRequest(router: .preparePayment, operation: preparePayment) - } else if let walletSdk = operations.first(where: { $0.firstTask(with: .walletSdk) != nil }), - let task = walletSdk.firstTask(with: .walletSdk) { - SwedbankPayAuthorization.shared.makeApplePayTransaction(task: task, merchantIdentifier: merchantIdentifier) { result in + } else if let attemptPayload = operations.first(where: { $0.rel == .attemptPayload }), + let task = attemptPayload.firstTask(with: .walletSdk) { + SwedbankPayAuthorization.shared.makeApplePayTransaction(operation: attemptPayload, task: task, merchantIdentifier: merchantIdentifier) { result in switch result { - case .success: - DispatchQueue.main.async { - if let model = self.ongoingModel { - self.sessionOperationHandling(paymentOutputModel: model, culture: culture) - } + case .success(let success): + if let paymentOutputModel = success { + self.sessionOperationHandling(paymentOutputModel: paymentOutputModel, culture: paymentOutputModel.paymentSession.culture) } case .failure(let failure): DispatchQueue.main.async { From 3de03876efec4d1f16e1c26528030480c1d7b351 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Thu, 26 Sep 2024 13:34:50 +0200 Subject: [PATCH 49/75] Clean up --- .../Api/SwedbankPayAPIEnpointRouter.swift | 9 +- .../ApplePay/SwedbankPayAuthorization.swift | 83 +++++-------------- .../Classes/SwedbankPayPaymentSession.swift | 52 ++++++------ 3 files changed, 50 insertions(+), 94 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index 8f9936f..e73b057 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -32,7 +32,7 @@ enum EnpointRouter { case preparePayment case acknowledgeFailedAttempt case abortPayment - case applePay(cardNetwork: String, paymentPayload: String, transactionIdentifier: String, shippingAddress: [String: String]) + case applePay(paymentPayload: String) } protocol EndpointRouterProtocol { @@ -115,12 +115,9 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { "client": ["userAgent": SwedbankPaySDK.VersionReporter.userAgent, "ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? ""], ] - case .applePay(let cardNetwork, let paymentPayload, let transactionIdentifier, let shippingAddress): + case .applePay(let paymentPayload): return ["instrument": "ApplePay", - "cardNetwork": cardNetwork, - "paymentPayload": paymentPayload, - "transactionIdentifier": transactionIdentifier, - "shippingAddress": shippingAddress] + "paymentPayload": paymentPayload] default: return nil } diff --git a/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift b/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift index c5416b3..11e374c 100644 --- a/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift +++ b/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift @@ -27,7 +27,7 @@ class SwedbankPayAuthorization: NSObject { private var errors: [Error]? private var status: PKPaymentAuthorizationStatus? - func makeApplePayTransaction(operation: OperationOutputModel, task: IntegrationTask, merchantIdentifier: String?, handler: @escaping (Result) -> Void) { + func showApplePay(operation: OperationOutputModel, task: IntegrationTask, merchantIdentifier: String?, handler: @escaping (Result) -> Void) { self.errors = nil self.status = nil @@ -77,10 +77,8 @@ class SwedbankPayAuthorization: NSObject { let paymentController = PKPaymentAuthorizationController(paymentRequest: paymentRequest) paymentController.delegate = self paymentController.present(completion: { (presented: Bool) in - if presented { - debugPrint("Presented payment controller") - } else { - debugPrint("Failed to present payment controller") + if !presented { + handler(.failure(SwedbankPayAPIError.unknown)) } }) } @@ -102,67 +100,24 @@ extension SwedbankPayAuthorization: PKPaymentAuthorizationControllerDelegate { } func paymentAuthorizationController(_ controller: PKPaymentAuthorizationController, didAuthorizePayment payment: PKPayment, handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) { - if let pkPaymentNetwork = payment.token.paymentMethod.network, - let cardNetwork = SwedbankPaymentNetwork(pkPaymentNetwork: pkPaymentNetwork) { - let paymentPayload = payment.token.paymentData.base64EncodedString() - let transactionIdentifier = payment.token.transactionIdentifier - - var shippingAddress: [String: String] = [:] - - if let postalAddress = payment.shippingContact?.postalAddress { - shippingAddress["postalAddress"] = postalAddress.postalCode - } - - if let name = payment.shippingContact?.name { - if #available(iOS 15.0, *) { - shippingAddress["name"] = name.formatted() - } else { - var nameArray: [String] = [] - - if let givenName = name.givenName { - nameArray.append(givenName) - } - - if let middleName = name.middleName { - nameArray.append(middleName) - } - - if let familyName = name.familyName { - nameArray.append(familyName) - } - - shippingAddress["name"] = nameArray.joined(separator: " ") - } - } - - if let phoneNumber = payment.shippingContact?.phoneNumber { - shippingAddress["phone"] = phoneNumber.stringValue + let paymentPayload = payment.token.paymentData.base64EncodedString() + + let router = EnpointRouter.applePay(paymentPayload: paymentPayload) + + SwedbankPayAPIEnpointRouter(endpoint: Endpoint(router: router, href: operation?.href, method: operation?.method), + sessionStartTimestamp: Date()).makeRequest { result in + switch result { + case .success(let success): + self.success = success + self.status = PKPaymentAuthorizationStatus.success + self.errors = [Error]() + case .failure(let error): + self.success = nil + self.status = PKPaymentAuthorizationStatus.failure + self.errors = [error] } - if let emailAddress = payment.shippingContact?.emailAddress { - shippingAddress["email"] = emailAddress - } - - let router = EnpointRouter.applePay(cardNetwork: cardNetwork.rawValue, - paymentPayload: paymentPayload, - transactionIdentifier: transactionIdentifier, - shippingAddress: shippingAddress) - - SwedbankPayAPIEnpointRouter(endpoint: Endpoint(router: router, href: operation?.href, method: operation?.method), - sessionStartTimestamp: Date()).makeRequest { result in - switch result { - case .success(let success): - self.success = success - 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)) - } + completion(PKPaymentAuthorizationResult(status: self.status!, errors: self.errors)) } } } diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index a861670..045b93d 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -378,6 +378,32 @@ public extension SwedbankPaySDK { } } + private func makeApplePayAuthorization(operation: OperationOutputModel, task: IntegrationTask) { + SwedbankPayAuthorization.shared.showApplePay(operation: operation, task: task, merchantIdentifier: merchantIdentifier) { result in + switch result { + case .success(let success): + if let paymentOutputModel = success { + self.sessionOperationHandling(paymentOutputModel: paymentOutputModel, culture: paymentOutputModel.paymentSession.culture) + } + case .failure(let failure): + DispatchQueue.main.async { + let problem = SwedbankPaySDK.PaymentSessionProblem.applePayFailed(error: failure) + + 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": error.code, + "errorDomain": error.domain])) + } + } + } + } + private func sessionOperationHandling(paymentOutputModel: PaymentOutputModel, culture: String? = nil) { ongoingModel = paymentOutputModel @@ -410,30 +436,8 @@ public extension SwedbankPaySDK { if let preparePayment = operations.first(where: { $0.rel == .preparePayment }) { makeRequest(router: .preparePayment, operation: preparePayment) } else if let attemptPayload = operations.first(where: { $0.rel == .attemptPayload }), - let task = attemptPayload.firstTask(with: .walletSdk) { - SwedbankPayAuthorization.shared.makeApplePayTransaction(operation: attemptPayload, task: task, merchantIdentifier: merchantIdentifier) { result in - switch result { - case .success(let success): - if let paymentOutputModel = success { - self.sessionOperationHandling(paymentOutputModel: paymentOutputModel, culture: paymentOutputModel.paymentSession.culture) - } - case .failure(let failure): - DispatchQueue.main.async { - let problem = SwedbankPaySDK.PaymentSessionProblem.applePayFailed(error: failure) - - 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": error.code, - "errorDomain": error.domain])) - } - } - } + let walletSdk = attemptPayload.firstTask(with: .walletSdk) { + makeApplePayAuthorization(operation: attemptPayload, task: walletSdk) } else if operations.contains(where: { $0.rel == .startPaymentAttempt }), let instrument = instrument, let startPaymentAttempt = ongoingModel?.paymentSession.methods? From 2a0986743c0aa8b5c69f2e6c8509cd36f0b0e159 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Wed, 18 Sep 2024 15:49:32 +0200 Subject: [PATCH 50/75] =?UTF-8?q?SP-77=20Undvik=20application=20callback-h?= =?UTF-8?q?antering=20n=C3=A4r=20merchant-appen=20=C3=A4r=20i=20webbfl?= =?UTF-8?q?=C3=B6de?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift | 8 +++++++- SwedbankPaySDK/Classes/SwedbankPaySDKController.swift | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 045b93d..75280ff 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -66,6 +66,7 @@ public extension SwedbankPaySDK { private var ongoingModel: PaymentOutputModel? = nil private var sessionIsOngoing: Bool = false + private var paymentViewSessionIsOngoing: Bool = false private var instrument: SwedbankPaySDK.PaymentAttemptInstrument? = nil private var hasShownAvailableInstruments: Bool = false private var merchantIdentifier: String? = nil @@ -103,6 +104,7 @@ public extension SwedbankPaySDK { /// - parameter with sessionURL: Session URL needed to start the native payment session public func fetchPaymentSession(sessionURL: URL) { sessionIsOngoing = true + paymentViewSessionIsOngoing = false instrument = nil merchantIdentifier = nil ongoingModel = nil @@ -149,6 +151,7 @@ public extension SwedbankPaySDK { return } + paymentViewSessionIsOngoing = false self.instrument = instrument switch instrument { @@ -251,6 +254,8 @@ public extension SwedbankPaySDK { succeeded: true, values: nil)) + paymentViewSessionIsOngoing = true + return viewController } @@ -568,7 +573,8 @@ public extension SwedbankPaySDK { } internal func handleCallbackUrl(_ url: URL) -> Bool { - guard url == orderInfo?.paymentUrl else { + guard url.appendingPathComponent("") == orderInfo?.paymentUrl?.appendingPathComponent(""), + paymentViewSessionIsOngoing == false else { return false } diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift b/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift index 325728a..d5f8805 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift @@ -751,7 +751,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 } } From e4e20bb7590d27289f3f59dc37da9886a0f68d00 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Tue, 17 Sep 2024 16:56:02 +0200 Subject: [PATCH 51/75] =?UTF-8?q?WIP:=20SP-58=20St=C3=B6d=20f=C3=B6r=20nyt?= =?UTF-8?q?t=20betalkort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Api/Models/OperationOutputModel.swift | 3 ++ .../Api/Models/PaymentAttemptInstrument.swift | 6 ++-- .../Api/Models/PaymentSessionModel.swift | 1 + .../Api/SwedbankPayAPIEnpointRouter.swift | 13 ++++++++ .../Classes/SwedbankPayPaymentSession.swift | 31 ++++++++++++++++--- 5 files changed, 47 insertions(+), 7 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift index 8bee347..31238d7 100644 --- a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift @@ -45,6 +45,7 @@ enum OperationRel: Codable, Equatable, Hashable { case eventLogging case viewPayment case attemptPayload + case customizePayment case unknown(String) @@ -65,6 +66,7 @@ enum OperationRel: Codable, Equatable, Hashable { case Self.eventLogging.rawValue: self = .eventLogging case Self.viewPayment.rawValue: self = .viewPayment case Self.attemptPayload.rawValue: self = .attemptPayload + case Self.customizePayment.rawValue: self = .customizePayment default: self = .unknown(type) } } @@ -88,6 +90,7 @@ enum OperationRel: Codable, Equatable, Hashable { case .eventLogging: "event-logging" case .viewPayment: "view-payment" case .attemptPayload: "attempt-payload" + case .customizePayment: "customize-payment" case .unknown(let value): value } } diff --git a/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift index 3af3365..c266c5d 100644 --- a/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift @@ -15,16 +15,18 @@ extension SwedbankPaySDK { /// Instrument with needed values to make a payment attempt. - public enum PaymentAttemptInstrument { + public enum PaymentAttemptInstrument: Equatable { case swish(msisdn: String?) case creditCard(prefill: CreditCardMethodPrefillModel) case applePay(merchantIdentifier: String) + case newCreditCard(enabledPaymentDetailsConsentCheckbox: Bool) var identifier: String { switch self { case .swish: return "Swish" - case .creditCard: + case .creditCard, + .newCreditCard: return "CreditCard" case .applePay: return "ApplePay" diff --git a/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift index c6d286b..b50aadd 100644 --- a/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift @@ -19,6 +19,7 @@ struct PaymentSessionModel: Codable, Hashable { let culture: String? let methods: [MethodBaseModel]? let urls: UrlsModel? + let instrumentModePaymentMethod: String? } extension PaymentSessionModel { diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index e73b057..b4a148a 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -33,6 +33,7 @@ enum EnpointRouter { case acknowledgeFailedAttempt case abortPayment case applePay(paymentPayload: String) + case customizePayment(instrument: SwedbankPaySDK.PaymentAttemptInstrument) } protocol EndpointRouterProtocol { @@ -80,6 +81,8 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { "screenWidth": String(Int32(UIScreen.main.nativeBounds.width)), "screenColorDepth": String(24)] ] + case .newCreditCard: + return nil } case .preparePayment: return ["integration": "HostedView", @@ -118,6 +121,16 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { case .applePay(let paymentPayload): return ["instrument": "ApplePay", "paymentPayload": paymentPayload] + case .customizePayment(let instrument): + switch instrument { + case .newCreditCard(let enabledPaymentDetailsConsentCheckbox): + return ["paymentMethod": "CreditCard", + "hideStoredPaymentOptions": true, + "showConsentAffirmation" : enabledPaymentDetailsConsentCheckbox, + ] + default: + return nil + } default: return nil } diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 75280ff..2d3ab8f 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -48,6 +48,8 @@ public protocol SwedbankPaySDKPaymentSessionDelegate: AnyObject { /// Called whenever the 3D secure view can be dismissed. func dismiss3DSecureViewController() + func showSwedbankPaySDKController(viewController: SwedbankPaySDKController) + /// Called if the 3D secure view loading failed. /// /// - parameter error: The error that caused the failure @@ -162,7 +164,16 @@ public extension SwedbankPaySDK { } var succeeded = false - if let operation = ongoingModel.paymentSession.methods? + + if case .newCreditCard = self.instrument, + (ongoingModel.paymentSession.instrumentModePaymentMethod == nil || ongoingModel.paymentSession.instrumentModePaymentMethod != "CreditCard"), + let operation = ongoingModel.operations?.first(where: { $0.rel == .customizePayment }) { + sessionStartTimestamp = Date() + + makeRequest(router: .customizePayment(instrument: instrument), operation: operation) + + succeeded = true + } else if let operation = ongoingModel.paymentSession.methods? .first(where: { $0.name == instrument.identifier })?.operations? .first(where: { $0.rel == .expandMethod || $0.rel == .startPaymentAttempt || $0.rel == .getPayment }) { @@ -211,6 +222,11 @@ public extension SwedbankPaySDK { BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", succeeded: succeeded, values: ["instrument": instrument.identifier])) + case .newCreditCard(enabledPaymentDetailsConsentCheckbox: let enabledPaymentDetailsConsentCheckbox): + BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", + succeeded: succeeded, + values: ["instrument": instrument.identifier, + "showConsentAffirmation": enabledPaymentDetailsConsentCheckbox])) } } @@ -220,7 +236,7 @@ public extension SwedbankPaySDK { /// There needs to be an active payment session before an payment attempt can be made. /// /// - returns:- SwedbankPaySDKController to be shown. - public func createSwedbankPaySDKController() -> SwedbankPaySDKController? { + public func createSwedbankPaySDKController() { guard let ongoingModel = ongoingModel, let operation = ongoingModel.operations?.first(where: { $0.rel == .viewPayment }), let orderInfo = orderInfo, @@ -232,7 +248,7 @@ public extension SwedbankPaySDK { succeeded: self.delegate != nil, values: ["problem": SwedbankPaySDK.PaymentSessionProblem.internalInconsistencyError.rawValue])) - return nil + return } let configuration = SwedbankPayConfiguration( @@ -249,14 +265,14 @@ public extension SwedbankPaySDK { consumer: nil, paymentOrder: nil, userData: nil) - + BeaconService.shared.log(type: .sdkMethodInvoked(name: "createSwedbankPaySDKController", succeeded: true, values: nil)) paymentViewSessionIsOngoing = true - return viewController + delegate?.showSwedbankPaySDKController(viewController: viewController) } /// Abort an active payment session. @@ -443,6 +459,11 @@ public extension SwedbankPaySDK { } else if let attemptPayload = operations.first(where: { $0.rel == .attemptPayload }), let walletSdk = attemptPayload.firstTask(with: .walletSdk) { makeApplePayAuthorization(operation: attemptPayload, task: walletSdk) + } else if case .newCreditCard = self.instrument, + ongoingModel?.paymentSession.instrumentModePaymentMethod == "CreditCard" { + DispatchQueue.main.async { + self.createSwedbankPaySDKController() + } } else if operations.contains(where: { $0.rel == .startPaymentAttempt }), let instrument = instrument, let startPaymentAttempt = ongoingModel?.paymentSession.methods? From 12d8c52edb147bb1b1fb5fa5fb7db097a0a53d79 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Fri, 27 Sep 2024 16:34:00 +0200 Subject: [PATCH 52/75] "sdk" changed to string and fixed date format --- .../Classes/Beacon/BeaconEndpointRouter.swift | 6 +++--- .../Classes/SwedbankPayPaymentSession.swift | 17 +++++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift b/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift index 86bc744..344f006 100644 --- a/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift +++ b/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift @@ -27,7 +27,7 @@ struct BeaconEndpointRouter: BeaconEndpointRouterProtocol { private let dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .iso8601) - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'Z" + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" return formatter }() @@ -47,14 +47,14 @@ struct BeaconEndpointRouter: BeaconEndpointRouterProtocol { switch beacon.actionType { case .sdkMethodInvoked(name: let name, succeeded: let succeeded, values: let values): body["method"] = ["name": name, - "sdk": true, + "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, + "sdk": "true", "succeeded": succeeded] if let values = values { body["extensions"] = ["values": values] diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 2d3ab8f..67260a1 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -165,14 +165,19 @@ public extension SwedbankPaySDK { var succeeded = false - if case .newCreditCard = self.instrument, - (ongoingModel.paymentSession.instrumentModePaymentMethod == nil || ongoingModel.paymentSession.instrumentModePaymentMethod != "CreditCard"), - let operation = ongoingModel.operations?.first(where: { $0.rel == .customizePayment }) { - sessionStartTimestamp = Date() + if case .newCreditCard = instrument { + if ongoingModel.paymentSession.instrumentModePaymentMethod == nil || ongoingModel.paymentSession.instrumentModePaymentMethod != "CreditCard", + let operation = ongoingModel.operations?.first(where: { $0.rel == .customizePayment }) { + sessionStartTimestamp = Date() - makeRequest(router: .customizePayment(instrument: instrument), operation: operation) + makeRequest(router: .customizePayment(instrument: instrument), operation: operation) - succeeded = true + succeeded = true + } else { + DispatchQueue.main.async { + self.createSwedbankPaySDKController() + } + } } else if let operation = ongoingModel.paymentSession.methods? .first(where: { $0.name == instrument.identifier })?.operations? .first(where: { $0.rel == .expandMethod || $0.rel == .startPaymentAttempt || $0.rel == .getPayment }) { From d5aabb4ef2d79d4b7d98013f67d6024f4ec479d8 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Mon, 23 Sep 2024 10:54:32 +0200 Subject: [PATCH 53/75] =?UTF-8?q?SP-79=20Skicka=20vanliga=20session=20call?= =?UTF-8?q?backs=20fr=C3=A5n=20SwedbankPaySDKController?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Classes/SwedbankPayPaymentSession.swift | 50 +++++++++++++++++++ .../Classes/SwedbankPaySDKController.swift | 16 +++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 67260a1..0aa98e1 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -277,6 +277,8 @@ public extension SwedbankPaySDK { paymentViewSessionIsOngoing = true + viewController.internalDelegate = self + delegate?.showSwedbankPaySDKController(viewController: viewController) } @@ -651,3 +653,51 @@ public extension SwedbankPaySDK { } } } + +extension SwedbankPaySDK.SwedbankPayPaymentSession: SwedbankPaySDKInternalDelegate { + public func updatePaymentOrderFailed(updateInfo: Any, error: any Error) { + let problem = SwedbankPaySDK.PaymentSessionProblem.applePayFailed(error: error) + + 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": 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.applePayFailed(error: error) + + 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": error.code, + "errorDomain": error.domain])) + } +} diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift b/SwedbankPaySDK/Classes/SwedbankPaySDKController.swift index d5f8805..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 } From 7395b8de4b4e46cfedc081ce95ab599e1cc6aad4 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Mon, 30 Sep 2024 13:12:53 +0200 Subject: [PATCH 54/75] Clean up --- .../Api/SwedbankPayAPIEnpointRouter.swift | 4 +-- .../Classes/Beacon/BeaconType.swift | 10 +++---- .../Classes/SwedbankPayPaymentSession.swift | 27 ++++++++++--------- .../PaymentSessionProblem.swift | 4 +-- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index b4a148a..005c64e 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -235,10 +235,10 @@ extension SwedbankPayAPIEnpointRouter { responseStatusCode = response.statusCode } - var values: [String: Any]? + var values: [String: String]? if let error = error as? NSError { values = ["errorDescription": error.localizedDescription, - "errorCode": error.code, + "errorCode": String(error.code), "errorDomain": error.domain] } diff --git a/SwedbankPaySDK/Classes/Beacon/BeaconType.swift b/SwedbankPaySDK/Classes/Beacon/BeaconType.swift index 27431fd..9b0f1e8 100644 --- a/SwedbankPaySDK/Classes/Beacon/BeaconType.swift +++ b/SwedbankPaySDK/Classes/Beacon/BeaconType.swift @@ -14,11 +14,11 @@ // limitations under the License. enum BeaconType { - case sdkMethodInvoked(name: String, succeeded: Bool, values: [String: Any?]?) - case sdkCallbackInvoked(name: String, succeeded: Bool, values: [String: Any?]?) - case httpRequest(duration: Int32, requestUrl: String, method: String, responseStatusCode: Int?, values: [String: Any?]?) - case launchClientApp(values: [String: Any?]?) - case clientAppCallback(values: [String: Any?]?) + 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 { diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 0aa98e1..7c76422 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -231,7 +231,7 @@ public extension SwedbankPaySDK { BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", succeeded: succeeded, values: ["instrument": instrument.identifier, - "showConsentAffirmation": enabledPaymentDetailsConsentCheckbox])) + "showConsentAffirmation": enabledPaymentDetailsConsentCheckbox.description])) } } @@ -357,7 +357,7 @@ public extension SwedbankPaySDK { succeeded: self.delegate != nil, values: ["problem": problem.rawValue, "errorDescription": error.localizedDescription, - "errorCode": error.code, + "errorCode": String(error.code), "errorDomain": error.domain])) } } @@ -400,7 +400,7 @@ public extension SwedbankPaySDK { BeaconService.shared.log(type: .launchClientApp(values: ["callbackUrl": self.orderInfo?.paymentUrl?.absoluteString ?? "", "clientAppLaunchUrl": url.absoluteString, - "launchSucceeded": complete])) + "launchSucceeded": complete.description])) } } } @@ -415,7 +415,8 @@ public extension SwedbankPaySDK { } case .failure(let failure): DispatchQueue.main.async { - let problem = SwedbankPaySDK.PaymentSessionProblem.applePayFailed(error: failure) + let problem = SwedbankPaySDK.PaymentSessionProblem.paymentSessionAPIRequestFailed(error: failure, + retry: nil) self.delegate?.sdkProblemOccurred(problem: problem) @@ -425,7 +426,7 @@ public extension SwedbankPaySDK { succeeded: self.delegate != nil, values: ["problem": problem.rawValue, "errorDescription": error.localizedDescription, - "errorCode": error.code, + "errorCode": String(error.code), "errorDomain": error.domain])) } } @@ -449,9 +450,9 @@ public extension SwedbankPaySDK { BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sessionProblemOccurred", succeeded: self.delegate != nil, - values: ["problemTitle": modelProblem.title, - "problemStatus": modelProblem.status, - "problemDetail": modelProblem.detail])) + values: ["problemTitle": modelProblem.title ?? "", + "problemStatus": String(modelProblem.status ?? 0), + "problemDetail": modelProblem.detail ?? ""])) } } @@ -646,7 +647,7 @@ public extension SwedbankPaySDK { BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentSession3DSecureViewControllerLoadFailed", succeeded: self.delegate != nil, values: ["errorDescription": error.localizedDescription, - "errorCode": error.code, + "errorCode": String(error.code), "errorDomain": error.domain])) } } @@ -656,7 +657,7 @@ public extension SwedbankPaySDK { extension SwedbankPaySDK.SwedbankPayPaymentSession: SwedbankPaySDKInternalDelegate { public func updatePaymentOrderFailed(updateInfo: Any, error: any Error) { - let problem = SwedbankPaySDK.PaymentSessionProblem.applePayFailed(error: error) + let problem = SwedbankPaySDK.PaymentSessionProblem.paymentSessionAPIRequestFailed(error: error, retry: nil) self.delegate?.sdkProblemOccurred(problem: problem) @@ -666,7 +667,7 @@ extension SwedbankPaySDK.SwedbankPayPaymentSession: SwedbankPaySDKInternalDelega succeeded: self.delegate != nil, values: ["problem": problem.rawValue, "errorDescription": error.localizedDescription, - "errorCode": error.code, + "errorCode": String(error.code), "errorDomain": error.domain])) } @@ -687,7 +688,7 @@ extension SwedbankPaySDK.SwedbankPayPaymentSession: SwedbankPaySDKInternalDelega } public func paymentFailed(error: any Error) { - let problem = SwedbankPaySDK.PaymentSessionProblem.applePayFailed(error: error) + let problem = SwedbankPaySDK.PaymentSessionProblem.paymentSessionAPIRequestFailed(error: error, retry: nil) self.delegate?.sdkProblemOccurred(problem: problem) @@ -697,7 +698,7 @@ extension SwedbankPaySDK.SwedbankPayPaymentSession: SwedbankPaySDKInternalDelega succeeded: self.delegate != nil, values: ["problem": problem.rawValue, "errorDescription": error.localizedDescription, - "errorCode": error.code, + "errorCode": String(error.code), "errorDomain": error.domain])) } } diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift index 16d25f4..3335b52 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift @@ -19,11 +19,10 @@ public extension SwedbankPaySDK { /// Payment session problem returned with `sdkProblemOccurred` enum PaymentSessionProblem { case paymentSessionEndStateReached - case paymentSessionAPIRequestFailed(error: Error, retry: ()->Void) + case paymentSessionAPIRequestFailed(error: Error, retry: (()->Void)?) case clientAppLaunchFailed case internalInconsistencyError case automaticConfigurationFailed - case applePayFailed(error: Error) var rawValue: String { switch self { @@ -32,7 +31,6 @@ public extension SwedbankPaySDK { case .clientAppLaunchFailed: "clientAppLaunchFailed" case .internalInconsistencyError: "internalInconsistencyError" case .automaticConfigurationFailed: "automaticConfigurationFailed" - case .applePayFailed: "applePayFailed" } } } From f5c670413dff260c1796ee99b8affa688a296ca1 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Tue, 1 Oct 2024 13:09:17 +0200 Subject: [PATCH 55/75] Code Review fixes --- .../Classes/Api/SwedbankPayAPIEnpointRouter.swift | 5 ++--- .../ApplePay/SwedbankPayAuthorization.swift | 6 ++---- .../Classes/SwedbankPayPaymentSession.swift | 14 +++++++++++++- .../PaymentSessionProblem.swift | 2 ++ 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index 005c64e..93368ea 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -122,13 +122,12 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { return ["instrument": "ApplePay", "paymentPayload": paymentPayload] case .customizePayment(let instrument): - switch instrument { - case .newCreditCard(let enabledPaymentDetailsConsentCheckbox): + if case .newCreditCard(let enabledPaymentDetailsConsentCheckbox) = instrument { return ["paymentMethod": "CreditCard", "hideStoredPaymentOptions": true, "showConsentAffirmation" : enabledPaymentDetailsConsentCheckbox, ] - default: + } else { return nil } default: diff --git a/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift b/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift index 11e374c..c2015b8 100644 --- a/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift +++ b/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift @@ -27,7 +27,7 @@ class SwedbankPayAuthorization: NSObject { private var errors: [Error]? private var status: PKPaymentAuthorizationStatus? - func showApplePay(operation: OperationOutputModel, task: IntegrationTask, merchantIdentifier: String?, handler: @escaping (Result) -> Void) { + func showApplePay(operation: OperationOutputModel, task: IntegrationTask, merchantIdentifier: String, handler: @escaping (Result) -> Void) { self.errors = nil self.status = nil @@ -43,9 +43,7 @@ class SwedbankPayAuthorization: NSObject { paymentRequest.paymentSummaryItems = [total] } - if let merchantIdentifier = merchantIdentifier { - paymentRequest.merchantIdentifier = merchantIdentifier - } + paymentRequest.merchantIdentifier = merchantIdentifier if let merchantCapabilities = task.expects?.first(where: { $0.name == "MerchantCapabilities" })?.stringArray?.contains(where: { $0 == "supports3DS" }) { paymentRequest.merchantCapabilities = .threeDSecure diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 7c76422..c91aa92 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -407,6 +407,16 @@ public extension SwedbankPaySDK { } private func makeApplePayAuthorization(operation: 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: operation, task: task, merchantIdentifier: merchantIdentifier) { result in switch result { case .success(let success): @@ -415,6 +425,8 @@ public extension SwedbankPaySDK { } case .failure(let failure): DispatchQueue.main.async { + // TODO: This is a temporary solution. In the future we need to send the error to the backend so they can provide the correct problem for us. + let problem = SwedbankPaySDK.PaymentSessionProblem.paymentSessionAPIRequestFailed(error: failure, retry: nil) @@ -688,7 +700,7 @@ extension SwedbankPaySDK.SwedbankPayPaymentSession: SwedbankPaySDKInternalDelega } public func paymentFailed(error: any Error) { - let problem = SwedbankPaySDK.PaymentSessionProblem.paymentSessionAPIRequestFailed(error: error, retry: nil) + let problem = SwedbankPaySDK.PaymentSessionProblem.paymentControllerPaymentFailed(error: error, retry: nil) self.delegate?.sdkProblemOccurred(problem: problem) diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift index 3335b52..a547649 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift @@ -20,6 +20,7 @@ public extension SwedbankPaySDK { enum PaymentSessionProblem { case paymentSessionEndStateReached case paymentSessionAPIRequestFailed(error: Error, retry: (()->Void)?) + case paymentControllerPaymentFailed(error: Error, retry: (()->Void)?) case clientAppLaunchFailed case internalInconsistencyError case automaticConfigurationFailed @@ -28,6 +29,7 @@ public extension SwedbankPaySDK { switch self { case .paymentSessionEndStateReached: "paymentSessionEndStateReached" case .paymentSessionAPIRequestFailed: "paymentSessionAPIRequestFailed" + case .paymentControllerPaymentFailed: "paymentControllerPaymentFailed" case .clientAppLaunchFailed: "clientAppLaunchFailed" case .internalInconsistencyError: "internalInconsistencyError" case .automaticConfigurationFailed: "automaticConfigurationFailed" From d4e969f77e81f288f23afc3faf6f901322aff576 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Tue, 1 Oct 2024 16:17:30 +0200 Subject: [PATCH 56/75] Move paymentSession3DSecureViewControllerLoadFailed into sdkProblemOccurred --- .../Classes/SwedbankPayPaymentSession.swift | 15 ++++++--------- .../PaymentSessionProblem.swift | 14 ++++++++------ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index c91aa92..3596e27 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -49,12 +49,6 @@ public protocol SwedbankPaySDKPaymentSessionDelegate: AnyObject { func dismiss3DSecureViewController() func showSwedbankPaySDKController(viewController: SwedbankPaySDKController) - - /// Called if the 3D secure view loading failed. - /// - /// - parameter error: The error that caused the failure - /// - parameter retry: A block that can be called to retry. - func paymentSession3DSecureViewControllerLoadFailed(error: Error, retry: @escaping ()->Void) } public extension SwedbankPaySDK { @@ -650,15 +644,18 @@ public extension SwedbankPaySDK { } } case .failure(let error): - self.delegate?.paymentSession3DSecureViewControllerLoadFailed(error: error, retry: { + 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: "paymentSession3DSecureViewControllerLoadFailed", + BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", succeeded: self.delegate != nil, - values: ["errorDescription": error.localizedDescription, + values: ["problem": problem.rawValue, + "errorDescription": error.localizedDescription, "errorCode": String(error.code), "errorDomain": error.domain])) } diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift index a547649..f944a39 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift @@ -21,18 +21,20 @@ public extension SwedbankPaySDK { case paymentSessionEndStateReached case paymentSessionAPIRequestFailed(error: Error, retry: (()->Void)?) case paymentControllerPaymentFailed(error: Error, retry: (()->Void)?) + case paymentSession3DSecureViewControllerLoadFailed(error: Error, retry: (()->Void)?) case clientAppLaunchFailed case internalInconsistencyError case automaticConfigurationFailed var rawValue: String { switch self { - case .paymentSessionEndStateReached: "paymentSessionEndStateReached" - case .paymentSessionAPIRequestFailed: "paymentSessionAPIRequestFailed" - case .paymentControllerPaymentFailed: "paymentControllerPaymentFailed" - case .clientAppLaunchFailed: "clientAppLaunchFailed" - case .internalInconsistencyError: "internalInconsistencyError" - case .automaticConfigurationFailed: "automaticConfigurationFailed" + case .paymentSessionEndStateReached: "paymentSessionEndStateReached" + case .paymentSessionAPIRequestFailed: "paymentSessionAPIRequestFailed" + case .paymentControllerPaymentFailed: "paymentControllerPaymentFailed" + case .paymentSession3DSecureViewControllerLoadFailed: "paymentSession3DSecureViewControllerLoadFailed" + case .clientAppLaunchFailed: "clientAppLaunchFailed" + case .internalInconsistencyError: "internalInconsistencyError" + case .automaticConfigurationFailed: "automaticConfigurationFailed" } } } From 7dd56055f7f562d359fddcac8ef89018068adbe9 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Fri, 4 Oct 2024 11:14:18 +0200 Subject: [PATCH 57/75] Bumped to iOS 11 and fixed all warnings --- SwedbankPaySDK.podspec | 2 +- SwedbankPaySDK.xcodeproj/project.pbxproj | 6 ++---- .../Classes/Api/Helpers/CustomDateDecoder.swift | 2 +- .../Classes/ApplePay/SwedbankPayAuthorization.swift | 4 ++-- .../Classes/Beacon/BeaconEndpointRouter.swift | 4 +--- .../Classes/SwedbankPayPaymentSession.swift | 2 +- .../ConfigurationAsync.swift | 12 ++++++++++-- .../Classes/WebView/SCAWebViewService.swift | 2 +- .../WebView/SwedbankPayExtraWebViewController.swift | 2 +- .../WebView/SwedbankPaySCAWebViewController.swift | 2 +- .../WebView/SwedbankPayWebViewController.swift | 2 +- .../WebView/SwedbankPayWebViewControllerBase.swift | 2 +- SwedbankPaySDKMerchantBackend.podspec | 2 +- 13 files changed, 24 insertions(+), 20 deletions(-) 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 0675999..654da11 100644 --- a/SwedbankPaySDK.xcodeproj/project.pbxproj +++ b/SwedbankPaySDK.xcodeproj/project.pbxproj @@ -1578,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; @@ -1637,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; @@ -1660,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", @@ -1685,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 index dd884b6..539a2c0 100644 --- a/SwedbankPaySDK/Classes/Api/Helpers/CustomDateDecoder.swift +++ b/SwedbankPaySDK/Classes/Api/Helpers/CustomDateDecoder.swift @@ -15,7 +15,7 @@ import Foundation -class CustomDateDecoder: JSONDecoder { +class CustomDateDecoder: JSONDecoder, @unchecked Sendable { let dateFormatter = { let formatter = ISO8601DateFormatter() formatter.timeZone = TimeZone(identifier: "UTC") diff --git a/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift b/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift index c2015b8..6f549a5 100644 --- a/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift +++ b/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift @@ -45,7 +45,7 @@ class SwedbankPayAuthorization: NSObject { paymentRequest.merchantIdentifier = merchantIdentifier - if let merchantCapabilities = task.expects?.first(where: { $0.name == "MerchantCapabilities" })?.stringArray?.contains(where: { $0 == "supports3DS" }) { + if (task.expects?.first(where: { $0.name == "MerchantCapabilities" })?.stringArray?.contains(where: { $0 == "supports3DS" })) != nil { paymentRequest.merchantCapabilities = .threeDSecure } @@ -85,7 +85,7 @@ class SwedbankPayAuthorization: NSObject { extension SwedbankPayAuthorization: PKPaymentAuthorizationControllerDelegate { func paymentAuthorizationControllerDidFinish(_ controller: PKPaymentAuthorizationController) { if let handler = self.handler { - if let status = status { + if status != nil { handler(.success((success))) } else { handler(.failure(self.errors?.first ?? SwedbankPayAPIError.unknown)) diff --git a/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift b/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift index 344f006..a7a4bb3 100644 --- a/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift +++ b/SwedbankPaySDK/Classes/Beacon/BeaconEndpointRouter.swift @@ -90,11 +90,9 @@ struct BeaconEndpointRouter: BeaconEndpointRouterProtocol { extension BeaconEndpointRouter { func makeRequest(handler: @escaping (Result) -> Void) { - let requestStartTimestamp: Date = Date() - requestWithDataResponse { result in switch result { - case .success(let data): + case .success: handler(.success(())) case .failure(let error): handler(.failure(error)) diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 3596e27..93079ae 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -499,7 +499,7 @@ public extension SwedbankPaySDK { switch result { case .success: self.scaMethodRequestDataPerformed.append((name: task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "null", value: "Y")) - case .failure(let error): + case .failure: self.scaMethodRequestDataPerformed.append((name: task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "null", value: "N")) } 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/WebView/SCAWebViewService.swift b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift index e933ab3..fea8dc1 100644 --- a/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift +++ b/SwedbankPaySDK/Classes/WebView/SCAWebViewService.swift @@ -14,7 +14,7 @@ // limitations under the License. import Foundation -import WebKit +@preconcurrency import WebKit class SCAWebViewService: NSObject, WKNavigationDelegate { private var handler: ((Result) -> Void)? 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 index 768ecab..6d8ba2e 100644 --- a/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift +++ b/SwedbankPaySDK/Classes/WebView/SwedbankPaySCAWebViewController.swift @@ -14,7 +14,7 @@ // limitations under the License. import UIKit -import WebKit +@preconcurrency import WebKit class SwedbankPaySCAWebViewController: UIViewController { internal var lastRootPage: (navigation: WKNavigation?, baseURL: URL?)? 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/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 From a26279991114f083de98edea3d40fa2460d9ac75 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Mon, 14 Oct 2024 14:51:34 +0200 Subject: [PATCH 58/75] =?UTF-8?q?SP-80=20St=C3=B6d=20f=C3=B6r=20att=20?= =?UTF-8?q?=C3=A5terst=C3=A4lla=20till=20Menu=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Classes/Api/SwedbankPayAPIEnpointRouter.swift | 2 +- .../Classes/SwedbankPayPaymentSession.swift | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index 93368ea..5be52b5 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -128,7 +128,7 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { "showConsentAffirmation" : enabledPaymentDetailsConsentCheckbox, ] } else { - return nil + return ["paymentMethod": nil] } default: return nil diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 93079ae..fe533b5 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -159,7 +159,14 @@ public extension SwedbankPaySDK { var succeeded = false - if case .newCreditCard = instrument { + if ongoingModel.paymentSession.instrumentModePaymentMethod != nil && ongoingModel.paymentSession.instrumentModePaymentMethod != instrument.identifier, + let operation = ongoingModel.operations?.first(where: { $0.rel == .customizePayment }) { + sessionStartTimestamp = Date() + + makeRequest(router: .customizePayment(instrument: instrument), operation: operation) + + succeeded = true + } else if case .newCreditCard = instrument { if ongoingModel.paymentSession.instrumentModePaymentMethod == nil || ongoingModel.paymentSession.instrumentModePaymentMethod != "CreditCard", let operation = ongoingModel.operations?.first(where: { $0.rel == .customizePayment }) { sessionStartTimestamp = Date() @@ -473,6 +480,9 @@ public extension SwedbankPaySDK { } else if let attemptPayload = operations.first(where: { $0.rel == .attemptPayload }), let walletSdk = attemptPayload.firstTask(with: .walletSdk) { makeApplePayAuthorization(operation: attemptPayload, task: walletSdk) + } else if let instrument = self.instrument, + ongoingModel?.paymentSession.instrumentModePaymentMethod == nil { + makeNativePaymentAttempt(instrument: instrument) } else if case .newCreditCard = self.instrument, ongoingModel?.paymentSession.instrumentModePaymentMethod == "CreditCard" { DispatchQueue.main.async { From ca6bc1f414718c91bb6b64baebd4eddb1863efbf Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Tue, 15 Oct 2024 13:39:30 +0200 Subject: [PATCH 59/75] Move logic from makeNativePaymentAttempt to sessionOperationHandling --- .../Api/SwedbankPayAPIEnpointRouter.swift | 2 +- .../Classes/SwedbankPayPaymentSession.swift | 94 +++++++------------ 2 files changed, 37 insertions(+), 59 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index 5be52b5..8f1034e 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -33,7 +33,7 @@ enum EnpointRouter { case acknowledgeFailedAttempt case abortPayment case applePay(paymentPayload: String) - case customizePayment(instrument: SwedbankPaySDK.PaymentAttemptInstrument) + case customizePayment(instrument: SwedbankPaySDK.PaymentAttemptInstrument?) } protocol EndpointRouterProtocol { diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index fe533b5..b18510f 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -157,68 +157,17 @@ public extension SwedbankPaySDK { break } - var succeeded = false - - if ongoingModel.paymentSession.instrumentModePaymentMethod != nil && ongoingModel.paymentSession.instrumentModePaymentMethod != instrument.identifier, - let operation = ongoingModel.operations?.first(where: { $0.rel == .customizePayment }) { - sessionStartTimestamp = Date() - - makeRequest(router: .customizePayment(instrument: instrument), operation: operation) - - succeeded = true - } else if case .newCreditCard = instrument { - if ongoingModel.paymentSession.instrumentModePaymentMethod == nil || ongoingModel.paymentSession.instrumentModePaymentMethod != "CreditCard", - let operation = ongoingModel.operations?.first(where: { $0.rel == .customizePayment }) { - sessionStartTimestamp = Date() - - makeRequest(router: .customizePayment(instrument: instrument), operation: operation) - - succeeded = true - } else { - DispatchQueue.main.async { - self.createSwedbankPaySDKController() - } - } - } else if let operation = ongoingModel.paymentSession.methods? - .first(where: { $0.name == instrument.identifier })?.operations? - .first(where: { $0.rel == .expandMethod || $0.rel == .startPaymentAttempt || $0.rel == .getPayment }) { - - sessionStartTimestamp = Date() - - switch operation.rel { - case .expandMethod: - makeRequest(router: .expandMethod(instrument: instrument), operation: operation) - case .startPaymentAttempt: - makeRequest(router: .startPaymentAttempt(instrument: instrument, culture: ongoingModel.paymentSession.culture), operation: operation) - self.instrument = nil - case .getPayment: - makeRequest(router: .getPayment, operation: operation) - default: - fatalError("Operantion rel is not supported for makeNativePaymentAttempt: \(String(describing: operation.rel))") - } - - succeeded = true - } else { - 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])) - } - - return - } + sessionOperationHandling(paymentOutputModel: ongoingModel, culture: ongoingModel.paymentSession.culture) switch instrument { case .swish(let msisdn): BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", - succeeded: succeeded, + succeeded: true, values: ["instrument": instrument.identifier, "msisdn": msisdn])) case .creditCard(let prefill): BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", - succeeded: succeeded, + succeeded: true, values: ["instrument": instrument.identifier, "paymentToken": prefill.paymentToken, "cardNumber": prefill.maskedPan, @@ -226,11 +175,11 @@ public extension SwedbankPaySDK { "cardExpiryYear": prefill.expiryYear])) case .applePay: BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", - succeeded: succeeded, + succeeded: true, values: ["instrument": instrument.identifier])) case .newCreditCard(enabledPaymentDetailsConsentCheckbox: let enabledPaymentDetailsConsentCheckbox): BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", - succeeded: succeeded, + succeeded: true, values: ["instrument": instrument.identifier, "showConsentAffirmation": enabledPaymentDetailsConsentCheckbox.description])) } @@ -481,8 +430,19 @@ public extension SwedbankPaySDK { let walletSdk = attemptPayload.firstTask(with: .walletSdk) { makeApplePayAuthorization(operation: attemptPayload, task: walletSdk) } else if let instrument = self.instrument, - ongoingModel?.paymentSession.instrumentModePaymentMethod == nil { - makeNativePaymentAttempt(instrument: instrument) + ongoingModel?.paymentSession.instrumentModePaymentMethod != nil && ongoingModel?.paymentSession.instrumentModePaymentMethod != instrument.identifier, + let customizePayment = ongoingModel?.operations?.first(where: { $0.rel == .customizePayment }) { + makeRequest(router: .customizePayment(instrument: nil), operation: customizePayment) + } else if let instrument = self.instrument, + case .newCreditCard = instrument { + if ongoingModel?.paymentSession.instrumentModePaymentMethod == nil || ongoingModel?.paymentSession.instrumentModePaymentMethod != "CreditCard", + let customizePayment = ongoingModel?.operations?.first(where: { $0.rel == .customizePayment }) { + makeRequest(router: .customizePayment(instrument: instrument), operation: customizePayment) + } else { + DispatchQueue.main.async { + self.createSwedbankPaySDKController() + } + } } else if case .newCreditCard = self.instrument, ongoingModel?.paymentSession.instrumentModePaymentMethod == "CreditCard" { DispatchQueue.main.async { @@ -601,6 +561,24 @@ public extension SwedbankPaySDK { succeeded: self.delegate != nil, values: ["instruments": availableInstruments.compactMap({ $0.identifier }).joined(separator: ";")])) } + } else if let instrument = self.instrument, + let operation = ongoingModel?.paymentSession.methods? + .first(where: { $0.name == instrument.identifier })?.operations? + .first(where: { $0.rel == .expandMethod || $0.rel == .startPaymentAttempt || $0.rel == .getPayment }) { + + sessionStartTimestamp = Date() + + switch operation.rel { + case .expandMethod: + makeRequest(router: .expandMethod(instrument: instrument), operation: operation) + case .startPaymentAttempt: + makeRequest(router: .startPaymentAttempt(instrument: instrument, culture: culture), operation: operation) + self.instrument = nil + case .getPayment: + makeRequest(router: .getPayment, operation: operation) + default: + fatalError("Operantion rel is not supported for makeNativePaymentAttempt: \(String(describing: operation.rel))") + } } else if let getPayment = operations.first(where: { $0.rel == .getPayment }) { DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { self.sessionStartTimestamp = Date() From 35e233bd46a0fd9bc647ac9a6946c8863a499637 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Tue, 15 Oct 2024 14:31:10 +0200 Subject: [PATCH 60/75] Code Review Fixes --- .../Classes/SwedbankPayPaymentSession.swift | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index b18510f..06ea768 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -434,15 +434,10 @@ public extension SwedbankPaySDK { let customizePayment = ongoingModel?.operations?.first(where: { $0.rel == .customizePayment }) { makeRequest(router: .customizePayment(instrument: nil), operation: customizePayment) } else if let instrument = self.instrument, - case .newCreditCard = instrument { - if ongoingModel?.paymentSession.instrumentModePaymentMethod == nil || ongoingModel?.paymentSession.instrumentModePaymentMethod != "CreditCard", - let customizePayment = ongoingModel?.operations?.first(where: { $0.rel == .customizePayment }) { - makeRequest(router: .customizePayment(instrument: instrument), operation: customizePayment) - } else { - DispatchQueue.main.async { - self.createSwedbankPaySDKController() - } - } + case .newCreditCard = instrument, + ongoingModel?.paymentSession.instrumentModePaymentMethod == nil || ongoingModel?.paymentSession.instrumentModePaymentMethod != "CreditCard", + let customizePayment = ongoingModel?.operations?.first(where: { $0.rel == .customizePayment }) { + makeRequest(router: .customizePayment(instrument: instrument), operation: customizePayment) } else if case .newCreditCard = self.instrument, ongoingModel?.paymentSession.instrumentModePaymentMethod == "CreditCard" { DispatchQueue.main.async { From e68e1c82ded3dfd00d858a4dddb5a256cfc27d2f Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Tue, 15 Oct 2024 15:58:54 +0200 Subject: [PATCH 61/75] More Code Review Fixes --- .../Classes/Api/SwedbankPayAPIEnpointRouter.swift | 13 ++++++++++--- .../Classes/SwedbankPayPaymentSession.swift | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index 8f1034e..bace98c 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -122,13 +122,20 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { return ["instrument": "ApplePay", "paymentPayload": paymentPayload] case .customizePayment(let instrument): - if case .newCreditCard(let enabledPaymentDetailsConsentCheckbox) = instrument { + guard let instrument = instrument else { + return ["paymentMethod": nil] + } + + switch instrument { + case .newCreditCard(let enabledPaymentDetailsConsentCheckbox): return ["paymentMethod": "CreditCard", "hideStoredPaymentOptions": true, "showConsentAffirmation" : enabledPaymentDetailsConsentCheckbox, ] - } else { - return ["paymentMethod": nil] + case .swish, + .creditCard, + .applePay: + return ["paymentMethod": instrument.identifier] } default: return nil diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 06ea768..60e4222 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -435,7 +435,7 @@ public extension SwedbankPaySDK { makeRequest(router: .customizePayment(instrument: nil), operation: customizePayment) } else if let instrument = self.instrument, case .newCreditCard = instrument, - ongoingModel?.paymentSession.instrumentModePaymentMethod == nil || ongoingModel?.paymentSession.instrumentModePaymentMethod != "CreditCard", + ongoingModel?.paymentSession.instrumentModePaymentMethod == nil || ongoingModel?.paymentSession.instrumentModePaymentMethod != instrument.identifier, let customizePayment = ongoingModel?.operations?.first(where: { $0.rel == .customizePayment }) { makeRequest(router: .customizePayment(instrument: instrument), operation: customizePayment) } else if case .newCreditCard = self.instrument, From f16723ed0465d6ad36031994b217a2da6bad4df3 Mon Sep 17 00:00:00 2001 From: Michael Balsiger Date: Wed, 16 Oct 2024 15:23:13 +0200 Subject: [PATCH 62/75] Fixed Warnings in MerchantBackend --- .../Classes/MerchantBackendApi.swift | 12 ++++++++---- .../Classes/MerchantBackendRequest.swift | 2 +- .../Classes/RequestDecorator.swift | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) 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/MerchantBackendRequest.swift b/SwedbankPaySDKMerchantBackend/Classes/MerchantBackendRequest.swift index 5e1a614..7756a0c 100644 --- a/SwedbankPaySDKMerchantBackend/Classes/MerchantBackendRequest.swift +++ b/SwedbankPaySDKMerchantBackend/Classes/MerchantBackendRequest.swift @@ -16,7 +16,7 @@ import SwedbankPaySDK import Alamofire -extension Request: SwedbankPaySDKRequest { +extension Request: @retroactive SwedbankPaySDKRequest { public func cancel() { _ = self.cancel() as 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) From 3172ef81ce0abfb46b50747af45141f8ea13edd9 Mon Sep 17 00:00:00 2001 From: Martin Alleus Date: Wed, 16 Oct 2024 16:33:03 +0200 Subject: [PATCH 63/75] Removed Xcode 16 fix --- .../Classes/MerchantBackendRequest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwedbankPaySDKMerchantBackend/Classes/MerchantBackendRequest.swift b/SwedbankPaySDKMerchantBackend/Classes/MerchantBackendRequest.swift index 7756a0c..5e1a614 100644 --- a/SwedbankPaySDKMerchantBackend/Classes/MerchantBackendRequest.swift +++ b/SwedbankPaySDKMerchantBackend/Classes/MerchantBackendRequest.swift @@ -16,7 +16,7 @@ import SwedbankPaySDK import Alamofire -extension Request: @retroactive SwedbankPaySDKRequest { +extension Request: SwedbankPaySDKRequest { public func cancel() { _ = self.cancel() as Request } From 36ee74556d2c1b521fde089de088ae5470bd441a Mon Sep 17 00:00:00 2001 From: Martin Alleus Date: Mon, 21 Oct 2024 11:58:27 +0200 Subject: [PATCH 64/75] Don't attempt loading empty SCA Method Request href strings --- SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 60e4222..9cc5203 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -457,7 +457,8 @@ public extension SwedbankPaySDK { self.launchClientApp(task: launchClientApp.firstTask(with: .launchClientApp)!) } else if let scaMethodRequest = operations.first(where: { $0.firstTask(with: .scaMethodRequest) != nil }), let task = scaMethodRequest.firstTask(with: .scaMethodRequest), - task.href != nil, + let href = task.href, + !href.isEmpty, !scaMethodRequestDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "null" }) { DispatchQueue.main.async { self.webViewService.load(task: task) { result in From 632a69ea800c32c8195e25fa797331f72716f846 Mon Sep 17 00:00:00 2001 From: Martin Alleus Date: Wed, 23 Oct 2024 13:01:31 +0200 Subject: [PATCH 65/75] Adding Sequence extensions for more readable sesson logic --- .../Classes/Api/Models/IntegrationTask.swift | 15 ++++ .../Classes/Api/Models/MethodBaseModel.swift | 7 ++ .../Api/Models/OperationOutputModel.swift | 13 +++- .../Classes/SwedbankPayPaymentSession.swift | 74 +++++++++---------- 4 files changed, 70 insertions(+), 39 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift b/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift index ab97c38..bce012b 100644 --- a/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift +++ b/SwedbankPaySDK/Classes/Api/Models/IntegrationTask.swift @@ -23,6 +23,21 @@ struct IntegrationTask: Codable, Hashable { 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 diff --git a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift index fbca225..3b17e99 100644 --- a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift @@ -103,6 +103,13 @@ enum MethodBaseModel: Codable, Equatable, Hashable { } } +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 { diff --git a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift index 31238d7..cbe025d 100644 --- a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift @@ -23,7 +23,7 @@ struct OperationOutputModel: Codable, Hashable { } extension OperationOutputModel { - func firstTask(with rel: IntegrationTaskRel) -> IntegrationTask? { + func firstTask(withRel rel: IntegrationTaskRel) -> IntegrationTask? { if let task = tasks?.first(where: { $0.rel == rel }) { return task } @@ -32,6 +32,17 @@ extension OperationOutputModel { } } +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 diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 9cc5203..afa26e4 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -193,7 +193,7 @@ public extension SwedbankPaySDK { /// - returns:- SwedbankPaySDKController to be shown. public func createSwedbankPaySDKController() { guard let ongoingModel = ongoingModel, - let operation = ongoingModel.operations?.first(where: { $0.rel == .viewPayment }), + let operation = ongoingModel.operations?.firstOperation(withRel: .viewPayment), let orderInfo = orderInfo, let href = operation.href, let viewPaymentLink = URL(string: href) else { @@ -247,8 +247,7 @@ public extension SwedbankPaySDK { } var succeeded = false - if let operation = ongoingModel.operations? - .first(where: { $0.rel == .abortPayment }) { + if let operation = ongoingModel.operations?.firstOperation(withRel: .abortPayment) { sessionStartTimestamp = Date() makeRequest(router: .abortPayment, operation: operation) succeeded = true @@ -285,7 +284,7 @@ public extension SwedbankPaySDK { termsOfServiceUrl: urls.termsOfServiceUrl) } - if let eventLogging = paymentOutputModel.operations?.first(where: { $0.rel == .eventLogging }) { + if let eventLogging = paymentOutputModel.operations?.firstOperation(withRel: .eventLogging) { BeaconService.shared.href = eventLogging.href } @@ -424,49 +423,49 @@ public extension SwedbankPaySDK { let operations = paymentOutputModel.prioritisedOperations - if let preparePayment = operations.first(where: { $0.rel == .preparePayment }) { + if let preparePayment = operations.firstOperation(withRel: .preparePayment) { makeRequest(router: .preparePayment, operation: preparePayment) - } else if let attemptPayload = operations.first(where: { $0.rel == .attemptPayload }), - let walletSdk = attemptPayload.firstTask(with: .walletSdk) { + } else if let attemptPayload = operations.firstOperation(withRel: .attemptPayload), + let walletSdk = attemptPayload.firstTask(withRel: .walletSdk) { makeApplePayAuthorization(operation: attemptPayload, task: walletSdk) } else if let instrument = self.instrument, ongoingModel?.paymentSession.instrumentModePaymentMethod != nil && ongoingModel?.paymentSession.instrumentModePaymentMethod != instrument.identifier, - let customizePayment = ongoingModel?.operations?.first(where: { $0.rel == .customizePayment }) { + let customizePayment = ongoingModel?.operations?.firstOperation(withRel: .customizePayment) { makeRequest(router: .customizePayment(instrument: nil), operation: customizePayment) } else if let instrument = self.instrument, case .newCreditCard = instrument, ongoingModel?.paymentSession.instrumentModePaymentMethod == nil || ongoingModel?.paymentSession.instrumentModePaymentMethod != instrument.identifier, - let customizePayment = ongoingModel?.operations?.first(where: { $0.rel == .customizePayment }) { + let customizePayment = ongoingModel?.operations?.firstOperation(withRel: .customizePayment) { makeRequest(router: .customizePayment(instrument: instrument), operation: customizePayment) } else if case .newCreditCard = self.instrument, ongoingModel?.paymentSession.instrumentModePaymentMethod == "CreditCard" { DispatchQueue.main.async { self.createSwedbankPaySDKController() } - } else if operations.contains(where: { $0.rel == .startPaymentAttempt }), + } else if operations.containsOperation(withRel: .startPaymentAttempt), let instrument = instrument, let startPaymentAttempt = ongoingModel?.paymentSession.methods? - .first(where: { $0.name == instrument.identifier })?.operations? - .first(where: { $0.rel == .startPaymentAttempt }) { + .firstMethod(withName: instrument.identifier)?.operations? + .firstOperation(withRel: .startPaymentAttempt) { makeRequest(router: .startPaymentAttempt(instrument: instrument, culture: culture), operation: startPaymentAttempt) self.instrument = nil - } else if let launchClientApp = operations.first(where: { $0.firstTask(with: .launchClientApp) != nil }), - let tasks = launchClientApp.firstTask(with: .launchClientApp), + } else if let launchClientApp = operations.first(where: { $0.firstTask(withRel: .launchClientApp) != nil }), + let tasks = launchClientApp.firstTask(withRel: .launchClientApp), !hasLaunchClientAppURLs.contains(where: { $0.absoluteString.contains(tasks.href ?? "") }) { - self.launchClientApp(task: launchClientApp.firstTask(with: .launchClientApp)!) - } else if let scaMethodRequest = operations.first(where: { $0.firstTask(with: .scaMethodRequest) != nil }), - let task = scaMethodRequest.firstTask(with: .scaMethodRequest), + self.launchClientApp(task: launchClientApp.firstTask(withRel: .launchClientApp)!) + } 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?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "null" }) { + !scaMethodRequestDataPerformed.contains(where: { $0.name == task.expects?.value(for: "threeDSMethodData") ?? "null" }) { DispatchQueue.main.async { self.webViewService.load(task: task) { result in switch result { case .success: - self.scaMethodRequestDataPerformed.append((name: task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "null", value: "Y")) + self.scaMethodRequestDataPerformed.append((name: task.expects?.value(for: "threeDSMethodData") ?? "null", value: "Y")) case .failure: - self.scaMethodRequestDataPerformed.append((name: task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "null", value: "N")) + self.scaMethodRequestDataPerformed.append((name: task.expects?.value(for: "threeDSMethodData") ?? "null", value: "N")) } if let model = self.ongoingModel { @@ -474,21 +473,21 @@ public extension SwedbankPaySDK { } } } - } else if let createAuthentication = operations.first(where: { $0.rel == .createAuthentication }), - let notificationUrl = createAuthentication.expects?.first(where: { $0.name == "NotificationUrl" })?.value { + } else if let createAuthentication = operations.firstOperation(withRel: .createAuthentication), + let notificationUrl = createAuthentication.expects?.value(for: "NotificationUrl") { self.notificationUrl = notificationUrl - if let task = createAuthentication.firstTask(with: .scaMethodRequest), - let scaMethod = scaMethodRequestDataPerformed.first(where: { $0.name == task.expects?.first(where: { $0.name == "threeDSMethodData" })?.value ?? "null" }) { + if let task = createAuthentication.firstTask(withRel: .scaMethodRequest), + let scaMethod = scaMethodRequestDataPerformed.first(where: { $0.name == task.expects?.value(for: "threeDSMethodData") ?? "null" }) { makeRequest(router: .createAuthentication(methodCompletionIndicator: scaMethod.value, notificationUrl: notificationUrl), operation: createAuthentication) - } else if let methodCompletionIndicator = createAuthentication.expects?.first(where: { $0.name == "methodCompletionIndicator" })?.value { + } else if let methodCompletionIndicator = createAuthentication.expects?.value(for: "methodCompletionIndicator") { makeRequest(router: .createAuthentication(methodCompletionIndicator: methodCompletionIndicator, notificationUrl: notificationUrl), operation: createAuthentication) } else { makeRequest(router: .createAuthentication(methodCompletionIndicator: "U", notificationUrl: notificationUrl), operation: createAuthentication) } - } else if let operation = operations.first(where: { $0.firstTask(with: .scaRedirect) != nil }), - let task = operation.firstTask(with: .scaRedirect), - !scaRedirectDataPerformed.contains(where: { $0.name == task.expects?.first(where: { $0.name == "creq" })?.value }) { + } 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") }) { DispatchQueue.main.async { self.webViewController.notificationUrl = self.notificationUrl @@ -500,11 +499,11 @@ public extension SwedbankPaySDK { self.scaRedirectDataPerformed(task: task, culture: culture) } - } else if let completeAuthentication = operations.first(where: { $0.rel == .completeAuthentication }), + } 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?.first(where: { $0.name == "creq" })?.value }) { + let scaRedirect = scaRedirectDataPerformed.first(where: { $0.name == task.expects?.value(for: "creq") }) { makeRequest(router: .completeAuthentication(cRes: scaRedirect.value), operation: completeAuthentication) - } else if let redirectPayer = operations.first(where: { $0.rel == .redirectPayer }) { + } else if let redirectPayer = operations.firstOperation(withRel: .redirectPayer) { DispatchQueue.main.async { if redirectPayer.href == self.orderInfo?.cancelUrl?.absoluteString { self.delegate?.paymentSessionCanceled() @@ -533,8 +532,8 @@ public extension SwedbankPaySDK { scaRedirectDataPerformed = [] notificationUrl = nil hasShownAvailableInstruments = false - } else if (operations.contains(where: { $0.rel == .expandMethod }) || operations.contains(where: { $0.rel == .startPaymentAttempt })) && - hasShownAvailableInstruments == false { + } else if (operations.containsOperation(withRel: .expandMethod) || operations.containsOperation(withRel: .startPaymentAttempt) && + hasShownAvailableInstruments == false) { DispatchQueue.main.async { let availableInstruments: [AvailableInstrument] = paymentOutputModel.paymentSession.methods?.compactMap({ model in switch model { @@ -559,7 +558,7 @@ public extension SwedbankPaySDK { } } else if let instrument = self.instrument, let operation = ongoingModel?.paymentSession.methods? - .first(where: { $0.name == instrument.identifier })?.operations? + .firstMethod(withName: instrument.identifier)?.operations? .first(where: { $0.rel == .expandMethod || $0.rel == .startPaymentAttempt || $0.rel == .getPayment }) { sessionStartTimestamp = Date() @@ -575,7 +574,7 @@ public extension SwedbankPaySDK { default: fatalError("Operantion rel is not supported for makeNativePaymentAttempt: \(String(describing: operation.rel))") } - } else if let getPayment = operations.first(where: { $0.rel == .getPayment }) { + } else if let getPayment = operations.firstOperation(withRel: .getPayment) { DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { self.sessionStartTimestamp = Date() self.makeRequest(router: .getPayment, operation: getPayment) @@ -598,8 +597,7 @@ public extension SwedbankPaySDK { } if let ongoingModel = ongoingModel { - if let operation = ongoingModel.paymentSession.allMethodOperations - .first(where: { $0.rel == .getPayment }) { + if let operation = ongoingModel.paymentSession.allMethodOperations.firstOperation(withRel: .getPayment) { sessionStartTimestamp = Date() makeRequest(router: .getPayment, operation: operation) } @@ -615,7 +613,7 @@ public extension SwedbankPaySDK { switch result { case .success(let value): if !self.scaRedirectDataPerformed.contains(where: { $0.value == value }) { - self.scaRedirectDataPerformed.append((name: task.expects!.first(where: { $0.name == "creq" })!.value!, value: value)) + self.scaRedirectDataPerformed.append((name: task.expects!.value(for: "creq")!, value: value)) self.delegate?.dismiss3DSecureViewController() From 55ed3c62635047bde21b5744c021e04a3d98dc32 Mon Sep 17 00:00:00 2001 From: Martin Alleus Date: Wed, 6 Nov 2024 17:14:59 +0100 Subject: [PATCH 66/75] Fix for incorrect logic after adding sequence extensions --- SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index afa26e4..46fde29 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -532,7 +532,7 @@ public extension SwedbankPaySDK { scaRedirectDataPerformed = [] notificationUrl = nil hasShownAvailableInstruments = false - } else if (operations.containsOperation(withRel: .expandMethod) || operations.containsOperation(withRel: .startPaymentAttempt) && + } else if ((operations.containsOperation(withRel: .expandMethod) || operations.containsOperation(withRel: .startPaymentAttempt)) && hasShownAvailableInstruments == false) { DispatchQueue.main.async { let availableInstruments: [AvailableInstrument] = paymentOutputModel.paymentSession.methods?.compactMap({ model in From b4e062e79827855ca76094367a7b1a6fd60bc68c Mon Sep 17 00:00:00 2001 From: Martin Alleus Date: Wed, 6 Nov 2024 17:12:06 +0100 Subject: [PATCH 67/75] Adding cancel and fail for Apple Pay, removing temporary error handling --- .../Api/Models/OperationOutputModel.swift | 3 ++ .../Api/SwedbankPayAPIEnpointRouter.swift | 4 +++ .../ApplePay/SwedbankPayAuthorization.swift | 19 ++++++---- .../Classes/SwedbankPayPaymentSession.swift | 35 ++++++------------- 4 files changed, 29 insertions(+), 32 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift index cbe025d..be4a6d7 100644 --- a/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/OperationOutputModel.swift @@ -57,6 +57,7 @@ enum OperationRel: Codable, Equatable, Hashable { case viewPayment case attemptPayload case customizePayment + case failPaymentAttempt case unknown(String) @@ -78,6 +79,7 @@ enum OperationRel: Codable, Equatable, Hashable { 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) } } @@ -102,6 +104,7 @@ enum OperationRel: Codable, Equatable, Hashable { case .viewPayment: "view-payment" case .attemptPayload: "attempt-payload" case .customizePayment: "customize-payment" + case .failPaymentAttempt: "fail-payment-attempt" case .unknown(let value): value } } diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index bace98c..268d2b4 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -34,6 +34,7 @@ enum EnpointRouter { case abortPayment case applePay(paymentPayload: String) case customizePayment(instrument: SwedbankPaySDK.PaymentAttemptInstrument?) + case failPaymentAttempt(problemType: String, errorCode: String?) } protocol EndpointRouterProtocol { @@ -137,6 +138,9 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { .applePay: return ["paymentMethod": instrument.identifier] } + case .failPaymentAttempt(let problemType, let errorCode): + return ["problemType": problemType, + "errorCode": errorCode] default: return nil } diff --git a/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift b/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift index 6f549a5..53024dc 100644 --- a/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift +++ b/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift @@ -16,18 +16,23 @@ 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 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) { + func showApplePay(operation: OperationOutputModel, task: IntegrationTask, merchantIdentifier: String, handler: @escaping (Result) -> Void) { self.errors = nil self.status = nil @@ -85,10 +90,10 @@ class SwedbankPayAuthorization: NSObject { extension SwedbankPayAuthorization: PKPaymentAuthorizationControllerDelegate { func paymentAuthorizationControllerDidFinish(_ controller: PKPaymentAuthorizationController) { if let handler = self.handler { - if status != nil { - handler(.success((success))) + if status != nil, let success = success { + handler(.success(success)) } else { - handler(.failure(self.errors?.first ?? SwedbankPayAPIError.unknown)) + handler(.failure(ApplePayError.userCancelled)) } } @@ -105,8 +110,8 @@ extension SwedbankPayAuthorization: PKPaymentAuthorizationControllerDelegate { SwedbankPayAPIEnpointRouter(endpoint: Endpoint(router: router, href: operation?.href, method: operation?.method), sessionStartTimestamp: Date()).makeRequest { result in switch result { - case .success(let success): - self.success = success + case .success(let paymentOutputModel): + self.success = paymentOutputModel self.status = PKPaymentAuthorizationStatus.success self.errors = [Error]() case .failure(let error): diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 46fde29..1e38b01 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -355,7 +355,7 @@ public extension SwedbankPaySDK { } } - private func makeApplePayAuthorization(operation: OperationOutputModel, task: IntegrationTask) { + private func makeApplePayAuthorization(attemptPayloadOperation: OperationOutputModel, failPaymentAttemptOperation: OperationOutputModel, task: IntegrationTask) { guard let merchantIdentifier = merchantIdentifier else { self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) @@ -366,30 +366,14 @@ public extension SwedbankPaySDK { return } - SwedbankPayAuthorization.shared.showApplePay(operation: operation, task: task, merchantIdentifier: merchantIdentifier) { result in + SwedbankPayAuthorization.shared.showApplePay(operation: attemptPayloadOperation, task: task, merchantIdentifier: merchantIdentifier) { result in switch result { - case .success(let success): - if let paymentOutputModel = success { - self.sessionOperationHandling(paymentOutputModel: paymentOutputModel, culture: paymentOutputModel.paymentSession.culture) - } - case .failure(let failure): - DispatchQueue.main.async { - // TODO: This is a temporary solution. In the future we need to send the error to the backend so they can provide the correct problem for us. - - let problem = SwedbankPaySDK.PaymentSessionProblem.paymentSessionAPIRequestFailed(error: failure, - retry: nil) - - 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])) - } + case .success(let paymentOutputModel): + self.sessionOperationHandling(paymentOutputModel: paymentOutputModel, culture: paymentOutputModel.paymentSession.culture) + case .failure(ApplePayError.userCancelled): + self.makeRequest(router: .failPaymentAttempt(problemType: "UserCancelled", errorCode: ""), operation: failPaymentAttemptOperation) + case .failure(let error): + self.makeRequest(router: .failPaymentAttempt(problemType: "TechnicalError", errorCode: error.localizedDescription), operation: failPaymentAttemptOperation) } } } @@ -426,8 +410,9 @@ public extension SwedbankPaySDK { if let preparePayment = operations.firstOperation(withRel: .preparePayment) { makeRequest(router: .preparePayment, operation: preparePayment) } else if let attemptPayload = operations.firstOperation(withRel: .attemptPayload), + let failPayment = paymentOutputModel.paymentSession.methods?.firstMethod(withName: AvailableInstrument.applePay.identifier)?.operations?.firstOperation(withRel: .failPaymentAttempt), let walletSdk = attemptPayload.firstTask(withRel: .walletSdk) { - makeApplePayAuthorization(operation: attemptPayload, task: walletSdk) + makeApplePayAuthorization(attemptPayloadOperation: attemptPayload, failPaymentAttemptOperation: failPayment, task: walletSdk) } else if let instrument = self.instrument, ongoingModel?.paymentSession.instrumentModePaymentMethod != nil && ongoingModel?.paymentSession.instrumentModePaymentMethod != instrument.identifier, let customizePayment = ongoingModel?.operations?.firstOperation(withRel: .customizePayment) { From 6dd2fd5a5e6859fe653cd4e35b61d3014b1e0ab1 Mon Sep 17 00:00:00 2001 From: Martin Alleus Date: Wed, 6 Nov 2024 17:26:09 +0100 Subject: [PATCH 68/75] Comments for main session logic --- .../Classes/SwedbankPayPaymentSession.swift | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 1e38b01..ab14a3b 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -408,10 +408,14 @@ public extension SwedbankPaySDK { 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.identifier)?.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, ongoingModel?.paymentSession.instrumentModePaymentMethod != nil && ongoingModel?.paymentSession.instrumentModePaymentMethod != instrument.identifier, @@ -432,18 +436,23 @@ public extension SwedbankPaySDK { let startPaymentAttempt = ongoingModel?.paymentSession.methods? .firstMethod(withName: instrument.identifier)?.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), !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: launchClientApp.firstTask(withRel: .launchClientApp)!) } 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 { @@ -460,19 +469,29 @@ public extension SwedbankPaySDK { } } 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 @@ -487,22 +506,32 @@ public extension SwedbankPaySDK { } 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", @@ -545,26 +574,39 @@ public extension SwedbankPaySDK { let operation = ongoingModel?.paymentSession.methods? .firstMethod(withName: instrument.identifier)?.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 !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) From f1d237ec9eda29eef1d0e95a95367bd76f08643e Mon Sep 17 00:00:00 2001 From: Martin Alleus Date: Mon, 11 Nov 2024 11:14:33 +0100 Subject: [PATCH 69/75] Extended support for web based SDK controller configuration and instrument mode payments --- .../Classes/Api/Models/MethodBaseModel.swift | 35 ++-- .../Api/Models/PaymentAttemptInstrument.swift | 11 +- .../Api/Models/PaymentSessionModel.swift | 21 +++ .../Api/SwedbankPayAPIEnpointRouter.swift | 36 ++-- .../ApplePay/SwedbankPayAuthorization.swift | 2 +- .../Classes/SwedbankPayPaymentSession.swift | 163 +++++++++++++----- 6 files changed, 184 insertions(+), 84 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift index 3b17e99..e662337 100644 --- a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift @@ -19,18 +19,17 @@ 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 unknown(String) + case webBased(paymentMethod: String) private enum CodingKeys: String, CodingKey { - case instrument, prefills, operations, cardBrands + case paymentMethod, prefills, operations, cardBrands } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let type = try container.decode(String.self, forKey: .instrument) - switch type { + 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), @@ -48,7 +47,7 @@ enum MethodBaseModel: Codable, Equatable, Hashable { cardBrands: try? container.decode([String]?.self, forKey: CodingKeys.cardBrands) ) default: - self = .unknown(type) + self = .webBased(paymentMethod: paymentMethod) } } @@ -65,8 +64,8 @@ enum MethodBaseModel: Codable, Equatable, Hashable { case .applePay(let operations, let cardBrands): try container.encode(operations) try container.encode(cardBrands) - case .unknown(let type): - try container.encode(type) + case .webBased(let paymentMethod): + try container.encode(paymentMethod) } } @@ -78,8 +77,8 @@ enum MethodBaseModel: Codable, Equatable, Hashable { return "CreditCard" case .applePay: return "ApplePay" - case .unknown: - return "Unknown" + case .webBased(let paymentMethod): + return paymentMethod } } @@ -91,16 +90,10 @@ enum MethodBaseModel: Codable, Equatable, Hashable { return opertations case .applePay(let operations, _): return operations - case .unknown: + case .webBased: return nil } } - - var isUnknown: Bool { - if case .unknown = self { return true } - - return false - } } extension Sequence where Iterator.Element == MethodBaseModel @@ -121,9 +114,9 @@ extension SwedbankPaySDK { case applePay - case webBased(identifier: String) + case webBased(paymentMethod: String) - var identifier: String { + var paymentMethod: String { switch self { case .swish: return "Swish" @@ -131,8 +124,8 @@ extension SwedbankPaySDK { return "CreditCard" case .applePay: return "ApplePay" - case .webBased(identifier: let identifier): - return identifier + case .webBased(let paymentMethod): + return paymentMethod } } } diff --git a/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift index c266c5d..9db75d0 100644 --- a/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentAttemptInstrument.swift @@ -21,7 +21,7 @@ extension SwedbankPaySDK { case applePay(merchantIdentifier: String) case newCreditCard(enabledPaymentDetailsConsentCheckbox: Bool) - var identifier: String { + var paymentMethod: String { switch self { case .swish: return "Swish" @@ -32,5 +32,14 @@ extension SwedbankPaySDK { return "ApplePay" } } + + var instrumentModeRequired: Bool { + switch self { + case .newCreditCard: + return true + case .swish, .applePay, .creditCard: + return false + } + } } } diff --git a/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift b/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift index b50aadd..2c2ffdd 100644 --- a/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/PaymentSessionModel.swift @@ -18,6 +18,7 @@ import Foundation struct PaymentSessionModel: Codable, Hashable { let culture: String? let methods: [MethodBaseModel]? + let settings: SettingsModel? let urls: UrlsModel? let instrumentModePaymentMethod: String? } @@ -38,6 +39,22 @@ extension PaymentSessionModel { 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 { @@ -47,3 +64,7 @@ struct UrlsModel: Codable, Hashable { let hostUrls: [URL]? let termsOfServiceUrl: URL? } + +struct SettingsModel: Codable, Hashable { + let enabledPaymentMethods: [String] +} diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index 268d2b4..73b94d6 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -32,8 +32,8 @@ enum EnpointRouter { case preparePayment case acknowledgeFailedAttempt case abortPayment - case applePay(paymentPayload: String) - case customizePayment(instrument: SwedbankPaySDK.PaymentAttemptInstrument?) + case attemptPayload(paymentPayload: String) + case customizePayment(instrument: SwedbankPaySDK.PaymentAttemptInstrument?, paymentMethod: String?, restrictToPaymentMethods: [String]?) case failPaymentAttempt(problemType: String, errorCode: String?) } @@ -50,7 +50,7 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { var body: [String: Any?]? { switch endpoint.router { case .expandMethod(instrument: let instrument): - return ["instrumentName": instrument.identifier] + return ["paymentMethod": instrument.paymentMethod] case .startPaymentAttempt(let instrument, let culture): switch instrument { case .swish(let msisdn): @@ -119,24 +119,30 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { "client": ["userAgent": SwedbankPaySDK.VersionReporter.userAgent, "ipAddress": NetworkStatusProvider.getAddress(for: .wifi) ?? NetworkStatusProvider.getAddress(for: .cellular) ?? ""], ] - case .applePay(let paymentPayload): - return ["instrument": "ApplePay", + case .attemptPayload(let paymentPayload): + return ["paymentMethod": "ApplePay", "paymentPayload": paymentPayload] - case .customizePayment(let instrument): - guard let instrument = instrument else { - return ["paymentMethod": nil] - } + case .customizePayment(let instrument, let paymentMethod, let restrictToPaymentMethods): - switch instrument { - case .newCreditCard(let enabledPaymentDetailsConsentCheckbox): + switch (instrument, paymentMethod, restrictToPaymentMethods) { + case (nil, nil, let restrictToPaymentMethods): + return ["paymentMethod": nil, + "restrictToPaymentMethods": restrictToPaymentMethods] + case (.newCreditCard(let enabledPaymentDetailsConsentCheckbox), _, _): return ["paymentMethod": "CreditCard", + "restrictToPaymentMethods": nil, "hideStoredPaymentOptions": true, "showConsentAffirmation" : enabledPaymentDetailsConsentCheckbox, ] - case .swish, - .creditCard, - .applePay: - return ["paymentMethod": instrument.identifier] + 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, diff --git a/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift b/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift index 53024dc..02fbab7 100644 --- a/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift +++ b/SwedbankPaySDK/Classes/ApplePay/SwedbankPayAuthorization.swift @@ -105,7 +105,7 @@ extension SwedbankPayAuthorization: PKPaymentAuthorizationControllerDelegate { func paymentAuthorizationController(_ controller: PKPaymentAuthorizationController, didAuthorizePayment payment: PKPayment, handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) { let paymentPayload = payment.token.paymentData.base64EncodedString() - let router = EnpointRouter.applePay(paymentPayload: paymentPayload) + let router = EnpointRouter.attemptPayload(paymentPayload: paymentPayload) SwedbankPayAPIEnpointRouter(endpoint: Endpoint(router: router, href: operation?.href, method: operation?.method), sessionStartTimestamp: Date()).makeRequest { result in diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index ab14a3b..2eda530 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -52,6 +52,11 @@ public protocol SwedbankPaySDKPaymentSessionDelegate: AnyObject { } 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. @@ -64,6 +69,7 @@ public extension SwedbankPaySDK { 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 @@ -102,6 +108,7 @@ public extension SwedbankPaySDK { sessionIsOngoing = true paymentViewSessionIsOngoing = false instrument = nil + sdkControllerMode = nil merchantIdentifier = nil ongoingModel = nil hasLaunchClientAppURLs = [] @@ -163,12 +170,12 @@ public extension SwedbankPaySDK { case .swish(let msisdn): BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", succeeded: true, - values: ["instrument": instrument.identifier, + values: ["instrument": instrument.paymentMethod, "msisdn": msisdn])) case .creditCard(let prefill): BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", succeeded: true, - values: ["instrument": instrument.identifier, + values: ["instrument": instrument.paymentMethod, "paymentToken": prefill.paymentToken, "cardNumber": prefill.maskedPan, "cardExpiryMonth": prefill.expiryMonth, @@ -176,11 +183,11 @@ public extension SwedbankPaySDK { case .applePay: BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", succeeded: true, - values: ["instrument": instrument.identifier])) + values: ["instrument": instrument.paymentMethod])) case .newCreditCard(enabledPaymentDetailsConsentCheckbox: let enabledPaymentDetailsConsentCheckbox): BeaconService.shared.log(type: .sdkMethodInvoked(name: "makePaymentAttempt", succeeded: true, - values: ["instrument": instrument.identifier, + values: ["instrument": instrument.paymentMethod, "showConsentAffirmation": enabledPaymentDetailsConsentCheckbox.description])) } @@ -191,7 +198,24 @@ public extension SwedbankPaySDK { /// There needs to be an active payment session before an payment attempt can be made. /// /// - returns:- SwedbankPaySDKController to be shown. - public func createSwedbankPaySDKController() { + 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 + } + + 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, @@ -412,29 +436,77 @@ public extension SwedbankPaySDK { makeRequest(router: .preparePayment, operation: preparePayment) } else if let attemptPayload = operations.firstOperation(withRel: .attemptPayload), - let failPayment = paymentOutputModel.paymentSession.methods?.firstMethod(withName: AvailableInstrument.applePay.identifier)?.operations?.firstOperation(withRel: .failPaymentAttempt), + 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, - ongoingModel?.paymentSession.instrumentModePaymentMethod != nil && ongoingModel?.paymentSession.instrumentModePaymentMethod != instrument.identifier, - let customizePayment = ongoingModel?.operations?.firstOperation(withRel: .customizePayment) { - makeRequest(router: .customizePayment(instrument: nil), operation: customizePayment) + (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, - case .newCreditCard = instrument, - ongoingModel?.paymentSession.instrumentModePaymentMethod == nil || ongoingModel?.paymentSession.instrumentModePaymentMethod != instrument.identifier, - let customizePayment = ongoingModel?.operations?.firstOperation(withRel: .customizePayment) { - makeRequest(router: .customizePayment(instrument: instrument), operation: customizePayment) - } else if case .newCreditCard = self.instrument, - ongoingModel?.paymentSession.instrumentModePaymentMethod == "CreditCard" { + 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 = ongoingModel?.paymentSession.methods? - .firstMethod(withName: instrument.identifier)?.operations? + 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 @@ -462,9 +534,7 @@ public extension SwedbankPaySDK { self.scaMethodRequestDataPerformed.append((name: task.expects?.value(for: "threeDSMethodData") ?? "null", value: "N")) } - if let model = self.ongoingModel { - self.sessionOperationHandling(paymentOutputModel: model, culture: culture) - } + self.sessionOperationHandling(paymentOutputModel: paymentOutputModel, culture: culture) } } } else if let createAuthentication = operations.firstOperation(withRel: .createAuthentication), @@ -546,33 +616,9 @@ public extension SwedbankPaySDK { scaRedirectDataPerformed = [] notificationUrl = nil hasShownAvailableInstruments = false - } else if ((operations.containsOperation(withRel: .expandMethod) || operations.containsOperation(withRel: .startPaymentAttempt)) && - hasShownAvailableInstruments == false) { - 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 .unknown(let identifier): - return AvailableInstrument.webBased(identifier: identifier) - } - }) ?? [] - - self.hasShownAvailableInstruments = true - - self.delegate?.paymentSessionFetched(availableInstruments: availableInstruments) - - BeaconService.shared.log(type: .sdkCallbackInvoked(name: "paymentSessionFetched", - succeeded: self.delegate != nil, - values: ["instruments": availableInstruments.compactMap({ $0.identifier }).joined(separator: ";")])) - } } else if let instrument = self.instrument, - let operation = ongoingModel?.paymentSession.methods? - .firstMethod(withName: instrument.identifier)?.operations? + 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) @@ -604,6 +650,31 @@ public extension SwedbankPaySDK { 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. From 9ff6eb7893a51acb00f35fbaaa8691a97b3363b1 Mon Sep 17 00:00:00 2001 From: Martin Alleus Date: Wed, 13 Nov 2024 11:57:28 +0100 Subject: [PATCH 70/75] Improving beacon logging for new web based modes --- .../Classes/SwedbankPayPaymentSession.swift | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 2eda530..64a5c8d 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -209,6 +209,25 @@ public extension SwedbankPaySDK { 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 @@ -245,10 +264,6 @@ public extension SwedbankPaySDK { paymentOrder: nil, userData: nil) - BeaconService.shared.log(type: .sdkMethodInvoked(name: "createSwedbankPaySDKController", - succeeded: true, - values: nil)) - paymentViewSessionIsOngoing = true viewController.internalDelegate = self From ce345ab57b47feb798251e96b5a0594c8d575459 Mon Sep 17 00:00:00 2001 From: Martin Alleus Date: Thu, 14 Nov 2024 12:23:02 +0100 Subject: [PATCH 71/75] Tweaking customize-payment request parameters --- .../Classes/Api/SwedbankPayAPIEnpointRouter.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index 73b94d6..b84fb60 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -125,16 +125,16 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { case .customizePayment(let instrument, let paymentMethod, let restrictToPaymentMethods): switch (instrument, paymentMethod, restrictToPaymentMethods) { - case (nil, nil, let restrictToPaymentMethods): + case (nil, nil, let restrictToPaymentMethods?): return ["paymentMethod": nil, - "restrictToPaymentMethods": restrictToPaymentMethods] + "restrictToPaymentMethods": restrictToPaymentMethods.isEmpty ? nil : restrictToPaymentMethods] case (.newCreditCard(let enabledPaymentDetailsConsentCheckbox), _, _): return ["paymentMethod": "CreditCard", "restrictToPaymentMethods": nil, "hideStoredPaymentOptions": true, "showConsentAffirmation" : enabledPaymentDetailsConsentCheckbox, ] - case (nil, let paymentMethod, nil): + case (nil, let paymentMethod?, nil): return ["paymentMethod": paymentMethod, "restrictToPaymentMethods": nil] case (let instrument?, nil, nil): From 1b1de4fd0315ea64216aaf589e3d6d6ba2c36eb6 Mon Sep 17 00:00:00 2001 From: Martin Alleus Date: Thu, 14 Nov 2024 12:23:20 +0100 Subject: [PATCH 72/75] Exposing paymentMethod on AvailableInstrument, for easier access --- SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift index e662337..6a01f12 100644 --- a/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift +++ b/SwedbankPaySDK/Classes/Api/Models/MethodBaseModel.swift @@ -116,7 +116,7 @@ extension SwedbankPaySDK { case webBased(paymentMethod: String) - var paymentMethod: String { + public var paymentMethod: String { switch self { case .swish: return "Swish" From ee72936b21686cffac4116e1697ac60c61b4ec8d Mon Sep 17 00:00:00 2001 From: Martin Alleus Date: Thu, 14 Nov 2024 12:25:13 +0100 Subject: [PATCH 73/75] Adding ClickToPay as device accepted wallet --- SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index 268d2b4..803d89e 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -87,7 +87,7 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { } case .preparePayment: return ["integration": "HostedView", - "deviceAcceptedWallets": PKPaymentAuthorizationController.canMakePayments() ? "ApplePay" : "", + "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)), From 3e53193e50bafb97eb06877ceb18c6ae6945df98 Mon Sep 17 00:00:00 2001 From: Martin Alleus Date: Fri, 15 Nov 2024 16:02:08 +0100 Subject: [PATCH 74/75] Sending problem ACK only once --- SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 64a5c8d..0e45603 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -438,10 +438,9 @@ public extension SwedbankPaySDK { "problemStatus": String(modelProblem.status ?? 0), "problemDetail": modelProblem.detail ?? ""])) } - } - - makeRequest(router: .acknowledgeFailedAttempt, operation: problemOperation) + makeRequest(router: .acknowledgeFailedAttempt, operation: problemOperation) + } } let operations = paymentOutputModel.prioritisedOperations From 2a6b3120a1bca2cdc65e5a91482c575665ca5409 Mon Sep 17 00:00:00 2001 From: Martin Alleus Date: Tue, 19 Nov 2024 09:58:21 +0100 Subject: [PATCH 75/75] Using server side problem node for ClientAppLaunchFailed error --- .../Api/SwedbankPayAPIEnpointRouter.swift | 10 ++++++++-- .../Classes/SwedbankPayPaymentSession.swift | 18 ++++++++---------- .../PaymentSessionProblem.swift | 2 -- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift index 6583eee..51ee0f0 100644 --- a/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift +++ b/SwedbankPaySDK/Classes/Api/SwedbankPayAPIEnpointRouter.swift @@ -23,6 +23,12 @@ struct Endpoint { 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?) @@ -34,7 +40,7 @@ enum EnpointRouter { case abortPayment case attemptPayload(paymentPayload: String) case customizePayment(instrument: SwedbankPaySDK.PaymentAttemptInstrument?, paymentMethod: String?, restrictToPaymentMethods: [String]?) - case failPaymentAttempt(problemType: String, errorCode: String?) + case failPaymentAttempt(problemType: FailPaymentAttemptProblemType, errorCode: String?) } protocol EndpointRouterProtocol { @@ -145,7 +151,7 @@ struct SwedbankPayAPIEnpointRouter: EndpointRouterProtocol { "restrictToPaymentMethods": nil] } case .failPaymentAttempt(let problemType, let errorCode): - return ["problemType": problemType, + return ["problemType": problemType.rawValue, "errorCode": errorCode] default: return nil diff --git a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift index 0e45603..9c35256 100644 --- a/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift +++ b/SwedbankPaySDK/Classes/SwedbankPayPaymentSession.swift @@ -352,7 +352,7 @@ public extension SwedbankPaySDK { } } - private func launchClientApp(task: IntegrationTask) { + private func launchClientApp(task: IntegrationTask, failPaymentAttemptOperation: OperationOutputModel) { guard let href = task.href, var components = URLComponents(string: href) else { self.delegate?.sdkProblemOccurred(problem: .internalInconsistencyError) @@ -375,15 +375,12 @@ public extension SwedbankPaySDK { if let url = components.url { DispatchQueue.main.async { UIApplication.shared.open(url) { complete in + self.instrument = nil + if complete { self.hasLaunchClientAppURLs.append(url) - self.instrument = nil } else { - self.delegate?.sdkProblemOccurred(problem: .clientAppLaunchFailed) - - BeaconService.shared.log(type: .sdkCallbackInvoked(name: "sdkProblemOccurred", - succeeded: self.delegate != nil, - values: ["problem": SwedbankPaySDK.PaymentSessionProblem.clientAppLaunchFailed.rawValue])) + self.makeRequest(router: .failPaymentAttempt(problemType: .clientAppLaunchFailed, errorCode: nil), operation: failPaymentAttemptOperation) } BeaconService.shared.log(type: .launchClientApp(values: ["callbackUrl": self.orderInfo?.paymentUrl?.absoluteString ?? "", @@ -410,9 +407,9 @@ public extension SwedbankPaySDK { case .success(let paymentOutputModel): self.sessionOperationHandling(paymentOutputModel: paymentOutputModel, culture: paymentOutputModel.paymentSession.culture) case .failure(ApplePayError.userCancelled): - self.makeRequest(router: .failPaymentAttempt(problemType: "UserCancelled", errorCode: ""), operation: failPaymentAttemptOperation) + 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) + self.makeRequest(router: .failPaymentAttempt(problemType: .technicalError, errorCode: error.localizedDescription), operation: failPaymentAttemptOperation) } } } @@ -528,10 +525,11 @@ public extension SwedbankPaySDK { 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: launchClientApp.firstTask(withRel: .launchClientApp)!) + 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, diff --git a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift index f944a39..108d90c 100644 --- a/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift +++ b/SwedbankPaySDK/Classes/SwedbankPaySDK+Extensions/PaymentSessionProblem.swift @@ -22,7 +22,6 @@ public extension SwedbankPaySDK { case paymentSessionAPIRequestFailed(error: Error, retry: (()->Void)?) case paymentControllerPaymentFailed(error: Error, retry: (()->Void)?) case paymentSession3DSecureViewControllerLoadFailed(error: Error, retry: (()->Void)?) - case clientAppLaunchFailed case internalInconsistencyError case automaticConfigurationFailed @@ -32,7 +31,6 @@ public extension SwedbankPaySDK { case .paymentSessionAPIRequestFailed: "paymentSessionAPIRequestFailed" case .paymentControllerPaymentFailed: "paymentControllerPaymentFailed" case .paymentSession3DSecureViewControllerLoadFailed: "paymentSession3DSecureViewControllerLoadFailed" - case .clientAppLaunchFailed: "clientAppLaunchFailed" case .internalInconsistencyError: "internalInconsistencyError" case .automaticConfigurationFailed: "automaticConfigurationFailed" }