diff --git a/CHANGELOG.md b/CHANGELOG.md index 20af0e3b2..38b6d2d9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,19 @@ # PayPal iOS SDK Release Notes -## unreleased +## 0.0.11 (2023-08-22) +* PayPalNativePayments + * Bump `PayPalCheckout` to `1.1.0` * CardPayments * Add `vault` method * Add `CardVaultRequest` and `CardVaultResult` types for interacting with `vault` method * Add `CardVaultDelegate` protocol to receive success and failure results * Add `CardVaultDelegate` property to `CardClient` - +* Breaking Changes + * FraudProtection + * Update `PayPalDataCollector` constructor to require a configuration instead of an environment + * Remove `PayPalDataCollectorEnvironment` enum + ## 0.0.10 (2023-08-14) * PayPalNativePayments * Bump `PayPalCheckout` to `1.0.0` diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index cb56d6749..fb5cf37ba 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -7,13 +7,27 @@ objects = { /* Begin PBXBuildFile section */ + 3B20273D2A89E3F00007907E /* CreateSetupTokenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B20273C2A89E3F00007907E /* CreateSetupTokenView.swift */; }; + 3B20273F2A89F24E0007907E /* CardVaultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B20273E2A89F24E0007907E /* CardVaultViewModel.swift */; }; + 3B2027412A8A72050007907E /* CardVaultState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2027402A8A72050007907E /* CardVaultState.swift */; }; + 3B2027432A8A95EF0007907E /* SetupTokenResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2027422A8A95EF0007907E /* SetupTokenResultView.swift */; }; + 3B2027452A8AA78B0007907E /* UpdateSetupTokenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2027442A8AA78B0007907E /* UpdateSetupTokenView.swift */; }; 3B22E8BA2A842D8900962E34 /* PaymentTokenRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B22E8B92A842D8900962E34 /* PaymentTokenRequest.swift */; }; 3B22E8BC2A84397600962E34 /* PaymentTokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B22E8BB2A84397600962E34 /* PaymentTokenResponse.swift */; }; + 3B4DD9A02A892A7000F4A716 /* CardVaultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4DD99F2A892A7000F4A716 /* CardVaultView.swift */; }; + 3B4DD9A22A8982B000F4A716 /* CardFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4DD9A12A8982B000F4A716 /* CardFormView.swift */; }; 3B80D50E2A291C0800D2EAC4 /* ClientIDRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B80D50D2A291C0800D2EAC4 /* ClientIDRequest.swift */; }; 3B80D5102A291CB100D2EAC4 /* ClientIDResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B80D50F2A291CB100D2EAC4 /* ClientIDResponse.swift */; }; + 3B8EF4DB2A932DA300A70D0B /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B8EF4DA2A932DA300A70D0B /* ErrorView.swift */; }; 3BB7A9772A5CA6FD00C05140 /* MerchantIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BB7A9762A5CA6FD00C05140 /* MerchantIntegration.swift */; }; + 3BC622072A97115700251B85 /* RoundedBlueButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BC622062A97115700251B85 /* RoundedBlueButtonStyle.swift */; }; + 3BC622092A97198500251B85 /* LeadingText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BC622082A97198500251B85 /* LeadingText.swift */; }; + 3BC6220B2A97204E00251B85 /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BC6220A2A97204E00251B85 /* CircularProgressView.swift */; }; 3BDB348E2A7CB02C008100D7 /* SetupTokenRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDB348D2A7CB02C008100D7 /* SetupTokenRequest.swift */; }; 3BDB34922A7CB5DE008100D7 /* SetupTokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDB34912A7CB5DE008100D7 /* SetupTokenResponse.swift */; }; + 3BF999762A8AC093009CBDF2 /* UpdateSetupTokenResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF999752A8AC093009CBDF2 /* UpdateSetupTokenResultView.swift */; }; + 3BF999782A8AD072009CBDF2 /* CreatePaymentTokenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF999772A8AD072009CBDF2 /* CreatePaymentTokenView.swift */; }; + 3BF9997A2A8AE12C009CBDF2 /* PaymentTokenResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF999792A8AE12C009CBDF2 /* PaymentTokenResultView.swift */; }; 5301468C28918B4D00184F22 /* ApprovalResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5301468B28918B4D00184F22 /* ApprovalResult.swift */; }; 536A5CA82898AA2A005C053D /* SwiftUINativeCheckoutDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536A5CA72898AA2A005C053D /* SwiftUINativeCheckoutDemo.swift */; }; 53B9E8EA28C93B4400719239 /* OrderRequestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53B9E8E928C93B4400719239 /* OrderRequestHelpers.swift */; }; @@ -101,13 +115,27 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 3B20273C2A89E3F00007907E /* CreateSetupTokenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSetupTokenView.swift; sourceTree = ""; }; + 3B20273E2A89F24E0007907E /* CardVaultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultViewModel.swift; sourceTree = ""; }; + 3B2027402A8A72050007907E /* CardVaultState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultState.swift; sourceTree = ""; }; + 3B2027422A8A95EF0007907E /* SetupTokenResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupTokenResultView.swift; sourceTree = ""; }; + 3B2027442A8AA78B0007907E /* UpdateSetupTokenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSetupTokenView.swift; sourceTree = ""; }; 3B22E8B92A842D8900962E34 /* PaymentTokenRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentTokenRequest.swift; sourceTree = ""; }; 3B22E8BB2A84397600962E34 /* PaymentTokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentTokenResponse.swift; sourceTree = ""; }; + 3B4DD99F2A892A7000F4A716 /* CardVaultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultView.swift; sourceTree = ""; }; + 3B4DD9A12A8982B000F4A716 /* CardFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardFormView.swift; sourceTree = ""; }; 3B80D50D2A291C0800D2EAC4 /* ClientIDRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientIDRequest.swift; sourceTree = ""; }; 3B80D50F2A291CB100D2EAC4 /* ClientIDResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientIDResponse.swift; sourceTree = ""; }; + 3B8EF4DA2A932DA300A70D0B /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; 3BB7A9762A5CA6FD00C05140 /* MerchantIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MerchantIntegration.swift; sourceTree = ""; }; + 3BC622062A97115700251B85 /* RoundedBlueButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedBlueButtonStyle.swift; sourceTree = ""; }; + 3BC622082A97198500251B85 /* LeadingText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeadingText.swift; sourceTree = ""; }; + 3BC6220A2A97204E00251B85 /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = ""; }; 3BDB348D2A7CB02C008100D7 /* SetupTokenRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupTokenRequest.swift; sourceTree = ""; }; 3BDB34912A7CB5DE008100D7 /* SetupTokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupTokenResponse.swift; sourceTree = ""; }; + 3BF999752A8AC093009CBDF2 /* UpdateSetupTokenResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSetupTokenResultView.swift; sourceTree = ""; }; + 3BF999772A8AD072009CBDF2 /* CreatePaymentTokenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePaymentTokenView.swift; sourceTree = ""; }; + 3BF999792A8AE12C009CBDF2 /* PaymentTokenResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentTokenResultView.swift; sourceTree = ""; }; 5301468B28918B4D00184F22 /* ApprovalResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApprovalResult.swift; sourceTree = ""; }; 536A5CA22898A48C005C053D /* PayPalNativeCheckout.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PayPalNativeCheckout.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 536A5CA72898AA2A005C053D /* SwiftUINativeCheckoutDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUINativeCheckoutDemo.swift; sourceTree = ""; }; @@ -190,6 +218,20 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3B43290F2A8FD7FD00C5441A /* CardVaultViews */ = { + isa = PBXGroup; + children = ( + 3B4DD99F2A892A7000F4A716 /* CardVaultView.swift */, + 3B20273C2A89E3F00007907E /* CreateSetupTokenView.swift */, + 3B2027422A8A95EF0007907E /* SetupTokenResultView.swift */, + 3B2027442A8AA78B0007907E /* UpdateSetupTokenView.swift */, + 3BF999752A8AC093009CBDF2 /* UpdateSetupTokenResultView.swift */, + 3BF999772A8AD072009CBDF2 /* CreatePaymentTokenView.swift */, + 3BF999792A8AE12C009CBDF2 /* PaymentTokenResultView.swift */, + ); + path = CardVaultViews; + sourceTree = ""; + }; 53B9E8E828C93B2B00719239 /* Helpers */ = { isa = PBXGroup; children = ( @@ -321,6 +363,8 @@ children = ( BE9F36E6275548A600AFC7DA /* BaseViewModel.swift */, CB34B32228BE3A9A001325B9 /* PayPalViewModel.swift */, + 3B20273E2A89F24E0007907E /* CardVaultViewModel.swift */, + 3B2027402A8A72050007907E /* CardVaultState.swift */, ); path = ViewModels; sourceTree = ""; @@ -348,12 +392,18 @@ BEDE3047275E998700D275FD /* SwiftUIComponents */ = { isa = PBXGroup; children = ( + 3B4DD9A12A8982B000F4A716 /* CardFormView.swift */, + 3B8EF4DA2A932DA300A70D0B /* ErrorView.swift */, + 3B43290F2A8FD7FD00C5441A /* CardVaultViews */, BE9F36DB274578D100AFC7DA /* FeatureBaseViewControllerRepresentable.swift */, BE9F36D72745490400AFC7DA /* FloatingLabelTextField.swift */, BE31187D273F02A80021C5A2 /* SwiftUICardDemo.swift */, BEDE304B275FA4A100D275FD /* SwiftUIPayPalDemo.swift */, CB9ED44D28411B110081F4DE /* SwiftUIPaymentButtonDemo.swift */, 536A5CA72898AA2A005C053D /* SwiftUINativeCheckoutDemo.swift */, + 3BC622062A97115700251B85 /* RoundedBlueButtonStyle.swift */, + 3BC622082A97198500251B85 /* LeadingText.swift */, + 3BC6220A2A97204E00251B85 /* CircularProgressView.swift */, ); path = SwiftUIComponents; sourceTree = ""; @@ -486,16 +536,23 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3B2027412A8A72050007907E /* CardVaultState.swift in Sources */, BECD84A427036E02007CCAE4 /* DemoType.swift in Sources */, 80F33CED26F8E7A9006811B1 /* Order.swift in Sources */, BE31187E273F02A80021C5A2 /* SwiftUICardDemo.swift in Sources */, + 3B4DD9A02A892A7000F4A716 /* CardVaultView.swift in Sources */, + 3BC622092A97198500251B85 /* LeadingText.swift in Sources */, 3B80D5102A291CB100D2EAC4 /* ClientIDResponse.swift in Sources */, CB34B32328BE3A9A001325B9 /* PayPalViewModel.swift in Sources */, + 3BC622072A97115700251B85 /* RoundedBlueButtonStyle.swift in Sources */, + 3B4DD9A22A8982B000F4A716 /* CardFormView.swift in Sources */, BED04233271084DF00C80954 /* CardFormatter.swift in Sources */, CB9ED44E28411B120081F4DE /* SwiftUIPaymentButtonDemo.swift in Sources */, 3B22E8BC2A84397600962E34 /* PaymentTokenResponse.swift in Sources */, 80561B3E26FB72D80023138C /* FeatureBaseViewController.swift in Sources */, BE9F36DC274578D100AFC7DA /* FeatureBaseViewControllerRepresentable.swift in Sources */, + 3B2027432A8A95EF0007907E /* SetupTokenResultView.swift in Sources */, + 3BF999762A8AC093009CBDF2 /* UpdateSetupTokenResultView.swift in Sources */, BECD84A027036DC2007CCAE4 /* Environment.swift in Sources */, 806F1E3D26B85367007A60E6 /* ViewController.swift in Sources */, 80F33CF126F8E7D9006811B1 /* ProcessOrderParams.swift in Sources */, @@ -512,17 +569,24 @@ BED041AF270CA0FB00C80954 /* CustomButton.swift in Sources */, 3BB7A9772A5CA6FD00C05140 /* MerchantIntegration.swift in Sources */, BE1766B326F911A2007EF438 /* URLResponseError.swift in Sources */, + 3B2027452A8AA78B0007907E /* UpdateSetupTokenView.swift in Sources */, + 3B8EF4DB2A932DA300A70D0B /* ErrorView.swift in Sources */, + 3BF9997A2A8AE12C009CBDF2 /* PaymentTokenResultView.swift in Sources */, 3BDB34922A7CB5DE008100D7 /* SetupTokenResponse.swift in Sources */, CBC16DD929ED90B600307117 /* UpdateOrderParams.swift in Sources */, BE9F36D82745490400AFC7DA /* FloatingLabelTextField.swift in Sources */, BE9F36E7275548A600AFC7DA /* BaseViewModel.swift in Sources */, 806F1E3926B85367007A60E6 /* AppDelegate.swift in Sources */, + 3B20273D2A89E3F00007907E /* CreateSetupTokenView.swift in Sources */, 3B22E8BA2A842D8900962E34 /* PaymentTokenRequest.swift in Sources */, 536A5CA82898AA2A005C053D /* SwiftUINativeCheckoutDemo.swift in Sources */, 806F1E3B26B85367007A60E6 /* SceneDelegate.swift in Sources */, + 3B20273F2A89F24E0007907E /* CardVaultViewModel.swift in Sources */, BC6460CD2A12A2A0002B974B /* EmptyBodyParams.swift in Sources */, + 3BF999782A8AD072009CBDF2 /* CreatePaymentTokenView.swift in Sources */, BED042312710833F00C80954 /* CardType.swift in Sources */, 5301468C28918B4D00184F22 /* ApprovalResult.swift in Sources */, + 3BC6220B2A97204E00251B85 /* CircularProgressView.swift in Sources */, BEDE304C275FA4A100D275FD /* SwiftUIPayPalDemo.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -795,7 +859,7 @@ repositoryURL = "https://github.com/paypal/paypalcheckout-ios"; requirement = { kind = exactVersion; - version = 1.0.0; + version = 1.1.0; }; }; BE1766D526FA7AC3007EF438 /* XCRemoteSwiftPackageReference "InAppSettingsKit" */ = { diff --git a/Demo/Demo/DemoSettings/DemoType.swift b/Demo/Demo/DemoSettings/DemoType.swift index 9f8af20df..b0bb4610c 100644 --- a/Demo/Demo/DemoSettings/DemoType.swift +++ b/Demo/Demo/DemoSettings/DemoType.swift @@ -3,6 +3,7 @@ import SwiftUI enum DemoType: String { case card + case cardVault case payPalWebCheckout case paymentButtonCustomization case payPalNativeCheckout @@ -11,6 +12,8 @@ enum DemoType: String { switch self { case .card: return AnyView(SwiftUICardDemo()) + case .cardVault: + return AnyView(CardVaultView()) case .payPalWebCheckout: return AnyView(SwiftUIPayPalDemo()) case .paymentButtonCustomization: diff --git a/Demo/Demo/Info.plist b/Demo/Demo/Info.plist index 5928ebc05..4784879bb 100644 --- a/Demo/Demo/Info.plist +++ b/Demo/Demo/Info.plist @@ -3,7 +3,7 @@ CFBundleShortVersionString - 0.0.10 + 0.0.11 UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/Demo/Demo/Models/PaymentTokenRequest.swift b/Demo/Demo/Models/PaymentTokenRequest.swift index d75778b4f..ade3afc6e 100644 --- a/Demo/Demo/Models/PaymentTokenRequest.swift +++ b/Demo/Demo/Models/PaymentTokenRequest.swift @@ -7,7 +7,7 @@ struct PaymentTokenRequest { var path: String { "/payment_tokens/" } - + var method: String { "POST" } @@ -25,7 +25,7 @@ struct PaymentTokenRequest { ] ] ] - + return try? JSONSerialization.data(withJSONObject: requestBody) } } diff --git a/Demo/Demo/Models/PaymentTokenResponse.swift b/Demo/Demo/Models/PaymentTokenResponse.swift index 8799e48ca..dfb58c1d2 100644 --- a/Demo/Demo/Models/PaymentTokenResponse.swift +++ b/Demo/Demo/Models/PaymentTokenResponse.swift @@ -1,6 +1,6 @@ import Foundation -struct PaymentTokenResponse: Decodable { +struct PaymentTokenResponse: Decodable, Equatable { let id: String let customer: Customer @@ -14,17 +14,17 @@ struct PaymentTokenResponse: Decodable { } } -struct Customer: Decodable { +struct Customer: Decodable, Equatable { let id: String } -struct PaymentSource: Decodable { +struct PaymentSource: Decodable, Equatable { let card: BasicCard } -struct BasicCard: Decodable { +struct BasicCard: Decodable, Equatable { let brand: String? let lastDigits: String diff --git a/Demo/Demo/Models/SetupTokenRequest.swift b/Demo/Demo/Models/SetupTokenRequest.swift index 9c3b34c8f..f584aae61 100644 --- a/Demo/Demo/Models/SetupTokenRequest.swift +++ b/Demo/Demo/Models/SetupTokenRequest.swift @@ -7,7 +7,7 @@ struct SetUpTokenRequest { var path: String { "/setup_tokens/" } - + var method: String { "POST" } @@ -29,7 +29,7 @@ struct SetUpTokenRequest { ] ] ] - + return try? JSONSerialization.data(withJSONObject: requestBody) } } diff --git a/Demo/Demo/Models/SetupTokenResponse.swift b/Demo/Demo/Models/SetupTokenResponse.swift index 9ebd82308..f79552b4d 100644 --- a/Demo/Demo/Models/SetupTokenResponse.swift +++ b/Demo/Demo/Models/SetupTokenResponse.swift @@ -1,6 +1,16 @@ import Foundation -struct SetUpTokenResponse: Decodable { +struct SetUpTokenResponse: Decodable, Equatable { + + static func == (lhs: SetUpTokenResponse, rhs: SetUpTokenResponse) -> Bool { + lhs.id == rhs.id + } let id, status: String + let customer: Customer? + + struct Customer: Decodable { + + let id: String + } } diff --git a/Demo/Demo/Networking/DemoMerchantAPI.swift b/Demo/Demo/Networking/DemoMerchantAPI.swift index da82c3321..46d06119c 100644 --- a/Demo/Demo/Networking/DemoMerchantAPI.swift +++ b/Demo/Demo/Networking/DemoMerchantAPI.swift @@ -21,6 +21,9 @@ final class DemoMerchantAPI { func getSetupToken(customerID: String? = nil, selectedMerchantIntegration: MerchantIntegration) async throws -> SetUpTokenResponse { do { + // TODO: pass in headers depending on integration type + // Different request struct or integration type property + // in SetUpTokenRequest to conditionally add header let request = SetUpTokenRequest(customerID: customerID) let urlRequest = try createSetupTokenUrlRequest( setupTokenRequest: request, @@ -43,8 +46,8 @@ final class DemoMerchantAPI { paymentTokenRequest: request, environment: DemoSettings.environment, selectedMerchantIntegration: selectedMerchantIntegration - ) - let data = try await data(for: urlRequest) + ) + let data = try await data(for: urlRequest) return try parse(from: data) } catch { print("error with the create payment token request: \(error.localizedDescription)") diff --git a/Demo/Demo/SceneDelegate.swift b/Demo/Demo/SceneDelegate.swift index 19cdc3579..4a6a524dd 100644 --- a/Demo/Demo/SceneDelegate.swift +++ b/Demo/Demo/SceneDelegate.swift @@ -57,6 +57,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { if launchArgs.contains("-DemoTypeCard") { DemoSettings.demoType = .card + } else if launchArgs.contains("-DemoTypeCardVault") { + DemoSettings.demoType = .cardVault } else if launchArgs.contains("-DemoTypePayPalWebCheckout") { DemoSettings.demoType = .payPalWebCheckout } else if launchArgs.contains("-DemoTypePaymentButtonCustomization") { diff --git a/Demo/Demo/SwiftUIComponents/CardFormView.swift b/Demo/Demo/SwiftUIComponents/CardFormView.swift new file mode 100644 index 000000000..25703d16c --- /dev/null +++ b/Demo/Demo/SwiftUIComponents/CardFormView.swift @@ -0,0 +1,42 @@ +import SwiftUI + +struct CardFormView: View { + + @Binding var cardNumberText: String + @Binding var expirationDateText: String + @Binding var cvvText: String + + private let cardFormatter = CardFormatter() + + var body: some View { + VStack(spacing: 16) { + FloatingLabelTextField(placeholder: "Card Number", text: $cardNumberText) + .onChange(of: cardNumberText) { newValue in + cardNumberText = cardFormatter.formatFieldWith(newValue, field: .cardNumber) + } + FloatingLabelTextField(placeholder: "Expiration Date", text: $expirationDateText) + .onChange(of: expirationDateText) { newValue in + expirationDateText = cardFormatter.formatFieldWith(newValue, field: .expirationDate) + } + FloatingLabelTextField(placeholder: "CVV", text: $cvvText) + .onChange(of: cvvText) { newValue in + cvvText = cardFormatter.formatFieldWith(newValue, field: .cvv) + } + } + } +} + +struct CardFormView_Previews: PreviewProvider { + + @State static var mockCardNumberText: String = "41111111111111111" + @State static var mockExpirationDateText: String = "01/25" + @State static var mockCvvText: String = "123" + + static var previews: some View { + CardFormView( + cardNumberText: $mockCardNumberText, + expirationDateText: $mockExpirationDateText, + cvvText: $mockCvvText + ) + } +} diff --git a/Demo/Demo/SwiftUIComponents/CardVaultViews/CardVaultView.swift b/Demo/Demo/SwiftUIComponents/CardVaultViews/CardVaultView.swift new file mode 100644 index 000000000..24d8d6e62 --- /dev/null +++ b/Demo/Demo/SwiftUIComponents/CardVaultViews/CardVaultView.swift @@ -0,0 +1,67 @@ +import SwiftUI + +struct CardVaultView: View { + + @StateObject var baseViewModel = BaseViewModel() + @StateObject var cardVaultViewModel = CardVaultViewModel() + + // MARK: Views + + var body: some View { + ScrollView { + ScrollViewReader { scrollView in + VStack(spacing: 16) { + CreateSetupTokenView( + selectedMerchantIntegration: baseViewModel.selectedMerchantIntegration, + cardVaultViewModel: cardVaultViewModel + ) + SetupTokenResultView(cardVaultViewModel: cardVaultViewModel) + if let setupToken = cardVaultViewModel.state.setupToken { + UpdateSetupTokenView(baseViewModel: baseViewModel, cardVaultViewModel: cardVaultViewModel, setupToken: setupToken.id) + } + UpdateSetupTokenResultView(cardVaultViewModel: cardVaultViewModel) + if let updateSetupToken = cardVaultViewModel.state.updateSetupToken { + CreatePaymentTokenView( + cardVaultViewModel: cardVaultViewModel, + selectedMerchantIntegration: baseViewModel.selectedMerchantIntegration, + setupToken: updateSetupToken.id + ) + } + PaymentTokenResultView(cardVaultViewModel: cardVaultViewModel) + switch cardVaultViewModel.state.paymentTokenResponse { + case .loaded, .error: + VStack { + Button("Reset") { + cardVaultViewModel.resetState() + } + .foregroundColor(.black) + .padding() + .frame(maxWidth: .infinity) + .background(.gray) + .cornerRadius(10) + } + .padding(5) + default: + EmptyView() + } + Text("") + .id("bottomView") + .frame(maxWidth: .infinity, alignment: .top) + .padding(.horizontal, 10) + .onChange(of: cardVaultViewModel.state) { _ in + withAnimation { + scrollView.scrollTo("bottomView") + } + } + } + } + } + } +} + +struct CardVault_Previews: PreviewProvider { + + static var previews: some View { + CardVaultView() + } +} diff --git a/Demo/Demo/SwiftUIComponents/CardVaultViews/CreatePaymentTokenView.swift b/Demo/Demo/SwiftUIComponents/CardVaultViews/CreatePaymentTokenView.swift new file mode 100644 index 000000000..d891e7ede --- /dev/null +++ b/Demo/Demo/SwiftUIComponents/CardVaultViews/CreatePaymentTokenView.swift @@ -0,0 +1,51 @@ +import SwiftUI + +struct CreatePaymentTokenView: View { + + let selectedMerchantIntegration: MerchantIntegration + let setupToken: String + + @ObservedObject var cardVaultViewModel: CardVaultViewModel + + public init(cardVaultViewModel: CardVaultViewModel, selectedMerchantIntegration: MerchantIntegration, setupToken: String) { + self.cardVaultViewModel = cardVaultViewModel + self.selectedMerchantIntegration = selectedMerchantIntegration + self.setupToken = setupToken + } + + var body: some View { + VStack(spacing: 16) { + HStack { + Text("Create a Payment Method Token") + .font(.system(size: 20)) + Spacer() + } + .frame(maxWidth: .infinity) + .font(.headline) + ZStack { + Button("Create Payment Token") { + Task { + do { + try await cardVaultViewModel.getPaymentToken( + setupToken: setupToken, + selectedMerchantIntegration: selectedMerchantIntegration + ) + } catch { + print("Error in getting setup token. \(error.localizedDescription)") + } + } + } + .buttonStyle(RoundedBlueButtonStyle()) + if case .loading = cardVaultViewModel.state.paymentTokenResponse { + CircularProgressView() + } + } + } + .padding() + .background( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.gray, lineWidth: 2) + .padding(5) + ) + } +} diff --git a/Demo/Demo/SwiftUIComponents/CardVaultViews/CreateSetupTokenView.swift b/Demo/Demo/SwiftUIComponents/CardVaultViews/CreateSetupTokenView.swift new file mode 100644 index 000000000..fb182cac2 --- /dev/null +++ b/Demo/Demo/SwiftUIComponents/CardVaultViews/CreateSetupTokenView.swift @@ -0,0 +1,51 @@ +import SwiftUI + +struct CreateSetupTokenView: View { + + let selectedMerchantIntegration: MerchantIntegration + + @State private var vaultCustomerID: String = "" + @ObservedObject var cardVaultViewModel: CardVaultViewModel + + public init(selectedMerchantIntegration: MerchantIntegration, cardVaultViewModel: CardVaultViewModel) { + self.selectedMerchantIntegration = selectedMerchantIntegration + self.cardVaultViewModel = cardVaultViewModel + } + + var body: some View { + VStack(spacing: 16) { + HStack { + Text("Vault without Purchase requires creation of setup token:") + .font(.system(size: 20)) + Spacer() + } + .frame(maxWidth: .infinity) + .font(.headline) + FloatingLabelTextField(placeholder: "Vault Customer ID (Optional)", text: $vaultCustomerID) + ZStack { + Button("Create Setup Token") { + Task { + do { + try await cardVaultViewModel.getSetupToken( + customerID: vaultCustomerID.isEmpty ? nil : vaultCustomerID, + selectedMerchantIntegration: selectedMerchantIntegration + ) + } catch { + print("Error in getting setup token. \(error.localizedDescription)") + } + } + } + .buttonStyle(RoundedBlueButtonStyle()) + if case .loading = cardVaultViewModel.state.setupTokenResponse { + CircularProgressView() + } + } + } + .padding() + .background( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.gray, lineWidth: 2) + .padding(5) + ) + } +} diff --git a/Demo/Demo/SwiftUIComponents/CardVaultViews/PaymentTokenResultView.swift b/Demo/Demo/SwiftUIComponents/CardVaultViews/PaymentTokenResultView.swift new file mode 100644 index 000000000..d3d9f22ac --- /dev/null +++ b/Demo/Demo/SwiftUIComponents/CardVaultViews/PaymentTokenResultView.swift @@ -0,0 +1,42 @@ +import SwiftUI + +struct PaymentTokenResultView: View { + + @ObservedObject var cardVaultViewModel: CardVaultViewModel + + var body: some View { + switch cardVaultViewModel.state.paymentTokenResponse { + case .idle, .loading: + EmptyView() + case .loaded(let paymentTokenResponse): + getSucessView(paymentTokenResponse: paymentTokenResponse) + case .error(let errorMessage): + ErrorView(errorMessage: errorMessage) + } + } + + func getSucessView(paymentTokenResponse: PaymentTokenResponse) -> some View { + VStack(spacing: 16) { + HStack { + Text("Payment Token") + .font(.system(size: 20)) + Spacer() + } + LeadingText("ID", weight: .bold) + LeadingText("\(paymentTokenResponse.id)") + LeadingText("Customer ID", weight: .bold) + LeadingText("\(paymentTokenResponse.customer.id)") + LeadingText("Card Brand", weight: .bold) + LeadingText("\(paymentTokenResponse.paymentSource.card.brand ?? "")") + LeadingText("Card Last 4", weight: .bold) + LeadingText("\(paymentTokenResponse.paymentSource.card.lastDigits)") + } + .frame(maxWidth: .infinity) + .padding() + .background( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.gray, lineWidth: 2) + .padding(5) + ) + } +} diff --git a/Demo/Demo/SwiftUIComponents/CardVaultViews/SetupTokenResultView.swift b/Demo/Demo/SwiftUIComponents/CardVaultViews/SetupTokenResultView.swift new file mode 100644 index 000000000..4e4bd054e --- /dev/null +++ b/Demo/Demo/SwiftUIComponents/CardVaultViews/SetupTokenResultView.swift @@ -0,0 +1,40 @@ +import SwiftUI + +struct SetupTokenResultView: View { + + @ObservedObject var cardVaultViewModel: CardVaultViewModel + + var body: some View { + switch cardVaultViewModel.state.setupTokenResponse { + case .idle, .loading: + EmptyView() + case .loaded(let setupTokenResponse): + getSuccessView(setupTokenResponse: setupTokenResponse) + case .error(let errorMessage): + ErrorView(errorMessage: errorMessage) + } + } + + func getSuccessView(setupTokenResponse: SetUpTokenResponse) -> some View { + VStack(spacing: 16) { + HStack { + Text("Setup Token") + .font(.system(size: 20)) + Spacer() + } + LeadingText("ID", weight: .bold) + LeadingText("\(setupTokenResponse.id)") + LeadingText("Customer ID", weight: .bold) + LeadingText("\(setupTokenResponse.customer?.id ?? "")") + LeadingText("Status", weight: .bold) + LeadingText("\(setupTokenResponse.status)") + } + .frame(maxWidth: .infinity) + .padding() + .background( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.gray, lineWidth: 2) + .padding(5) + ) + } +} diff --git a/Demo/Demo/SwiftUIComponents/CardVaultViews/UpdateSetupTokenResultView.swift b/Demo/Demo/SwiftUIComponents/CardVaultViews/UpdateSetupTokenResultView.swift new file mode 100644 index 000000000..e4501b4e8 --- /dev/null +++ b/Demo/Demo/SwiftUIComponents/CardVaultViews/UpdateSetupTokenResultView.swift @@ -0,0 +1,38 @@ +import SwiftUI + +struct UpdateSetupTokenResultView: View { + + @ObservedObject var cardVaultViewModel: CardVaultViewModel + + var body: some View { + switch cardVaultViewModel.state.updateSetupTokenResponse { + case .idle, .loading: + EmptyView() + case .loaded(let updateSetupTokenResponse): + getSuccessView(updateSetupTokenResponse: updateSetupTokenResponse) + case .error(let errorMessage): + ErrorView(errorMessage: errorMessage) + } + } + + func getSuccessView(updateSetupTokenResponse: CardVaultState.UpdateSetupTokenResult) -> some View { + VStack(spacing: 16) { + HStack { + Text("Vault Success") + .font(.system(size: 20)) + Spacer() + } + LeadingText("ID", weight: .bold) + LeadingText("\(updateSetupTokenResponse.id)") + LeadingText("Status", weight: .bold) + LeadingText("\(updateSetupTokenResponse.status)") + } + .frame(maxWidth: .infinity) + .padding() + .background( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.gray, lineWidth: 2) + .padding(5) + ) + } +} diff --git a/Demo/Demo/SwiftUIComponents/CardVaultViews/UpdateSetupTokenView.swift b/Demo/Demo/SwiftUIComponents/CardVaultViews/UpdateSetupTokenView.swift new file mode 100644 index 000000000..d94a79af3 --- /dev/null +++ b/Demo/Demo/SwiftUIComponents/CardVaultViews/UpdateSetupTokenView.swift @@ -0,0 +1,71 @@ +import SwiftUI + +struct UpdateSetupTokenView: View { + + let setupToken: String + + @State private var cardNumberText: String = "4111 1111 1111 1111" + @State private var expirationDateText: String = "01 / 25" + @State private var cvvText: String = "123" + + @ObservedObject var cardVaultViewModel: CardVaultViewModel + @ObservedObject var baseViewModel: BaseViewModel + + public init(baseViewModel: BaseViewModel, cardVaultViewModel: CardVaultViewModel, setupToken: String) { + self.cardVaultViewModel = cardVaultViewModel + self.baseViewModel = baseViewModel + self.setupToken = setupToken + } + + var body: some View { + VStack(spacing: 16) { + HStack { + Text("Vault Card") + .font(.system(size: 20)) + Spacer() + } + + CardFormView( + cardNumberText: $cardNumberText, + expirationDateText: $expirationDateText, + cvvText: $cvvText + ) + + let card = baseViewModel.createCard( + cardNumber: cardNumberText, + expirationDate: expirationDateText, + cvv: cvvText + ) + + ZStack { + Button("Vault Card") { + Task { + let config = await baseViewModel.getCoreConfig() + if let config, let card { + await cardVaultViewModel.vault( + config: config, + card: card, + setupToken: setupToken + ) + } else { + DispatchQueue.main.async { + cardVaultViewModel.state.updateSetupTokenResponse = .error(message: "Error getting Config or Card") + } + print("Error getting Config or Card") + } + } + } + .buttonStyle(RoundedBlueButtonStyle()) + if case .loading = cardVaultViewModel.state.updateSetupTokenResponse { + CircularProgressView() + } + } + } + .padding() + .background( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.gray, lineWidth: 2) + .padding(5) + ) + } +} diff --git a/Demo/Demo/SwiftUIComponents/CircularProgressView.swift b/Demo/Demo/SwiftUIComponents/CircularProgressView.swift new file mode 100644 index 000000000..fed9d8960 --- /dev/null +++ b/Demo/Demo/SwiftUIComponents/CircularProgressView.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct CircularProgressView: View { + + var body: some View { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: Color.white)) + .background(Color.black.opacity(0.4)) + .cornerRadius(10) + .frame(maxWidth: .infinity) + } +} + +struct CircularProgressView_Previews: PreviewProvider { + + static var previews: some View { + CircularProgressView() + } +} diff --git a/Demo/Demo/SwiftUIComponents/ErrorView.swift b/Demo/Demo/SwiftUIComponents/ErrorView.swift new file mode 100644 index 000000000..55cad0659 --- /dev/null +++ b/Demo/Demo/SwiftUIComponents/ErrorView.swift @@ -0,0 +1,20 @@ +import SwiftUI + +struct ErrorView: View { + + let errorMessage: String + + var body: some View { + VStack { + Text("\(errorMessage)") + .frame(maxWidth: .infinity) + } + .padding() + .background( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.gray, lineWidth: 2) + .padding(5) + ) + .frame(maxWidth: .infinity) + } +} diff --git a/Demo/Demo/SwiftUIComponents/LeadingText.swift b/Demo/Demo/SwiftUIComponents/LeadingText.swift new file mode 100644 index 000000000..97687b62e --- /dev/null +++ b/Demo/Demo/SwiftUIComponents/LeadingText.swift @@ -0,0 +1,18 @@ +import SwiftUI + +struct LeadingText: View { + + var text: String + var weight: Font.Weight? + + var body: some View { + Text(text) + .fontWeight(weight) + .frame(maxWidth: .infinity, alignment: .leading) + } + + init(_ text: String, weight: Font.Weight? = nil) { + self.text = text + self.weight = weight + } +} diff --git a/Demo/Demo/SwiftUIComponents/RoundedBlueButtonStyle.swift b/Demo/Demo/SwiftUIComponents/RoundedBlueButtonStyle.swift new file mode 100644 index 000000000..dbf234987 --- /dev/null +++ b/Demo/Demo/SwiftUIComponents/RoundedBlueButtonStyle.swift @@ -0,0 +1,13 @@ +import SwiftUI + +struct RoundedBlueButtonStyle: ButtonStyle { + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .foregroundColor(.white) + .padding() + .frame(maxWidth: .infinity) + .background(.blue) + .cornerRadius(10) + } +} diff --git a/Demo/Demo/SwiftUIComponents/SwiftUICardDemo.swift b/Demo/Demo/SwiftUIComponents/SwiftUICardDemo.swift index d308d26ae..8ee6e7ba6 100644 --- a/Demo/Demo/SwiftUIComponents/SwiftUICardDemo.swift +++ b/Demo/Demo/SwiftUIComponents/SwiftUICardDemo.swift @@ -6,9 +6,9 @@ struct SwiftUICardDemo: View { // MARK: Variables - @State private var cardNumberText: String = "" - @State private var expirationDateText: String = "" - @State private var cvvText: String = "" + @State private var cardNumberText: String = "4111 1111 1111 1111" + @State private var expirationDateText: String = "01 / 25" + @State private var cvvText: String = "123" @State private var vaultCustomerID: String = "" @State var shouldVaultSelected = false @@ -22,18 +22,7 @@ struct SwiftUICardDemo: View { ZStack { FeatureBaseViewControllerRepresentable(baseViewModel: baseViewModel) VStack(spacing: 16) { - FloatingLabelTextField(placeholder: "Card Number", text: $cardNumberText) - .onChange(of: cardNumberText) { newValue in - cardNumberText = cardFormatter.formatFieldWith(newValue, field: .cardNumber) - } - FloatingLabelTextField(placeholder: "Expiration Date", text: $expirationDateText) - .onChange(of: expirationDateText) { newValue in - expirationDateText = cardFormatter.formatFieldWith(newValue, field: .expirationDate) - } - FloatingLabelTextField(placeholder: "CVV", text: $cvvText) - .onChange(of: cvvText) { newValue in - cvvText = cardFormatter.formatFieldWith(newValue, field: .cvv) - } + CardFormView(cardNumberText: $cardNumberText, expirationDateText: $expirationDateText, cvvText: $cvvText) HStack { Toggle("Should Vault with Purchase", isOn: $shouldVaultSelected) // TODO: turn on if vault with purchase on sample server is implemented @@ -69,23 +58,6 @@ struct SwiftUICardDemo: View { ) .cornerRadius(10) .disabled(!baseViewModel.isCardFormValid(cardNumber: cardNumberText, expirationDate: expirationDateText, cvv: cvvText)) - Button("Vault Card without Purchase") { - guard let card = baseViewModel.createCard( - cardNumber: cardNumberText, - expirationDate: expirationDateText, - cvv: cvvText - ) else { - return - } - Task { - await baseViewModel.vaultCard(card: card, customerID: vaultCustomerID.isEmpty ? nil : vaultCustomerID) - } - } - .foregroundColor(.white) - .padding() - .frame(maxWidth: .infinity) - .background(.blue) - .cornerRadius(10) } .padding(.horizontal, 16) } diff --git a/Demo/Demo/ViewModels/BaseViewModel.swift b/Demo/Demo/ViewModels/BaseViewModel.swift index b1f0c21a7..8edf6e812 100644 --- a/Demo/Demo/ViewModels/BaseViewModel.swift +++ b/Demo/Demo/ViewModels/BaseViewModel.swift @@ -8,7 +8,7 @@ import PayPalCheckout /// This class is used to share the orderID across shared views, update the text of `bottomStatusLabel` in our `FeatureBaseViewController` /// as well as share the logic of `processOrder` across our duplicate (SwiftUI and UIKit) card views. -class BaseViewModel: ObservableObject, PayPalWebCheckoutDelegate, CardDelegate, CardVaultDelegate { +class BaseViewModel: ObservableObject, PayPalWebCheckoutDelegate, CardDelegate { /// Weak reference to associated view weak var view: FeatureBaseViewController? @@ -93,22 +93,6 @@ class BaseViewModel: ObservableObject, PayPalWebCheckoutDelegate, CardDelegate, let cardRequest = CardRequest(orderID: orderID, card: card, sca: .scaAlways) cardClient.approveOrder(request: cardRequest) } - - func vaultCard(card: Card, customerID: String? = nil) async { - do { - guard let config = await getCoreConfig() else { return } - let cardClient = CardClient(config: config) - cardClient.vaultDelegate = self - let tokenResponse = try await DemoMerchantAPI.sharedService.getSetupToken( - customerID: customerID, selectedMerchantIntegration: selectedMerchantIntegration - ) - - let cardVaultRequest = CardVaultRequest(card: card, setupTokenID: tokenResponse.id) - cardClient.vault(cardVaultRequest) - } catch { - print("Error in getSetupToken: \(error.localizedDescription)") - } - } func isCardFormValid(cardNumber: String, expirationDate: String, cvv: String) -> Bool { guard orderID != nil else { @@ -232,20 +216,6 @@ class BaseViewModel: ObservableObject, PayPalWebCheckoutDelegate, CardDelegate, updateTitle("3DS challenge has finished") print("3DS challenge has finished") } - - // MARK: - CardVault Delegate - - func card(_ cardClient: CardClient, didFinishWithVaultResult vaultResult: CardVaultResult) { - updateTitle( - "Vault without Purchase has finished. \n SetupTokenID: \(vaultResult.setupTokenID) \nVault Status: \(vaultResult.status)" - ) - print("Vault without purchase has finished: \(vaultResult)") - } - - func card(_ cardClient: CardClient, didFinishWithVaultError vaultError: CoreSDKError) { - updateTitle("Vault without purchase has failed: \(vaultError.localizedDescription)") - print("❌ There was an error: \(vaultError)") - } func getClientID() async -> String? { await DemoMerchantAPI.sharedService.getClientID( diff --git a/Demo/Demo/ViewModels/CardVaultState.swift b/Demo/Demo/ViewModels/CardVaultState.swift new file mode 100644 index 000000000..21bb946a5 --- /dev/null +++ b/Demo/Demo/ViewModels/CardVaultState.swift @@ -0,0 +1,47 @@ +import Foundation +import CardPayments + +struct CardVaultState: Equatable { + + struct UpdateSetupTokenResult: Decodable, Equatable { + + var id: String + var status: String + } + + var setupToken: SetUpTokenResponse? + var updateSetupToken: UpdateSetupTokenResult? + var paymentToken: PaymentTokenResponse? + + var setupTokenResponse: LoadingState = .idle { + didSet { + if case .loaded(let value) = setupTokenResponse { + setupToken = value + } + } + } + + var updateSetupTokenResponse: LoadingState = .idle { + didSet { + if case .loaded(let value) = updateSetupTokenResponse { + updateSetupToken = value + } + } + } + + var paymentTokenResponse: LoadingState = .idle { + didSet { + if case .loaded(let value) = paymentTokenResponse { + paymentToken = value + } + } + } +} + +enum LoadingState: Equatable { + + case idle + case loading + case error(message: String) + case loaded(_ value: T) +} diff --git a/Demo/Demo/ViewModels/CardVaultViewModel.swift b/Demo/Demo/ViewModels/CardVaultViewModel.swift new file mode 100644 index 000000000..77ca63180 --- /dev/null +++ b/Demo/Demo/ViewModels/CardVaultViewModel.swift @@ -0,0 +1,103 @@ +import UIKit +import CardPayments +import CorePayments + +class CardVaultViewModel: ObservableObject, CardVaultDelegate { + + @Published var state = CardVaultState() + + func getSetupToken( + customerID: String? = nil, + selectedMerchantIntegration: MerchantIntegration + ) async throws { + do { + DispatchQueue.main.async { + self.state.setupTokenResponse = .loading + } + let setupTokenResult = try await DemoMerchantAPI.sharedService.getSetupToken( + customerID: customerID, + selectedMerchantIntegration: selectedMerchantIntegration + ) + DispatchQueue.main.async { + self.state.setupTokenResponse = .loaded(setupTokenResult) + } + } catch { + DispatchQueue.main.async { + self.state.setupTokenResponse = .error(message: error.localizedDescription) + } + throw error + } + } + + func resetState() { + state = CardVaultState() + } + + func getPaymentToken( + setupToken: String, + selectedMerchantIntegration: MerchantIntegration + ) async throws { + do { + DispatchQueue.main.async { + self.state.paymentTokenResponse = .loading + } + let paymentTokenResult = try await DemoMerchantAPI.sharedService.getPaymentToken( + setupToken: setupToken, + selectedMerchantIntegration: selectedMerchantIntegration + ) + DispatchQueue.main.async { + self.state.paymentTokenResponse = .loaded(paymentTokenResult) + } + } catch { + DispatchQueue.main.async { + self.state.paymentTokenResponse = .error(message: error.localizedDescription) + } + throw error + } + } + + func vault(config: CoreConfig, card: Card, setupToken: String) async { + DispatchQueue.main.async { + self.state.updateSetupTokenResponse = .loading + } + let cardClient = CardClient(config: config) + cardClient.vaultDelegate = self + let cardVaultRequest = CardVaultRequest(card: card, setupTokenID: setupToken) + cardClient.vault(cardVaultRequest) + } + + func isCardFormValid(cardNumber: String, expirationDate: String, cvv: String) -> Bool { + let cleanedCardNumber = cardNumber.replacingOccurrences(of: " ", with: "") + let cleanedExpirationDate = expirationDate.replacingOccurrences(of: " / ", with: "") + + let enabled = cleanedCardNumber.count >= 15 && cleanedCardNumber.count <= 19 + && cleanedExpirationDate.count == 4 && cvv.count >= 3 && cvv.count <= 4 + return enabled + } + + func setUpTokenSuccessResult(vaultResult: CardPayments.CardVaultResult) { + DispatchQueue.main.async { + self.state.updateSetupTokenResponse = .loaded( + CardVaultState.UpdateSetupTokenResult(id: vaultResult.setupTokenID, status: vaultResult.status) + ) + } + } + + func setUpdateSetupTokenFailureResult(vaultError: CorePayments.CoreSDKError) { + DispatchQueue.main.async { + self.state.updateSetupTokenResponse = .error(message: vaultError.localizedDescription) + } + } + + // MARK: - CardVault Delegate + + func card(_ cardClient: CardPayments.CardClient, didFinishWithVaultResult vaultResult: CardPayments.CardVaultResult) { + print("vaultResult: \(vaultResult)") + setUpTokenSuccessResult(vaultResult: vaultResult) + } + + func card(_ cardClient: CardPayments.CardClient, didFinishWithVaultError vaultError: CorePayments.CoreSDKError) { + print("error: \(vaultError.errorDescription ?? "")") + setUpdateSetupTokenFailureResult(vaultError: vaultError) + } +} diff --git a/Demo/Settings.bundle/Root.plist b/Demo/Settings.bundle/Root.plist index 7e1ab4d4b..278b3707f 100644 --- a/Demo/Settings.bundle/Root.plist +++ b/Demo/Settings.bundle/Root.plist @@ -70,6 +70,7 @@ Titles Card + CardVault PayPal Web Checkout Payment Button Customization PayPal Native Checkout @@ -77,6 +78,7 @@ Values card + cardVault payPalWebCheckout paymentButtonCustomization payPalNativeCheckout diff --git a/Package.resolved b/Package.resolved index 5785df8f6..dc6f8d058 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/paypal/paypalcheckout-ios", "state" : { - "revision" : "773568fbbffd54f900d6d78be7793ae1871d3b35", - "version" : "1.0.0" + "revision" : "7c6750e1316c6a3d656e90497271de68c63219f1", + "version" : "1.1.0" } } ], diff --git a/Package.swift b/Package.swift index e1de028c4..0f1c345ef 100644 --- a/Package.swift +++ b/Package.swift @@ -35,7 +35,7 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(name: "PayPalCheckout", url: "https://github.com/paypal/paypalcheckout-ios", .exact("1.0.0")) + .package(name: "PayPalCheckout", url: "https://github.com/paypal/paypalcheckout-ios", .exact("1.1.0")) ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -62,7 +62,7 @@ let package = Package( ), .target( name: "FraudProtection", - dependencies: ["PPRiskMagnes"] + dependencies: ["CorePayments", "PPRiskMagnes"] ), .binaryTarget( name: "PPRiskMagnes", diff --git a/PayPal.podspec b/PayPal.podspec index 0ac342e0e..bb44f2c8c 100644 --- a/PayPal.podspec +++ b/PayPal.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "PayPal" - s.version = "0.0.10" + s.version = "0.0.11" s.summary = "The PayPal iOS SDK: Helps you accept card, PayPal, and alternative payment methods in your iOS app." s.homepage = "https://developer.paypal.com/home" s.license = "MIT" @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.subspec "PayPalNativePayments" do |s| s.source_files = "Sources/PayPalNativePayments/**/*.swift" s.dependency "PayPal/CorePayments" - s.dependency "PayPalCheckout", "1.0.0" + s.dependency "PayPalCheckout", "1.1.0" end s.subspec "PaymentButtons" do |s| @@ -37,6 +37,7 @@ Pod::Spec.new do |s| s.subspec "FraudProtection" do |s| s.source_files = "Sources/FraudProtection/*.swift" + s.dependency "PayPal/CorePayments" s.vendored_frameworks = "Frameworks/XCFrameworks/PPRiskMagnes.xcframework" end diff --git a/PayPal.xcodeproj/project.pbxproj b/PayPal.xcodeproj/project.pbxproj index 126a5ec47..b15bef42d 100644 --- a/PayPal.xcodeproj/project.pbxproj +++ b/PayPal.xcodeproj/project.pbxproj @@ -76,6 +76,8 @@ BC04837427B2FC7300FA7B46 /* URLSession+URLSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC04837327B2FC7300FA7B46 /* URLSession+URLSessionProtocol.swift */; }; BC0A82A5270B9533006E9A21 /* ConfirmPaymentSourceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06CE009F26F3DF100000CC46 /* ConfirmPaymentSourceRequest.swift */; }; BC0A82A6270B9954006E9A21 /* ConfirmPaymentSourceRequest_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065A4DC226FCE1D20007014A /* ConfirmPaymentSourceRequest_Tests.swift */; }; + BC171FB12A8C156300B26DCB /* CoreConfig+MagnesSDK.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC171FB02A8C156300B26DCB /* CoreConfig+MagnesSDK.swift */; }; + BC171FB22A8D6FE500B26DCB /* CorePayments.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 80B9F85126B8750000D67843 /* CorePayments.framework */; }; BC43406F27E91C2700F70193 /* PayPalCheckout in Frameworks */ = {isa = PBXBuildFile; productRef = BC43406E27E91C2700F70193 /* PayPalCheckout */; }; BC7F8123275FC1350011EDC8 /* CardRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC7F8122275FC1350011EDC8 /* CardRequest.swift */; }; BC900B7D27AC307A00D48DBA /* PayPalNativeCheckoutDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC900B7C27AC307A00D48DBA /* PayPalNativeCheckoutDelegate.swift */; }; @@ -134,7 +136,6 @@ CB1A48052822BCED00BD8184 /* ImageAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB1A48042822BCED00BD8184 /* ImageAsset.swift */; }; CB1AC3C22982CDB10081AED6 /* MockNativeCheckoutProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D25238B273979170099E4EB /* MockNativeCheckoutProvider.swift */; }; CB22C018291049500097E592 /* PayPalPayLaterButton_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB22C017291049500097E592 /* PayPalPayLaterButton_Tests.swift */; }; - CB38546327F4B616003CE179 /* PayPalDataCollectorEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB38546227F4B616003CE179 /* PayPalDataCollectorEnvironment.swift */; }; CB4BE27D2847AF6F00EA2DD1 /* SCA.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB4BE27C2847AF6F00EA2DD1 /* SCA.swift */; }; CB4BE27E2847EA7D00EA2DD1 /* WebAuthenticationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE4F784827EB629100FF4C0E /* WebAuthenticationSession.swift */; }; CB4BE2802847F01000EA2DD1 /* CardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB4BE27F2847F01000EA2DD1 /* CardDelegate.swift */; }; @@ -268,6 +269,7 @@ 80FC261C29847AC7008EC841 /* HTTP_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTP_Tests.swift; sourceTree = ""; }; BC04836E27B2FB3600FA7B46 /* URLSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionProtocol.swift; sourceTree = ""; }; BC04837327B2FC7300FA7B46 /* URLSession+URLSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+URLSessionProtocol.swift"; sourceTree = ""; }; + BC171FB02A8C156300B26DCB /* CoreConfig+MagnesSDK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoreConfig+MagnesSDK.swift"; sourceTree = ""; }; BC7F8122275FC1350011EDC8 /* CardRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardRequest.swift; sourceTree = ""; }; BC900B7C27AC307A00D48DBA /* PayPalNativeCheckoutDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalNativeCheckoutDelegate.swift; sourceTree = ""; }; BC9C18D327D2775A0019B541 /* MockDeviceInspector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDeviceInspector.swift; sourceTree = ""; }; @@ -328,7 +330,6 @@ CB1A47FD2820C10700BD8184 /* PaymentButtonFundingSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentButtonFundingSource.swift; sourceTree = ""; }; CB1A48042822BCED00BD8184 /* ImageAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAsset.swift; sourceTree = ""; }; CB22C017291049500097E592 /* PayPalPayLaterButton_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalPayLaterButton_Tests.swift; sourceTree = ""; }; - CB38546227F4B616003CE179 /* PayPalDataCollectorEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalDataCollectorEnvironment.swift; sourceTree = ""; }; CB4BE27C2847AF6F00EA2DD1 /* SCA.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCA.swift; sourceTree = ""; }; CB4BE27F2847F01000EA2DD1 /* CardDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardDelegate.swift; sourceTree = ""; }; CB4BE285284802C200EA2DD1 /* PaymentSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSource.swift; sourceTree = ""; }; @@ -418,6 +419,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + BC171FB22A8D6FE500B26DCB /* CorePayments.framework in Frameworks */, BCF735E327D158B400A52E03 /* PPRiskMagnes.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -498,6 +500,7 @@ isa = PBXGroup; children = ( 808EEA80291321FE001B6765 /* AnalyticsEventData_Tests.swift */, + 8036C1E0270F9BE700C0F091 /* APIClient_Tests.swift */, 802C4A752945676E00896A5D /* AnalyticsService_Tests.swift */, 8036C1E0270F9BE700C0F091 /* APIClient_Tests.swift */, 8036C1E1270F9BE700C0F091 /* Environment_Tests.swift */, @@ -647,13 +650,13 @@ BCF735C127D157CD00A52E03 /* FraudProtection */ = { isa = PBXGroup; children = ( + BC171FB02A8C156300B26DCB /* CoreConfig+MagnesSDK.swift */, BCF735EC27D1650800A52E03 /* DeviceInspector.swift */, BCF735EE27D1652100A52E03 /* DeviceInspectorProtocol.swift */, BCF735E827D15D2600A52E03 /* MagnesSDKProtocol.swift */, BC9C18D727D279C70019B541 /* MagnesSDKResult.swift */, BCF735EA27D160E800A52E03 /* MagnesSetupParams.swift */, BCF735E627D15AB800A52E03 /* PayPalDataCollector.swift */, - CB38546227F4B616003CE179 /* PayPalDataCollectorEnvironment.swift */, ); name = FraudProtection; path = Sources/FraudProtection; @@ -1362,6 +1365,7 @@ 80E2FDBE2A83528B0045593D /* CheckoutOrdersAPI.swift in Sources */, 3B79E4F72A8503CA00C01D06 /* UpdateVaultVariables.swift in Sources */, 8048D28C270B9DE00072214A /* ConfirmPaymentSourceResponse.swift in Sources */, + 3B109B3D2A85D1CA00D8135F /* MockGraphQLClient.swift in Sources */, 3D1763A22720722A00652E1C /* CardResult.swift in Sources */, BC0A82A5270B9533006E9A21 /* ConfirmPaymentSourceRequest.swift in Sources */, CB4BE2802847F01000EA2DD1 /* CardDelegate.swift in Sources */, @@ -1424,7 +1428,7 @@ buildActionMask = 2147483647; files = ( BCF735EF27D1652100A52E03 /* DeviceInspectorProtocol.swift in Sources */, - CB38546327F4B616003CE179 /* PayPalDataCollectorEnvironment.swift in Sources */, + BC171FB12A8C156300B26DCB /* CoreConfig+MagnesSDK.swift in Sources */, BCF735E727D15AB800A52E03 /* PayPalDataCollector.swift in Sources */, BCF735EB27D160E800A52E03 /* MagnesSetupParams.swift in Sources */, BCF735E927D15D2600A52E03 /* MagnesSDKProtocol.swift in Sources */, @@ -3411,7 +3415,7 @@ repositoryURL = "https://github.com/paypal/paypalcheckout-ios"; requirement = { kind = exactVersion; - version = 1.0.0; + version = 1.1.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/PayPal.xcworkspace/xcshareddata/swiftpm/Package.resolved b/PayPal.xcworkspace/xcshareddata/swiftpm/Package.resolved index 746db9a92..221ebe44d 100644 --- a/PayPal.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/PayPal.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/paypal/paypalcheckout-ios", "state" : { - "revision" : "773568fbbffd54f900d6d78be7793ae1871d3b35", - "version" : "1.0.0" + "revision" : "7c6750e1316c6a3d656e90497271de68c63219f1", + "version" : "1.1.0" } } ], diff --git a/README.md b/README.md index 915952f1b..68470f489 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,14 @@ Each feature module has its own onboarding guide: To accept a certain payment method in your app, you only need to include that payment-specific submodule. +## Demo + +1. Open `PayPal.xcworkspace` in Xcode +2. Resolve the Swift Package Manager packages if needed: `File` > `Packages` > `Resolve Package Versions` or by running `swift package resolve` in Terminal +3. Select the `Demo` scheme, and then run + +Xcode 14.3+ is required to run the demo app. + ## Testing This project uses the `XCTest` framework provided by Xcode. Every code path should be unit tested. Unit tests should make up most of the test coverage, with integration, and then UI tests following. diff --git a/SampleApps/SPMTest/SPMTest.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SampleApps/SPMTest/SPMTest.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b7993a2a7..49c3f73ac 100644 --- a/SampleApps/SPMTest/SPMTest.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SampleApps/SPMTest/SPMTest.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,7 +6,7 @@ "location" : "https://github.com/paypal/paypal-ios", "state" : { "branch" : "main", - "revision" : "80bf151ba2a5d74b08cef1c55a596e9e7ecfe497" + "revision" : "924418c4dafbcbd820c35c02c13acdda2d9a820e" } }, { @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/paypal/paypalcheckout-ios", "state" : { - "revision" : "773568fbbffd54f900d6d78be7793ae1871d3b35", - "version" : "1.0.0" + "revision" : "7c6750e1316c6a3d656e90497271de68c63219f1", + "version" : "1.1.0" } } ], diff --git a/Sources/CardPayments/CardClient.swift b/Sources/CardPayments/CardClient.swift index ca3078d7d..2d30ca62c 100644 --- a/Sources/CardPayments/CardClient.swift +++ b/Sources/CardPayments/CardClient.swift @@ -44,7 +44,7 @@ public class CardClient: NSObject { let result = try await vaultAPI.updateSetupToken(cardVaultRequest: vaultRequest).updateVaultSetupToken // TODO: handle 3DS contingency with helios link & add unit tests - if let link = result.links.first(where: { $0.rel == "approve" && $0.href.contains("helios") }) { + if let link = result.links.first(where: { $0.rel == "approve" && $0.href.contains("helios") }) { let url = link.href print("3DS url \(url)") } else { diff --git a/Sources/FraudProtection/PayPalDataCollectorEnvironment.swift b/Sources/FraudProtection/CoreConfig+MagnesSDK.swift similarity index 54% rename from Sources/FraudProtection/PayPalDataCollectorEnvironment.swift rename to Sources/FraudProtection/CoreConfig+MagnesSDK.swift index 2627d21eb..7a7e10090 100644 --- a/Sources/FraudProtection/PayPalDataCollectorEnvironment.swift +++ b/Sources/FraudProtection/CoreConfig+MagnesSDK.swift @@ -1,12 +1,12 @@ import PPRiskMagnes +#if canImport(CorePayments) +import CorePayments +#endif -/// Enum of environments to use with PayPalDataCollector -public enum PayPalDataCollectorEnvironment { - case sandbox - case live - +extension CoreConfig { + var magnesEnvironment: MagnesSDK.Environment { - switch self { + switch environment { case .sandbox: return .SANDBOX case .live: diff --git a/Sources/FraudProtection/PayPalDataCollector.swift b/Sources/FraudProtection/PayPalDataCollector.swift index cc7c30001..2566fee42 100644 --- a/Sources/FraudProtection/PayPalDataCollector.swift +++ b/Sources/FraudProtection/PayPalDataCollector.swift @@ -1,9 +1,12 @@ import Foundation import PPRiskMagnes +#if canImport(CorePayments) +import CorePayments +#endif /// Enables you to collect data about a customer's device and correlate it with a session identifier on your server. public class PayPalDataCollector { - + // MARK: - Properties private let magnesSDK: MagnesSDKProtocol @@ -14,13 +17,13 @@ public class PayPalDataCollector { /// Construct an instance to collect device data to send to your server. /// - Parameter environment: enviroment for the data collector - public convenience init(environment: PayPalDataCollectorEnvironment) { - self.init(environment: environment, magnesSDK: MagnesSDK.shared(), deviceInspector: DeviceInspector()) + public convenience init(config: CoreConfig) { + self.init(config: config, magnesSDK: MagnesSDK.shared(), deviceInspector: DeviceInspector()) } /// internal constructor for testing - init(environment: PayPalDataCollectorEnvironment, magnesSDK: MagnesSDKProtocol, deviceInspector: DeviceInspectorProtocol) { - self.magnesEnvironment = environment.magnesEnvironment + init(config: CoreConfig, magnesSDK: MagnesSDKProtocol, deviceInspector: DeviceInspectorProtocol) { + self.magnesEnvironment = config.magnesEnvironment self.magnesSDK = magnesSDK self.deviceInspector = deviceInspector } diff --git a/UnitTests/CardPaymentsTests/MockCardVaultDelegate.swift b/UnitTests/CardPaymentsTests/MockCardVaultDelegate.swift new file mode 100644 index 000000000..26c2d30a3 --- /dev/null +++ b/UnitTests/CardPaymentsTests/MockCardVaultDelegate.swift @@ -0,0 +1,24 @@ +@testable import CorePayments +@testable import CardPayments + +class MockCardVaultDelegate: CardVaultDelegate { + + private var success: ((CardClient, CardVaultResult) -> Void)? + private var failure: ((CardClient, CoreSDKError) -> Void)? + + required init( + success: ((CardClient, CardVaultResult) -> Void)? = nil, + error: ((CardClient, CoreSDKError) -> Void)? = nil + ) { + self.success = success + self.failure = error + } + + func card(_ cardClient: CardClient, didFinishWithVaultResult vaultResult: CardPayments.CardVaultResult) { + success?(cardClient, vaultResult) + } + + func card(_ cardClient: CardClient, didFinishWithVaultError vaultError: CorePayments.CoreSDKError) { + failure?(cardClient, vaultError) + } +} diff --git a/UnitTests/CardPaymentsTests/MockGraphQLClient.swift b/UnitTests/CardPaymentsTests/MockGraphQLClient.swift new file mode 100644 index 000000000..cbf96af2f --- /dev/null +++ b/UnitTests/CardPaymentsTests/MockGraphQLClient.swift @@ -0,0 +1,20 @@ +@testable import CorePayments + +class MockGraphQLClient: GraphQLClient { + + var mockSuccessResponse: GraphQLQueryResponse? + var mockErrorResponse: Error? + + override func callGraphQL( + name: String, + query: Q + ) async throws -> GraphQLQueryResponse where T: Decodable, T: Encodable, Q: GraphQLQuery { + if let response = mockSuccessResponse as? GraphQLQueryResponse { + return response + } else if let error = mockErrorResponse { + throw error + } else { + fatalError("MockGraphQLClient - either mockSuccessResponse or mockErrorResponse must be set") + } + } +} diff --git a/UnitTests/FraudProtectionTests/DeviceInspector_Tests.swift b/UnitTests/FraudProtectionTests/DeviceInspector_Tests.swift index c454f2f07..94a839b72 100644 --- a/UnitTests/FraudProtectionTests/DeviceInspector_Tests.swift +++ b/UnitTests/FraudProtectionTests/DeviceInspector_Tests.swift @@ -20,7 +20,7 @@ class DeviceInspector_Tests: XCTestCase { #else let sut = DeviceInspector() let newIdentifier = UUID() - let result = sut.paypalDeviceIdentifier(newIdentifier: newIdentifier) + let result = sut.payPalDeviceIdentifier(newIdentifier: newIdentifier) XCTAssertNotEqual(newIdentifier.uuidString, result) #endif diff --git a/UnitTests/FraudProtectionTests/PayPalDataCollector_Tests.swift b/UnitTests/FraudProtectionTests/PayPalDataCollector_Tests.swift index 8dd047078..203abde53 100644 --- a/UnitTests/FraudProtectionTests/PayPalDataCollector_Tests.swift +++ b/UnitTests/FraudProtectionTests/PayPalDataCollector_Tests.swift @@ -1,20 +1,24 @@ import XCTest +@testable import CorePayments @testable import FraudProtection class PayPalDataCollector_Tests: XCTestCase { private var deviceInspector = MockDeviceInspector() private var magnesSDK = MockMagnesSDK() - + + let sandboxConfig = CoreConfig(clientID: "mockClientID", environment: .sandbox) + let liveConfig = CoreConfig(clientID: "mockClientID", environment: .live) + func testCollectDeviceData_setsMagnesEnvironmentToSANDBOX() { - let sut = PayPalDataCollector(environment: .sandbox, magnesSDK: magnesSDK, deviceInspector: deviceInspector) + let sut = PayPalDataCollector(config: sandboxConfig, magnesSDK: magnesSDK, deviceInspector: deviceInspector) _ = sut.collectDeviceData() XCTAssertEqual(.SANDBOX, magnesSDK.capturedSetupParams?.env) } func testCollectDeviceData_setsMagnesEnvironmentToLIVE() { - let sut = PayPalDataCollector(environment: .live, magnesSDK: magnesSDK, deviceInspector: deviceInspector) + let sut = PayPalDataCollector(config: liveConfig, magnesSDK: magnesSDK, deviceInspector: deviceInspector) _ = sut.collectDeviceData() XCTAssertEqual(.LIVE, magnesSDK.capturedSetupParams?.env) @@ -23,28 +27,28 @@ class PayPalDataCollector_Tests: XCTestCase { func testCollectDeviceData_setsMagnesAppGUIDToTheCurrentDeviceID() { deviceInspector.stubPayPalDeviceIdentifierWithValue("sample_device_identifier") - let sut = PayPalDataCollector(environment: .sandbox, magnesSDK: magnesSDK, deviceInspector: deviceInspector) + let sut = PayPalDataCollector(config: sandboxConfig, magnesSDK: magnesSDK, deviceInspector: deviceInspector) _ = sut.collectDeviceData() XCTAssertEqual("sample_device_identifier", magnesSDK.capturedSetupParams?.appGuid) } func testCollectDeviceData_disablesMagnesRemoteConfiguration() { - let sut = PayPalDataCollector(environment: .sandbox, magnesSDK: magnesSDK, deviceInspector: deviceInspector) + let sut = PayPalDataCollector(config: sandboxConfig, magnesSDK: magnesSDK, deviceInspector: deviceInspector) _ = sut.collectDeviceData() XCTAssertEqual(false, magnesSDK.capturedSetupParams?.isRemoteConfigDisabled) } func testCollectDeviceData_disablesMagnesBeacon() { - let sut = PayPalDataCollector(environment: .sandbox, magnesSDK: magnesSDK, deviceInspector: deviceInspector) + let sut = PayPalDataCollector(config: sandboxConfig, magnesSDK: magnesSDK, deviceInspector: deviceInspector) _ = sut.collectDeviceData() XCTAssertEqual(false, magnesSDK.capturedSetupParams?.isBeaconDisabled) } func testCollectDeviceData_setsMagnesSourceToPAYPAL() { - let sut = PayPalDataCollector(environment: .sandbox, magnesSDK: magnesSDK, deviceInspector: deviceInspector) + let sut = PayPalDataCollector(config: sandboxConfig, magnesSDK: magnesSDK, deviceInspector: deviceInspector) _ = sut.collectDeviceData() XCTAssertEqual(.PAYPAL, magnesSDK.capturedSetupParams?.source) @@ -55,7 +59,7 @@ class PayPalDataCollector_Tests: XCTestCase { let magnesResult = MockMagnesSDKResult(payPalClientMetaDataId: "new_client_metadata_id") magnesSDK.stubCollectDeviceData(forArgs: args, withValue: magnesResult) - let sut = PayPalDataCollector(environment: .sandbox, magnesSDK: magnesSDK, deviceInspector: deviceInspector) + let sut = PayPalDataCollector(config: sandboxConfig, magnesSDK: magnesSDK, deviceInspector: deviceInspector) let result = sut.collectDeviceData(additionalData: ["sample": "data"]) let expected = """ diff --git a/docs/FraudProtection/README.md b/docs/FraudProtection/README.md new file mode 100644 index 000000000..18ed4d154 --- /dev/null +++ b/docs/FraudProtection/README.md @@ -0,0 +1,73 @@ +# Fraud Protection + +The FraudProtection module in the PayPal SDK enables merchants to collect user device data and associate it with payment transactions to reduce the risk of processing a fraudulent transaction. + +Follow these steps to add fraud protection: + +1. [Setup a PayPal Developer Account](#setup-a-paypal-developer-account) +2. [Add FraudProtection Module](#add-fraudprotection-module) +3. [Go Live](#go-live) + +## Setup a PayPal Developer Account + +You will need to set up authorization to use the PayPal Payments SDK. +Follow the steps in [Get Started](https://developer.paypal.com/api/rest/#link-getstarted) to create a client ID. + +You will need a server integration to create an order and capture funds using [PayPal Orders v2 API](https://developer.paypal.com/docs/api/orders/v2). +For initial setup, the `curl` commands below can be used in place of a server SDK. + +## Add FraudProtection Module + +### 1. Add the Payments SDK FraudProtection module to your app + +#### Swift Package Manager + +In Xcode, add the PayPal SDK as a [package dependency](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app) to your Xcode project. Enter https://github.com/paypal/paypal-ios as the package URL. Tick the `FraudProtection` checkbox to add the Fraud Protection library to your app. + +#### CocoaPods + +Include the `FraudProtection` sub-module in your `Podfile`: + +```ruby +pod 'PayPal/FraudProtection' +``` + +### 2. Initiate the Payments SDK + +Create a `CoreConfig` using a [client id](https://developer.paypal.com/api/rest/): + +```swift +let config = CoreConfig(clientID:"", environment: .sandbox) +``` + +Create a `PayPalDataCollector` to retrieve a PayPal Client Metadata ID (CMID). You can use a CMID when calling Authorize or Capture to add fraud protection to your transactions. + +```swift +let payPalDataCollector = PayPalDataCollector(config: config) +let cmid = payPalDataCollector.collectDeviceData() +``` + +### 3. Send PayPal-Client-Metadata-Id Header on Capture / Authorize + +To enable fraud protection, add a `PayPal-Client-Metadata-Id` HTTP header to your call to Capture (or Authorize), and set its value to the `cmid` value obtained in the previous step: + +```bash +# for capture +curl --location --request POST 'https://api.sandbox.paypal.com/v2/checkout/orders//capture' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer ' \ +--header 'PayPal-Client-Metadata-Id: ' \ +--data-raw '' + +# for authorize +curl --location --request POST 'https://api.sandbox.paypal.com/v2/checkout/orders//authorize' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer ' \ +--header 'PayPal-Client-Metadata-Id: ' \ +--data-raw '' +``` + +## Go Live + +Follow [these instructions](https://developer.paypal.com/api/rest/production/) to prepare your integration to go live. +