diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index 09864f64a..64e7f630d 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -70,8 +70,8 @@ BC9D4D2627C6D1720089E5B1 /* XCUIElement+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC9D4D2527C6D1720089E5B1 /* XCUIElement+Helpers.swift */; }; BE1766B326F911A2007EF438 /* URLResponseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE1766B226F911A2007EF438 /* URLResponseError.swift */; }; BE1766D926FA7BC8007EF438 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = BE1766D826FA7BC8007EF438 /* Settings.bundle */; }; + BE5898952B2B91F800AA196E /* LabelViewText.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE5898942B2B91F800AA196E /* LabelViewText.swift */; }; BE8117642B07E778009867B9 /* PayPalWebResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE8117632B07E778009867B9 /* PayPalWebResultView.swift */; }; - BE8117662B080202009867B9 /* PayPalWebStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE8117652B080202009867B9 /* PayPalWebStatusView.swift */; }; BE8117682B080472009867B9 /* CurrentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE8117672B080472009867B9 /* CurrentState.swift */; }; BE9F36D82745490400AFC7DA /* FloatingLabelTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE9F36D72745490400AFC7DA /* FloatingLabelTextField.swift */; }; BECD84A027036DC2007CCAE4 /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = BECD849F27036DC2007CCAE4 /* Environment.swift */; }; @@ -198,8 +198,8 @@ BE1766B226F911A2007EF438 /* URLResponseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLResponseError.swift; sourceTree = ""; }; BE1766D826FA7BC8007EF438 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; BE420F3628189A7A00D8D66A /* PayPalUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PayPalUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BE5898942B2B91F800AA196E /* LabelViewText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelViewText.swift; sourceTree = ""; }; BE8117632B07E778009867B9 /* PayPalWebResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalWebResultView.swift; sourceTree = ""; }; - BE8117652B080202009867B9 /* PayPalWebStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalWebStatusView.swift; sourceTree = ""; }; BE8117672B080472009867B9 /* CurrentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentState.swift; sourceTree = ""; }; BE9F36D72745490400AFC7DA /* FloatingLabelTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingLabelTextField.swift; sourceTree = ""; }; BECD849F27036DC2007CCAE4 /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = ""; }; @@ -287,7 +287,6 @@ 3BA570022AA053AE0081D14F /* PayPalWebCreateOrderView.swift */, 3BA570002AA052E80081D14F /* PayPalWebPaymentsView.swift */, BE8117632B07E778009867B9 /* PayPalWebResultView.swift */, - BE8117652B080202009867B9 /* PayPalWebStatusView.swift */, 3B6472A62AFAEB3A004745C4 /* PayPalWebTransactionView.swift */, 3BA56FFB2A9FEFE90081D14F /* PayPalWebViewModel.swift */, ); @@ -313,6 +312,7 @@ 3BC6220A2A97204E00251B85 /* CircularProgressView.swift */, 3BC622082A97198500251B85 /* LeadingText.swift */, 3BA5700A2AA13C1C0081D14F /* CoreConfigManager.swift */, + BE5898942B2B91F800AA196E /* LabelViewText.swift */, ); path = CommonComponents; sourceTree = ""; @@ -606,6 +606,7 @@ 3BC622092A97198500251B85 /* LeadingText.swift in Sources */, 3B80D5102A291CB100D2EAC4 /* ClientIDResponse.swift in Sources */, 3BCCFE462A9D47AC00C5102F /* CardExtensions.swift in Sources */, + BE5898952B2B91F800AA196E /* LabelViewText.swift in Sources */, CB34B32328BE3A9A001325B9 /* PayPalViewModel.swift in Sources */, 3BC622072A97115700251B85 /* RoundedBlueButtonStyle.swift in Sources */, 3B4DD9A22A8982B000F4A716 /* CardFormView.swift in Sources */, @@ -629,7 +630,6 @@ 3BA0A58B2B1E240300330681 /* VaultViewModel.swift in Sources */, 80F33CF326F8EA50006811B1 /* DemoSettings.swift in Sources */, 3BA56FE72A9DC9D70081D14F /* CardPaymentViewModel.swift in Sources */, - BE8117662B080202009867B9 /* PayPalWebStatusView.swift in Sources */, 3BA5700B2AA13C1C0081D14F /* CoreConfigManager.swift in Sources */, 80E4300C2AD82C8D003CA748 /* ShippingPreference.swift in Sources */, BEDE304A275EA33500D275FD /* UIViewController+Extension.swift in Sources */, diff --git a/Demo/Demo/SwiftUIComponents/CommonComponents/LabelViewText.swift b/Demo/Demo/SwiftUIComponents/CommonComponents/LabelViewText.swift new file mode 100644 index 000000000..a1f63367c --- /dev/null +++ b/Demo/Demo/SwiftUIComponents/CommonComponents/LabelViewText.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct LabelViewText: View { + + var titleText: String + var bodyText: String + + var body: some View { + HStack { + Text(titleText).fontWeight(.bold) + Text(bodyText) + } + } + + init(_ titleText: String, bodyText: String) { + self.titleText = titleText + self.bodyText = bodyText + } +} diff --git a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebButtonsView.swift b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebButtonsView.swift index e213cb94e..5a2d631d2 100644 --- a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebButtonsView.swift +++ b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebButtonsView.swift @@ -1,49 +1,52 @@ import SwiftUI import PaymentButtons +import PayPalWebPayments struct PayPalWebButtonsView: View { @ObservedObject var payPalWebViewModel: PayPalWebViewModel + @State private var selectedFundingSource: PayPalWebCheckoutFundingSource = .paypal + var body: some View { VStack { - VStack(alignment: .center, spacing: 40) { - PayPalButton.Representable(color: .blue, size: .mini) { - payPalWebViewModel.paymentButtonTapped(funding: .paypal) + VStack(alignment: .center, spacing: 16) { + HStack { + Text("Checkout with PayPal") + .font(.system(size: 20)) + Spacer() } - .frame(maxWidth: .infinity, maxHeight: 40) - PayPalCreditButton.Representable(color: .black, edges: .softEdges, size: .expanded) { - payPalWebViewModel.paymentButtonTapped(funding: .paypalCredit) + .frame(maxWidth: .infinity) + .font(.headline) + Picker("Funding Source", selection: $selectedFundingSource) { + Text("PayPal").tag(PayPalWebCheckoutFundingSource.paypal) + Text("PayPal Credit").tag(PayPalWebCheckoutFundingSource.paylater) + Text("Pay Later").tag(PayPalWebCheckoutFundingSource.paypalCredit) } - .frame(maxWidth: .infinity, maxHeight: 40) - PayPalPayLaterButton.Representable(color: .silver, edges: .rounded, size: .full) { - payPalWebViewModel.paymentButtonTapped(funding: .paylater) + .pickerStyle(SegmentedPickerStyle()) + + switch selectedFundingSource { + case .paypalCredit: + PayPalCreditButton.Representable(color: .black, size: .full) { + payPalWebViewModel.paymentButtonTapped(funding: .paypalCredit) + } + case .paylater: + PayPalPayLaterButton.Representable(color: .silver, edges: .softEdges, size: .full) { + payPalWebViewModel.paymentButtonTapped(funding: .paylater) + } + case .paypal: + PayPalButton.Representable(color: .blue, size: .full) { + payPalWebViewModel.paymentButtonTapped(funding: .paypal) + } } - .frame(maxWidth: .infinity, maxHeight: 40) } + .frame(height: 150) .padding(20) - .padding() .background( RoundedRectangle(cornerRadius: 10) .stroke(.gray, lineWidth: 2) .padding(5) ) - - if payPalWebViewModel.checkoutResult != nil && payPalWebViewModel.state == .success { - PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .approved) - NavigationLink { - PayPalWebTransactionView(payPalWebViewModel: payPalWebViewModel) - .navigationTitle("Complete Transaction") - } label: { - Text("Complete Transaction") - } - .navigationViewStyle(StackNavigationViewStyle()) - .buttonStyle(RoundedBlueButtonStyle()) - .padding() - } else if case .error = payPalWebViewModel.state { - PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .error) - } - Spacer() } } } diff --git a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebCreateOrderView.swift b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebCreateOrderView.swift index 0af1754d6..ba995324d 100644 --- a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebCreateOrderView.swift +++ b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebCreateOrderView.swift @@ -13,6 +13,9 @@ struct PayPalWebCreateOrderView: View { Text("Create an Order") .font(.system(size: 20)) Spacer() + Button("Reset") { + payPalWebViewModel.resetState() + } } .frame(maxWidth: .infinity) .font(.headline) @@ -37,7 +40,7 @@ struct PayPalWebCreateOrderView: View { } } .buttonStyle(RoundedBlueButtonStyle()) - if payPalWebViewModel.state == .loading { + if payPalWebViewModel.state == .loading && payPalWebViewModel.checkoutResult == nil { CircularProgressView() } } diff --git a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebPaymentsView.swift b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebPaymentsView.swift index 88a03e406..3b47ac991 100644 --- a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebPaymentsView.swift +++ b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebPaymentsView.swift @@ -6,20 +6,31 @@ struct PayPalWebPaymentsView: View { var body: some View { ScrollView { - VStack(spacing: 16) { - PayPalWebCreateOrderView(payPalWebViewModel: payPalWebViewModel) - if payPalWebViewModel.createOrderResult != nil && payPalWebViewModel.state == .success { - PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .created) - NavigationLink { + ScrollViewReader { scrollView in + VStack(spacing: 16) { + PayPalWebCreateOrderView(payPalWebViewModel: payPalWebViewModel) + + if payPalWebViewModel.orderID != nil { PayPalWebButtonsView(payPalWebViewModel: payPalWebViewModel) - .navigationTitle("Checkout with PayPal") - } label: { - Text("Checkout with PayPal") } - .buttonStyle(RoundedBlueButtonStyle()) - .padding() - } else if case .error = payPalWebViewModel.state { - PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .error) + + PayPalWebResultView(payPalWebViewModel: payPalWebViewModel) + + if payPalWebViewModel.checkoutResult != nil { + PayPalWebTransactionView(payPalWebViewModel: payPalWebViewModel) + .padding(.bottom, 20) + .id("bottomView") + .onAppear { + withAnimation { + scrollView.scrollTo("bottomView") + } + } + } + } + .onChange(of: payPalWebViewModel.state) { _ in + withAnimation { + scrollView.scrollTo("bottomView") + } } } } diff --git a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebResultView.swift b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebResultView.swift index 85cd823c0..bb3bfa5ac 100644 --- a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebResultView.swift +++ b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebResultView.swift @@ -1,26 +1,57 @@ import SwiftUI -enum OrderStatus { - case created - case approved - case completed - case error -} - struct PayPalWebResultView: View { @ObservedObject var payPalWebViewModel: PayPalWebViewModel - var status: OrderStatus - var body: some View { switch payPalWebViewModel.state { case .idle, .loading: EmptyView() case .success: - PayPalWebStatusView(status: status, payPalWebViewModel: payPalWebViewModel) + successView case .error(let errorMessage): ErrorView(errorMessage: errorMessage) } } + + var successView: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("Order Details") + .font(.system(size: 20)) + Spacer() + } + if let orderID = payPalWebViewModel.orderID { + LabelViewText("Order ID:", bodyText: orderID) + } + + if let status = payPalWebViewModel.order?.status { + LabelViewText("Status:", bodyText: status) + } + + if let payerID = payPalWebViewModel.checkoutResult?.payerID { + LabelViewText("Payer ID:", bodyText: payerID) + } + + if let emailAddress = payPalWebViewModel.order?.paymentSource?.paypal?.emailAddress { + LabelViewText("Email:", bodyText: emailAddress) + } + + if let vaultID = payPalWebViewModel.order?.paymentSource?.paypal?.attributes?.vault.id { + LabelViewText("Payment Token:", bodyText: vaultID) + } + + if let customerID = payPalWebViewModel.order?.paymentSource?.paypal?.attributes?.vault.customer.id { + LabelViewText("Customer ID:", bodyText: customerID) + } + } + .frame(maxWidth: .infinity) + .padding() + .background( + RoundedRectangle(cornerRadius: 10) + .stroke(.gray, lineWidth: 2) + .padding(5) + ) + } } diff --git a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebStatusView.swift b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebStatusView.swift deleted file mode 100644 index 2ec377e39..000000000 --- a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebStatusView.swift +++ /dev/null @@ -1,76 +0,0 @@ -import SwiftUI - -struct PayPalWebStatusView: View { - - var status: OrderStatus - var payPalWebViewModel: PayPalWebViewModel - - var body: some View { - VStack(spacing: 16) { - switch status { - case .created: - HStack { - Text("Order Created") - .font(.system(size: 20)) - Spacer() - } - if let order = payPalWebViewModel.createOrderResult { - LeadingText("Order ID", weight: .bold) - LeadingText("\(order.id)") - LeadingText("Status", weight: .bold) - LeadingText("\(order.status)") - } - case .approved: - HStack { - Text("Order Approved") - .font(.system(size: 20)) - Spacer() - } - if let order = payPalWebViewModel.createOrderResult { - LeadingText("Intent", weight: .bold) - LeadingText("\(payPalWebViewModel.intent)") - LeadingText("Order ID", weight: .bold) - LeadingText("\(order.id)") - LeadingText("Payer ID", weight: .bold) - LeadingText("\(payPalWebViewModel.checkoutResult?.payerID ?? "")") - } - case .completed: - if let order = payPalWebViewModel.transactionResult { - HStack { - Text("Order \(payPalWebViewModel.intent.rawValue.capitalized)d") - .font(.system(size: 20)) - Spacer() - } - LeadingText("Order ID", weight: .bold) - LeadingText("\(order.id)") - LeadingText("Status", weight: .bold) - LeadingText("\(order.status)") - - if let emailAddress = order.paymentSource?.paypal?.emailAddress { - LeadingText("Email", weight: .bold) - LeadingText("\(emailAddress)") - } - - if let vaultID = order.paymentSource?.paypal?.attributes?.vault.id { - LeadingText("Vault ID / Payment Token", weight: .bold) - LeadingText("\(vaultID)") - } - - if let customerID = order.paymentSource?.paypal?.attributes?.vault.customer.id { - LeadingText("Customer ID", weight: .bold) - LeadingText("\(customerID)") - } - } - default: - Text("") - } - } - .frame(maxWidth: .infinity) - .padding() - .background( - RoundedRectangle(cornerRadius: 10) - .stroke(.gray, lineWidth: 2) - .padding(5) - ) - } -} diff --git a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebTransactionView.swift b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebTransactionView.swift index 88b075434..e8b0a132e 100644 --- a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebTransactionView.swift +++ b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebTransactionView.swift @@ -5,41 +5,23 @@ struct PayPalWebTransactionView: View { @ObservedObject var payPalWebViewModel: PayPalWebViewModel var body: some View { - ScrollView { - ScrollViewReader { scrollView in - VStack { - PayPalWebStatusView(status: .approved, payPalWebViewModel: payPalWebViewModel) - ZStack { - Button("\(payPalWebViewModel.intent.rawValue.capitalized) Order") { - Task { - do { - try await payPalWebViewModel.completeTransaction() - } catch { - print("Error capturing order: \(error.localizedDescription)") - } - } + VStack { + ZStack { + Button("\(payPalWebViewModel.intent.rawValue.capitalized) Order") { + Task { + do { + try await payPalWebViewModel.completeTransaction() + } catch { + print("Error capturing order: \(error.localizedDescription)") } - .buttonStyle(RoundedBlueButtonStyle()) - .padding() - - if payPalWebViewModel.state == .loading { - CircularProgressView() - } - } - - if payPalWebViewModel.transactionResult != nil && payPalWebViewModel.state == .success { - PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .completed) - .id("bottomView") - } else if case .error = payPalWebViewModel.state { - PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .error) } } - .onChange(of: payPalWebViewModel.transactionResult) { _ in - withAnimation { - scrollView.scrollTo("bottomView") - } + .buttonStyle(RoundedBlueButtonStyle()) + .padding() + + if payPalWebViewModel.state == .loading { + CircularProgressView() } - Spacer() } } } diff --git a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebViewModel.swift b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebViewModel.swift index 4b2f4b0be..224c435ba 100644 --- a/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebViewModel.swift +++ b/Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebViewModel.swift @@ -6,12 +6,14 @@ class PayPalWebViewModel: ObservableObject, PayPalWebCheckoutDelegate { @Published var state: CurrentState = .idle @Published var intent: Intent = .authorize - @Published var createOrderResult: Order? - @Published var transactionResult: Order? + @Published var order: Order? @Published var checkoutResult: PayPalWebCheckoutResult? var payPalWebCheckoutClient: PayPalWebCheckoutClient? - var orderID: String? + + var orderID: String? { + order?.id + } let configManager = CoreConfigManager(domain: "PayPalWeb Payments") @@ -46,11 +48,7 @@ class PayPalWebViewModel: ObservableObject, PayPalWebCheckoutDelegate { selectedMerchantIntegration: DemoSettings.merchantIntegration ) - self.orderID = order.id - - DispatchQueue.main.async { - self.createOrderResult = order - } + updateOrder(order) updateState(.success) print("✅ fetched orderID: \(order.id) with status: \(order.status)") } catch { @@ -73,18 +71,23 @@ class PayPalWebViewModel: ObservableObject, PayPalWebCheckoutDelegate { let payPalRequest = PayPalWebCheckoutRequest(orderID: orderID, fundingSource: funding) payPalWebCheckoutClient.start(request: payPalRequest) } + updateState(.success) } catch { print("Error starting PayPalWebCheckoutClient") - state = .error(message: error.localizedDescription) + updateState(.error(message: error.localizedDescription)) } } } - func getPayPalClient() async throws -> PayPalWebCheckoutClient { + func getPayPalClient() async throws -> PayPalWebCheckoutClient? { do { let config = try await configManager.getCoreConfig() let payPalClient = PayPalWebCheckoutClient(config: config) return payPalClient + } catch { + updateState(.error(message: error.localizedDescription)) + print("❌ failed to create PayPalWebCheckoutClient with error: \(error.localizedDescription)") + return nil } } @@ -94,9 +97,7 @@ class PayPalWebViewModel: ObservableObject, PayPalWebCheckoutDelegate { if let orderID { let order = try await DemoMerchantAPI.sharedService.completeOrder(intent: intent, orderID: orderID) - DispatchQueue.main.async { - self.transactionResult = order - } + updateOrder(order) updateState(.success) } } catch { @@ -105,6 +106,18 @@ class PayPalWebViewModel: ObservableObject, PayPalWebCheckoutDelegate { } } + func resetState() { + updateState(.idle) + order = nil + checkoutResult = nil + } + + private func updateOrder(_ order: Order) { + DispatchQueue.main.async { + self.order = order + } + } + private func updateState(_ state: CurrentState) { DispatchQueue.main.async { self.state = state