Skip to content

Commit

Permalink
PayPal Web Demo App Refactor (#223)
Browse files Browse the repository at this point in the history
* consolidate order states in PayPal Web views

* add generic SuccessView

* WIP - shared result view

* WIP - start consolidating result views

* WIP - continue refactoring state for PayPal Web

* WIP - continued paypal web demo app refactor

* WIP - start cleaning up file names; method signatures; etc for PayPal Web

* cleanup how data is passed

* add new state and general cleanup

* cleanup funding source name, remove redundant types

* cleanup views and remove order completion view

* rename PayPalWebCompleteTransactionView to PayPalWebTransactionView; replace result view with status view so they do not dissapear on change for transaction view

* update funding source

* extract intent from view model directly

* minor cleanup

* sort paypal web files by name

* cleanup authorize/capture order into singular method

* add logic to reset state; combine loaded and success state; extract update logic into methods

* merge main into demo-app-refactor

* add ScrollView to PayPalWebTransactionView

* PR feedback: rename Status enum to OrderStatus

* PR feedback: rely on status instead or resetting state

* update name of payPalViewModel to payPalWebViewModel

* add scroll to bottom of PayPalWebTransactionView on vault with purchase

* update order status from started to created to more accurately refect status

* update PayPalWebDemoView to PayPalWebPaymentsView

* add navigationViewStyle

* update delegate to use updateState

* move PayPalWebViewModel into PayPalWebPayments feature directory

* remove OrderStatus and add cases to CurrentState

* refactor current state; add back in OrderStatus; fix bug with ErrorView
  • Loading branch information
jaxdesmarais authored Dec 5, 2023
1 parent ae1adc3 commit a0ec619
Show file tree
Hide file tree
Showing 21 changed files with 386 additions and 534 deletions.
74 changes: 33 additions & 41 deletions Demo/Demo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Demo/Demo/Models/Order.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ struct Order: Codable, Equatable {

let id: String
let status: String
var paymentSource: PaymentSource?
let paymentSource: PaymentSource?

struct PaymentSource: Codable, Equatable {

Expand Down
16 changes: 15 additions & 1 deletion Demo/Demo/Networking/DemoMerchantAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,21 @@ final class DemoMerchantAPI {
throw error
}
}


func completeOrder(intent: Intent, orderID: String) async throws -> Order {
let intent = intent == .authorize ? "authorize" : "capture"
guard let url = buildBaseURL(
with: "/orders/\(orderID)/\(intent)",
selectedMerchantIntegration: DemoSettings.merchantIntegration
) else {
throw URLResponseError.invalidURL
}

let urlRequest = buildURLRequest(method: "POST", url: url, body: EmptyBodyParams())
let data = try await data(for: urlRequest)
return try parse(from: data)
}

func captureOrder(orderID: String, selectedMerchantIntegration: MerchantIntegration) async throws -> Order {
guard let url = buildBaseURL(with: "/orders/\(orderID)/capture", selectedMerchantIntegration: selectedMerchantIntegration) else {
throw URLResponseError.invalidURL
Expand Down
8 changes: 8 additions & 0 deletions Demo/Demo/SwiftUIComponents/CurrentState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Foundation

enum CurrentState: Equatable {
case idle
case loading
case success
case error(message: String)
}
6 changes: 3 additions & 3 deletions Demo/Demo/SwiftUIComponents/FeatureSelectionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ struct FeatureSelectionView: View {
Text("Card Vaulting")
}
NavigationLink {
PayPalWebView()
.navigationTitle("PayPalWeb Payment")
PayPalWebPaymentsView()
.navigationTitle("PayPal Web")
} label: {
Text("PayPalWeb Payment")
Text("PayPal Web")
}
NavigationLink {
SwiftUINativeCheckoutDemo()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import SwiftUI
import PaymentButtons

struct PayPalTransactionView: View {
struct PayPalWebButtonsView: View {

@ObservedObject var paypalWebViewModel: PayPalWebViewModel
let orderID: String
@ObservedObject var payPalWebViewModel: PayPalWebViewModel

var body: some View {
VStack {
VStack(alignment: .center, spacing: 40) {
PayPalButton.Representable(color: .blue, size: .mini) {
paypalWebViewModel.paymentButtonTapped(orderID: orderID, funding: .paypal)
payPalWebViewModel.paymentButtonTapped(funding: .paypal)
}
.frame(maxWidth: .infinity, maxHeight: 40)
PayPalCreditButton.Representable(color: .black, edges: .softEdges, size: .expanded) {
paypalWebViewModel.paymentButtonTapped(orderID: orderID, funding: .paypalCredit)
payPalWebViewModel.paymentButtonTapped(funding: .paypalCredit)
}
.frame(maxWidth: .infinity, maxHeight: 40)
PayPalPayLaterButton.Representable(color: .silver, edges: .rounded, size: .full) {
paypalWebViewModel.paymentButtonTapped(orderID: orderID, funding: .paylater)
payPalWebViewModel.paymentButtonTapped(funding: .paylater)
}
.frame(maxWidth: .infinity, maxHeight: 40)
}
Expand All @@ -29,15 +28,20 @@ struct PayPalTransactionView: View {
.stroke(.gray, lineWidth: 2)
.padding(5)
)
PayPalWebApprovalResultView(paypalWebViewModel: paypalWebViewModel)
if paypalWebViewModel.state.checkoutResult != nil {

if payPalWebViewModel.checkoutResult != nil && payPalWebViewModel.state == .success {
PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .approved)
NavigationLink {
PayPalWebOrderCompletionView(orderID: orderID, payPalWebViewModel: paypalWebViewModel)
PayPalWebTransactionView(payPalWebViewModel: payPalWebViewModel)
.navigationTitle("Complete Transaction")
} label: {
Text("Complete Order Transaction")
Text("Complete Transaction")
}
.navigationViewStyle(StackNavigationViewStyle())
.buttonStyle(RoundedBlueButtonStyle())
.padding()
} else if case .error = payPalWebViewModel.state {
PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .error)
}
Spacer()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import SwiftUI

struct CreateOrderPayPalWebView: View {
struct PayPalWebCreateOrderView: View {

@ObservedObject var paypalWebViewModel: PayPalWebViewModel
@ObservedObject var payPalWebViewModel: PayPalWebViewModel

@State private var selectedIntent: Intent = .authorize
@State var shouldVaultSelected = false

let selectedMerchantIntegration: MerchantIntegration

var body: some View {
VStack(spacing: 16) {
HStack {
Expand All @@ -31,20 +29,15 @@ struct CreateOrderPayPalWebView: View {
Button("Create an Order") {
Task {
do {
paypalWebViewModel.state.intent = selectedIntent
try await paypalWebViewModel.createOrder(
amount: "10.00",
selectedMerchantIntegration: DemoSettings.merchantIntegration,
intent: selectedIntent.rawValue,
shouldVault: shouldVaultSelected
)
payPalWebViewModel.intent = selectedIntent
try await payPalWebViewModel.createOrder(shouldVault: shouldVaultSelected)
} catch {
print("Error in getting setup token. \(error.localizedDescription)")
}
}
}
.buttonStyle(RoundedBlueButtonStyle())
if case .loading = paypalWebViewModel.state.createdOrderResponse {
if payPalWebViewModel.state == .loading {
CircularProgressView()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import SwiftUI

struct PayPalWebPaymentsView: View {

@StateObject var payPalWebViewModel = PayPalWebViewModel()

var body: some View {
ScrollView {
VStack(spacing: 16) {
PayPalWebCreateOrderView(payPalWebViewModel: payPalWebViewModel)
if payPalWebViewModel.createOrderResult != nil && payPalWebViewModel.state == .success {
PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .created)
NavigationLink {
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)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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)
case .error(let errorMessage):
ErrorView(errorMessage: errorMessage)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import SwiftUI

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)")
}
}
}
.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")
}
}
Spacer()
}
}
}
}
Loading

0 comments on commit a0ec619

Please sign in to comment.