diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index 6b593e4ca..b0f528ef9 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 3B80D5102A291CB100D2EAC4 /* ClientIDResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B80D50F2A291CB100D2EAC4 /* ClientIDResponse.swift */; }; 3B8EF4DB2A932DA300A70D0B /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B8EF4DA2A932DA300A70D0B /* ErrorView.swift */; }; 3BA0A58B2B1E240300330681 /* VaultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA0A58A2B1E240300330681 /* VaultViewModel.swift */; }; + 3BA3643C2D134857008926B7 /* CardPaySheet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BA3643B2D134857008926B7 /* CardPaySheet.framework */; }; 3BA56FE72A9DC9D70081D14F /* CardPaymentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA56FE62A9DC9D70081D14F /* CardPaymentViewModel.swift */; }; 3BA56FE92A9DCA520081D14F /* CardPaymentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA56FE82A9DCA520081D14F /* CardPaymentState.swift */; }; 3BA56FEC2A9DCBF30081D14F /* CreateOrderCardPaymentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA56FEB2A9DCBF30081D14F /* CreateOrderCardPaymentView.swift */; }; @@ -139,6 +140,7 @@ 3B80D50F2A291CB100D2EAC4 /* ClientIDResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientIDResponse.swift; sourceTree = ""; }; 3B8EF4DA2A932DA300A70D0B /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; 3BA0A58A2B1E240300330681 /* VaultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultViewModel.swift; sourceTree = ""; }; + 3BA3643B2D134857008926B7 /* CardPaySheet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CardPaySheet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3BA56FE62A9DC9D70081D14F /* CardPaymentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPaymentViewModel.swift; sourceTree = ""; }; 3BA56FE82A9DCA520081D14F /* CardPaymentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPaymentState.swift; sourceTree = ""; }; 3BA56FEB2A9DCBF30081D14F /* CreateOrderCardPaymentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateOrderCardPaymentView.swift; sourceTree = ""; }; @@ -214,6 +216,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3BA3643C2D134857008926B7 /* CardPaySheet.framework in Frameworks */, CB1AC3B82982AAD70081AED6 /* CardPayments.framework in Frameworks */, CB1AC3C02982C4030081AED6 /* PayPalWebPayments.framework in Frameworks */, CB1AC3BB2982BB130081AED6 /* PaymentButtons.framework in Frameworks */, @@ -406,6 +409,7 @@ 805AB84C26B87A87003BEE0D /* Frameworks */ = { isa = PBXGroup; children = ( + 3BA3643B2D134857008926B7 /* CardPaySheet.framework */, 8052E2A229B684A600B33FBC /* PPRiskMagnes.xcframework */, CBDEEA212989990200A460A6 /* CorePayments.framework */, CB1AC3C62982E32D0081AED6 /* FraudProtection.framework */, @@ -794,7 +798,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -849,7 +853,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; diff --git a/Demo/Demo/Assets.xcassets/headphonePic.imageset/Contents.json b/Demo/Demo/Assets.xcassets/headphonePic.imageset/Contents.json new file mode 100644 index 000000000..6b7ac26e8 --- /dev/null +++ b/Demo/Demo/Assets.xcassets/headphonePic.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "headphonePic.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/Demo/Assets.xcassets/headphonePic.imageset/headphonePic.png b/Demo/Demo/Assets.xcassets/headphonePic.imageset/headphonePic.png new file mode 100644 index 000000000..5a0626151 Binary files /dev/null and b/Demo/Demo/Assets.xcassets/headphonePic.imageset/headphonePic.png differ diff --git a/Demo/Demo/CardPayments/CardPaymentViewModel/CardPaymentViewModel.swift b/Demo/Demo/CardPayments/CardPaymentViewModel/CardPaymentViewModel.swift index 578339eeb..a0e34279b 100644 --- a/Demo/Demo/CardPayments/CardPaymentViewModel/CardPaymentViewModel.swift +++ b/Demo/Demo/CardPayments/CardPaymentViewModel/CardPaymentViewModel.swift @@ -7,11 +7,17 @@ class CardPaymentViewModel: ObservableObject { @Published var state = CardPaymentState() private var payPalDataCollector: PayPalDataCollector? + public var config: CoreConfig? let configManager = CoreConfigManager(domain: "Card Payments") private var cardClient: CardClient? + func getConfig() async throws { + let config = try await configManager.getCoreConfig() + self.config = config + } + func createOrder( amount: String, selectedMerchantIntegration: MerchantIntegration, diff --git a/Demo/Demo/CardPayments/CardViewComponents/CardOrderApproveView.swift b/Demo/Demo/CardPayments/CardViewComponents/CardOrderApproveView.swift index 1fcc938d3..b471ae924 100644 --- a/Demo/Demo/CardPayments/CardViewComponents/CardOrderApproveView.swift +++ b/Demo/Demo/CardPayments/CardViewComponents/CardOrderApproveView.swift @@ -1,72 +1,64 @@ import SwiftUI import CardPayments import CorePayments +import CardPaySheet struct CardOrderApproveView: View { - - let cardSections: [CardSection] = [ - CardSection(title: "Successful Authentication Visa", numbers: ["4868 7194 6070 7704"]), - CardSection(title: "Vault with Purchase (no 3DS)", numbers: ["4000 0000 0000 0002"]), - CardSection(title: "Step up", numbers: ["5314 6090 4083 0349"]), - CardSection(title: "Frictionless - LiabilityShift Possible", numbers: ["4005 5192 0000 0004"]), - CardSection(title: "Frictionless - LiabilityShift NO", numbers: ["4020 0278 5185 3235"]), - CardSection(title: "No Challenge", numbers: ["4111 1111 1111 1111"]) - ] + let orderID: String - + var config: CoreConfig? + @ObservedObject var cardPaymentViewModel: CardPaymentViewModel @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 showingCardSheet = false + var body: some View { ScrollView { ScrollViewReader { scrollView in VStack { VStack(spacing: 16) { HStack { - Text("Enter Card Information") - .font(.system(size: 20)) + Text("Your cart") + .font(.system(size: 22, weight: .semibold)) Spacer() } - - CardFormView( - cardSections: cardSections, - cardNumberText: $cardNumberText, - expirationDateText: $expirationDateText, - cvvText: $cvvText - ) - - let card = Card.createCard( - cardNumber: cardNumberText, - expirationDate: expirationDateText, - cvv: cvvText - ) - + + HStack(spacing: 20) { + Image("headphonePic") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 120, height: 120) + VStack(alignment: .leading, spacing: 4) { + Text("Bose Rose Gold QC35II") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.primary) + Text("$10.00") + .font(.system(size: 16)) + .foregroundColor(.secondary) + } + } + + Spacer().frame(height: 20) + Picker("SCA", selection: $cardPaymentViewModel.state.scaSelection) { Text(SCA.scaWhenRequired.rawValue).tag(SCA.scaWhenRequired) Text(SCA.scaAlways.rawValue).tag(SCA.scaAlways) } .pickerStyle(SegmentedPickerStyle()) .frame(height: 50) - - ZStack { - Button("Approve Order") { - Task { - do { - await cardPaymentViewModel.checkoutWith( - card: card, - orderID: orderID, - sca: cardPaymentViewModel.state.scaSelection - ) + Button("Pay with Card") { + Task { + do { + try await cardPaymentViewModel.getConfig() + await MainActor.run { + showingCardSheet = true } } } - .buttonStyle(RoundedBlueButtonStyle()) - if case .loading = cardPaymentViewModel.state.approveResultResponse { - CircularProgressView() - } } + .buttonStyle(RoundedBlueButtonStyle()) } .padding() .background( @@ -88,6 +80,28 @@ struct CardOrderApproveView: View { .id("bottomView") Spacer() } + .sheet(isPresented: $showingCardSheet) { + if let config = cardPaymentViewModel.config { + CardPaySheetView(config: config, orderID: orderID, sca: cardPaymentViewModel.state.scaSelection) { result in + switch result { + case .success(let cardResult): + print("success!: \(cardResult.orderID)") + cardPaymentViewModel.setApprovalSuccessResult( + approveResult: + CardPaymentState.CardResult( + id: cardResult.orderID, + status: cardResult.status, + didAttemptThreeDSecureAuthentication: cardResult.didAttemptThreeDSecureAuthentication + ) + ) + case .failure(let error): + cardPaymentViewModel.setApprovalFailureResult(error: error) + } + showingCardSheet = false + } + .presentationDetents([.fraction(0.75)]) + } + } .onChange(of: cardPaymentViewModel.state) { _ in withAnimation { scrollView.scrollTo("bottomView") diff --git a/PayPal.xcodeproj/project.pbxproj b/PayPal.xcodeproj/project.pbxproj index 2a7bb6dc9..872158814 100644 --- a/PayPal.xcodeproj/project.pbxproj +++ b/PayPal.xcodeproj/project.pbxproj @@ -19,6 +19,14 @@ 3B783DC32B7A69C2004623DB /* FakeUpdateSetupTokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B783DC22B7A69C2004623DB /* FakeUpdateSetupTokenResponse.swift */; }; 3B79E4F72A8503CA00C01D06 /* UpdateVaultVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B79E4F62A8503C900C01D06 /* UpdateVaultVariables.swift */; }; 3B80D50C2A27979000D2EAC4 /* FailingJSONEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B80D50B2A27979000D2EAC4 /* FailingJSONEncoder.swift */; }; + 3BA364052D133C97008926B7 /* CardPaySheet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BA363FB2D133C97008926B7 /* CardPaySheet.framework */; }; + 3BA364342D134672008926B7 /* CardPaySheet.h in Headers */ = {isa = PBXBuildFile; fileRef = 3BA364302D134672008926B7 /* CardPaySheet.h */; }; + 3BA364352D134672008926B7 /* CardFormViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA3642E2D134672008926B7 /* CardFormViewController.swift */; }; + 3BA364362D134672008926B7 /* CardType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA364322D134672008926B7 /* CardType.swift */; }; + 3BA364372D134672008926B7 /* CardPaysheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA3642F2D134672008926B7 /* CardPaysheet.swift */; }; + 3BA364382D134672008926B7 /* CardFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA3642D2D134672008926B7 /* CardFormatter.swift */; }; + 3BA364392D134672008926B7 /* CardPaySheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA364312D134672008926B7 /* CardPaySheetView.swift */; }; + 3BA3643A2D134672008926B7 /* CardForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA3642C2D134672008926B7 /* CardForm.swift */; }; 3BD82DBB2A835AF900CBE764 /* UpdateSetupTokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD82DBA2A835AF900CBE764 /* UpdateSetupTokenResponse.swift */; }; 3BDB34942A80CE6E008100D7 /* CardVaultRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDB34932A80CE6E008100D7 /* CardVaultRequest.swift */; }; 3BE738682B9A66D800598F05 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3BE738622B9A482800598F05 /* PrivacyInfo.xcprivacy */; }; @@ -133,6 +141,20 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 3B4B210A2D134F3100068AD5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = OBJ_1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 80E8DAD826B8783800FAFC3F; + remoteInfo = CardPayments; + }; + 3BA364062D133C97008926B7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = OBJ_1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3BA363FA2D133C97008926B7; + remoteInfo = CardPaySheet; + }; 8034A9E826B875C90055AF13 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = OBJ_1 /* Project object */; @@ -186,6 +208,15 @@ 3B783DC22B7A69C2004623DB /* FakeUpdateSetupTokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeUpdateSetupTokenResponse.swift; sourceTree = ""; }; 3B79E4F62A8503C900C01D06 /* UpdateVaultVariables.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateVaultVariables.swift; sourceTree = ""; }; 3B80D50B2A27979000D2EAC4 /* FailingJSONEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailingJSONEncoder.swift; sourceTree = ""; }; + 3BA363FB2D133C97008926B7 /* CardPaySheet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CardPaySheet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3BA364042D133C97008926B7 /* CardPaySheetTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CardPaySheetTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3BA3642C2D134672008926B7 /* CardForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardForm.swift; sourceTree = ""; }; + 3BA3642D2D134672008926B7 /* CardFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardFormatter.swift; sourceTree = ""; }; + 3BA3642E2D134672008926B7 /* CardFormViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardFormViewController.swift; sourceTree = ""; }; + 3BA3642F2D134672008926B7 /* CardPaysheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPaysheet.swift; sourceTree = ""; }; + 3BA364302D134672008926B7 /* CardPaySheet.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CardPaySheet.h; sourceTree = ""; }; + 3BA364312D134672008926B7 /* CardPaySheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPaySheetView.swift; sourceTree = ""; }; + 3BA364322D134672008926B7 /* CardType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardType.swift; sourceTree = ""; }; 3BD82DBA2A835AF900CBE764 /* UpdateSetupTokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSetupTokenResponse.swift; sourceTree = ""; }; 3BDB34932A80CE6E008100D7 /* CardVaultRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultRequest.swift; sourceTree = ""; }; 3BE738622B9A482800598F05 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; @@ -299,6 +330,21 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 3BA363F82D133C97008926B7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3BA364012D133C97008926B7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3BA364052D133C97008926B7 /* CardPaySheet.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 8034A9E026B875C90055AF13 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -444,6 +490,21 @@ path = Models; sourceTree = ""; }; + 3BA364332D134672008926B7 /* CardPaySheet */ = { + isa = PBXGroup; + children = ( + 3BA3642C2D134672008926B7 /* CardForm.swift */, + 3BA3642D2D134672008926B7 /* CardFormatter.swift */, + 3BA3642E2D134672008926B7 /* CardFormViewController.swift */, + 3BA3642F2D134672008926B7 /* CardPaysheet.swift */, + 3BA364302D134672008926B7 /* CardPaySheet.h */, + 3BA364312D134672008926B7 /* CardPaySheetView.swift */, + 3BA364322D134672008926B7 /* CardType.swift */, + ); + name = CardPaySheet; + path = Sources/CardPaySheet; + sourceTree = ""; + }; 8036C1DE270F9BCF00C0F091 /* PaymentsCoreTests */ = { isa = PBXGroup; children = ( @@ -707,6 +768,8 @@ BCD5C49427E9201400B074D5 /* PayPalWebCheckoutTests.xctest */, BCFAC70927ED042500C3AF00 /* PaymentButtons.framework */, BCFAC71827ED043200C3AF00 /* PayPalUITests.xctest */, + 3BA363FB2D133C97008926B7 /* CardPaySheet.framework */, + 3BA364042D133C97008926B7 /* CardPaySheetTests.xctest */, ); name = Products; path = ..; @@ -728,6 +791,7 @@ OBJ_7 /* Sources */ = { isa = PBXGroup; children = ( + 3BA364332D134672008926B7 /* CardPaySheet */, 80E8DADA26B8783800FAFC3F /* CardPayments */, 80B9F85226B8750000D67843 /* CorePayments */, BCF735C127D157CD00A52E03 /* FraudProtection */, @@ -740,6 +804,14 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ + 3BA363F62D133C97008926B7 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 3BA364342D134672008926B7 /* CardPaySheet.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 80E743F3270E40CE00BACECA /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -764,6 +836,47 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + 3BA363FA2D133C97008926B7 /* CardPaySheet */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3BA3640D2D133C98008926B7 /* Build configuration list for PBXNativeTarget "CardPaySheet" */; + buildPhases = ( + 3BA363F62D133C97008926B7 /* Headers */, + 3BA363F72D133C97008926B7 /* Sources */, + 3BA363F82D133C97008926B7 /* Frameworks */, + 3BA363F92D133C97008926B7 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3B4B210B2D134F3100068AD5 /* PBXTargetDependency */, + ); + name = CardPaySheet; + packageProductDependencies = ( + ); + productName = CardPaySheet; + productReference = 3BA363FB2D133C97008926B7 /* CardPaySheet.framework */; + productType = "com.apple.product-type.framework"; + }; + 3BA364032D133C97008926B7 /* CardPaySheetTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3BA364102D133C98008926B7 /* Build configuration list for PBXNativeTarget "CardPaySheetTests" */; + buildPhases = ( + 3BA364002D133C97008926B7 /* Sources */, + 3BA364012D133C97008926B7 /* Frameworks */, + 3BA364022D133C97008926B7 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3BA364072D133C97008926B7 /* PBXTargetDependency */, + ); + name = CardPaySheetTests; + packageProductDependencies = ( + ); + productName = CardPaySheetTests; + productReference = 3BA364042D133C97008926B7 /* CardPaySheetTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 8034A9E226B875C90055AF13 /* CorePaymentsTests */ = { isa = PBXNativeTarget; buildConfigurationList = 8034A9EA26B875C90055AF13 /* Build configuration list for PBXNativeTarget "CorePaymentsTests" */; @@ -969,9 +1082,15 @@ isa = PBXProject; attributes = { LastSwiftMigration = 9999; - LastSwiftUpdateCheck = 1300; + LastSwiftUpdateCheck = 1610; LastUpgradeCheck = 9999; TargetAttributes = { + 3BA363FA2D133C97008926B7 = { + CreatedOnToolsVersion = 16.1; + }; + 3BA364032D133C97008926B7 = { + CreatedOnToolsVersion = 16.1; + }; 8034A9E226B875C90055AF13 = { CreatedOnToolsVersion = 13.0; DevelopmentTeam = 43253H4X22; @@ -1034,11 +1153,27 @@ BCD5C46727E9200800B074D5 /* PayPalWebPayments */, BCD5C48627E9201400B074D5 /* PayPalWebPaymentsTests */, 80E743F7270E40CE00BACECA /* TestShared */, + 3BA363FA2D133C97008926B7 /* CardPaySheet */, + 3BA364032D133C97008926B7 /* CardPaySheetTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 3BA363F92D133C97008926B7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3BA364022D133C97008926B7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 8034A9E126B875C90055AF13 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1133,6 +1268,26 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 3BA363F72D133C97008926B7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3BA364352D134672008926B7 /* CardFormViewController.swift in Sources */, + 3BA364362D134672008926B7 /* CardType.swift in Sources */, + 3BA364372D134672008926B7 /* CardPaysheet.swift in Sources */, + 3BA364382D134672008926B7 /* CardFormatter.swift in Sources */, + 3BA364392D134672008926B7 /* CardPaySheetView.swift in Sources */, + 3BA3643A2D134672008926B7 /* CardForm.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3BA364002D133C97008926B7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 8034A9DF26B875C90055AF13 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1316,6 +1471,16 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 3B4B210B2D134F3100068AD5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 80E8DAD826B8783800FAFC3F /* CardPayments */; + targetProxy = 3B4B210A2D134F3100068AD5 /* PBXContainerItemProxy */; + }; + 3BA364072D133C97008926B7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3BA363FA2D133C97008926B7 /* CardPaySheet */; + targetProxy = 3BA364062D133C97008926B7 /* PBXContainerItemProxy */; + }; 8034A9E926B875C90055AF13 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 80B9F85026B8750000D67843 /* CorePayments */; @@ -1344,6 +1509,306 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + 3BA3640E2D133C98008926B7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 5YRAV27ZNE; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = victoria.com.CardPaySheet; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 3BA3640F2D133C98008926B7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 5YRAV27ZNE; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = victoria.com.CardPaySheet; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 3BA364112D133C98008926B7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5YRAV27ZNE; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = victoria.com.CardPaySheetTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3BA364122D133C98008926B7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5YRAV27ZNE; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = victoria.com.CardPaySheetTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; 8034A9EB26B875C90055AF13 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2869,6 +3334,24 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 3BA3640D2D133C98008926B7 /* Build configuration list for PBXNativeTarget "CardPaySheet" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3BA3640E2D133C98008926B7 /* Debug */, + 3BA3640F2D133C98008926B7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3BA364102D133C98008926B7 /* Build configuration list for PBXNativeTarget "CardPaySheetTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3BA364112D133C98008926B7 /* Debug */, + 3BA364122D133C98008926B7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 8034A9EA26B875C90055AF13 /* Build configuration list for PBXNativeTarget "CorePaymentsTests" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Sources/CardPaySheet/CardForm.swift b/Sources/CardPaySheet/CardForm.swift new file mode 100644 index 000000000..42a82ad51 --- /dev/null +++ b/Sources/CardPaySheet/CardForm.swift @@ -0,0 +1,155 @@ +import UIKit + +public class CardForm: UIView { + + private let cardFormatter = CardFormatter() + + private let cardInfoTitle: UILabel = { + let label = UILabel() + label.text = "Card Information" + label.font = UIFont.systemFont(ofSize: 18, weight: .semibold) + label.textColor = .black + label.textAlignment = .left + return label + }() + + private let countryRegionTitle: UILabel = { + let label = UILabel() + label.text = "Country or Region" + label.font = UIFont.systemFont(ofSize: 18, weight: .semibold) + label.textColor = .black + label.textAlignment = .left + return label + }() + + private let cardNumberField: UITextField = { + let field = UITextField() + field.placeholder = "Card number" + field.borderStyle = .roundedRect + field.backgroundColor = .systemBackground + field.layer.borderColor = UIColor.systemGray5.cgColor + field.layer.borderWidth = 1 + field.layer.cornerRadius = 8 + field.keyboardType = .numberPad + return field + }() + + private let expiryDateField: UITextField = { + let field = UITextField() + field.placeholder = "MM/YY" + field.borderStyle = .roundedRect + field.backgroundColor = .systemBackground + field.layer.borderColor = UIColor.systemGray5.cgColor + field.layer.borderWidth = 1 + field.layer.cornerRadius = 8 + field.keyboardType = .numberPad + return field + }() + + private let cvvField: UITextField = { + let field = UITextField() + field.placeholder = "CVV" + field.borderStyle = .roundedRect + field.backgroundColor = .systemBackground + field.layer.borderColor = UIColor.systemGray5.cgColor + field.layer.borderWidth = 1 + field.layer.cornerRadius = 8 + field.keyboardType = .numberPad + field.isSecureTextEntry = true + return field + }() + + private let countryRegionField: UITextField = { + let field = UITextField() + field.placeholder = "United States" + field.borderStyle = .roundedRect + field.backgroundColor = .systemBackground + field.layer.borderWidth = 1 + field.layer.cornerRadius = 8 + field.layer.borderColor = UIColor.systemGray5.cgColor + field.layer.borderWidth = 1 + field.layer.cornerRadius = 8 + return field + }() + + private let zipField: UITextField = { + let field = UITextField() + field.placeholder = "ZIP" + field.borderStyle = .roundedRect + field.backgroundColor = .systemBackground + field.layer.borderColor = UIColor.systemGray5.cgColor + field.layer.borderWidth = 1 + field.layer.cornerRadius = 8 + return field + }() + + private lazy var stackView: UIStackView = { + let stack = UIStackView( + arrangedSubviews: + [cardInfoTitle, cardNumberField, expiryDateField, cvvField, countryRegionTitle, countryRegionField, zipField] + ) + stack.axis = .vertical + stack.spacing = 12 + stack.distribution = .fill + return stack + }() + + public var cardNumber: String? { cardNumberField.text?.replacingOccurrences(of: " ", with: "")} + public var expiryDate: String? { expiryDateField.text } + public var cvv: String? { cvvField.text } + + public override init(frame: CGRect) { + super.init(frame: frame) + setupViews() + setupTextFieldDelegates() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupViews() + setupTextFieldDelegates() + } + + private func setupTextFieldDelegates() { + cardNumberField.addTarget(self, action: #selector(cardNumberDidChange), for: .editingChanged) + expiryDateField.addTarget(self, action: #selector(expiryDidChange), for: .editingChanged) + cvvField.addTarget(self, action: #selector(cvvDidChange), for: .editingChanged) + } + + @objc private func cardNumberDidChange(_ textField: UITextField) { + if let text = textField.text { + textField.text = cardFormatter.formatFieldWith(text, field: .cardNumber) + } + } + + @objc private func expiryDidChange(_ textField: UITextField) { + if let text = textField.text { + textField.text = cardFormatter.formatFieldWith(text, field: .expirationDate) + } + } + + @objc private func cvvDidChange(_ textField: UITextField) { + if let text = textField.text { + textField.text = cardFormatter.formatFieldWith(text, field: .cvv) + } + } + + private func setupViews() { + backgroundColor = .white + + layer.borderColor = UIColor.systemGray4.cgColor + layer.borderWidth = 1 + layer.cornerRadius = 12 + layer.masksToBounds = true + + addSubview(stackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: topAnchor, constant: 16), + stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), + stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16) + ]) + } +} diff --git a/Sources/CardPaySheet/CardFormViewController.swift b/Sources/CardPaySheet/CardFormViewController.swift new file mode 100644 index 000000000..381639c60 --- /dev/null +++ b/Sources/CardPaySheet/CardFormViewController.swift @@ -0,0 +1,173 @@ +import UIKit +#if canImport(CardPayments) +import CardPayments +#endif + +class CardFormViewController: UIViewController { + + private let cardForm = CardForm() + private let cardClient: CardClient + private let orderID: String + private let sca: SCA + private let completion: (Result) -> Void + private lazy var continueButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Continue", for: .normal) + button.backgroundColor = .systemBlue + button.setTitleColor(.white, for: .normal) + button.layer.cornerRadius = 8 + button.heightAnchor.constraint(equalToConstant: 50).isActive = true + button.addTarget(self, action: #selector(handleContinueButton), for: .touchUpInside) + + button.addSubview(activityIndicator) + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + activityIndicator.centerXAnchor.constraint(equalTo: button.centerXAnchor), + activityIndicator.centerYAnchor.constraint(equalTo: button.centerYAnchor) + ]) + return button + }() + + private lazy var activityIndicator: UIActivityIndicatorView = { + let indicator = UIActivityIndicatorView(style: .medium) + indicator.color = .white + indicator.hidesWhenStopped = true + return indicator + }() + + private var isLoading = false { + didSet { + updateButtonState() + } + } + + private func updateButtonState() { + continueButton.isEnabled = !isLoading + if isLoading { + continueButton.setTitle("", for: .normal) + activityIndicator.startAnimating() + } else { + continueButton.setTitle("Continue", for: .normal) + activityIndicator.stopAnimating() + } + } + + init(cardClient: CardClient, orderID: String, sca: SCA, completion: @escaping (Result) -> Void) { + self.cardClient = cardClient + self.orderID = orderID + self.completion = completion + self.sca = sca + super.init(nibName: nil, bundle: nil) + + self.modalPresentationStyle = .pageSheet + if #available(iOS 15.0, *) { + if let sheet = self.sheetPresentationController { + sheet.detents = [.medium()] + sheet.prefersGrabberVisible = true + sheet.preferredCornerRadius = 12 + } + } else { + // Fallback on earlier versions + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + if #available(iOS 15.0, *) { + if let sheet = sheetPresentationController { + sheet.detents = [.medium()] + sheet.prefersGrabberVisible = true + sheet.preferredCornerRadius = 12 + } + } else { + // Fallback on earlier versions + } + setupViews() + } + + private func setupViews() { + view.backgroundColor = .white + + let stackView = UIStackView(arrangedSubviews: [cardForm, continueButton]) + stackView.axis = .vertical + stackView.spacing = 24 + stackView.isLayoutMarginsRelativeArrangement = true + stackView.layoutMargins = UIEdgeInsets(top: 0, left: 24, bottom: 24, right: 24) + + view.addSubview(stackView) + stackView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) + + navigationItem.leftBarButtonItem = UIBarButtonItem( + title: "Cancel", + style: .plain, + target: self, + action: #selector(dismissForm) + ) + } + + @objc private func dismissForm() { + dismiss(animated: true) + } + + @objc private func handleContinueButton() { + let card = Card.createCard( + cardNumber: cardForm.cardNumber ?? "", + expirationDate: cardForm.expiryDate ?? "", + cvv: cardForm.cvv ?? "" + ) + + let request = CardRequest( + orderID: orderID, + card: card, + sca: sca + ) + + isLoading = true + + Task { + do { + let result = try await cardClient.approveOrder(request: request) + completion(.success(result)) + await MainActor.run { + isLoading = false + self.dismiss(animated: true) + } + } catch { + isLoading = false + completion(.failure(error)) + } + } + } +} + +extension Card { + + static func createCard(cardNumber: String, expirationDate: String, cvv: String) -> Card { + let cleanedCardText = cardNumber.replacingOccurrences(of: " ", with: "") + + let expirationComponents = expirationDate.components(separatedBy: " / ") + let expirationMonth = expirationComponents[0] + let expirationYear = "20" + expirationComponents[1] + + return Card(number: cleanedCardText, expirationMonth: expirationMonth, expirationYear: expirationYear, securityCode: cvv) + } + + static 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 + } +} diff --git a/Sources/CardPaySheet/CardFormatter.swift b/Sources/CardPaySheet/CardFormatter.swift new file mode 100644 index 000000000..85b4d93b5 --- /dev/null +++ b/Sources/CardPaySheet/CardFormatter.swift @@ -0,0 +1,62 @@ +enum Fields { + case cardNumber + case expirationDate + case cvv +} + +class CardFormatter { + + func formatCardNumber(_ cardNumber: String) -> String { + /// remove spaces from card string + var formattedCardNumber: String = cardNumber.replacingOccurrences(of: " ", with: "") + + /// gets the card type to know how to format card string + let cardType = CardType.unknown.getCardType(formattedCardNumber) + + /// loops through where the space should be based on the card types indices and inserts a space at the desired index + cardType.spaceDelimiterIndices.forEach { index in + if index < formattedCardNumber.count { + let index = formattedCardNumber.index(formattedCardNumber.startIndex, offsetBy: index) + formattedCardNumber.insert(" ", at: index) + } + } + return formattedCardNumber + } + + func formatExpirationDate( _ expirationDate: String) -> String { + /// holder for the current expiration date + var formattedDate = expirationDate + + /// if the date count is greater than 2 append " / " after the month to format as MM/YY + if formattedDate.count > 2 { + formattedDate.insert(contentsOf: " / ", at: formattedDate.index(formattedDate.startIndex, offsetBy: 2)) + } + return formattedDate + } + + func formatFieldWith(_ text: String, field: Fields) -> String { + switch field { + case .cardNumber: + var cardNumberText: String + let cleanedText = text.replacingOccurrences(of: " ", with: "") + cardNumberText = String(cleanedText.prefix(CardType.unknown.getCardType(text).maxLength)) + cardNumberText = cardNumberText.filter { ("0"..."9").contains($0) } + cardNumberText = formatCardNumber(cardNumberText) + return cardNumberText + + case .expirationDate: + var expirationDateText: String + let cleanedText = text.replacingOccurrences(of: " / ", with: "") + expirationDateText = String(cleanedText.prefix(4)) + expirationDateText = expirationDateText.filter { ("0"..."9").contains($0) } + expirationDateText = formatExpirationDate(expirationDateText) + return expirationDateText + + case .cvv: + var cvvText: String + cvvText = String(text.prefix(4)) + cvvText = cvvText.filter { ("0"..."9").contains($0) } + return cvvText + } + } +} diff --git a/Sources/CardPaySheet/CardPaySheet.h b/Sources/CardPaySheet/CardPaySheet.h new file mode 100644 index 000000000..59272cc8d --- /dev/null +++ b/Sources/CardPaySheet/CardPaySheet.h @@ -0,0 +1,11 @@ +#import + +//! Project version number for CardPaySheet. +FOUNDATION_EXPORT double CardPaySheetVersionNumber; + +//! Project version string for CardPaySheet. +FOUNDATION_EXPORT const unsigned char CardPaySheetVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Sources/CardPaySheet/CardPaySheetView.swift b/Sources/CardPaySheet/CardPaySheetView.swift new file mode 100644 index 000000000..0bdb8b3ae --- /dev/null +++ b/Sources/CardPaySheet/CardPaySheetView.swift @@ -0,0 +1,34 @@ +import SwiftUI +#if canImport(CardPayments) +import CardPayments +#endif +#if canImport(CorePayments) +import CorePayments +#endif + +public struct CardPaySheetView: UIViewControllerRepresentable { + + private let cardPaySheet: CardPaySheet + private let orderID: String + private let sca: SCA + private let completion: (Result) -> Void + + public init( + config: CoreConfig, + orderID: String, + sca: SCA = .scaWhenRequired, + completion: @escaping (Result) -> Void + ) { + self.cardPaySheet = CardPaySheet(config: config, orderID: orderID, sca: sca) + self.orderID = orderID + self.sca = sca + self.completion = completion + } + + public func makeUIViewController(context: Context) -> UIViewController { + return cardPaySheet.createCardFormViewController(orderID: orderID, sca: sca, completion: completion) + } + + public func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + } +} diff --git a/Sources/CardPaySheet/CardPaysheet.swift b/Sources/CardPaySheet/CardPaysheet.swift new file mode 100644 index 000000000..76a3fe05c --- /dev/null +++ b/Sources/CardPaySheet/CardPaysheet.swift @@ -0,0 +1,44 @@ +import UIKit +#if canImport(CardPayments) +import CardPayments +#endif +#if canImport(CorePayments) +import CorePayments +#endif + +public class CardPaySheet { + + private let cardClient: CardClient + private let orderID: String + private let sca: SCA + + // need to pass in price info to display on pay button + public init(config: CoreConfig, orderID: String, sca: SCA = .scaWhenRequired) { + self.cardClient = CardClient(config: config) + self.orderID = orderID + self.sca = sca + } + + func createCardFormViewController( + orderID: String, + sca: SCA, + completion: @escaping (Result) -> Void + ) -> UIViewController { + let cardFormVC = CardFormViewController(cardClient: cardClient, orderID: orderID, sca: sca, completion: completion) + let nav = UINavigationController(rootViewController: cardFormVC) + + nav.modalPresentationStyle = .pageSheet + + if #available(iOS 15.0, *) { + if let sheet = nav.sheetPresentationController { + sheet.detents = [.medium()] + sheet.prefersGrabberVisible = true + sheet.preferredCornerRadius = 12 + } + } else { + // Fallback + } + + return nav + } +} diff --git a/Sources/CardPaySheet/CardType.swift b/Sources/CardPaySheet/CardType.swift new file mode 100644 index 000000000..6960fc304 --- /dev/null +++ b/Sources/CardPaySheet/CardType.swift @@ -0,0 +1,48 @@ +enum CardType { + + case americanExpress, visa, discover, masterCard, unknown + + var spaceDelimiterIndices: [Int] { + switch self { + case .americanExpress: + return [4, 11] + case .visa: + return [4, 9, 14] + case .discover: + return [4, 9, 14] + case .masterCard: + return [4, 9, 14] + case .unknown: + return [4, 9, 14] + } + } + + var maxLength: Int { + switch self { + case .americanExpress: + return 15 + case .visa: + return 16 + case .discover: + return 19 + case .masterCard: + return 16 + case .unknown: + return 16 + } + } + + func getCardType(_ cardNumber: String) -> CardType { + if cardNumber.starts(with: "34") || cardNumber.starts(with: "37") { + return .americanExpress + } else if cardNumber.starts(with: "4") { + return .visa + } else if cardNumber.starts(with: "6011") || cardNumber.starts(with: "65") { + return .discover + } else if cardNumber.starts(with: "51") || cardNumber.starts(with: "52") || cardNumber.starts(with: "53") || cardNumber.starts(with: "54") || cardNumber.starts(with: "55") { + return .masterCard + } else { + return .unknown + } + } +}