Skip to content

Commit a0ec619

Browse files
authored
PayPal Web Demo App Refactor (#223)
* 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
1 parent ae1adc3 commit a0ec619

21 files changed

+386
-534
lines changed

Demo/Demo.xcodeproj/project.pbxproj

+33-41
Large diffs are not rendered by default.

Demo/Demo/Models/Order.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ struct Order: Codable, Equatable {
22

33
let id: String
44
let status: String
5-
var paymentSource: PaymentSource?
5+
let paymentSource: PaymentSource?
66

77
struct PaymentSource: Codable, Equatable {
88

Demo/Demo/Networking/DemoMerchantAPI.swift

+15-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,21 @@ final class DemoMerchantAPI {
5353
throw error
5454
}
5555
}
56-
56+
57+
func completeOrder(intent: Intent, orderID: String) async throws -> Order {
58+
let intent = intent == .authorize ? "authorize" : "capture"
59+
guard let url = buildBaseURL(
60+
with: "/orders/\(orderID)/\(intent)",
61+
selectedMerchantIntegration: DemoSettings.merchantIntegration
62+
) else {
63+
throw URLResponseError.invalidURL
64+
}
65+
66+
let urlRequest = buildURLRequest(method: "POST", url: url, body: EmptyBodyParams())
67+
let data = try await data(for: urlRequest)
68+
return try parse(from: data)
69+
}
70+
5771
func captureOrder(orderID: String, selectedMerchantIntegration: MerchantIntegration) async throws -> Order {
5872
guard let url = buildBaseURL(with: "/orders/\(orderID)/capture", selectedMerchantIntegration: selectedMerchantIntegration) else {
5973
throw URLResponseError.invalidURL
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Foundation
2+
3+
enum CurrentState: Equatable {
4+
case idle
5+
case loading
6+
case success
7+
case error(message: String)
8+
}

Demo/Demo/SwiftUIComponents/FeatureSelectionView.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ struct FeatureSelectionView: View {
3838
Text("Card Vaulting")
3939
}
4040
NavigationLink {
41-
PayPalWebView()
42-
.navigationTitle("PayPalWeb Payment")
41+
PayPalWebPaymentsView()
42+
.navigationTitle("PayPal Web")
4343
} label: {
44-
Text("PayPalWeb Payment")
44+
Text("PayPal Web")
4545
}
4646
NavigationLink {
4747
SwiftUINativeCheckoutDemo()

Demo/Demo/SwiftUIComponents/PayPalWebViews/PayPalTransactionView.swift renamed to Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebButtonsView.swift

+14-10
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
import SwiftUI
22
import PaymentButtons
33

4-
struct PayPalTransactionView: View {
4+
struct PayPalWebButtonsView: View {
55

6-
@ObservedObject var paypalWebViewModel: PayPalWebViewModel
7-
let orderID: String
6+
@ObservedObject var payPalWebViewModel: PayPalWebViewModel
87

98
var body: some View {
109
VStack {
1110
VStack(alignment: .center, spacing: 40) {
1211
PayPalButton.Representable(color: .blue, size: .mini) {
13-
paypalWebViewModel.paymentButtonTapped(orderID: orderID, funding: .paypal)
12+
payPalWebViewModel.paymentButtonTapped(funding: .paypal)
1413
}
1514
.frame(maxWidth: .infinity, maxHeight: 40)
1615
PayPalCreditButton.Representable(color: .black, edges: .softEdges, size: .expanded) {
17-
paypalWebViewModel.paymentButtonTapped(orderID: orderID, funding: .paypalCredit)
16+
payPalWebViewModel.paymentButtonTapped(funding: .paypalCredit)
1817
}
1918
.frame(maxWidth: .infinity, maxHeight: 40)
2019
PayPalPayLaterButton.Representable(color: .silver, edges: .rounded, size: .full) {
21-
paypalWebViewModel.paymentButtonTapped(orderID: orderID, funding: .paylater)
20+
payPalWebViewModel.paymentButtonTapped(funding: .paylater)
2221
}
2322
.frame(maxWidth: .infinity, maxHeight: 40)
2423
}
@@ -29,15 +28,20 @@ struct PayPalTransactionView: View {
2928
.stroke(.gray, lineWidth: 2)
3029
.padding(5)
3130
)
32-
PayPalWebApprovalResultView(paypalWebViewModel: paypalWebViewModel)
33-
if paypalWebViewModel.state.checkoutResult != nil {
31+
32+
if payPalWebViewModel.checkoutResult != nil && payPalWebViewModel.state == .success {
33+
PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .approved)
3434
NavigationLink {
35-
PayPalWebOrderCompletionView(orderID: orderID, payPalWebViewModel: paypalWebViewModel)
35+
PayPalWebTransactionView(payPalWebViewModel: payPalWebViewModel)
36+
.navigationTitle("Complete Transaction")
3637
} label: {
37-
Text("Complete Order Transaction")
38+
Text("Complete Transaction")
3839
}
40+
.navigationViewStyle(StackNavigationViewStyle())
3941
.buttonStyle(RoundedBlueButtonStyle())
4042
.padding()
43+
} else if case .error = payPalWebViewModel.state {
44+
PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .error)
4145
}
4246
Spacer()
4347
}

Demo/Demo/SwiftUIComponents/PayPalWebViews/CreateOrderPayPalWebView.swift renamed to Demo/Demo/SwiftUIComponents/PayPalWebPayments/PayPalWebCreateOrderView.swift

+5-12
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import SwiftUI
22

3-
struct CreateOrderPayPalWebView: View {
3+
struct PayPalWebCreateOrderView: View {
44

5-
@ObservedObject var paypalWebViewModel: PayPalWebViewModel
5+
@ObservedObject var payPalWebViewModel: PayPalWebViewModel
66

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

10-
let selectedMerchantIntegration: MerchantIntegration
11-
1210
var body: some View {
1311
VStack(spacing: 16) {
1412
HStack {
@@ -31,20 +29,15 @@ struct CreateOrderPayPalWebView: View {
3129
Button("Create an Order") {
3230
Task {
3331
do {
34-
paypalWebViewModel.state.intent = selectedIntent
35-
try await paypalWebViewModel.createOrder(
36-
amount: "10.00",
37-
selectedMerchantIntegration: DemoSettings.merchantIntegration,
38-
intent: selectedIntent.rawValue,
39-
shouldVault: shouldVaultSelected
40-
)
32+
payPalWebViewModel.intent = selectedIntent
33+
try await payPalWebViewModel.createOrder(shouldVault: shouldVaultSelected)
4134
} catch {
4235
print("Error in getting setup token. \(error.localizedDescription)")
4336
}
4437
}
4538
}
4639
.buttonStyle(RoundedBlueButtonStyle())
47-
if case .loading = paypalWebViewModel.state.createdOrderResponse {
40+
if payPalWebViewModel.state == .loading {
4841
CircularProgressView()
4942
}
5043
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import SwiftUI
2+
3+
struct PayPalWebPaymentsView: View {
4+
5+
@StateObject var payPalWebViewModel = PayPalWebViewModel()
6+
7+
var body: some View {
8+
ScrollView {
9+
VStack(spacing: 16) {
10+
PayPalWebCreateOrderView(payPalWebViewModel: payPalWebViewModel)
11+
if payPalWebViewModel.createOrderResult != nil && payPalWebViewModel.state == .success {
12+
PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .created)
13+
NavigationLink {
14+
PayPalWebButtonsView(payPalWebViewModel: payPalWebViewModel)
15+
.navigationTitle("Checkout with PayPal")
16+
} label: {
17+
Text("Checkout with PayPal")
18+
}
19+
.buttonStyle(RoundedBlueButtonStyle())
20+
.padding()
21+
} else if case .error = payPalWebViewModel.state {
22+
PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .error)
23+
}
24+
}
25+
}
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import SwiftUI
2+
3+
enum OrderStatus {
4+
case created
5+
case approved
6+
case completed
7+
case error
8+
}
9+
10+
struct PayPalWebResultView: View {
11+
12+
@ObservedObject var payPalWebViewModel: PayPalWebViewModel
13+
14+
var status: OrderStatus
15+
16+
var body: some View {
17+
switch payPalWebViewModel.state {
18+
case .idle, .loading:
19+
EmptyView()
20+
case .success:
21+
PayPalWebStatusView(status: status, payPalWebViewModel: payPalWebViewModel)
22+
case .error(let errorMessage):
23+
ErrorView(errorMessage: errorMessage)
24+
}
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import SwiftUI
2+
3+
struct PayPalWebStatusView: View {
4+
5+
var status: OrderStatus
6+
var payPalWebViewModel: PayPalWebViewModel
7+
8+
var body: some View {
9+
VStack(spacing: 16) {
10+
switch status {
11+
case .created:
12+
HStack {
13+
Text("Order Created")
14+
.font(.system(size: 20))
15+
Spacer()
16+
}
17+
if let order = payPalWebViewModel.createOrderResult {
18+
LeadingText("Order ID", weight: .bold)
19+
LeadingText("\(order.id)")
20+
LeadingText("Status", weight: .bold)
21+
LeadingText("\(order.status)")
22+
}
23+
case .approved:
24+
HStack {
25+
Text("Order Approved")
26+
.font(.system(size: 20))
27+
Spacer()
28+
}
29+
if let order = payPalWebViewModel.createOrderResult {
30+
LeadingText("Intent", weight: .bold)
31+
LeadingText("\(payPalWebViewModel.intent)")
32+
LeadingText("Order ID", weight: .bold)
33+
LeadingText("\(order.id)")
34+
LeadingText("Payer ID", weight: .bold)
35+
LeadingText("\(payPalWebViewModel.checkoutResult?.payerID ?? "")")
36+
}
37+
case .completed:
38+
if let order = payPalWebViewModel.transactionResult {
39+
HStack {
40+
Text("Order \(payPalWebViewModel.intent.rawValue.capitalized)d")
41+
.font(.system(size: 20))
42+
Spacer()
43+
}
44+
LeadingText("Order ID", weight: .bold)
45+
LeadingText("\(order.id)")
46+
LeadingText("Status", weight: .bold)
47+
LeadingText("\(order.status)")
48+
49+
if let emailAddress = order.paymentSource?.paypal?.emailAddress {
50+
LeadingText("Email", weight: .bold)
51+
LeadingText("\(emailAddress)")
52+
}
53+
54+
if let vaultID = order.paymentSource?.paypal?.attributes?.vault.id {
55+
LeadingText("Vault ID / Payment Token", weight: .bold)
56+
LeadingText("\(vaultID)")
57+
}
58+
59+
if let customerID = order.paymentSource?.paypal?.attributes?.vault.customer.id {
60+
LeadingText("Customer ID", weight: .bold)
61+
LeadingText("\(customerID)")
62+
}
63+
}
64+
default:
65+
Text("")
66+
}
67+
}
68+
.frame(maxWidth: .infinity)
69+
.padding()
70+
.background(
71+
RoundedRectangle(cornerRadius: 10)
72+
.stroke(.gray, lineWidth: 2)
73+
.padding(5)
74+
)
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import SwiftUI
2+
3+
struct PayPalWebTransactionView: View {
4+
5+
@ObservedObject var payPalWebViewModel: PayPalWebViewModel
6+
7+
var body: some View {
8+
ScrollView {
9+
ScrollViewReader { scrollView in
10+
VStack {
11+
PayPalWebStatusView(status: .approved, payPalWebViewModel: payPalWebViewModel)
12+
ZStack {
13+
Button("\(payPalWebViewModel.intent.rawValue.capitalized) Order") {
14+
Task {
15+
do {
16+
try await payPalWebViewModel.completeTransaction()
17+
} catch {
18+
print("Error capturing order: \(error.localizedDescription)")
19+
}
20+
}
21+
}
22+
.buttonStyle(RoundedBlueButtonStyle())
23+
.padding()
24+
25+
if payPalWebViewModel.state == .loading {
26+
CircularProgressView()
27+
}
28+
}
29+
30+
if payPalWebViewModel.transactionResult != nil && payPalWebViewModel.state == .success {
31+
PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .completed)
32+
.id("bottomView")
33+
} else if case .error = payPalWebViewModel.state {
34+
PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .error)
35+
}
36+
}
37+
.onChange(of: payPalWebViewModel.transactionResult) { _ in
38+
withAnimation {
39+
scrollView.scrollTo("bottomView")
40+
}
41+
}
42+
Spacer()
43+
}
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)