Skip to content
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

Vault Without Purchase PayPal #225

Merged
merged 17 commits into from
Dec 18, 2023
Merged

Vault Without Purchase PayPal #225

merged 17 commits into from
Dec 18, 2023

Conversation

KunJeongPark
Copy link
Collaborator

@KunJeongPark KunJeongPark commented Nov 30, 2023

Summary of changes

  • Change PaymentSource in demo app to allow SetUpToken creation for both card and PayPal
  • Implement parent class VaultViewModel inherited by CardVaultViewModel and PayPalVaultViewModel
    to reuse common properties and views.
  • Rename CardVaultState to VaultState to define all common properties, move payment specific properties as
    published properties in their respective view models
  • Implement getting url from SetUpToken request
  • Create UI for PayPal Vault without Purchase
  • Implement vault(url:) function to launch ASWebSession
  • Parse results from ASWebSession
  • Define PayPalVaultDelegate protocol, methods. Add PayPalVaultDelegate protocol conformance to PayPalVaultViewModel and implement methods in PayPalVaultViewModel
  • Update states for UI with vault method.
  • Handle success, failure results from vault(url:) in SDK through PayPalWebCheckoutClient's vault delegate functions
  • Merchant server side PaymentToken call and Demo app output
  • Unit tests for vault(url:) function
PayPalVaultNPVid.mp4

Checklist

  • Added a changelog entry

Authors

@KunJeongPark

@@ -12,6 +12,7 @@ struct CardVaultState: Equatable {
var setupToken: SetUpTokenResponse?
var updateSetupToken: UpdateSetupTokenResult?
var paymentToken: PaymentTokenResponse?
var paypalVaultToken: String?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if I should add cancel state in LoadingState.
I do provide delegate function for cancel so merchant can handle it the way they want
but I didn't include it as a separate state in the demo app because it didn't make sense for me to
display anything for canceling.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it depends on the Merchant's needs. For now, we can leave it as is; It LGTM. In fact, if you'd like, we can even consider the approach used in the func cardDidCancel(_ cardClient: CardPayments.CardClient) within the CardPaymentViewModel, where we use the .idle state.

@KunJeongPark KunJeongPark marked this pull request as ready for review December 7, 2023 23:59
@KunJeongPark KunJeongPark requested a review from a team as a code owner December 7, 2023 23:59
@@ -21,6 +22,14 @@ struct CardVaultState: Equatable {
}
}

