-
Notifications
You must be signed in to change notification settings - Fork 35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PayPal Web Demo App Refactor #223
Changes from 17 commits
b34ed3f
32c4b9b
e327686
9d956dd
bc4a9e3
d685080
8aed081
4474449
72aaa7a
5a02090
9e8019b
d96b216
994a3ac
3dd128e
c01d5c5
664f56c
e1faf50
a7b9b64
af90a4e
5f95aa4
ee4f5da
6c30414
94f8f9b
bb5596f
c2785ae
fa6cd08
0a3305e
c137eb7
8cd8cd2
5a95765
af04b83
518e8ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,7 +53,21 @@ final class DemoMerchantAPI { | |
throw error | ||
} | ||
} | ||
|
||
|
||
func completeOrder(intent: Intent, orderID: String) async throws -> Order { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When we refactor card we should use this singular method - the only difference between authorize and capture is the intent so we don't need 2 separate methods for that |
||
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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import Foundation | ||
|
||
enum CurrentState: Equatable { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally we can use this across the other demo features as well vs the individual states per feature that exist today |
||
case idle | ||
case loading | ||
case loaded | ||
case success | ||
case error(message: String) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,23 @@ | ||
import SwiftUI | ||
import PaymentButtons | ||
|
||
struct PayPalTransactionView: View { | ||
struct PayPalWebButtonsView: View { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Renamed from "TransactionView" to "ButtonView" since this contains all of the payment buttons and does not do anything with transactions |
||
|
||
@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) | ||
} | ||
|
@@ -29,12 +28,14 @@ struct PayPalTransactionView: View { | |
.stroke(.gray, lineWidth: 2) | ||
.padding(5) | ||
) | ||
PayPalWebApprovalResultView(paypalWebViewModel: paypalWebViewModel) | ||
if paypalWebViewModel.state.checkoutResult != nil { | ||
|
||
jaxdesmarais marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if payPalWebViewModel.checkoutResult != nil { | ||
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") | ||
} | ||
.buttonStyle(RoundedBlueButtonStyle()) | ||
.padding() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,11 @@ | ||
import SwiftUI | ||
|
||
struct CreateOrderPayPalWebView: View { | ||
struct PayPalWebCreateOrderView: View { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We may be able to reuse some sort of create order view across all of the features - left separate for now but could be something to consider as we continue this refactor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed! |
||
|
||
@ObservedObject var paypalWebViewModel: PayPalWebViewModel | ||
@ObservedObject var payPalWebViewModel: PayPalWebViewModel | ||
|
||
@State private var selectedIntent: Intent = .authorize | ||
|
||
let selectedMerchantIntegration: MerchantIntegration | ||
|
||
var body: some View { | ||
VStack(spacing: 16) { | ||
HStack { | ||
|
@@ -26,18 +24,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) | ||
payPalWebViewModel.intent = selectedIntent | ||
try await payPalWebViewModel.createOrder() | ||
} catch { | ||
print("Error in getting setup token. \(error.localizedDescription)") | ||
} | ||
} | ||
} | ||
.buttonStyle(RoundedBlueButtonStyle()) | ||
if case .loading = paypalWebViewModel.state.createdOrderResponse { | ||
if payPalWebViewModel.state == .loading { | ||
CircularProgressView() | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import SwiftUI | ||
|
||
struct PayPalWebDemoView: View { | ||
|
||
@StateObject var payPalWebViewModel = PayPalWebViewModel() | ||
|
||
var body: some View { | ||
ScrollView { | ||
VStack(spacing: 16) { | ||
PayPalWebCreateOrderView(payPalWebViewModel: payPalWebViewModel) | ||
if payPalWebViewModel.order != nil { | ||
PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .started) | ||
NavigationLink { | ||
PayPalWebButtonsView(payPalWebViewModel: payPalWebViewModel) | ||
.navigationTitle("Checkout with PayPal") | ||
} label: { | ||
Text("Checkout with PayPal") | ||
} | ||
.buttonStyle(RoundedBlueButtonStyle()) | ||
.padding() | ||
} | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import SwiftUI | ||
|
||
enum Status { | ||
scannillo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
case started | ||
case approved | ||
case completed | ||
} | ||
|
||
struct PayPalWebResultView: View { | ||
|
||
@ObservedObject var payPalWebViewModel: PayPalWebViewModel | ||
|
||
var status: Status | ||
|
||
var body: some View { | ||
switch payPalWebViewModel.state { | ||
case .idle, .loading: | ||
EmptyView() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be better to show some text like "Loading ..." onscreen vs empty? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So we do add a loading indicator to the button when the state is loading (example here). But I don't think we want the entire view to show the loading indicator which is why we set the View to EmptyView here. Once the state is updated from loading we display the entire result view. |
||
case .loaded, .success: | ||
PayPalWebStatusView(status: status, payPalViewModel: payPalWebViewModel) | ||
case .error(let errorMessage): | ||
ErrorView(errorMessage: errorMessage) | ||
} | ||
} | ||
} |
jaxdesmarais marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import SwiftUI | ||
|
||
struct PayPalWebStatusView: View { | ||
jaxdesmarais marked this conversation as resolved.
Show resolved
Hide resolved
jaxdesmarais marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
var status: Status | ||
var payPalViewModel: PayPalWebViewModel | ||
jaxdesmarais marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
var body: some View { | ||
VStack(spacing: 16) { | ||
switch status { | ||
case .started: | ||
HStack { | ||
Text("Order Created") | ||
.font(.system(size: 20)) | ||
Spacer() | ||
} | ||
if let order = payPalViewModel.order { | ||
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 = payPalViewModel.order { | ||
LeadingText("Intent", weight: .bold) | ||
LeadingText("\(payPalViewModel.intent)") | ||
LeadingText("Order ID", weight: .bold) | ||
LeadingText("\(order.id)") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These 4 lines should apply the same to each case, right? Maybe we can pull the intent & orderID part out of the switch statement There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They don't apply to all of them, each view has the following:
I posed the question here of do we need different data for different states. We could consider displaying the same data for all views and adding additional data in later states. I am not totally sure why we need to display the status for created and the intent + payer ID for approved. Do we find this data useful? If not we can still update the headers but display a more consistent set of data. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Is there a reason we only need status for order started and completed? Same with intent and payer ID only for approved? Imo I don't think we need status displayed at all. Instead we could have: order ID, and intent for all views. Then we could append payer ID with approved and append email, customer ID and vault ID with completed. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually I take that back - I think we do want different views here otherwise we end up updating all views based on the latest order data. For now I think it makes sense to have some data tied to status to avoid updating the earlier views on other screens. |
||
LeadingText("Payer ID", weight: .bold) | ||
LeadingText("\(payPalViewModel.checkoutResult?.payerID ?? "")") | ||
} | ||
case .completed: | ||
if let order = payPalViewModel.order { | ||
HStack { | ||
Text("Order \(payPalViewModel.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 = payPalViewModel.order?.paymentSource?.paypal?.emailAddress { | ||
LeadingText("Email", weight: .bold) | ||
LeadingText("\(emailAddress)") | ||
} | ||
} | ||
} | ||
} | ||
.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,34 @@ | ||
import SwiftUI | ||
|
||
struct PayPalWebTransactionView: View { | ||
|
||
@ObservedObject var payPalWebViewModel: PayPalWebViewModel | ||
|
||
var body: some View { | ||
VStack { | ||
PayPalWebStatusView(status: .approved, payPalViewModel: 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.state == .success { | ||
PayPalWebResultView(payPalWebViewModel: payPalWebViewModel, status: .completed) | ||
} | ||
} | ||
Spacer() | ||
} | ||
} |
This file was deleted.
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't related to this PR but at one point I was poking around at this file and noticed we had this one as a variable - should be a constant to match all other structs in this file