diff --git a/ExampleApp.xcodeproj/project.pbxproj b/ExampleApp.xcodeproj/project.pbxproj index 9a83a935..214d052a 100644 --- a/ExampleApp.xcodeproj/project.pbxproj +++ b/ExampleApp.xcodeproj/project.pbxproj @@ -457,7 +457,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/omise/omise-ios.git"; requirement = { - branch = "feature/MIT-1904"; + branch = "feature/MIT-2470"; kind = branch; }; }; diff --git a/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ebb6ef4b..ebd97824 100644 --- a/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/omise/omise-ios.git", "state" : { - "branch" : "feature/MIT-1904", - "revision" : "0ed8ce39701b08433a2d5c0a678e95edd8b2b8ef" + "branch" : "feature/MIT-2470", + "revision" : "4f1bbf9a0f0828c7531664276fc244fd8433a869" + } + }, + { + "identity" : "spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ios-3ds-sdk/SPM", + "state" : { + "revision" : "a4e55d7f10294ea81abd1db393ae05606a5efa46", + "version" : "2.4.0" } } ], diff --git a/ExampleApp/Config.local.plist b/ExampleApp/Config.local.plist index db08d795..5538ecee 100644 --- a/ExampleApp/Config.local.plist +++ b/ExampleApp/Config.local.plist @@ -4,9 +4,5 @@ publicKey pkey_ - devVaultBaseURL - - devApiBaseURL - diff --git a/ExampleApp/Views/ProductDetailViewController.swift b/ExampleApp/Views/ProductDetailViewController.swift index 112707c1..aeaf8662 100644 --- a/ExampleApp/Views/ProductDetailViewController.swift +++ b/ExampleApp/Views/ProductDetailViewController.swift @@ -55,12 +55,11 @@ class ProductDetailViewController: BaseViewController { let text = textField.text, let url = URL(string: text) else { return } - let deeplinkRedirectURLPattern = AppDeeplink.threeDSChallenge.urlString - let webRedirectURLPattern = "https://exampleapp.opn.ooo" self.omiseSDK.presentAuthorizingPayment( from: self, authorizeURL: url, - returnURLs: [deeplinkRedirectURLPattern, webRedirectURLPattern], + expectedReturnURLStrings: ["https://omise.co"], + threeDSRequestorAppURLString: AppDeeplink.threeDSChallenge.urlString, delegate: self ) }) @@ -70,13 +69,13 @@ class ProductDetailViewController: BaseViewController { // MARK: - Authorizing Payment View Controller Delegate -extension ProductDetailViewController: AuthorizingPaymentViewControllerDelegate { - func authorizingPaymentViewController(_ viewController: AuthorizingPaymentViewController, didCompleteAuthorizingPaymentWithRedirectedURL redirectedURL: URL) { - print("Payment is authorized with redirect url `\(redirectedURL)`") +extension ProductDetailViewController: AuthorizingPaymentDelegate { + func authorizingPaymentDidComplete(with redirectedURL: URL?) { + print("Payment is authorized with redirect url `\(String(describing: redirectedURL))`") omiseSDK.dismiss() } - - func authorizingPaymentViewControllerDidCancel(_ viewController: AuthorizingPaymentViewController) { + func authorizingPaymentDidCancel() { + print("Payment is not authorized") omiseSDK.dismiss() } } diff --git a/OmiseSDK/Sources/3DS/Crypto/CryptData.swift b/OmiseSDK/Sources/3DS/Crypto/CryptData.swift new file mode 100644 index 00000000..bac2cadb --- /dev/null +++ b/OmiseSDK/Sources/3DS/Crypto/CryptData.swift @@ -0,0 +1,98 @@ +import Foundation +import CommonCrypto + +enum CryptoError: Error { + case invalidKeyLength + case creationError(Int) + case updateError(Int) + case finalError(Int) +} + +// swiftlint:disable:next function_parameter_count function_body_length +func cryptData( + _ dataIn: Data, + operation: CCOperation, // kCCEncrypt, kCCDecrypt + mode: CCMode, // kCCModeECB, kCCModeCBC, etc. + algorithm: CCAlgorithm, // kCCAlgorithmAES, kCCAlgorithmDES, etc. + padding: CCPadding, // ccNoPadding, ccPKCS7Padding + keyLength: size_t, + iv: Data?, + key: Data +) throws -> Data { + guard key.count == keyLength else { + throw CryptoError.invalidKeyLength + } + + var cryptor: CCCryptorRef? + var status = CCCryptorCreateWithMode(operation, + mode, + algorithm, + padding, + iv?.withUnsafeBytes { $0.baseAddress }, + key.withUnsafeBytes { $0.baseAddress }, + keyLength, + nil, + 0, + 0, // tweak XTS mode, numRounds + 0, // CCModeOptions + &cryptor) + + if status != kCCSuccess { + throw CryptoError.creationError(Int(status)) + } + + guard let cryptor = cryptor else { + throw CryptoError.creationError(Int(status)) + } + + defer { + CCCryptorRelease(cryptor) + } + + let dataOutLength = CCCryptorGetOutputLength(cryptor, dataIn.count, true) + var dataOut = Data(count: dataOutLength) + var dataOutMoved = 0 + + status = dataOut.withUnsafeMutableBytes { dataOutPointer in + dataIn.withUnsafeBytes { dataInPointer -> CCCryptorStatus in + guard let dataInPointerBaseAddress = dataInPointer.baseAddress, + let dataOutPointerBaseAddress = dataOutPointer.baseAddress else { + return Int32(kCCParamError) + } + return CCCryptorUpdate( + cryptor, + dataInPointerBaseAddress, + dataIn.count, + dataOutPointerBaseAddress, + dataOutLength, + &dataOutMoved + ) + } + } + + if status != kCCSuccess { + throw CryptoError.updateError(Int(status)) + } + + var dataOutMovedFinal = 0 + status = dataOut.withUnsafeMutableBytes { dataOutPointer in + guard let dataOutPointerBaseAddress = dataOutPointer.baseAddress else { + return Int32(kCCParamError) + } + + return CCCryptorFinal( + cryptor, + dataOutPointerBaseAddress.advanced(by: dataOutMoved), + dataOutLength - dataOutMoved, + &dataOutMovedFinal + ) + } + + if status != kCCSuccess { + throw CryptoError.finalError(Int(status)) + } + + dataOut.count = dataOutMoved + dataOutMovedFinal + + return dataOut +} diff --git a/OmiseSDK/Sources/3DS/Crypto/String+PemCert.swift b/OmiseSDK/Sources/3DS/Crypto/String+PemCert.swift new file mode 100644 index 00000000..e65eb7d1 --- /dev/null +++ b/OmiseSDK/Sources/3DS/Crypto/String+PemCert.swift @@ -0,0 +1,10 @@ +import Foundation + +extension String { + var pemCertificate: String { + self + .replacingOccurrences(of: "-----BEGIN CERTIFICATE-----", with: "") + .replacingOccurrences(of: "-----END CERTIFICATE-----", with: "") + .replacingOccurrences(of: "\r\n", with: "") + } +} diff --git a/OmiseSDK/Sources/3DS/Crypto/String+sha512.swift b/OmiseSDK/Sources/3DS/Crypto/String+sha512.swift new file mode 100644 index 00000000..6f569661 --- /dev/null +++ b/OmiseSDK/Sources/3DS/Crypto/String+sha512.swift @@ -0,0 +1,15 @@ +import Foundation +import CommonCrypto + +extension String { + public var sha512: Data { + let data = Data(self.utf8) + var hash = [UInt8](repeating: 0, count: Int(CC_SHA512_DIGEST_LENGTH)) + + data.withUnsafeBytes { + _ = CC_SHA512($0.baseAddress, CC_LONG(data.count), &hash) + } + + return Data(hash) + } +} diff --git a/OmiseSDK/Sources/3DS/NetceteraConfig.swift b/OmiseSDK/Sources/3DS/NetceteraConfig.swift new file mode 100644 index 00000000..7fd6460a --- /dev/null +++ b/OmiseSDK/Sources/3DS/NetceteraConfig.swift @@ -0,0 +1,24 @@ +import Foundation + +public struct NetceteraConfig { + public let id: String + public let deviceInfoEncryptionAlg: String + public let deviceInfoEncryptionEnc: String + public let deviceInfoEncryptionCertPem: String + public let directoryServerId: String + public let key: String + public let messageVersion: String +} + +extension NetceteraConfig: Decodable { + /// Mapping keys to encode/decode JSON string + private enum CodingKeys: String, CodingKey { + case id = "identifier" + case deviceInfoEncryptionAlg = "device_info_encryption_alg" + case deviceInfoEncryptionEnc = "device_info_encryption_enc" + case deviceInfoEncryptionCertPem = "device_info_encryption_cert_pem" + case directoryServerId = "directory_server_id" + case key + case messageVersion = "message_version" + } +} diff --git a/OmiseSDK/Sources/3DS/NetceteraThreeDSController.swift b/OmiseSDK/Sources/3DS/NetceteraThreeDSController.swift new file mode 100644 index 00000000..459daf7a --- /dev/null +++ b/OmiseSDK/Sources/3DS/NetceteraThreeDSController.swift @@ -0,0 +1,374 @@ +import Foundation +import ThreeDS_SDK +import CommonCrypto + +struct AuthResponse: Codable { + var serverStatus: String + var ares: ARes? + + private enum CodingKeys: String, CodingKey { + case serverStatus = "status" + case ares + } + + enum Status { + case challenge + case failed + case success + case unknown + } + + struct ARes: Codable { + var threeDSServerTransID: String + var acsTransID: String + var acsSignedContent: String? + var acsUIType: String? + var acsReferenceNumber: String? + } + + var status: Status { + switch serverStatus { + case "success": return .success + case "challenge": return .challenge + case "failed": return .failed + default: return .unknown + } + } +} + +protocol NetceteraThreeDSControllerProtocol: AnyObject { + typealias NetceteraThreeDSControllerResult = Result + func appOpen3DSDeeplinkURL(_ url: URL) -> Bool + func processAuthorizedURL( + _ authorizeUrl: URL, + threeDSRequestorAppURL: String?, + uiCustomization: ThreeDSUICustomization?, + in viewController: UIViewController, + onComplete: @escaping (NetceteraThreeDSControllerResult) -> Void + ) +} + +class NetceteraThreeDSController { + + enum Errors: Error { + case configInvalid + case apiKeyInvalid + case cancelled + case timedout + case presentChallenge(error: Error) + case protocolError(event: ThreeDS_SDK.ProtocolErrorEvent?) + case runtimeError(event: ThreeDS_SDK.RuntimeErrorEvent?) + case incomplete(event: ThreeDS_SDK.CompletionEvent?) + + case authResInvalid + case authResStatusFailed + case authResStatusUnknown(_ status: String) + } + +// static var sharedController: NetceteraThreeDSControllerProtocol = NetceteraThreeDSController() + private static var uiCustomization: ThreeDSUICustomization? + + private var network = NetworkService() + private var challengeParameters: ChallengeParameters? + private var receiver: OmiseChallengeStatusReceiver? + private var transaction: Transaction? + + private func newScheme(config: NetceteraConfig) -> Scheme? { + let formattedCert = config.deviceInfoEncryptionCertPem.pemCertificate + + let scheme = Scheme(name: config.id) + scheme.ids = [config.directoryServerId] + scheme.encryptionKeyValue = formattedCert + scheme.rootCertificateValue = formattedCert + return scheme + } + +func apiKey(config: NetceteraConfig) throws -> String { + let decryptionKey = config.directoryServerId.sha512.subdata(in: 0..<32) + + guard let ciphertext = Data(base64Encoded: config.key) else { + throw Errors.configInvalid + } + + let iv = ciphertext.subdata(in: 0..<16) + let ciphertextWithoutIV = ciphertext.subdata(in: 16.. ThreeDS2ServiceSDK { + let threeDS2Service = ThreeDS2ServiceSDK() + + let configBuilder = ConfigurationBuilder() + do { + if let scheme = self.newScheme(config: config) { + try configBuilder.add(scheme) + } else { + print("Scheme wasn't created") + } + let apiKey = try apiKey(config: config) + try configBuilder.api(key: apiKey) + try configBuilder.log(to: .info) + let configParameters = configBuilder.configParameters() + + var netceteraUICustomization: UiCustomization? + if let uiCustomization = Self.uiCustomization { + netceteraUICustomization = try UiCustomization(uiCustomization) + } + + try threeDS2Service.initialize(configParameters, + locale: nil, + uiCustomization: netceteraUICustomization) + + } catch { + print(error) + } + + return threeDS2Service + } +} + +extension NetceteraThreeDSController: NetceteraThreeDSControllerProtocol { + public func appOpen3DSDeeplinkURL(_ url: URL) -> Bool { + ThreeDSSDKAppDelegate.shared.appOpened(url: url) + } + + // swiftlint:disable:next function_body_length + func processAuthorizedURL( + _ authorizeUrl: URL, + threeDSRequestorAppURL: String?, + uiCustomization: ThreeDSUICustomization?, + in viewController: UIViewController, + onComplete: @escaping ((Result) -> Void) + ) { + if let uiCustomization = uiCustomization { + Self.uiCustomization = uiCustomization + } + + // swiftlint:disable:next closure_body_length + netceteraConfig(authUrl: authorizeUrl) { [weak self] result in + guard let self = self else { return } + + switch result { + case .failure(let error): + onComplete(.failure(error)) + case .success(let config): + do { + let threeDS2Service = self.newService(config: config) + let transaction = try threeDS2Service.createTransaction( + directoryServerId: config.directoryServerId, + messageVersion: config.messageVersion + ) + + let deviceInfo = try transaction.getAuthenticationRequestParameters().getDeviceData() + + try self.sendAuthenticationRequest( + deviceInfo: deviceInfo, + transaction: transaction, + authorizeUrl: authorizeUrl) { response in + guard let response = response else { + onComplete(.failure(NetceteraThreeDSController.Errors.authResInvalid)) + return + } + + switch response.status { + case .success: + onComplete(.success(())) + return + case .failed: + onComplete(.failure(NetceteraThreeDSController.Errors.authResStatusFailed)) + return + case .unknown: + onComplete(.failure(NetceteraThreeDSController.Errors.authResStatusUnknown(response.serverStatus))) + return + case .challenge: + break + } + + DispatchQueue.main.async { + do { + try self.presentChallenge( + authResponse: response, + threeDSRequestorAppURL: threeDSRequestorAppURL, + transaction: transaction, + from: viewController, + onComplete: onComplete + ) + } catch { + onComplete(.failure(error)) + } + } + } + } catch { + onComplete(.failure(error)) + } + } + } + } + + func prepareChallengeParameters( + aRes: AuthResponse.ARes, + threeDSRequestorAppURL: String? + ) -> ChallengeParameters { + let serverTransactionID = aRes.threeDSServerTransID + let acsTransactionID = aRes.acsTransID + let acsSignedContent = aRes.acsSignedContent + let acsRefNumber = aRes.acsReferenceNumber + + let challengeParameters = ChallengeParameters( + threeDSServerTransactionID: serverTransactionID, + acsTransactionID: acsTransactionID, + acsRefNumber: acsRefNumber, + acsSignedContent: acsSignedContent) + + if let appUrl = updateAppURLString(threeDSRequestorAppURL, transactionID: serverTransactionID) { + challengeParameters.setThreeDSRequestorAppURL(threeDSRequestorAppURL: appUrl) + } + + return challengeParameters + } +} + +private extension NetceteraThreeDSController { + func sendAuthenticationRequest(deviceInfo: String, transaction: Transaction, authorizeUrl: URL, onAuthResponse: @escaping (AuthResponse?) -> Void ) throws { + let authParams = try transaction.getAuthenticationRequestParameters() + let body = +""" + { + "areq": { + "sdkAppID": "\(authParams.getSDKAppID())", + "sdkEphemPubKey": \(authParams.getSDKEphemeralPublicKey()), + "sdkMaxTimeout": 5, + "sdkTransID": "\(authParams.getSDKTransactionId())" + }, + "encrypted_device_info": "\(deviceInfo)", + "device_type": "iOS" + } +""" + let bodyData = body.trimmingCharacters(in: .whitespacesAndNewlines).data(using: .utf8) + + var request = URLRequest(url: authorizeUrl) + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + request.httpBody = bodyData + + let task = URLSession.shared.dataTask(with: request) {(data, _, _) in + guard let data = data else { + onAuthResponse(nil) + return + } + + do { + let decoder = JSONDecoder() + let response = try decoder.decode(AuthResponse.self, from: data) + onAuthResponse(response) + + } catch { + onAuthResponse(nil) + } + + } + + task.resume() + } + + func presentChallenge( + authResponse: AuthResponse, + threeDSRequestorAppURL: String?, + transaction: Transaction, + from viewController: UIViewController, + onComplete: @escaping ((Result) -> Void) + ) throws { + guard let aRes = authResponse.ares else { return } + + let receiver = OmiseChallengeStatusReceiver() + receiver.onComplete = onComplete + + let challengeParameters = prepareChallengeParameters( + aRes: aRes, + threeDSRequestorAppURL: threeDSRequestorAppURL + ) + + self.receiver = receiver + self.transaction = transaction + self.challengeParameters = challengeParameters + + do { + try transaction.doChallenge( + challengeParameters: challengeParameters, + challengeStatusReceiver: receiver, + timeOut: 5, + inViewController: viewController + ) + } catch { + onComplete(.failure(NetceteraThreeDSController.Errors.presentChallenge(error: error))) + } + } + + func updateAppURLString(_ urlString: String?, transactionID: String) -> String? { + guard let urlString = urlString, let url = URL(string: urlString) else { + return nil + } + + return url.appendQueryItem(name: "transID", value: transactionID)?.absoluteString + } +} + +class OmiseChallengeStatusReceiver: ChallengeStatusReceiver { + var onComplete: ((Result) -> Void)? + + func completed(completionEvent: ThreeDS_SDK.CompletionEvent) { + if completionEvent.getTransactionStatus() == "Y" { + onComplete?(.success(())) + } else { + onComplete?(.failure(NetceteraThreeDSController.Errors.incomplete(event: completionEvent))) + } + } + + func cancelled() { + onComplete?(.failure(NetceteraThreeDSController.Errors.cancelled)) + } + + func timedout() { + onComplete?(.failure(NetceteraThreeDSController.Errors.timedout)) + } + + func protocolError(protocolErrorEvent: ThreeDS_SDK.ProtocolErrorEvent) { + onComplete?(.failure(NetceteraThreeDSController.Errors.protocolError(event: protocolErrorEvent))) + } + + func runtimeError(runtimeErrorEvent: ThreeDS_SDK.RuntimeErrorEvent) { + onComplete?(.failure(NetceteraThreeDSController.Errors.runtimeError(event: runtimeErrorEvent))) + } +} + +extension NetceteraThreeDSController { + func netceteraConfig(authUrl: URL, completion: @escaping (Result) -> Void) { + let api = OmiseAPI.netceteraConfig(baseUrl: authUrl.removeLastPathComponent) + var urlRequest = URLRequest(url: api.url) + urlRequest.httpMethod = api.method.rawValue + urlRequest.setValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaders.contentType.rawValue) + + network.send( + urlRequest: urlRequest, + dateFormatter: api.dateFormatter, + completion: completion + ) + + } +} diff --git a/OmiseSDK/Sources/3DS/UICustomization/ThreeDSButtonCustomization.swift b/OmiseSDK/Sources/3DS/UICustomization/ThreeDSButtonCustomization.swift new file mode 100644 index 00000000..367227c1 --- /dev/null +++ b/OmiseSDK/Sources/3DS/UICustomization/ThreeDSButtonCustomization.swift @@ -0,0 +1,70 @@ +import Foundation +import ThreeDS_SDK + +public class ThreeDSButtonCustomization: ThreeDSCustomization { + /// Background color of the button in Hex format. + public var backgroundColorHex: String? + + /// Dark background color of the button in Hex format. + public var darkBackgroundColorHex: String? + + /// Radius of the button corners. + public var cornerRadius: Int? + + public init( + backgroundColorHex: String? = nil, + darkBackgroundColorHex: String? = nil, + cornerRadius: Int? = nil, + textFontName: String? = nil, + textColorHex: String? = nil, + darkTextColorHex: String? = nil, + textFontSize: Int? = nil + ) { + super.init(textFontName: textFontName, + textColorHex: textColorHex, + darkTextColorHex: darkTextColorHex, + textFontSize: textFontSize) + self.backgroundColorHex = backgroundColorHex + self.darkBackgroundColorHex = darkBackgroundColorHex + self.cornerRadius = cornerRadius + } + + @discardableResult + public func backgroundColorHex(_ backgroundColorHex: String?) -> Self { + self.backgroundColorHex = backgroundColorHex + return self + } + @discardableResult + public func darkBackgroundColorHex(_ darkBackgroundColorHex: String?) -> Self { + self.darkBackgroundColorHex = darkBackgroundColorHex + return self + } + @discardableResult + public func cornerRadius(_ cornerRadius: Int?) -> Self { + self.cornerRadius = cornerRadius + return self + } +} + +extension ThreeDS_SDK.ButtonCustomization { + convenience init(_ custom: ThreeDSButtonCustomization) throws { + self.init() + try customize(custom) + } + + func customize(_ custom: ThreeDSButtonCustomization) throws { + try customize(omiseThreeDSCustomization: custom) + + if let backgroundColorHex = custom.backgroundColorHex { + try setBackgroundColor(hexColorCode: backgroundColorHex) + } + + if let darkBackgroundColorHex = custom.darkBackgroundColorHex { + try setDarkBackgroundColor(hexColorCode: darkBackgroundColorHex) + } + + if let cornerRadius = custom.cornerRadius { + try setCornerRadius(cornerRadius: cornerRadius) + } + } +} diff --git a/OmiseSDK/Sources/3DS/UICustomization/ThreeDSCustomization.swift b/OmiseSDK/Sources/3DS/UICustomization/ThreeDSCustomization.swift new file mode 100644 index 00000000..d45c92c8 --- /dev/null +++ b/OmiseSDK/Sources/3DS/UICustomization/ThreeDSCustomization.swift @@ -0,0 +1,70 @@ +import Foundation +import ThreeDS_SDK + +public class ThreeDSCustomization { + /// Text font name. + public var textFontName: String? + + /// Text color in Hex format. + public var textColorHex: String? + + /// Dark text color in Hex format. + public var darkTextColorHex: String? + + /// Text font size. + public var textFontSize: Int? + + public init( + textFontName: String? = nil, + textColorHex: String? = nil, + darkTextColorHex: String? = nil, + textFontSize: Int? = nil + ) { + self.textFontName = textFontName + self.textColorHex = textColorHex + self.darkTextColorHex = darkTextColorHex + self.textFontSize = textFontSize + } + + @discardableResult + public func textFontName(_ textFontName: String?) -> Self { + self.textFontName = textFontName + return self + } + + @discardableResult + public func textColorHex(_ textColorHex: String?) -> Self { + self.textColorHex = textColorHex + return self + } + @discardableResult + public func darkTextColorHex(_ darkTextColorHex: String?) -> Self { + self.darkTextColorHex = darkTextColorHex + return self + } + @discardableResult + public func textFontSize(_ textFontSize: Int?) -> Self { + self.textFontSize = textFontSize + return self + } +} + +extension ThreeDS_SDK.Customization { + func customize(omiseThreeDSCustomization custom: ThreeDSCustomization) throws { + if let textFontName = custom.textFontName { + try setTextFontName(fontName: textFontName) + } + + if let textColorHex = custom.textColorHex { + try setTextColor(hexColorCode: textColorHex) + } + + if let darkTextColorHex = custom.darkTextColorHex { + try setDarkTextColor(hexColorCode: darkTextColorHex) + } + + if let textFontSize = custom.textFontSize { + try setTextFontSize(fontSize: textFontSize) + } + } +} diff --git a/OmiseSDK/Sources/3DS/UICustomization/ThreeDSLabelCustomization.swift b/OmiseSDK/Sources/3DS/UICustomization/ThreeDSLabelCustomization.swift new file mode 100644 index 00000000..a2cca41e --- /dev/null +++ b/OmiseSDK/Sources/3DS/UICustomization/ThreeDSLabelCustomization.swift @@ -0,0 +1,84 @@ +import Foundation +import ThreeDS_SDK + +public class ThreeDSLabelCustomization: ThreeDSCustomization { + /// Color of the heading label text in Hex format. + public var headingTextColorHex: String? + + /// Dark color of the heading label text in Hex format. + public var headingDarkTextColorHex: String? + + /// Font name of the heading label text. + public var headingTextFontName: String? + + /// Font size of the heading label text. + public var headingTextFontSize: Int? + + public init( + headingTextColorHex: String? = nil, + headingDarkTextColorHex: String? = nil, + headingTextFontName: String? = nil, + headingTextFontSize: Int? = nil, + textFontName: String? = nil, + textColorHex: String? = nil, + darkTextColorHex: String? = nil, + textFontSize: Int? = nil + ) { + super.init(textFontName: textFontName, + textColorHex: textColorHex, + darkTextColorHex: darkTextColorHex, + textFontSize: textFontSize) + self.headingTextColorHex = headingTextColorHex + self.headingDarkTextColorHex = headingDarkTextColorHex + self.headingTextFontName = headingTextFontName + self.headingTextFontSize = headingTextFontSize + } + + @discardableResult + public func headingTextColorHex(_ headingTextColorHex: String?) -> Self { + self.headingTextColorHex = headingTextColorHex + return self + } + @discardableResult + public func headingDarkTextColorHex(_ headingDarkTextColorHex: String?) -> Self { + self.headingDarkTextColorHex = headingDarkTextColorHex + return self + } + @discardableResult + public func headingTextFontName(_ headingTextFontName: String?) -> Self { + self.headingTextFontName = headingTextFontName + return self + } + @discardableResult + public func headingTextFontSize(_ headingTextFontSize: Int?) -> Self { + self.headingTextFontSize = headingTextFontSize + return self + } +} + +extension ThreeDS_SDK.LabelCustomization { + convenience init(_ custom: ThreeDSLabelCustomization) throws { + self.init() + try customize(custom) + } + + func customize(_ custom: ThreeDSLabelCustomization) throws { + try customize(omiseThreeDSCustomization: custom) + + if let headingTextColorHex = custom.headingTextColorHex { + try setHeadingTextColor(hexColorCode: headingTextColorHex) + } + + if let headingDarkTextColorHex = custom.headingDarkTextColorHex { + try setHeadingDarkTextColor(hexColorCode: headingDarkTextColorHex) + } + + if let headingTextFontName = custom.headingTextFontName { + try setHeadingTextFontName(fontName: headingTextFontName) + } + + if let headingTextFontSize = custom.headingTextFontSize { + try setHeadingTextFontSize(fontSize: headingTextFontSize) + } + } +} diff --git a/OmiseSDK/Sources/3DS/UICustomization/ThreeDSTextBoxCustomization.swift b/OmiseSDK/Sources/3DS/UICustomization/ThreeDSTextBoxCustomization.swift new file mode 100644 index 00000000..1805986e --- /dev/null +++ b/OmiseSDK/Sources/3DS/UICustomization/ThreeDSTextBoxCustomization.swift @@ -0,0 +1,84 @@ +import Foundation +import ThreeDS_SDK + +public class ThreeDSTextBoxCustomization: ThreeDSCustomization { + /// Width of the text box border. + public var borderWidth: Int? + + /// Color of the text box border in Hex format. + public var borderColorHex: String? + + /// Dark color of the text box border in Hex format. + public var darkBorderColorHex: String? + + /// Corner radius of the text box corners. + public var cornerRadius: Int? + + public init( + borderWidth: Int? = nil, + borderColorHex: String? = nil, + darkBorderColorHex: String? = nil, + cornerRadius: Int? = nil, + textFontName: String? = nil, + textColorHex: String? = nil, + darkTextColorHex: String? = nil, + textFontSize: Int? = nil + ) { + super.init(textFontName: textFontName, + textColorHex: textColorHex, + darkTextColorHex: darkTextColorHex, + textFontSize: textFontSize) + self.borderWidth = borderWidth + self.borderColorHex = borderColorHex + self.darkBorderColorHex = darkBorderColorHex + self.cornerRadius = cornerRadius + } + + @discardableResult + public func borderColorHex(_ borderColorHex: String?) -> Self { + self.borderColorHex = borderColorHex + return self + } + @discardableResult + public func darkBorderColorHex(_ darkBorderColorHex: String?) -> Self { + self.darkBorderColorHex = darkBorderColorHex + return self + } + @discardableResult + public func cornerRadius(_ cornerRadius: Int?) -> Self { + self.cornerRadius = cornerRadius + return self + } + @discardableResult + public func borderWidth(_ borderWidth: Int?) -> Self { + self.borderWidth = borderWidth + return self + } +} + +extension ThreeDS_SDK.TextBoxCustomization { + convenience init(_ custom: ThreeDSTextBoxCustomization) throws { + self.init() + try customize(custom) + } + + func customize(_ custom: ThreeDSTextBoxCustomization) throws { + try customize(omiseThreeDSCustomization: custom) + + if let borderWidth = custom.borderWidth { + try setBorderWidth(borderWidth: borderWidth) + } + + if let borderColorHex = custom.borderColorHex { + try setBorderColor(hexColorCode: borderColorHex) + } + + if let darkBorderColorHex = custom.darkBorderColorHex { + try setDarkBorderColor(hexColorCode: darkBorderColorHex) + } + + if let textFontSize = custom.textFontSize { + try setTextFontSize(fontSize: textFontSize) + } + } +} diff --git a/OmiseSDK/Sources/3DS/UICustomization/ThreeDSToolbarCustomization.swift b/OmiseSDK/Sources/3DS/UICustomization/ThreeDSToolbarCustomization.swift new file mode 100644 index 00000000..be3c6855 --- /dev/null +++ b/OmiseSDK/Sources/3DS/UICustomization/ThreeDSToolbarCustomization.swift @@ -0,0 +1,84 @@ +import Foundation +import ThreeDS_SDK + +public class ThreeDSToolbarCustomization: ThreeDSCustomization { + /// Background color for the toolbar in Hex format. + public var backgroundColorHex: String? + + /// Dark background color for the toolbar in Hex format. + public var darkBackgroundColorHex: String? + + /// Header text of the toolbar. + public var headerText: String? + + /// Button text of the toolbar. For example, “Cancel”. + public var buttonText: String? + + public init( + backgroundColorHex: String? = nil, + darkBackgroundColorHex: String? = nil, + headerText: String? = nil, + buttonText: String? = nil, + textFontName: String? = nil, + textColorHex: String? = nil, + darkTextColorHex: String? = nil, + textFontSize: Int? = nil + ) { + super.init(textFontName: textFontName, + textColorHex: textColorHex, + darkTextColorHex: darkTextColorHex, + textFontSize: textFontSize) + self.backgroundColorHex = backgroundColorHex + self.darkBackgroundColorHex = darkBackgroundColorHex + self.headerText = headerText + self.buttonText = buttonText + } + + @discardableResult + public func backgroundColorHex(_ backgroundColorHex: String?) -> Self { + self.backgroundColorHex = backgroundColorHex + return self + } + @discardableResult + public func darkBackgroundColorHex(_ darkBackgroundColorHex: String?) -> Self { + self.darkBackgroundColorHex = darkBackgroundColorHex + return self + } + @discardableResult + public func headerText(_ headerText: String?) -> Self { + self.headerText = headerText + return self + } + @discardableResult + public func buttonText(_ buttonText: String?) -> Self { + self.buttonText = buttonText + return self + } +} + +extension ThreeDS_SDK.ToolbarCustomization { + convenience init(_ custom: ThreeDSToolbarCustomization) throws { + self.init() + try customize(custom) + } + + func customize(_ custom: ThreeDSToolbarCustomization) throws { + try customize(omiseThreeDSCustomization: custom) + + if let backgroundColorHex = custom.backgroundColorHex { + try setBackgroundColor(hexColorCode: backgroundColorHex) + } + + if let darkBackgroundColorHex = custom.darkBackgroundColorHex { + try setDarkBackgroundColor(hexColorCode: darkBackgroundColorHex) + } + + if let headerText = custom.headerText { + try setHeaderText(headerText: headerText) + } + + if let buttonText = custom.buttonText { + try setButtonText(buttonText: buttonText) + } + } +} diff --git a/OmiseSDK/Sources/3DS/UICustomization/ThreeDSUICustomization.swift b/OmiseSDK/Sources/3DS/UICustomization/ThreeDSUICustomization.swift new file mode 100644 index 00000000..b02f90c1 --- /dev/null +++ b/OmiseSDK/Sources/3DS/UICustomization/ThreeDSUICustomization.swift @@ -0,0 +1,77 @@ +import Foundation +import ThreeDS_SDK + +public class ThreeDSUICustomization { + + public static var shared: ThreeDSUICustomization? + + /// Sets the attributes of a ButtonCustomization object for a particular button type. + public var buttonCustomization: [ThreeDS_SDK.UiCustomization.ButtonType: ThreeDSButtonCustomization]? + + /// Sets the attributes of a ButtonCustomization object for an implementer-specific button type. + public var buttonCustomizationStrings: [String: ThreeDSButtonCustomization]? + + /// Sets the attributes of a ToolbarCustomization object. + public var toolbarCustomization: ThreeDSToolbarCustomization? + + /// Sets the attributes of a LabelCustomization object. + public var labelCustomization: ThreeDSLabelCustomization? + + /// Sets the attributes of a TextBoxCustomization object. + public var textBoxCustomization: ThreeDSTextBoxCustomization? + + public init( + buttonCustomization: [ThreeDS_SDK.UiCustomization.ButtonType: ThreeDSButtonCustomization]? = nil, + buttonCustomizationStrings: [String: ThreeDSButtonCustomization]? = nil, + toolbarCustomization: ThreeDSToolbarCustomization? = nil, + labelCustomization: ThreeDSLabelCustomization? = nil, + textBoxCustomization: ThreeDSTextBoxCustomization? = nil + ) { + self.buttonCustomization = buttonCustomization + self.buttonCustomizationStrings = buttonCustomizationStrings + self.toolbarCustomization = toolbarCustomization + self.labelCustomization = labelCustomization + self.textBoxCustomization = textBoxCustomization + } +} + +extension ThreeDS_SDK.UiCustomization { + convenience init(_ custom: ThreeDSUICustomization) throws { + self.init() + try customize(custom) + } + + func customize(_ custom: ThreeDSUICustomization) throws { + if let buttonCustomization = custom.buttonCustomization { + for (buttonType, style) in buttonCustomization { + let omiseButtonCustomization = try ThreeDS_SDK.ButtonCustomization(style) + setButtonCustomization(buttonCustomization: omiseButtonCustomization, + buttonType: buttonType) + } + } + + if let buttonCustomization = custom.buttonCustomizationStrings { + for (buttonType, style) in buttonCustomization { + let omiseButtonCustomization = try ThreeDS_SDK.ButtonCustomization(style) + try setButtonCustomization(buttonCustomization: omiseButtonCustomization, + btnType: buttonType) + } + } + + if let toolbarCustomization = custom.toolbarCustomization { + let omiseToolbarCustomization = try ThreeDS_SDK.ToolbarCustomization(toolbarCustomization) + setToolbarCustomization(toolbarCustomization: omiseToolbarCustomization) + + } + + if let labelCustomization = custom.labelCustomization { + let omiseLabelCustomization = try ThreeDS_SDK.LabelCustomization(labelCustomization) + setLabelCustomization(labelCustomization: omiseLabelCustomization) + } + + if let textBoxCustomization = custom.textBoxCustomization { + let omiseTextBoxCustomization = try ThreeDS_SDK.TextBoxCustomization(textBoxCustomization) + setTextBoxCustomization(textBoxCustomization: omiseTextBoxCustomization) + } + } +} diff --git a/OmiseSDK/Sources/AuthorizingPaymentDelegate.swift b/OmiseSDK/Sources/AuthorizingPaymentDelegate.swift new file mode 100644 index 00000000..4e8ee967 --- /dev/null +++ b/OmiseSDK/Sources/AuthorizingPaymentDelegate.swift @@ -0,0 +1,7 @@ +import Foundation + +/// Omise Payment delegate protocol is required to process 3DS authorization +public protocol AuthorizingPaymentDelegate: AnyObject { + func authorizingPaymentDidComplete(with redirectedURL: URL?) + func authorizingPaymentDidCancel() +} diff --git a/OmiseSDK/Sources/OmiseAPI/Helpers/APIProtocol.swift b/OmiseSDK/Sources/OmiseAPI/Helpers/APIProtocol.swift index d0226d1c..dc4e65b9 100644 --- a/OmiseSDK/Sources/OmiseAPI/Helpers/APIProtocol.swift +++ b/OmiseSDK/Sources/OmiseAPI/Helpers/APIProtocol.swift @@ -19,7 +19,9 @@ enum ContentType: String { case json = "application/json; charset=utf8" } -enum OmiseServerType { - case api - case vault +enum HTTPHeaders: String { + case authorization = "Authorization" + case userAgent = "User-Agent" + case contentType = "Content-Type" + case omiseVersion = "Omise-Version" } diff --git a/OmiseSDK/Sources/OmiseAPI/Helpers/Client+URLRequest.swift b/OmiseSDK/Sources/OmiseAPI/Helpers/Client+URLRequest.swift index c9b2a7ae..8c1c11f9 100644 --- a/OmiseSDK/Sources/OmiseAPI/Helpers/Client+URLRequest.swift +++ b/OmiseSDK/Sources/OmiseAPI/Helpers/Client+URLRequest.swift @@ -7,6 +7,7 @@ extension Client { switch api.server { case .api: return customApiURL case .vault: return customVaultURL + case .other(let baseURL): return baseURL } }() @@ -47,13 +48,6 @@ extension Client { // MARK: HTTP Headers extension Client { - enum HTTPHeaders: String { - case authorization = "Authorization" - case userAgent = "User-Agent" - case contentType = "Content-Type" - case omiseVersion = "Omise-Version" - } - func httpHeaders( publicKey: String, userAgent: String, diff --git a/OmiseSDK/Sources/OmiseAPI/OmiseAPI.swift b/OmiseSDK/Sources/OmiseAPI/OmiseAPI.swift index 95d53314..242c72e8 100644 --- a/OmiseSDK/Sources/OmiseAPI/OmiseAPI.swift +++ b/OmiseSDK/Sources/OmiseAPI/OmiseAPI.swift @@ -1,14 +1,21 @@ import Foundation +enum OmiseServerType { + case api + case vault + case other(baseURL: URL) +} + extension OmiseServerType { // swiftlint:disable force_unwrapping private var apiServerURL: String { "https://vault.omise.co" } private var vaultServerURL: String { "https://vault.omise.co" } - + var url: URL { switch self { case .api: return URL(string: apiServerURL)! case .vault: return URL(string: vaultServerURL)! + case .other(let url): return url } } // swiftlint:enable force_unwrapping @@ -19,6 +26,7 @@ enum OmiseAPI { case token(tokenID: String) case createToken(payload: CreateTokenPayload) case createSource(payload: CreateSourcePayload) + case netceteraConfig(baseUrl: URL) } extension OmiseAPI: APIProtocol { @@ -32,6 +40,8 @@ extension OmiseAPI: APIProtocol { return .api case .token, .createToken: return .vault + case .netceteraConfig(let baseURL): + return .other(baseURL: baseURL) } } @@ -45,15 +55,23 @@ extension OmiseAPI: APIProtocol { return "tokens" case .createSource: return "sources" + case .netceteraConfig: + return "config" } } + var url: URL { + server.url.appendingLastPathComponent(path) + } + var method: HTTPMethod { switch self { case .capability, .token: return .get case .createToken, .createSource: return .post + case .netceteraConfig: + return .get } } diff --git a/OmiseSDK/Sources/OmiseSDK.swift b/OmiseSDK/Sources/OmiseSDK.swift index 3ea86ba8..493e064b 100644 --- a/OmiseSDK/Sources/OmiseSDK.swift +++ b/OmiseSDK/Sources/OmiseSDK.swift @@ -24,7 +24,8 @@ public class OmiseSDK { public private(set) weak var presentedViewController: UIViewController? - private var handleURLs: [String] = [] + private var expectedReturnURLStrings: [String] = [] + private var netceteraThreeDSController: NetceteraThreeDSController? /// Creates a new instance of Omise SDK that provides interface to functionallity that SDK provides /// @@ -140,40 +141,61 @@ public class OmiseSDK { /// - from: ViewController is used to present Choose Payment Methods /// - animated: Presents controller with animation if `true` /// - authorizeURL: The authorize URL given in `Charge` object - /// - expectedReturnURLPatterns: The expected return URL patterns. + /// - expectedReturnURLStrings: The expected return URL patterns for web view 3DS. + /// - threeDSRequestorAppURLString: 3DS requestor app URL string (deeplink). /// - delegate: A delegate object that will recieved authorizing payment events. @available(iOSApplicationExtension, unavailable) public func presentAuthorizingPayment( from topViewController: UIViewController, animated: Bool = true, authorizeURL: URL, - returnURLs: [String], - delegate: AuthorizingPaymentViewControllerDelegate + expectedReturnURLStrings: [String], + threeDSRequestorAppURLString: String, + threeDSUICustomization: ThreeDSUICustomization? = nil, + delegate: AuthorizingPaymentDelegate ) { - dismiss(animated: false) - - handleURLs = returnURLs - let returnURLComponents = returnURLs.compactMap { - URLComponents(string: $0) + guard authorizeURL.absoluteString.contains("acs=true") else { + presentWebViewAuthorizingPayment( + from: topViewController, + animated: animated, + authorizeURL: authorizeURL, + expectedReturnURLStrings: expectedReturnURLStrings, + delegate: delegate + ) + return } - let viewController = AuthorizingPaymentViewController(nibName: nil, bundle: .omiseSDK) - viewController.authorizeURL = authorizeURL - viewController.expectedReturnURLPatterns = returnURLComponents - viewController.delegate = delegate - viewController.applyNavigationBarStyle() - viewController.title = "AuthorizingPayment.title".localized() - - let navigationController = UINavigationController(rootViewController: viewController) - navigationController.navigationBar.isTranslucent = false - navigationController.navigationBar.backgroundColor = .white - - if #available(iOSApplicationExtension 11.0, *) { - navigationController.navigationBar.prefersLargeTitles = false + netceteraThreeDSController = NetceteraThreeDSController() + netceteraThreeDSController?.processAuthorizedURL( + authorizeURL, + threeDSRequestorAppURL: threeDSRequestorAppURLString, + uiCustomization: threeDSUICustomization, + in: topViewController) { [weak self] result in + switch result { + case .success: + delegate.authorizingPaymentDidComplete(with: nil) + case .failure(let error): + typealias NetceteraFlowError = NetceteraThreeDSController.Errors + switch error { + case NetceteraFlowError.incomplete, + NetceteraFlowError.cancelled, + NetceteraFlowError.timedout, + NetceteraFlowError.authResStatusFailed, + NetceteraFlowError.authResStatusUnknown: + delegate.authorizingPaymentDidCancel() + default: + DispatchQueue.main.async { + self?.presentWebViewAuthorizingPayment( + from: topViewController, + animated: animated, + authorizeURL: authorizeURL, + expectedReturnURLStrings: expectedReturnURLStrings, + delegate: delegate + ) + } + } + } } - - topViewController.present(navigationController, animated: animated, completion: nil) - presentedViewController = navigationController } /// Dismiss any presented UI form by OmiseSDK @@ -185,7 +207,7 @@ public class OmiseSDK { /// Handle URL Callback received by AppDelegate public func handleURLCallback(_ url: URL) -> Bool { // Will handle callback with 3ds library - let containsURL = handleURLs + let containsURL = expectedReturnURLStrings .map { url.match(string: $0) } @@ -200,3 +222,46 @@ private extension OmiseSDK { client.capability { _ in } } } + +private extension OmiseSDK { + @available(iOSApplicationExtension, unavailable) + func presentWebViewAuthorizingPayment( + from topViewController: UIViewController, + animated: Bool = true, + authorizeURL: URL, + expectedReturnURLStrings: [String], + delegate: AuthorizingPaymentDelegate + ) { + dismiss(animated: false) + + let viewController = AuthorizingPaymentWebViewController(nibName: nil, bundle: .omiseSDK) + viewController.authorizeURL = authorizeURL + viewController.expectedReturnURLStrings = expectedReturnURLStrings.compactMap { + URLComponents(string: $0) + } + viewController.completion = { [weak delegate] result in + switch result { + case .cancel: + delegate?.authorizingPaymentDidCancel() + case .complete(let redirectedURL): + delegate?.authorizingPaymentDidComplete(with: redirectedURL) + } + } + + viewController.applyNavigationBarStyle() + viewController.title = "AuthorizingPayment.title".localized() + + let navigationController = UINavigationController(rootViewController: viewController) + navigationController.navigationBar.isTranslucent = false + navigationController.navigationBar.backgroundColor = .white + + if #available(iOSApplicationExtension 11.0, *) { + navigationController.navigationBar.prefersLargeTitles = false + } + + topViewController.present(navigationController, animated: animated, completion: nil) + presentedViewController = navigationController + + self.expectedReturnURLStrings = expectedReturnURLStrings + } +} diff --git a/OmiseSDK/Sources/ToolKit/Extensions/URL+Helpers.swift b/OmiseSDK/Sources/ToolKit/Extensions/URL+Helpers.swift deleted file mode 100644 index ab34e49d..00000000 --- a/OmiseSDK/Sources/ToolKit/Extensions/URL+Helpers.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation - -extension URL { - func match(string: String) -> Bool { - guard - let expectedURLComponents = URLComponents(string: string), - let components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { - return false - } - - return expectedURLComponents.match(components: components) - } -} - -extension URLComponents { - func match(string: String) -> Bool { - guard let expectedURLComponents = URLComponents(string: string) else { return false } - return match(components: expectedURLComponents) - } - - func match(components expectedURLComponents: URLComponents) -> Bool { - return expectedURLComponents.scheme?.lowercased() == scheme?.lowercased() - && expectedURLComponents.host?.lowercased() == host?.lowercased() - && path.lowercased().hasPrefix(expectedURLComponents.path.lowercased()) - } -} diff --git a/OmiseSDK/Sources/Views/Screens/Authorizing Payment/AuthorizingPaymentViewController.swift b/OmiseSDK/Sources/Views/Screens/Authorizing Payment/AuthorizingPaymentWebViewController.swift similarity index 82% rename from OmiseSDK/Sources/Views/Screens/Authorizing Payment/AuthorizingPaymentViewController.swift rename to OmiseSDK/Sources/Views/Screens/Authorizing Payment/AuthorizingPaymentWebViewController.swift index e0016ac6..02600c78 100644 --- a/OmiseSDK/Sources/Views/Screens/Authorizing Payment/AuthorizingPaymentViewController.swift +++ b/OmiseSDK/Sources/Views/Screens/Authorizing Payment/AuthorizingPaymentWebViewController.swift @@ -2,17 +2,6 @@ import Foundation import WebKit import os -@available(iOSApplicationExtension, unavailable) -public protocol AuthorizingPaymentViewControllerDelegate: AnyObject { - /// A delegation method called when the authorizing payment process is completed. - /// - parameter viewController: The authorizing payment controller that call this method. - /// - parameter redirectedURL: A URL returned from the authorizing payment process. - func authorizingPaymentViewController(_ viewController: AuthorizingPaymentViewController, didCompleteAuthorizingPaymentWithRedirectedURL redirectedURL: URL) - - /// A delegation method called when user cancel the authorizing payment process. - func authorizingPaymentViewControllerDidCancel(_ viewController: AuthorizingPaymentViewController) -} - /*: Drop-in authorizing payment handler view controller that automatically display the authorizing payment verification form which supports `3DS`, `Internet Banking` and other offsite payment methods those need to be authorized via a web browser. @@ -21,7 +10,7 @@ public protocol AuthorizingPaymentViewControllerDelegate: AnyObject { This is still an experimental API. If you encountered with any problem with this API, please feel free to report to Omise. */ @available(iOSApplicationExtension, unavailable) -public class AuthorizingPaymentViewController: UIViewController { +class AuthorizingPaymentWebViewController: UIViewController { /// Authorize URL given from Omise in the created `Charge` object. var authorizeURL: URL? { didSet { @@ -33,21 +22,25 @@ public class AuthorizingPaymentViewController: UIViewController { } } + enum CompletionState { + case complete(redirectedURL: URL?) + case cancel + } + /// The expected return URL patterns described in the URLComponents object. /// /// The rule is the scheme and host must be matched and must have the path as a prefix. /// Example: if the return URL is `https://www.example.com/products/12345` the expected return URL should have a URLComponents with scheme of `https`, host of `www.example.com` and the path of `/products/` - var expectedReturnURLPatterns: [URLComponents] = [] - - /// A delegate object that will recieved the authorizing payment events. - weak var delegate: AuthorizingPaymentViewControllerDelegate? + var expectedReturnURLStrings: [URLComponents] = [] + var completion: ParamClosure? + let webView = WKWebView(frame: CGRect.zero, configuration: WKWebViewConfiguration()) let okButtonTitle = NSLocalizedString("OK", comment: "OK button for JavaScript panel") let confirmButtonTitle = NSLocalizedString("Confirm", comment: "Confirm button for JavaScript panel") let cancelButtonTitle = NSLocalizedString("Cancel", comment: "Cancel button for JavaScript panel") - public override func viewDidLoad() { + override func viewDidLoad() { super.viewDidLoad() webView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(webView) @@ -68,21 +61,22 @@ public class AuthorizingPaymentViewController: UIViewController { navigationItem.rightBarButtonItem = cancelButtonItem } - public override func viewDidAppear(_ animated: Bool) { + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) startAuthorizingPaymentProcess() } @IBAction private func cancelAuthorizingPaymentProcess(_ sender: UIBarButtonItem) { os_log("Authorization process was cancelled, trying to notify the delegate", log: uiLogObject, type: .info) - delegate?.authorizingPaymentViewControllerDidCancel(self) - if delegate == nil { + if let completion = completion { + completion(.cancel) + } else { os_log("Authorization process was cancelled but no delegate to be notified", log: uiLogObject, type: .default) } } private func startAuthorizingPaymentProcess() { - guard let authorizeURL = authorizeURL, !expectedReturnURLPatterns.isEmpty else { + guard let authorizeURL = authorizeURL, !expectedReturnURLStrings.isEmpty else { assertionFailure("Insufficient authorizing payment information") os_log("Refusing to initialize sdk client with a non-public key: %{private}@", log: uiLogObject, type: .error) return @@ -98,7 +92,7 @@ public class AuthorizingPaymentViewController: UIViewController { return false } - return expectedReturnURLPatterns.contains { expectedURLComponents -> Bool in + return expectedReturnURLStrings.contains { expectedURLComponents -> Bool in components.match(components: expectedURLComponents) } } @@ -106,7 +100,7 @@ public class AuthorizingPaymentViewController: UIViewController { } @available(iOSApplicationExtension, unavailable) -extension AuthorizingPaymentViewController: WKNavigationDelegate { +extension AuthorizingPaymentWebViewController: WKNavigationDelegate { public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Swift.Void) { if let url = navigationAction.request.url, verifyPaymentURL(url) { os_log("Redirected to expected %{private}@ URL, trying to notify the delegate", @@ -114,8 +108,8 @@ extension AuthorizingPaymentViewController: WKNavigationDelegate { type: .info, url.absoluteString) decisionHandler(.cancel) - delegate?.authorizingPaymentViewController(self, didCompleteAuthorizingPaymentWithRedirectedURL: url) - if delegate == nil { + completion?(.complete(redirectedURL: url)) + if completion == nil { os_log("Redirected to expected %{private}@ URL but no delegate to be notified", log: uiLogObject, type: .default, @@ -140,7 +134,7 @@ extension AuthorizingPaymentViewController: WKNavigationDelegate { } @available(iOSApplicationExtension, unavailable) -extension AuthorizingPaymentViewController: WKUIDelegate { +extension AuthorizingPaymentWebViewController: WKUIDelegate { public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) @@ -151,7 +145,7 @@ extension AuthorizingPaymentViewController: WKUIDelegate { alertController.addAction(okAction) - // NOTE: Must present an UIAlertController on AuthorizingPaymentViewController + // NOTE: Must present an UIAlertController on AuthorizingPaymentWebViewController self.present(alertController, animated: true) } @@ -168,7 +162,7 @@ extension AuthorizingPaymentViewController: WKUIDelegate { alertController.addAction(confirmAction) alertController.addAction(cancelAction) - // NOTE: Must present an UIAlertController on AuthorizingPaymentViewController + // NOTE: Must present an UIAlertController on AuthorizingPaymentWebViewController self.present(alertController, animated: true, completion: nil) } @@ -191,7 +185,7 @@ extension AuthorizingPaymentViewController: WKUIDelegate { alertController.addAction(okAction) alertController.addAction(cancelAction) - // NOTE: Must present an UIAlertController on AuthorizingPaymentViewController + // NOTE: Must present an UIAlertController on AuthorizingPaymentWebViewController self.present(alertController, animated: true, completion: nil) } } diff --git a/OmiseSDK/Sources/Views/Screens/Authorizing Payment/AuthorizingPaymentViewController.xib b/OmiseSDK/Sources/Views/Screens/Authorizing Payment/AuthorizingPaymentWebViewController.xib similarity index 91% rename from OmiseSDK/Sources/Views/Screens/Authorizing Payment/AuthorizingPaymentViewController.xib rename to OmiseSDK/Sources/Views/Screens/Authorizing Payment/AuthorizingPaymentWebViewController.xib index 3ca6f73e..bc509d1e 100644 --- a/OmiseSDK/Sources/Views/Screens/Authorizing Payment/AuthorizingPaymentViewController.xib +++ b/OmiseSDK/Sources/Views/Screens/Authorizing Payment/AuthorizingPaymentWebViewController.xib @@ -2,13 +2,12 @@ - - + diff --git a/OmiseSDK/Sources/ToolKit/Extensions/Array+Helpers.swift b/OmiseSDK/ToolKit/Extensions/Array+Helpers.swift similarity index 100% rename from OmiseSDK/Sources/ToolKit/Extensions/Array+Helpers.swift rename to OmiseSDK/ToolKit/Extensions/Array+Helpers.swift diff --git a/OmiseSDK/Sources/ToolKit/Extensions/Optional+Helpers.swift b/OmiseSDK/ToolKit/Extensions/Optional+Helpers.swift similarity index 100% rename from OmiseSDK/Sources/ToolKit/Extensions/Optional+Helpers.swift rename to OmiseSDK/ToolKit/Extensions/Optional+Helpers.swift diff --git a/OmiseSDK/Sources/ToolKit/Extensions/String+Helpers.swift b/OmiseSDK/ToolKit/Extensions/String+Helpers.swift similarity index 100% rename from OmiseSDK/Sources/ToolKit/Extensions/String+Helpers.swift rename to OmiseSDK/ToolKit/Extensions/String+Helpers.swift diff --git a/OmiseSDK/ToolKit/Extensions/URL+Helpers.swift b/OmiseSDK/ToolKit/Extensions/URL+Helpers.swift new file mode 100644 index 00000000..5e92dce0 --- /dev/null +++ b/OmiseSDK/ToolKit/Extensions/URL+Helpers.swift @@ -0,0 +1,85 @@ +import Foundation + +extension URL { + func match(string: String) -> Bool { + guard + let expectedURLComponents = URLComponents(string: string), + let components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { + return false + } + + return expectedURLComponents.match(components: components) + } +} + +extension URLComponents { + func match(string: String) -> Bool { + guard let expectedURLComponents = URLComponents(string: string) else { return false } + return match(components: expectedURLComponents) + } + + func match(components expectedURLComponents: URLComponents) -> Bool { + return expectedURLComponents.scheme?.lowercased() == scheme?.lowercased() + && expectedURLComponents.host?.lowercased() == host?.lowercased() + && path.lowercased().hasPrefix(expectedURLComponents.path.lowercased()) + } +} + +extension URL { + var removeLastPathComponent: URL { + guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { + return self + } + + // Remove query items to eliminate GET parameters + components.queryItems = nil + + // Split the path into components, replace the last one, and reassemble the path + var pathComponents = components.path.split(separator: "/").map(String.init) + + if !pathComponents.isEmpty { + pathComponents.removeLast() + components.path = "/" + pathComponents.joined(separator: "/") + } + + return components.url ?? self + } + + func appendingLastPathComponent(_ lastPathComponent: String) -> URL { + guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { + return self + } + + var pathComponents = components.path.split(separator: "/").map(String.init) + + if !pathComponents.isEmpty { + pathComponents.append(lastPathComponent) + components.path = "/" + pathComponents.joined(separator: "/") + } else { + // If there are no path components, just add the new endpoint + components.path = lastPathComponent + } + + return components.url ?? self + } + + func appendQueryItem(name: String, value: String?) -> URL? { + + guard var urlComponents = URLComponents(string: absoluteString) else { return nil } + + // Create array of existing query items + var queryItems: [URLQueryItem] = urlComponents.queryItems ?? [] + + // Create query item + let queryItem = URLQueryItem(name: name, value: value) + + // Append the new query item in the existing query items array + queryItems.append(queryItem) + + // Append updated query items array in the url component object + urlComponents.queryItems = queryItems + + // Returns the url from new url components + return urlComponents.url + } +} diff --git a/OmiseSDK/Sources/ToolKit/Property Wrappers/ProxyProperty.swift b/OmiseSDK/ToolKit/Property Wrappers/ProxyProperty.swift similarity index 100% rename from OmiseSDK/Sources/ToolKit/Property Wrappers/ProxyProperty.swift rename to OmiseSDK/ToolKit/Property Wrappers/ProxyProperty.swift diff --git a/OmiseSDK/ToolKit/Typedefs/Closures.swift b/OmiseSDK/ToolKit/Typedefs/Closures.swift new file mode 100644 index 00000000..5d3a83df --- /dev/null +++ b/OmiseSDK/ToolKit/Typedefs/Closures.swift @@ -0,0 +1,4 @@ +import Foundation + +typealias VoidClosure = () -> Void +typealias ParamClosure = (T) -> Void diff --git a/OmiseSDK/Sources/ToolKit/UI Extensions/UIButton+Helpers.swift b/OmiseSDK/ToolKit/UI Extensions/UIButton+Helpers.swift similarity index 100% rename from OmiseSDK/Sources/ToolKit/UI Extensions/UIButton+Helpers.swift rename to OmiseSDK/ToolKit/UI Extensions/UIButton+Helpers.swift diff --git a/OmiseSDK/Sources/ToolKit/UI Extensions/UIColor+Helpers.swift b/OmiseSDK/ToolKit/UI Extensions/UIColor+Helpers.swift similarity index 100% rename from OmiseSDK/Sources/ToolKit/UI Extensions/UIColor+Helpers.swift rename to OmiseSDK/ToolKit/UI Extensions/UIColor+Helpers.swift diff --git a/OmiseSDK/Sources/ToolKit/UI Extensions/UIControl.State+Hashable.swift b/OmiseSDK/ToolKit/UI Extensions/UIControl.State+Hashable.swift similarity index 100% rename from OmiseSDK/Sources/ToolKit/UI Extensions/UIControl.State+Hashable.swift rename to OmiseSDK/ToolKit/UI Extensions/UIControl.State+Hashable.swift diff --git a/OmiseSDK/Sources/ToolKit/UI Extensions/UIImage+Helpers.swift b/OmiseSDK/ToolKit/UI Extensions/UIImage+Helpers.swift similarity index 100% rename from OmiseSDK/Sources/ToolKit/UI Extensions/UIImage+Helpers.swift rename to OmiseSDK/ToolKit/UI Extensions/UIImage+Helpers.swift diff --git a/OmiseSDK/Sources/ToolKit/UI Extensions/UILabel+Helpers.swift b/OmiseSDK/ToolKit/UI Extensions/UILabel+Helpers.swift similarity index 100% rename from OmiseSDK/Sources/ToolKit/UI Extensions/UILabel+Helpers.swift rename to OmiseSDK/ToolKit/UI Extensions/UILabel+Helpers.swift diff --git a/OmiseSDK/Sources/ToolKit/UI Extensions/UIScrollView+Helpers.swift b/OmiseSDK/ToolKit/UI Extensions/UIScrollView+Helpers.swift similarity index 100% rename from OmiseSDK/Sources/ToolKit/UI Extensions/UIScrollView+Helpers.swift rename to OmiseSDK/ToolKit/UI Extensions/UIScrollView+Helpers.swift diff --git a/OmiseSDK/Sources/ToolKit/UI Extensions/UIStackView+Helpers.swift b/OmiseSDK/ToolKit/UI Extensions/UIStackView+Helpers.swift similarity index 100% rename from OmiseSDK/Sources/ToolKit/UI Extensions/UIStackView+Helpers.swift rename to OmiseSDK/ToolKit/UI Extensions/UIStackView+Helpers.swift diff --git a/OmiseSDK/Sources/ToolKit/UI Extensions/UIView+Helpers.swift b/OmiseSDK/ToolKit/UI Extensions/UIView+Helpers.swift similarity index 100% rename from OmiseSDK/Sources/ToolKit/UI Extensions/UIView+Helpers.swift rename to OmiseSDK/ToolKit/UI Extensions/UIView+Helpers.swift diff --git a/OmiseSDKTests/3DS/AuthorizingPaymentTests.swift b/OmiseSDKTests/3DS/AuthorizingPaymentTests.swift new file mode 100644 index 00000000..593cdc2f --- /dev/null +++ b/OmiseSDKTests/3DS/AuthorizingPaymentTests.swift @@ -0,0 +1,280 @@ +import XCTest +@testable import OmiseSDK +import ThreeDS_SDK + +class NetceteraThreeDSControllerMock: NetceteraThreeDSControllerProtocol { + func setAPIKey(_ apiKey: String) { + } + + func appOpen3DSDeeplinkURL(_ url: URL) -> Bool { + return true + } + + var processAuthorizedURLResult = NetceteraThreeDSControllerResult.success(()) + func processAuthorizedURL( + _ authorizeUrl: URL, + threeDSRequestorAppURL: String?, + uiCustomization: ThreeDSUICustomization?, + in viewController: UIViewController, + onComplete: @escaping (NetceteraThreeDSControllerResult) -> Void + ) { + onComplete(processAuthorizedURLResult) + } +} + +class TopViewControllerMock: UIViewController { + var onPresentViewController: ((UIViewController) -> Void)? + override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) { + onPresentViewController?(viewControllerToPresent) + } +} + +class AuthorizingPaymentTests: XCTestCase { + + struct AnyError: Error { + let message: String + init(_ message: String? = nil) { + self.message = message ?? "" + } + } + + // swiftlint:disable force_unwrapping + let authorizeURL = URL(string: "https://co.omise.com/authorize")! + let expectedWebReturnURL = URL(string: "https://co.omise.com/callback")! + let deeplinkURL = URL(string: "omiseExampleApp://authorizePayment")! + // swiftlint:enable force_unwrapping + + typealias NetceteraError = NetceteraThreeDSController.Errors + let netceteraMockController = NetceteraThreeDSControllerMock() + let authorizingPayment = AuthorizingPayment() + let topViewController = TopViewControllerMock() + let timeout: TimeInterval = 15.0 + + override func setUp() { + super.setUp() + NetceteraThreeDSController.sharedController = netceteraMockController + } + + func test3DSUserFailedChallenge() { + let expectation = self.expectation(description: "callback") + + netceteraMockController.processAuthorizedURLResult = .failure(NetceteraError.incomplete(event: nil)) + authorizingPayment.presentAuthPaymentController( + from: topViewController, + url: authorizeURL, + expectedWebViewReturnURL: expectedWebReturnURL, + deeplinkURL: deeplinkURL) { result in + defer { expectation.fulfill() } + XCTAssertEqual(result, AuthorizingPayment.Status.cancel) + } + + waitForExpectations(timeout: timeout, handler: nil) + } + + func test3DSUserCancelledChallenge() { + let expectation = self.expectation(description: "callback") + + netceteraMockController.processAuthorizedURLResult = .failure(NetceteraError.cancelled) + authorizingPayment.presentAuthPaymentController( + from: topViewController, + url: authorizeURL, + expectedWebViewReturnURL: expectedWebReturnURL, + deeplinkURL: deeplinkURL) { result in + defer { expectation.fulfill() } + XCTAssertEqual(result, AuthorizingPayment.Status.cancel) + } + + waitForExpectations(timeout: timeout, handler: nil) + } + + func test3DSUserTimedOutChallenge() { + let expectation = self.expectation(description: "callback") + + netceteraMockController.processAuthorizedURLResult = .failure(NetceteraError.timedout) + authorizingPayment.presentAuthPaymentController( + from: topViewController, + url: authorizeURL, + expectedWebViewReturnURL: expectedWebReturnURL, + deeplinkURL: deeplinkURL) { result in + defer { expectation.fulfill() } + XCTAssertEqual(result, AuthorizingPayment.Status.cancel) + } + + waitForExpectations(timeout: timeout, handler: nil) + } + + func test3DSAuthResponseStatusFailed() { + let expectation = self.expectation(description: "callback") + + netceteraMockController.processAuthorizedURLResult = .failure(NetceteraError.authResStatusFailed) + authorizingPayment.presentAuthPaymentController( + from: topViewController, + url: authorizeURL, + expectedWebViewReturnURL: expectedWebReturnURL, + deeplinkURL: deeplinkURL) { result in + defer { expectation.fulfill() } + XCTAssertEqual(result, AuthorizingPayment.Status.cancel) + } + + waitForExpectations(timeout: timeout, handler: nil) + } + + func test3DSAuthResponseStatusUnknown() { + let expectation = self.expectation(description: "callback") + + netceteraMockController.processAuthorizedURLResult = .failure(NetceteraError.authResStatusUnknown("Some Unknown Status")) + authorizingPayment.presentAuthPaymentController( + from: topViewController, + url: authorizeURL, + expectedWebViewReturnURL: expectedWebReturnURL, + deeplinkURL: deeplinkURL) { result in + defer { expectation.fulfill() } + XCTAssertEqual(result, AuthorizingPayment.Status.cancel) + } + + waitForExpectations(timeout: timeout, handler: nil) + } + + func test3DSSuccess() { + let expectation = self.expectation(description: "callback") + + netceteraMockController.processAuthorizedURLResult = .success(()) + authorizingPayment.presentAuthPaymentController( + from: topViewController, + url: authorizeURL, + expectedWebViewReturnURL: expectedWebReturnURL, + deeplinkURL: deeplinkURL) { result in + defer { expectation.fulfill() } + XCTAssertEqual(result, AuthorizingPayment.Status.complete(redirectURL: nil)) + } + + waitForExpectations(timeout: timeout, handler: nil) + } + + func testWebViewOn3DSAuthResponseInvalid() { + let expectation = self.expectation(description: "callback") + + topViewController.onPresentViewController = { viewController in + defer { expectation.fulfill() } + let presentedVC = (viewController as? UINavigationController)?.topViewController + XCTAssertTrue(presentedVC is AuthorizingPaymentWebViewController) + } + + netceteraMockController.processAuthorizedURLResult = .failure(NetceteraError.authResInvalid) + authorizingPayment.presentAuthPaymentController( + from: topViewController, + url: authorizeURL, + expectedWebViewReturnURL: expectedWebReturnURL, + deeplinkURL: deeplinkURL) { _ in + XCTFail("Should open webview and don't call this callback") + } + + waitForExpectations(timeout: timeout, handler: nil) + } + + func testWebViewOn3DSProtocolError() { + let expectation = self.expectation(description: "callback") + + topViewController.onPresentViewController = { viewController in + defer { expectation.fulfill() } + let presentedVC = (viewController as? UINavigationController)?.topViewController + XCTAssertTrue(presentedVC is AuthorizingPaymentWebViewController) + } + + netceteraMockController.processAuthorizedURLResult = .failure(NetceteraError.protocolError(event: nil)) + authorizingPayment.presentAuthPaymentController( + from: topViewController, + url: authorizeURL, + expectedWebViewReturnURL: expectedWebReturnURL, + deeplinkURL: deeplinkURL) { _ in + XCTFail("Should open webview and don't call this callback") + } + + waitForExpectations(timeout: timeout, handler: nil) + } + + func testWebViewOn3DSRuntimeError() { + let expectation = self.expectation(description: "callback") + + topViewController.onPresentViewController = { viewController in + defer { expectation.fulfill() } + let presentedVC = (viewController as? UINavigationController)?.topViewController + XCTAssertTrue(presentedVC is AuthorizingPaymentWebViewController) + } + + netceteraMockController.processAuthorizedURLResult = .failure(NetceteraError.runtimeError(event: nil)) + authorizingPayment.presentAuthPaymentController( + from: topViewController, + url: authorizeURL, + expectedWebViewReturnURL: expectedWebReturnURL, + deeplinkURL: deeplinkURL) { _ in + XCTFail("Should open webview and don't call this callback") + } + + waitForExpectations(timeout: timeout, handler: nil) + } + + func testWebViewOn3DSPresentChallengeFailed() { + let expectation = self.expectation(description: "callback") + + topViewController.onPresentViewController = { viewController in + defer { expectation.fulfill() } + let presentedVC = (viewController as? UINavigationController)?.topViewController + XCTAssertTrue(presentedVC is AuthorizingPaymentWebViewController) + } + + let challengeError = AnyError("Any error on presenting Netcetera Challenge") + netceteraMockController.processAuthorizedURLResult = .failure(NetceteraError.presentChallenge(error: challengeError)) + authorizingPayment.presentAuthPaymentController( + from: topViewController, + url: authorizeURL, + expectedWebViewReturnURL: expectedWebReturnURL, + deeplinkURL: deeplinkURL) { _ in + XCTFail("Should open webview and don't call this callback") + } + + waitForExpectations(timeout: timeout, handler: nil) + } + + func testWebViewOn3DSApiKeyInvalid() { + let expectation = self.expectation(description: "callback") + + topViewController.onPresentViewController = { viewController in + defer { expectation.fulfill() } + let presentedVC = (viewController as? UINavigationController)?.topViewController + XCTAssertTrue(presentedVC is AuthorizingPaymentWebViewController) + } + + netceteraMockController.processAuthorizedURLResult = .failure(NetceteraError.apiKeyInvalid) + authorizingPayment.presentAuthPaymentController( + from: topViewController, + url: authorizeURL, + expectedWebViewReturnURL: expectedWebReturnURL, + deeplinkURL: deeplinkURL) { _ in + XCTFail("Should open webview and don't call this callback") + } + + waitForExpectations(timeout: timeout, handler: nil) + } + + func testWebViewOnInitializationFailed() { + let expectation = self.expectation(description: "callback") + + topViewController.onPresentViewController = { viewController in + defer { expectation.fulfill() } + let presentedVC = (viewController as? UINavigationController)?.topViewController + XCTAssertTrue(presentedVC is AuthorizingPaymentWebViewController) + } + + netceteraMockController.processAuthorizedURLResult = .failure(AnyError("Any error during initialization")) + authorizingPayment.presentAuthPaymentController( + from: topViewController, + url: authorizeURL, + expectedWebViewReturnURL: expectedWebReturnURL, + deeplinkURL: deeplinkURL) { _ in + XCTFail("Should open webview and don't call this callback") + } + + waitForExpectations(timeout: timeout, handler: nil) + } +} diff --git a/OmiseSDKTests/3DS/NetceteraThreeDSControllerTests.swift b/OmiseSDKTests/3DS/NetceteraThreeDSControllerTests.swift new file mode 100644 index 00000000..fee21426 --- /dev/null +++ b/OmiseSDKTests/3DS/NetceteraThreeDSControllerTests.swift @@ -0,0 +1,40 @@ +import XCTest +@testable import OmiseSDK +import ThreeDS_SDK + +class NetceteraThreeDSControllerTests: XCTestCase { + + func testThreeDSRequestorAppURLTransaction() { + let transactionId = "1234-5678-9012-ABCD" + + let aRes = AuthResponse.ARes( + threeDSServerTransID: transactionId, + acsTransID: "123", + acsSignedContent: nil, + acsUIType: nil + ) + + let netceteraController = NetceteraThreeDSController() + + let tests = [ + "": nil, + + "someApp://": + "someApp://?transID=\(transactionId)", + + "someApp://3DSChallenge": + "someApp://3DSChallenge?transID=\(transactionId)", + + "someApp://3ds/challenge?source=omiseSDK": + "someApp://3ds/challenge?source=omiseSDK&transID=\(transactionId)" + ] + + for (url, result) in tests { + let params = netceteraController.prepareChallengeParameters( + aRes: aRes, + threeDSRequestorAppURL: url + ) + XCTAssertEqual(params.getThreeDSRequestorAppURL(), result) + } + } +} diff --git a/OmiseSDKTests/3DS/SHA512Tests.swift b/OmiseSDKTests/3DS/SHA512Tests.swift new file mode 100644 index 00000000..5537188a --- /dev/null +++ b/OmiseSDKTests/3DS/SHA512Tests.swift @@ -0,0 +1,38 @@ +import XCTest +@testable import OmiseSDK +import Foundation + +class SHA512Tests: XCTestCase { + + func testSHA512EmptyString() { + let emptyString = "" + let expectedHash = Data( + // swiftlint:disable:next multiline_literal_brackets + [ // expected SHA-512 hash of empty string + 0xcf, 0x83, 0xe1, 0x35, 0x7e, 0xef, 0xb8, 0xbd, + 0xf1, 0x54, 0x28, 0x50, 0xd6, 0x6d, 0x80, 0x07, + 0xd6, 0x20, 0xe4, 0x05, 0x0b, 0x57, 0x15, 0xdc, + 0x83, 0xf4, 0xa9, 0x21, 0xd3, 0x6c, 0xe9, 0xce, + 0x47, 0xd0, 0xd1, 0x3c, 0x5d, 0x85, 0xf2, 0xb0, + 0xff, 0x83, 0x18, 0xd2, 0x87, 0x7e, 0xec, 0x2f, + 0x63, 0xb9, 0x31, 0xbd, 0x47, 0x41, 0x7a, 0x81, + 0xa5, 0x38, 0x32, 0x7a, 0xf9, 0x27, 0xda, 0x3e + ] + ) + XCTAssertEqual(emptyString.sha512, expectedHash) + } + + func testSHA512NonEmptyString() { + let testString = "Hello, World!" + // swiftlint:disable:next line_length + let expectedHexString = "374d794a95cdcfd8b35993185fef9ba368f160d8daf432d08ba9f1ed1e5abe6cc69291e0fa2fe0006a52570ef18c19def4e617c33ce52ef0a6e5fbe318cb0387" + + XCTAssertEqual(testString.sha512.hexadecimalString, expectedHexString) + } +} + +extension Data { + var hexadecimalString: String { + return map { String(format: "%02x", $0) }.joined() + } +} diff --git a/OmiseSDKTests/3DS/ThreeDSButtonCustomizationTests.swift b/OmiseSDKTests/3DS/ThreeDSButtonCustomizationTests.swift new file mode 100644 index 00000000..1465a91c --- /dev/null +++ b/OmiseSDKTests/3DS/ThreeDSButtonCustomizationTests.swift @@ -0,0 +1,64 @@ +import XCTest +@testable import OmiseSDK +import ThreeDS_SDK + +class ThreeDSButtonCustomizationTests: XCTestCase { + let backgroundColorHex = "#FFFFFF" + let darkBackgroundColorHex = "#000000" + let cornerRadius: Int = 12 + + let testTextFontName = "Arial-BoldItalicMT" + let testTextColorHex = "#C4A484" + let testDarkTextColorHex = "#964B00" + let testTextFontSize = 14 + + func testNetceteraInit() { + let uiCustomization = ThreeDSButtonCustomization( + backgroundColorHex: backgroundColorHex, + darkBackgroundColorHex: darkBackgroundColorHex, + cornerRadius: cornerRadius, + textFontName: testTextFontName, + textColorHex: testTextColorHex, + darkTextColorHex: testDarkTextColorHex, + textFontSize: testTextFontSize) + + let netCustomization: ThreeDS_SDK.ButtonCustomization + do { + try netCustomization = ThreeDS_SDK.ButtonCustomization(uiCustomization) + } catch { + XCTAssert(false, error.localizedDescription) + return + } + + XCTAssertEqual(netCustomization.getBackgroundColor(), backgroundColorHex) + XCTAssertEqual(netCustomization.getCornerRadius(), cornerRadius) + + XCTAssertEqual(netCustomization.getTextFontName(), testTextFontName) + XCTAssertEqual(netCustomization.getTextColor(), testTextColorHex) + XCTAssertEqual(netCustomization.getTextFontSize(), testTextFontSize) + } + + func testNetceteraCustomization() { + let uiCustomization = ThreeDSButtonCustomization( + backgroundColorHex: backgroundColorHex, + darkBackgroundColorHex: darkBackgroundColorHex, + cornerRadius: cornerRadius, + textFontName: testTextFontName, + textColorHex: testTextColorHex, + darkTextColorHex: testDarkTextColorHex, + textFontSize: testTextFontSize) + + let netCustomization = ThreeDS_SDK.ButtonCustomization() + do { + try netCustomization.customize(uiCustomization) + } catch { + XCTAssert(false, error.localizedDescription) + } + + XCTAssertEqual(netCustomization.getBackgroundColor(), backgroundColorHex) + XCTAssertEqual(netCustomization.getCornerRadius(), cornerRadius) + XCTAssertEqual(netCustomization.getTextFontName(), testTextFontName) + XCTAssertEqual(netCustomization.getTextColor(), testTextColorHex) + XCTAssertEqual(netCustomization.getTextFontSize(), testTextFontSize) + } +} diff --git a/OmiseSDKTests/3DS/UICustomizationTests.swift b/OmiseSDKTests/3DS/UICustomizationTests.swift new file mode 100644 index 00000000..abb2cd76 --- /dev/null +++ b/OmiseSDKTests/3DS/UICustomizationTests.swift @@ -0,0 +1,29 @@ +import XCTest +@testable import OmiseSDK +import ThreeDS_SDK + +class ThreeDSCustomizationTests: XCTestCase { + let testTextFontName = "Arial-BoldItalicMT" + let testTextColorHex = "#C4A484" + let testDarkTextColorHex = "#964B00" + let testTextFontSize = 14 + + func testNetceteraCustomization() { + let uiCustomization = ThreeDSCustomization( + textFontName: testTextFontName, + textColorHex: testTextColorHex, + darkTextColorHex: testDarkTextColorHex, + textFontSize: testTextFontSize) + + let netCustomization = ThreeDS_SDK.Customization() + do { + try netCustomization.customize(omiseThreeDSCustomization: uiCustomization) + } catch { + XCTAssert(false, error.localizedDescription) + } + + XCTAssertEqual(netCustomization.getTextFontName(), testTextFontName) + XCTAssertEqual(netCustomization.getTextColor(), testTextColorHex) + XCTAssertEqual(netCustomization.getTextFontSize(), testTextFontSize) + } +} diff --git a/OmiseSDKTests/URL+PathComponentsTests.swift b/OmiseSDKTests/URL+PathComponentsTests.swift new file mode 100644 index 00000000..0e7f1a09 --- /dev/null +++ b/OmiseSDKTests/URL+PathComponentsTests.swift @@ -0,0 +1,28 @@ +import XCTest +@testable import OmiseSDK + +private let timeout: TimeInterval = 15.0 + +class URLPathComponentsTests: XCTestCase { + + func testRemoveLastPathComponent() { + let tests = [ + "https://test.com": "https://test.com/test", + "https://test.com/endpoint": "https://test.com/test", + "https://test.com/path/endpoint": "https://test.com/path/test", + "https://test.com/path/endpoint?test=2": "https://test.com/path/test" + ] + + for (urlString, expectedResult) in tests { + guard let url = URL(string: urlString) else { + XCTFail("Can't resolve test URL") + return + } + + let result = url.removeLastPathComponent + .appendingPathComponent("test", isDirectory: false) + + XCTAssertEqual(result.absoluteString, expectedResult) + } + } +} diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 00000000..b8079cd0 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "ThreeDS_SDK", + "repositoryURL": "https://github.com/ios-3ds-sdk/SPM", + "state": { + "branch": null, + "revision": "a4e55d7f10294ea81abd1db393ae05606a5efa46", + "version": "2.4.0" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift index 41fcedb4..83238631 100644 --- a/Package.swift +++ b/Package.swift @@ -15,10 +15,15 @@ let package = Package( targets: ["OmiseSDK"] ) ], + dependencies: [ + .package(url: "https://github.com/ios-3ds-sdk/SPM", .exact("2.4.0")) + ], targets: [ .target( name: "OmiseSDK", - dependencies: [], + dependencies: [ + .product(name: "ThreeDS_SDK", package: "SPM") + ], path: "OmiseSDK", exclude: ["Info.plist"], resources: [.process("Resources")] diff --git a/README.md b/README.md index ee226795..db76cde6 100644 --- a/README.md +++ b/README.md @@ -352,13 +352,13 @@ omiseSDK.presentAuthorizingPayment( Then implement the delegate to receive the `Source` or `Token` object after the user has selected: ```swift -extension ViewController: AuthorizingPaymentViewControllerDelegate { - func authorizingPaymentViewController(_ viewController: AuthorizingPaymentViewController, didCompleteAuthorizingPaymentWithRedirectedURL redirectedURL: URL) { +extension ViewController: AuthorizingPaymentWebViewDelegate { + func authorizingPaymentViewController(_ viewController: AuthorizingPaymentWebViewController, didCompleteAuthorizingPaymentWithRedirectedURL redirectedURL: URL) { // Handle the `redirected URL` here omiseSDK.dismiss() } - func authorizingPaymentViewControllerDidCancel(_ viewController: AuthorizingPaymentViewController) { + func authorizingPaymentViewControllerDidCancel(_ viewController: AuthorizingPaymentWebViewController) { // Handle the case that the user taps cancel. omiseSDK.dismiss() } diff --git a/dev.xcodeproj/project.pbxproj b/dev.xcodeproj/project.pbxproj index b873a4e1..bce17807 100644 --- a/dev.xcodeproj/project.pbxproj +++ b/dev.xcodeproj/project.pbxproj @@ -25,7 +25,7 @@ 750126722B8A49BC00ED5E14 /* FPX.Bank+ViewPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750126712B8A49BC00ED5E14 /* FPX.Bank+ViewPresentable.swift */; }; 750126742B8A580500ED5E14 /* SelectDuitNowOBWBankViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750126732B8A580500ED5E14 /* SelectDuitNowOBWBankViewModel.swift */; }; 750126772B95971A00ED5E14 /* ChoosePaymentCoordinator+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 750126762B95971A00ED5E14 /* ChoosePaymentCoordinator+Helpers.swift */; }; - 750126792B95A34000ED5E14 /* AuthorizingPaymentViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 750126782B95A34000ED5E14 /* AuthorizingPaymentViewController.xib */; }; + 750126792B95A34000ED5E14 /* AuthorizingPaymentWebViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 750126782B95A34000ED5E14 /* AuthorizingPaymentWebViewController.xib */; }; 7502CA532A3011A900766E7D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7502CA562A3011A900766E7D /* LaunchScreen.storyboard */; }; 7502CA5F2A31A7D600766E7D /* CountryListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7502CA5E2A31A7D600766E7D /* CountryListViewController.swift */; }; 7502CA602A31A7D600766E7D /* CountryListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7502CA5E2A31A7D600766E7D /* CountryListViewController.swift */; }; @@ -90,6 +90,26 @@ 757E14352BD61DFE005F08AD /* ChoosePaymentMethodDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 757E14342BD61DFE005F08AD /* ChoosePaymentMethodDelegate.swift */; }; 7588011E2BAAB37C00F02934 /* AppDeeplink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7588011D2BAAB37C00F02934 /* AppDeeplink.swift */; }; 758801202BAAC70200F02934 /* URL+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7588011F2BAAC70200F02934 /* URL+Helpers.swift */; }; + 758A4E6E2BE38A5F005E7B5A /* CryptData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758A4E6B2BE38A5E005E7B5A /* CryptData.swift */; }; + 758A4E6F2BE38A5F005E7B5A /* String+sha512.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758A4E6C2BE38A5E005E7B5A /* String+sha512.swift */; }; + 758A4E702BE38A5F005E7B5A /* String+PemCert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758A4E6D2BE38A5E005E7B5A /* String+PemCert.swift */; }; + 758A4E732BE38A7E005E7B5A /* NetceteraThreeDSController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758A4E712BE38A7E005E7B5A /* NetceteraThreeDSController.swift */; }; + 758A4E742BE38A7E005E7B5A /* NetceteraConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758A4E722BE38A7E005E7B5A /* NetceteraConfig.swift */; }; + 758A4E7C2BE38A89005E7B5A /* ThreeDSButtonCustomization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758A4E762BE38A88005E7B5A /* ThreeDSButtonCustomization.swift */; }; + 758A4E7D2BE38A89005E7B5A /* ThreeDSLabelCustomization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758A4E772BE38A89005E7B5A /* ThreeDSLabelCustomization.swift */; }; + 758A4E7E2BE38A89005E7B5A /* ThreeDSToolbarCustomization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758A4E782BE38A89005E7B5A /* ThreeDSToolbarCustomization.swift */; }; + 758A4E7F2BE38A89005E7B5A /* ThreeDSCustomization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758A4E792BE38A89005E7B5A /* ThreeDSCustomization.swift */; }; + 758A4E802BE38A89005E7B5A /* ThreeDSUICustomization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758A4E7A2BE38A89005E7B5A /* ThreeDSUICustomization.swift */; }; + 758A4E812BE38A89005E7B5A /* ThreeDSTextBoxCustomization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758A4E7B2BE38A89005E7B5A /* ThreeDSTextBoxCustomization.swift */; }; + 758A4E842BE38AC3005E7B5A /* ThreeDS_SDK in Frameworks */ = {isa = PBXBuildFile; productRef = 758A4E832BE38AC3005E7B5A /* ThreeDS_SDK */; }; + 758A4E862BE38AD0005E7B5A /* ThreeDS_SDK in Frameworks */ = {isa = PBXBuildFile; productRef = 758A4E852BE38AD0005E7B5A /* ThreeDS_SDK */; }; + 758A4E902BE38AFD005E7B5A /* NetceteraThreeDSControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758A4E882BE38AFD005E7B5A /* NetceteraThreeDSControllerTests.swift */; }; + 758A4E922BE38AFD005E7B5A /* UICustomizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758A4E8A2BE38AFD005E7B5A /* UICustomizationTests.swift */; }; + 758A4E962BE38AFD005E7B5A /* ThreeDSButtonCustomizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758A4E8E2BE38AFD005E7B5A /* ThreeDSButtonCustomizationTests.swift */; }; + 758A4E972BE38AFD005E7B5A /* SHA512Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758A4E8F2BE38AFD005E7B5A /* SHA512Tests.swift */; }; + 758A4E9F2BE3DFDF005E7B5A /* URL+PathComponentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758A4E9E2BE3DFDF005E7B5A /* URL+PathComponentsTests.swift */; }; + 758A4EA62BE9D5C0005E7B5A /* Closures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758A4EA52BE9D5C0005E7B5A /* Closures.swift */; }; + 758A4EA82BE9D722005E7B5A /* AuthorizingPaymentDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758A4EA72BE9D722005E7B5A /* AuthorizingPaymentDelegate.swift */; }; 759856242A286C880087B605 /* UIScrollView+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759856232A286C880087B605 /* UIScrollView+Helpers.swift */; }; 759C7EC02B7DC7F50029D555 /* SourceType+MobileBanking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759C7EBF2B7DC7F50029D555 /* SourceType+MobileBanking.swift */; }; 759C7EC22B7DC80A0029D555 /* SourceType+InternetBanking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 759C7EC12B7DC80A0029D555 /* SourceType+InternetBanking.swift */; }; @@ -176,7 +196,7 @@ 75F2A0BB2A1A60630038FA54 /* AtomePaymentFormViewContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F2A0B52A1A5F020038FA54 /* AtomePaymentFormViewContext.swift */; }; 75F2A0C12A1A84900038FA54 /* UIColor+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F2A0C02A1A84900038FA54 /* UIColor+Helpers.swift */; }; 75F2A0C22A1A87AE0038FA54 /* UIColor+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F2A0C02A1A84900038FA54 /* UIColor+Helpers.swift */; }; - 8A188BB01D951E3C00FFF467 /* AuthorizingPaymentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A188BAF1D951E3C00FFF467 /* AuthorizingPaymentViewController.swift */; }; + 8A188BB01D951E3C00FFF467 /* AuthorizingPaymentWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A188BAF1D951E3C00FFF467 /* AuthorizingPaymentWebViewController.swift */; }; 8A2C451920ECC7A60033E2EB /* OmiseSDK.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 225931181CE3210700841B86 /* OmiseSDK.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 8A40DA29216B441E00749F45 /* PaymentSettingTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7A9EBC2166160300F12D86 /* PaymentSettingTableViewController.swift */; }; 8A40DA2B216B53E900749F45 /* Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A40DA2A216B53E900749F45 /* Tools.swift */; }; @@ -287,7 +307,7 @@ 750126712B8A49BC00ED5E14 /* FPX.Bank+ViewPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FPX.Bank+ViewPresentable.swift"; sourceTree = ""; }; 750126732B8A580500ED5E14 /* SelectDuitNowOBWBankViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectDuitNowOBWBankViewModel.swift; sourceTree = ""; }; 750126762B95971A00ED5E14 /* ChoosePaymentCoordinator+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChoosePaymentCoordinator+Helpers.swift"; sourceTree = ""; }; - 750126782B95A34000ED5E14 /* AuthorizingPaymentViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AuthorizingPaymentViewController.xib; sourceTree = ""; }; + 750126782B95A34000ED5E14 /* AuthorizingPaymentWebViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AuthorizingPaymentWebViewController.xib; sourceTree = ""; }; 7502CA552A3011A900766E7D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 7502CA582A3011B000766E7D /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/LaunchScreen.strings; sourceTree = ""; }; 7502CA5A2A3011B100766E7D /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/LaunchScreen.strings; sourceTree = ""; }; @@ -339,6 +359,25 @@ 757E14342BD61DFE005F08AD /* ChoosePaymentMethodDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChoosePaymentMethodDelegate.swift; sourceTree = ""; }; 7588011D2BAAB37C00F02934 /* AppDeeplink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDeeplink.swift; sourceTree = ""; }; 7588011F2BAAC70200F02934 /* URL+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Helpers.swift"; sourceTree = ""; }; + 758A4E6B2BE38A5E005E7B5A /* CryptData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptData.swift; sourceTree = ""; }; + 758A4E6C2BE38A5E005E7B5A /* String+sha512.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+sha512.swift"; sourceTree = ""; }; + 758A4E6D2BE38A5E005E7B5A /* String+PemCert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+PemCert.swift"; sourceTree = ""; }; + 758A4E712BE38A7E005E7B5A /* NetceteraThreeDSController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetceteraThreeDSController.swift; sourceTree = ""; }; + 758A4E722BE38A7E005E7B5A /* NetceteraConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetceteraConfig.swift; sourceTree = ""; }; + 758A4E762BE38A88005E7B5A /* ThreeDSButtonCustomization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreeDSButtonCustomization.swift; sourceTree = ""; }; + 758A4E772BE38A89005E7B5A /* ThreeDSLabelCustomization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreeDSLabelCustomization.swift; sourceTree = ""; }; + 758A4E782BE38A89005E7B5A /* ThreeDSToolbarCustomization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreeDSToolbarCustomization.swift; sourceTree = ""; }; + 758A4E792BE38A89005E7B5A /* ThreeDSCustomization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreeDSCustomization.swift; sourceTree = ""; }; + 758A4E7A2BE38A89005E7B5A /* ThreeDSUICustomization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreeDSUICustomization.swift; sourceTree = ""; }; + 758A4E7B2BE38A89005E7B5A /* ThreeDSTextBoxCustomization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreeDSTextBoxCustomization.swift; sourceTree = ""; }; + 758A4E882BE38AFD005E7B5A /* NetceteraThreeDSControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetceteraThreeDSControllerTests.swift; sourceTree = ""; }; + 758A4E8A2BE38AFD005E7B5A /* UICustomizationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UICustomizationTests.swift; sourceTree = ""; }; + 758A4E8B2BE38AFD005E7B5A /* AuthorizingPaymentTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizingPaymentTests.swift; sourceTree = ""; }; + 758A4E8E2BE38AFD005E7B5A /* ThreeDSButtonCustomizationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreeDSButtonCustomizationTests.swift; sourceTree = ""; }; + 758A4E8F2BE38AFD005E7B5A /* SHA512Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SHA512Tests.swift; sourceTree = ""; }; + 758A4E9E2BE3DFDF005E7B5A /* URL+PathComponentsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+PathComponentsTests.swift"; sourceTree = ""; }; + 758A4EA52BE9D5C0005E7B5A /* Closures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Closures.swift; sourceTree = ""; }; + 758A4EA72BE9D722005E7B5A /* AuthorizingPaymentDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizingPaymentDelegate.swift; sourceTree = ""; }; 759856232A286C880087B605 /* UIScrollView+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIScrollView+Helpers.swift"; sourceTree = ""; }; 759C7EBF2B7DC7F50029D555 /* SourceType+MobileBanking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceType+MobileBanking.swift"; sourceTree = ""; }; 759C7EC12B7DC80A0029D555 /* SourceType+InternetBanking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceType+InternetBanking.swift"; sourceTree = ""; }; @@ -421,7 +460,7 @@ 75F2A0C02A1A84900038FA54 /* UIColor+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Helpers.swift"; sourceTree = ""; }; 75F8C0B62B1F78E300AE78D9 /* PaymentChooserViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentChooserViewControllerTests.swift; sourceTree = ""; }; 8A00FEC21F1DE88600246078 /* README.md */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; tabWidth = 2; }; - 8A188BAF1D951E3C00FFF467 /* AuthorizingPaymentViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizingPaymentViewController.swift; sourceTree = ""; }; + 8A188BAF1D951E3C00FFF467 /* AuthorizingPaymentWebViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizingPaymentWebViewController.swift; sourceTree = ""; }; 8A25E929213CF6F300D48252 /* EContextPaymentFormController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EContextPaymentFormController.swift; sourceTree = ""; }; 8A275CBB21241E4500C5716F /* MainActionButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainActionButton.swift; sourceTree = ""; }; 8A30FB7C214929C900DC09DC /* PaymentCreatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentCreatorController.swift; sourceTree = ""; }; @@ -461,6 +500,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 758A4E842BE38AC3005E7B5A /* ThreeDS_SDK in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -486,6 +526,7 @@ files = ( 987A02CE1CE35D5E0035417A /* OmiseSDK.framework in Frameworks */, 757B0A742AD71E3100DEAE8F /* OmiseUnitTestKit in Frameworks */, + 758A4E862BE38AD0005E7B5A /* ThreeDS_SDK in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -521,6 +562,7 @@ 2259311A1CE3210700841B86 /* OmiseSDK */ = { isa = PBXGroup; children = ( + 75348D4C29C3FC8F0008C8A3 /* ToolKit */, 75E0EB872B7BCEA000E3198A /* Sources */, 8A00FEC21F1DE88600246078 /* README.md */, 2259311D1CE3210700841B86 /* Info.plist */, @@ -662,6 +704,7 @@ 75348D4C29C3FC8F0008C8A3 /* ToolKit */ = { isa = PBXGroup; children = ( + 758A4EA42BE9D599005E7B5A /* Typedefs */, 75F2A0AE2A1A5BE30038FA54 /* Property Wrappers */, 75405A852A26340E008C21F6 /* UI Extensions */, 75405A7E2A262B14008C21F6 /* Extensions */, @@ -707,6 +750,60 @@ path = ViewModels; sourceTree = ""; }; + 758A4E682BE389B2005E7B5A /* 3DS */ = { + isa = PBXGroup; + children = ( + 758A4E752BE38A84005E7B5A /* UICustomization */, + 758A4E722BE38A7E005E7B5A /* NetceteraConfig.swift */, + 758A4E712BE38A7E005E7B5A /* NetceteraThreeDSController.swift */, + 758A4E6A2BE38A49005E7B5A /* Crypto */, + ); + path = 3DS; + sourceTree = ""; + }; + 758A4E6A2BE38A49005E7B5A /* Crypto */ = { + isa = PBXGroup; + children = ( + 758A4E6B2BE38A5E005E7B5A /* CryptData.swift */, + 758A4E6D2BE38A5E005E7B5A /* String+PemCert.swift */, + 758A4E6C2BE38A5E005E7B5A /* String+sha512.swift */, + ); + path = Crypto; + sourceTree = ""; + }; + 758A4E752BE38A84005E7B5A /* UICustomization */ = { + isa = PBXGroup; + children = ( + 758A4E762BE38A88005E7B5A /* ThreeDSButtonCustomization.swift */, + 758A4E792BE38A89005E7B5A /* ThreeDSCustomization.swift */, + 758A4E772BE38A89005E7B5A /* ThreeDSLabelCustomization.swift */, + 758A4E7B2BE38A89005E7B5A /* ThreeDSTextBoxCustomization.swift */, + 758A4E782BE38A89005E7B5A /* ThreeDSToolbarCustomization.swift */, + 758A4E7A2BE38A89005E7B5A /* ThreeDSUICustomization.swift */, + ); + path = UICustomization; + sourceTree = ""; + }; + 758A4E872BE38AE8005E7B5A /* 3DS */ = { + isa = PBXGroup; + children = ( + 758A4E8B2BE38AFD005E7B5A /* AuthorizingPaymentTests.swift */, + 758A4E882BE38AFD005E7B5A /* NetceteraThreeDSControllerTests.swift */, + 758A4E8F2BE38AFD005E7B5A /* SHA512Tests.swift */, + 758A4E8E2BE38AFD005E7B5A /* ThreeDSButtonCustomizationTests.swift */, + 758A4E8A2BE38AFD005E7B5A /* UICustomizationTests.swift */, + ); + path = 3DS; + sourceTree = ""; + }; + 758A4EA42BE9D599005E7B5A /* Typedefs */ = { + isa = PBXGroup; + children = ( + 758A4EA52BE9D5C0005E7B5A /* Closures.swift */, + ); + path = Typedefs; + sourceTree = ""; + }; 75CFC4EF2B753AB900422A8F /* JSON Models */ = { isa = PBXGroup; children = ( @@ -893,8 +990,8 @@ 75D13DFA2B866CF40073A831 /* Authorizing Payment */ = { isa = PBXGroup; children = ( - 8A188BAF1D951E3C00FFF467 /* AuthorizingPaymentViewController.swift */, - 750126782B95A34000ED5E14 /* AuthorizingPaymentViewController.xib */, + 8A188BAF1D951E3C00FFF467 /* AuthorizingPaymentWebViewController.swift */, + 750126782B95A34000ED5E14 /* AuthorizingPaymentWebViewController.xib */, ); path = "Authorizing Payment"; sourceTree = ""; @@ -976,13 +1073,14 @@ 75E0EB872B7BCEA000E3198A /* Sources */ = { isa = PBXGroup; children = ( - 75348D4C29C3FC8F0008C8A3 /* ToolKit */, + 758A4E682BE389B2005E7B5A /* 3DS */, 75E0EBBD2B7D0DB600E3198A /* Components */, 75E0EB942B7BD00F00E3198A /* Models */, 75E0EBBF2B7D13D200E3198A /* OmiseAPI */, 75D13DEE2B866BCA0073A831 /* Views */, 75CFC4D12B73AFD500422A8F /* OmiseSDK.swift */, 757E14342BD61DFE005F08AD /* ChoosePaymentMethodDelegate.swift */, + 758A4EA72BE9D722005E7B5A /* AuthorizingPaymentDelegate.swift */, 755CD9222BA2000600E8FCDA /* ClientProtocol.swift */, ); path = Sources; @@ -1103,6 +1201,8 @@ 987A02CA1CE35D5E0035417A /* OmiseSDKTests */ = { isa = PBXGroup; children = ( + 758A4E9E2BE3DFDF005E7B5A /* URL+PathComponentsTests.swift */, + 758A4E872BE38AE8005E7B5A /* 3DS */, 757B0A722AD71DCD00DEAE8F /* OmiseUnitTestKit */, 987A02CD1CE35D5E0035417A /* Info.plist */, 987A03AC1CE5CD450035417A /* Fixtures */, @@ -1167,6 +1267,7 @@ ); name = OmiseSDK; packageProductDependencies = ( + 758A4E832BE38AC3005E7B5A /* ThreeDS_SDK */, ); productName = OmiseSDK; productReference = 225931181CE3210700841B86 /* OmiseSDK.framework */; @@ -1180,6 +1281,7 @@ 22D480991D0EB29C00544CE1 /* Sources */, 22D4809A1D0EB29C00544CE1 /* Frameworks */, 8A0928211D545455008D393F /* Embed Frameworks */, + 752970442BF4A8F100DE1AA5 /* Skip Config file changes */, ); buildRules = ( ); @@ -1232,6 +1334,7 @@ name = OmiseSDKTests; packageProductDependencies = ( 757B0A732AD71E3100DEAE8F /* OmiseUnitTestKit */, + 758A4E852BE38AD0005E7B5A /* ThreeDS_SDK */, ); productName = OmiseSDKTests; productReference = 987A02C91CE35D5E0035417A /* OmiseSDKTests.xctest */; @@ -1279,6 +1382,9 @@ Base, ); mainGroup = 2259310E1CE3210700841B86; + packageReferences = ( + 758A4E822BE38AC3005E7B5A /* XCRemoteSwiftPackageReference "SPM" */, + ); productRefGroup = 225931191CE3210700841B86 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -1301,7 +1407,7 @@ 75D13E1B2B86FD2D0073A831 /* CCVInfoController.xib in Resources */, 75405AA82A27F97E008C21F6 /* Localizable.strings in Resources */, 7501266E2B8A411C00ED5E14 /* FPXPaymentFormController.xib in Resources */, - 750126792B95A34000ED5E14 /* AuthorizingPaymentViewController.xib in Resources */, + 750126792B95A34000ED5E14 /* AuthorizingPaymentWebViewController.xib in Resources */, 8A776647214A5FC60029D166 /* NoticeView.xib in Resources */, 2225B3541D0FDD63003EB396 /* Assets.xcassets in Resources */, 8AF5517A213D4C0100D61C98 /* Localizable.stringsdict in Resources */, @@ -1342,6 +1448,24 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 752970442BF4A8F100DE1AA5 /* Skip Config file changes */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Skip Config file changes"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if git --version >/dev/null 2>&1; then\n git update-index --assume-unchanged ExampleApp/Config.local.plist\nelse\n echo \"Git is not installed, skipping command.\"\nfi\n"; + }; 75341FF329A716FD003027E6 /* Swiftlint */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -1384,6 +1508,7 @@ 73F9BA4B25E6053700F55CA8 /* FPXPaymentFormController.swift in Sources */, 98FF99BB2577789800476487 /* Configuration.swift in Sources */, 759C7EC02B7DC7F50029D555 /* SourceType+MobileBanking.swift in Sources */, + 758A4EA82BE9D722005E7B5A /* AuthorizingPaymentDelegate.swift in Sources */, 75B420612B776D8A0036134D /* APIProtocol.swift in Sources */, 75CFC4D22B73AFD500422A8F /* OmiseSDK.swift in Sources */, 75131F862AC2BD9D008BCB17 /* UIViewController+NavigationBarStyle.swift in Sources */, @@ -1406,6 +1531,7 @@ 75CFC4EE2B7537BC00422A8F /* Client+URLRequest.swift in Sources */, 75D13DAC2B83B58B0073A831 /* Source.Payment.EContext.swift in Sources */, 75D13D8C2B8385820073A831 /* UIBarButtonItem+Helpers.swift in Sources */, + 758A4E7C2BE38A89005E7B5A /* ThreeDSButtonCustomization.swift in Sources */, 75E0EBA22B7BD02200E3198A /* Token.Card.swift in Sources */, 75405A8B2A26340E008C21F6 /* UIStackView+Helpers.swift in Sources */, 75D13DA82B83B58B0073A831 /* Source.Payment.DuitNowOBW.swift in Sources */, @@ -1419,9 +1545,10 @@ 8AF9857121479F38007E18ED /* CCVInfoController.swift in Sources */, 75E0EB7C2B7B410600E3198A /* Client+NetworkService.swift in Sources */, 75D13D602B807AC90073A831 /* ChoosePaymentCoordinator.swift in Sources */, + 758A4E7D2BE38A89005E7B5A /* ThreeDSLabelCustomization.swift in Sources */, 75D13DB42B83B71D0073A831 /* DuitNowOBW.Bank+ViewPresentable.swift in Sources */, 75405A7C2A25EC88008C21F6 /* Array+Helpers.swift in Sources */, - 8A188BB01D951E3C00FFF467 /* AuthorizingPaymentViewController.swift in Sources */, + 8A188BB01D951E3C00FFF467 /* AuthorizingPaymentWebViewController.swift in Sources */, 75D13DFC2B866DFE0073A831 /* PaymentCreatorController.swift in Sources */, 14ACEF831D02D641001C5319 /* CardExpiryDatePicker.swift in Sources */, 75CFC4F32B753BCD00422A8F /* Capability.PaymentMethod.swift in Sources */, @@ -1439,29 +1566,37 @@ 986406301CFEA7E4004BCF51 /* CardCVVTextField.swift in Sources */, 75D13DAA2B83B58B0073A831 /* Source.Payment.Installment.swift in Sources */, 75D13DD62B8653360073A831 /* UIControl.State+Hashable.swift in Sources */, + 758A4E7F2BE38A89005E7B5A /* ThreeDSCustomization.swift in Sources */, 751E7BEC2B7F7CE50031B4A2 /* ViewPresentable.swift in Sources */, 751E7BEA2B7F77120031B4A2 /* Localized+Omise.swift in Sources */, 75D13DA52B83B58B0073A831 /* Source.swift in Sources */, 22445FFC1CF41DD300801D0F /* Globals.swift in Sources */, + 758A4E702BE38A5F005E7B5A /* String+PemCert.swift in Sources */, 7501265F2B8893C900ED5E14 /* PaymentFormController.swift in Sources */, 756C8F222A40694F00D53059 /* CreditCardPaymentViewContext.swift in Sources */, 751E7BF02B7FBA760031B4A2 /* UIColor+Omise.swift in Sources */, 751E7BF22B7FC90C0031B4A2 /* SourceType+Collections.swift in Sources */, 750126722B8A49BC00ED5E14 /* FPX.Bank+ViewPresentable.swift in Sources */, + 758A4E812BE38A89005E7B5A /* ThreeDSTextBoxCustomization.swift in Sources */, 75D13DE42B8666CC0073A831 /* SelectSourceTypeDelegate.swift in Sources */, 75CFC4DD2B73F75C00422A8F /* OmiseAPI.swift in Sources */, 75348D4E29C3FCC70008C8A3 /* Bundle+OmiseSDK.swift in Sources */, 755CD9232BA2000600E8FCDA /* ClientProtocol.swift in Sources */, 757E14352BD61DFE005F08AD /* ChoosePaymentMethodDelegate.swift in Sources */, + 758A4E802BE38A89005E7B5A /* ThreeDSUICustomization.swift in Sources */, 75E0EB9D2B7BD00F00E3198A /* CardBrand.swift in Sources */, + 758A4EA62BE9D5C0005E7B5A /* Closures.swift in Sources */, + 758A4E6E2BE38A5F005E7B5A /* CryptData.swift in Sources */, 75E0EB9A2B7BD00F00E3198A /* Currency.swift in Sources */, 75405A9C2A266F50008C21F6 /* AtomePaymentFormViewModel.swift in Sources */, 75D13DA72B83B58B0073A831 /* Source.Payment.BarcodeAlipay.swift in Sources */, 75CFC4DB2B73F5AF00422A8F /* NetworkService.swift in Sources */, + 758A4E6F2BE38A5F005E7B5A /* String+sha512.swift in Sources */, 75E0EBA32B7BD02200E3198A /* Token.swift in Sources */, 75405A932A26340E008C21F6 /* UIButton+Helpers.swift in Sources */, 751E7BD92B7E5D130031B4A2 /* PaymentMethod.swift in Sources */, 754B78182B8741CD00973B71 /* TapGestureHandler.swift in Sources */, + 758A4E742BE38A7E005E7B5A /* NetceteraConfig.swift in Sources */, 75D13DCB2B8531D20073A831 /* Assets.swift in Sources */, 986406321CFEA7E4004BCF51 /* CardNumberTextField.swift in Sources */, 750126742B8A580500ED5E14 /* SelectDuitNowOBWBankViewModel.swift in Sources */, @@ -1482,6 +1617,7 @@ 75E0EBBB2B7BD02F00E3198A /* SourceType.swift in Sources */, 75D13DAF2B83B58B0073A831 /* Source.Payment.Atome.swift in Sources */, 75CFC4F12B753B0F00422A8F /* Capability.swift in Sources */, + 758A4E7E2BE38A89005E7B5A /* ThreeDSToolbarCustomization.swift in Sources */, 75CFC4CE2B73AF0E00422A8F /* Client.swift in Sources */, 7502CA622A31A7E100766E7D /* CountryListViewModelProtocol.swift in Sources */, 75D13DC12B8400610073A831 /* SelectPaymentMethodViewModel.swift in Sources */, @@ -1491,6 +1627,7 @@ 755CD9212BA1C81600E8FCDA /* UIViewController+KeyboardAppearance.swift in Sources */, 8AEF7804224CABF800787470 /* EContextPaymentFormController.swift in Sources */, 75D13DC92B8529790073A831 /* SelectSourceTypePaymentViewModel.swift in Sources */, + 758A4E732BE38A7E005E7B5A /* NetceteraThreeDSController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1539,18 +1676,23 @@ buildActionMask = 2147483647; files = ( 7509D4E02A1AA1450050AB38 /* AtomeFormViewControllerTests.swift in Sources */, + 758A4E962BE38AFD005E7B5A /* ThreeDSButtonCustomizationTests.swift in Sources */, + 758A4E902BE38AFD005E7B5A /* NetceteraThreeDSControllerTests.swift in Sources */, 7509D4E42A1C89F10050AB38 /* AtomeFormViewModelMockup.swift in Sources */, 750708F42B7A765500A48DD0 /* OmiseSDKTests.swift in Sources */, 75E0EB7E2B7B5EE400E3198A /* CreateTokenPayloadTests.swift in Sources */, 75E0EB712B7A904100E3198A /* SourceFlowTests.swift in Sources */, 75E0EB832B7B79E700E3198A /* CapabilityTests.swift in Sources */, F615CBF8261565D600E1A2D9 /* CardExpiryDatePickerTests.swift in Sources */, + 758A4E9F2BE3DFDF005E7B5A /* URL+PathComponentsTests.swift in Sources */, + 758A4E922BE38AFD005E7B5A /* UICustomizationTests.swift in Sources */, 75E0EB722B7A962600E3198A /* CreateSourcePayloadTests.swift in Sources */, 753279382A31B40F008048AD /* CountryListViewModelMockup.swift in Sources */, 7509D4E72A1C8E3D0050AB38 /* AtomeFormViewModelTests.swift in Sources */, 7509D4E22A1C876B0050AB38 /* AtomeFormViewContextMockup.swift in Sources */, 750708E32B790B8300A48DD0 /* String+JSON.swift in Sources */, 750708E52B790BD600A48DD0 /* SampleData.swift in Sources */, + 758A4E972BE38AFD005E7B5A /* SHA512Tests.swift in Sources */, 750708E02B7909BB00A48DD0 /* SourceTests.swift in Sources */, 75CFC4D42B73B21100422A8F /* ClientTests.swift in Sources */, ); @@ -2049,6 +2191,17 @@ }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + 758A4E822BE38AC3005E7B5A /* XCRemoteSwiftPackageReference "SPM" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ios-3ds-sdk/SPM.git"; + requirement = { + kind = exactVersion; + version = 2.4.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ 7509D4ED2A1D1F9F0050AB38 /* OmiseTestSDK */ = { isa = XCSwiftPackageProductDependency; @@ -2058,6 +2211,16 @@ isa = XCSwiftPackageProductDependency; productName = OmiseUnitTestKit; }; + 758A4E832BE38AC3005E7B5A /* ThreeDS_SDK */ = { + isa = XCSwiftPackageProductDependency; + package = 758A4E822BE38AC3005E7B5A /* XCRemoteSwiftPackageReference "SPM" */; + productName = ThreeDS_SDK; + }; + 758A4E852BE38AD0005E7B5A /* ThreeDS_SDK */ = { + isa = XCSwiftPackageProductDependency; + package = 758A4E822BE38AC3005E7B5A /* XCRemoteSwiftPackageReference "SPM" */; + productName = ThreeDS_SDK; + }; 75D13E0E2B8678530073A831 /* OmiseSwiftUIKit */ = { isa = XCSwiftPackageProductDependency; productName = OmiseSwiftUIKit; diff --git a/dev.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/dev.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..2d52cade --- /dev/null +++ b/dev.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "ThreeDS_SDK", + "repositoryURL": "https://github.com/ios-3ds-sdk/SPM.git", + "state": { + "branch": null, + "revision": "a4e55d7f10294ea81abd1db393ae05606a5efa46", + "version": "2.4.0" + } + } + ] + }, + "version": 1 +} diff --git a/sonar-project.properties b/sonar-project.properties index d4e62d10..eab5e4e5 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -10,7 +10,7 @@ sonar.tests=OmiseSDKTests sonar.swift.simulator=platform=iOS Simulator,name=iPhone 12,OS=latest sonar.swift.excludedPathsFromCoverage=.*Tests.* -sonar.swift.project=OmiseSDK.xcodeproj +sonar.swift.project=dev.xcodeproj sonar.swift.appScheme=OmiseSDK sonar.language=swift