var paypalVaultTokenResponse: LoadingState<String> = .idle {
didSet {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I prefer the separate variables of loading states for each API response.
This way, I can have a result view to toggle errorView and successView.
I prefer separated result views here instead of toggling generic one with different options from the
container view, especially with just a few different result views here that are shared across different payment methods.

@KunJeongPark
Copy link
Collaborator Author

KunJeongPark commented Dec 8, 2023

I've been thinking about the approach to minimize the number of views in the demo app as suggested in previous PR's such as suggestion to use general view that toggles content for each API result and approach that I explored above, which was to use parent viewModel between two payment methods for vaulting without payment so that SetupTokenView and PaymentTokenView and their result views can be reused across different payment methods when applicable.

I considered other payment methods being added as stand alone vault features in this approach to reducing the number of views by having a parent VaultViewModel class with shared state and payment specific properties as published properties in their respective viewModels. In JS, so far we have PayPal and Card vaulting and in Braintree, we have PayPal, Card and Venmo vaulting if I remember correctly.

Assuming that purpose of our demo app is for internal testing, demonstration of features and giving example usage of SDK to merchants, I think simplicity might be the best approach.

We can adapt as we add more payment methods to vault. As far as I know, the flow for vault without pay is the same: create setupToken, do approval in SDK with payment method and merchants use this temporary setupToken to create paymentToken.

class PayPalVaultViewModel: VaultViewModel, PayPalVaultDelegate {

@Published var paypalVaultToken: PayPalVaultResult?

Copy link
Collaborator Author

@KunJeongPark KunJeongPark Dec 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered having same VaultResult for card and PayPal vaulting but UpdateSetupToken endpoint
for card vaulting returns a lot more values than url string from PayPal approval web session, so I opted to keep them separate in case there is request in future to return more values to merchant.

If only minimum values that I display here are needed, they can be consolidated into a single VaultResult and VaultView and included in the VaultState.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my perspective, communicating the viewModel with the view without using the State structure somewhat breaks the paradigm. For now, I think it's better to move PayPalVaultViewModel.paypalVaultTokenResponse and CardVaultViewModel.updateSetupTokenResponse back to VaultState, as it was in previous commits. Maybe someone else on the team could provide some input. Considering that the purpose of the Demo app is all about internal testing, showcase functionalities, and providing merchants with examples of SDK usage

Copy link
Collaborator Author

@KunJeongPark KunJeongPark Dec 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I thought I might get comment that it's confusing to be able to access updateSetupToken from PayPalVaultViewModel but I prefer them all in vaultState as well.

@@ -28,6 +28,10 @@ struct SetupTokenResultView: View {
LeadingText("\(setupTokenResponse.customer?.id ?? "")")
LeadingText("Status", weight: .bold)
LeadingText("\(setupTokenResponse.status)")
if let url = setupTokenResponse.paypalURL {
LeadingText("PayPalURL", weight: .bold)
LeadingText("\(setupTokenResponse.paypalURL ?? "No url")")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
LeadingText("\(setupTokenResponse.paypalURL ?? "No url")")
LeadingText(url)

Copy link
Collaborator Author

@KunJeongPark KunJeongPark Dec 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for catching that.


struct CardVaultState: Equatable {
struct VaultState: Equatable {

struct UpdateSetupTokenResult: Decodable, Equatable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This structure is duplicated on CardVaultViewModel. I see that we don't have any rules for handling nested structures in the States structures. For now, I think we should just define which one to work with

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that was a mistake leaving it there. Good catch.

)
SetupTokenResultView(cardVaultViewModel: cardVaultViewModel)
SetupTokenResultView(vaultViewModel: cardVaultViewModel)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: How about we just use viewModel instead of specifying which viewModel it is (cardVaultViewModel or vaulViewModel)? It's most convenient for a view to have only one ViewModel, so it's not really necessary to specify the type of ViewModel in the parameter

Suggested change
SetupTokenResultView(vaultViewModel: cardVaultViewModel)
SetupTokenResultView(viewModel: cardVaultViewModel)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's true, we are only dealing with vaultViewModels here, so it's redundant.


class PayPalVaultViewModel: VaultViewModel, PayPalVaultDelegate {

@Published var paypalVaultToken: PayPalVaultResult?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: I think renaming it to paypalVaultResult would be the most convenient option. This name was from before the refactor. What do you think?

Suggested change
@Published var paypalVaultToken: PayPalVaultResult?
@Published var paypalVaultResult: PayPalVaultResult?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember why I used Token as the name: to be consistent with updateSetupToken of UpdateSetupTokenResult type for card vaulting. In the same vault state, especially, I wanted the to have same naming convention

@@ -12,6 +12,7 @@ struct CardVaultState: Equatable {
var setupToken: SetUpTokenResponse?
var updateSetupToken: UpdateSetupTokenResult?
var paymentToken: PaymentTokenResponse?
var paypalVaultToken: String?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it depends on the Merchant's needs. For now, we can leave it as is; It LGTM. In fact, if you'd like, we can even consider the approach used in the func cardDidCancel(_ cardClient: CardPayments.CardClient) within the CardPaymentViewModel, where we use the .idle state.

class PayPalVaultViewModel: VaultViewModel, PayPalVaultDelegate {

@Published var paypalVaultToken: PayPalVaultResult?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my perspective, communicating the viewModel with the view without using the State structure somewhat breaks the paradigm. For now, I think it's better to move PayPalVaultViewModel.paypalVaultTokenResponse and CardVaultViewModel.updateSetupTokenResponse back to VaultState, as it was in previous commits. Maybe someone else on the team could provide some input. Considering that the purpose of the Demo app is all about internal testing, showcase functionalities, and providing merchants with examples of SDK usage

@@ -14,7 +14,8 @@ struct Customer: Codable, Equatable {

struct PaymentSource: Decodable, Equatable {

let card: BasicCard
var card: BasicCard?
var paypal: BasicPayPal?
}

struct BasicCard: Decodable, Equatable {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does BasicCard and BasicPayPal mean?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

meaning they contain very minimal results like last four digits and email.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2cents: On Android we went with CardPaymentSource and PayPalPaymentSource. JSON encoding of this request is weird on Android too.

Comment on lines 26 to 46
var paymentSourceDict: [String: Any] = [:]

switch paymentSource {
case .card:
paymentSourceDict["card"] = [:]
case .paypal(let usageType):
paymentSourceDict["paypal"] = [
"usage_type": usageType,
"experience_context": [
"return_url": "sdk.ios.paypal://vault/success",
"cancel_url": "sdk.ios.paypal://vault/cancel"
]
]
}

let requestBody: [String: Any] = [
"customer": ["id": customerID],
"payment_source": paymentSourceDict
]

return try? JSONSerialization.data(withJSONObject: requestBody)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using a raw dict + JSONSerizliation, throughout the rest of the demo app we conform structs to Encodable & then use JSONEncoder

We should keep things consistent

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Sammy, I did this because I didn't know a way of using struct to make blank payment JSON object
{
"payment_source":
"card:" {}
}

Copy link
Collaborator Author

@KunJeongPark KunJeongPark Dec 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know when we get the update setup token endpoint to work with custom url scheme, we will use experience context field but right now, it errors out with that field set with our custom url scheme so I am sending an empty card in the card vault request.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember looking at this a while back and not finding a quick answer on Encodable support for empty objects. Maybe we can use an empty struct to represent [:] like here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never seen this. Thanks Steven. I will check it out.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We found EmptyParams()


struct PayPalVaultResultView: View {

@ObservedObject var paypalVaultViewModel: PayPalVaultViewModel
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@ObservedObject var paypalVaultViewModel: PayPalVaultViewModel
@ObservedObject var viewModel: PayPalVaultViewModel

🧶 : Similar to Rich's callout, since the var is strongly typed here we can omit type from the name.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to fix this one on last PR feedback fix commit

Comment on lines 12 to 18
var paypalURL: String? {
if let link = links.first(where: { $0.rel == "approve" }) {
let url = link.href
return url
}
return nil
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var paypalURL: String? {
if let link = links.first(where: { $0.rel == "approve" }) {
let url = link.href
return url
}
return nil
}
var paypalURL: String? {
return links.first { $0.rel == "approve" }?.href
}

🧶 : ^ would be technically equivalent.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😅

Suggested change
var paypalURL: String? {
if let link = links.first(where: { $0.rel == "approve" }) {
let url = link.href
return url
}
return nil
}
var paypalURL: String? {
links.first { $0.rel == "approve" }?.href
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nice haha. 🙌

@KunJeongPark KunJeongPark merged commit 52f2106 into main Dec 18, 2023
4 checks passed
@KunJeongPark KunJeongPark deleted the vaultWithoutPurchasePayPal branch December 18, 2023 14:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants