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 with Purchase PayPalWeb #221

Merged
merged 7 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions Demo/Demo/Models/CreateOrderParams.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ struct CreateOrderParams: Encodable {
let applicationContext: ApplicationContext?
let intent: String
var purchaseUnits: [PurchaseUnit]?
var paymentSource: VaultCardPaymentSource?
var paymentSource: VaultPaymentSource?
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just to clarify my understanding - a merchant only needs to set the paymentSource in their create order call if they want to vault, correct?

How do we document this to a merchant?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, JS SDK has documentation on this on their developer site. We should do something similar.

}

struct ApplicationContext: Codable {
Expand All @@ -17,6 +17,38 @@ struct ApplicationContext: Codable {
}
}

enum VaultPaymentSource: Encodable {
case card(VaultCardPaymentSource)
case paypal(VaultPayPalPaymentSource)

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .card(let cardSource):
try container.encode(cardSource)
case .paypal(let paypalSource):
try container.encode(paypalSource)
}
}
}

struct VaultPayPalPaymentSource: Encodable {

let paypal: VaultPayPal
}

struct VaultPayPal: Encodable {

let attributes: Attributes
let experienceContext: ExperienceContext
}

struct ExperienceContext: Encodable {

let returnURL: String
let cancelURL: String
}

struct VaultCardPaymentSource: Encodable {

let card: VaultCard
Expand All @@ -30,15 +62,17 @@ struct VaultCard: Encodable {
struct Attributes: Encodable {

let vault: Vault
let customer: CardVaultCustomer?
let customer: VaultCustomer?
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if we can just call this Customer instead of VaultCustomer.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Addressed here: 62480f6

}

struct Vault: Encodable {

let storeInVault: String
let usageType: String?
let customerType: String?
}

struct CardVaultCustomer: Encodable {
struct VaultCustomer: Encodable {

let id: String
}
Expand Down
5 changes: 3 additions & 2 deletions Demo/Demo/Models/Order.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ struct Order: Codable, Equatable {
let id: String
let status: String
var paymentSource: PaymentSource?

struct PaymentSource: Codable, Equatable {

let card: Card?
Expand All @@ -25,7 +25,8 @@ struct Order: Codable, Equatable {

struct PayPal: Codable, Equatable {

let emailAddress: String
let emailAddress: String?
let attributes: Attributes?
}

struct Attributes: Codable, Equatable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ struct CreateOrderPayPalWebView: View {
@ObservedObject var paypalWebViewModel: PayPalWebViewModel

@State private var selectedIntent: Intent = .authorize
@State private var vaultCustomerID: String = ""
Copy link
Collaborator

Choose a reason for hiding this comment

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

I thought you had said during stand that regardless of what's customer ID is passed in the API always generates one? If that's the case, I think we should remove this from our demo.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, my plan is to do that today.

Copy link
Collaborator Author

@KunJeongPark KunJeongPark Nov 27, 2023

Choose a reason for hiding this comment

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

Addressed in commit dc0c32e and f07d0bc

@State var shouldVaultSelected = false

let selectedMerchantIntegration: MerchantIntegration

Expand All @@ -22,6 +24,11 @@ struct CreateOrderPayPalWebView: View {
Text("CAPTURE").tag(Intent.capture)
}
.pickerStyle(SegmentedPickerStyle())
HStack {
Toggle("Should Vault with Purchase", isOn: $shouldVaultSelected)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks great!

Spacer()
}
FloatingLabelTextField(placeholder: "Vault Customer ID (Optional)", text: $vaultCustomerID)
ZStack {
Button("Create an Order") {
Task {
Expand All @@ -30,7 +37,10 @@ struct CreateOrderPayPalWebView: View {
try await paypalWebViewModel.createOrder(
amount: "10.00",
selectedMerchantIntegration: DemoSettings.merchantIntegration,
intent: selectedIntent.rawValue)
intent: selectedIntent.rawValue,
shouldVault: shouldVaultSelected,
customerID: vaultCustomerID.isEmpty ? nil : vaultCustomerID
)
} catch {
print("Error in getting setup token. \(error.localizedDescription)")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ struct PayPalWebOrderCompletionResultView: View {
LeadingText("Email", weight: .bold)
LeadingText("\(email)")
}
if let vaultID = orderResponse.paymentSource?.paypal?.attributes?.vault.id {
LeadingText("Vault ID / Payment Token", weight: .bold)
LeadingText("\(vaultID)")
}
Comment on lines +42 to +45
Copy link
Collaborator

Choose a reason for hiding this comment

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

General question: is there an agreed upon name that JS calls this in our public docs? I see we use both vault ID and payment token here. We should use whichever name is used in our public docs.

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 for the customer point of view, we call it payment token , but we reference it as vault ID in the APIs.
I just referenced vault Id on the label to clarify that they are the same.

if let customerID = orderResponse.paymentSource?.paypal?.attributes?.vault.customer.id {
LeadingText("Customer ID", weight: .bold)
LeadingText("\(customerID)")
}
Text("")
.id("bottomView")
}
Expand Down
15 changes: 10 additions & 5 deletions Demo/Demo/ViewModels/CardPaymentViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,20 @@ class CardPaymentViewModel: ObservableObject, CardDelegate {
let amountRequest = Amount(currencyCode: "USD", value: amount)
// TODO: might need to pass in payee as payee object or as auth header

var vaultPaymentSource: VaultCardPaymentSource?
var vaultCardPaymentSource: VaultCardPaymentSource?
if shouldVault {
var customer: CardVaultCustomer?
var customer: VaultCustomer?
if let customerID {
customer = CardVaultCustomer(id: customerID)
customer = VaultCustomer(id: customerID)
}
let attributes = Attributes(vault: Vault(storeInVault: "ON_SUCCESS"), customer: customer)
let attributes = Attributes(vault: Vault(storeInVault: "ON_SUCCESS", usageType: nil, customerType: nil), customer: customer)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we default usageType and customerType to nil in the struct?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

addressed here: c77464c

let card = VaultCard(attributes: attributes)
vaultPaymentSource = VaultCardPaymentSource(card: card)
vaultCardPaymentSource = VaultCardPaymentSource(card: card)
}

var vaultPaymentSource: VaultPaymentSource?
if let vaultCardPaymentSource {
vaultPaymentSource = .card(vaultCardPaymentSource)
}

let orderRequestParams = CreateOrderParams(
Expand Down
35 changes: 33 additions & 2 deletions Demo/Demo/ViewModels/PayPalWebViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,46 @@ class PayPalWebViewModel: ObservableObject, PayPalWebCheckoutDelegate {

let configManager = CoreConfigManager(domain: "PayPalWeb Payments")

func createOrder(amount: String, selectedMerchantIntegration: MerchantIntegration, intent: String) async throws {
func createOrder(
amount: String,
selectedMerchantIntegration: MerchantIntegration,
intent: String,
shouldVault: Bool,
customerID: String? = nil
) async throws {
// might need to pass in payee as payee object or as auth header

let amountRequest = Amount(currencyCode: "USD", value: amount)
// TODO: might need to pass in payee as payee object or as auth header

var vaultPayPalPaymentSource: VaultPayPalPaymentSource?
if shouldVault {
var customer: VaultCustomer?
if let customerID {
customer = VaultCustomer(id: customerID)
}
let attributes = Attributes(
vault: Vault(
storeInVault: "ON_SUCCESS",
usageType: "MERCHANT",
customerType: "CONSUMER"
),
Copy link
Collaborator Author

@KunJeongPark KunJeongPark Nov 20, 2023

Choose a reason for hiding this comment

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

Just hard coded this for direct merchant but I am investigating what different parameters are for different selected merchant integrations.

Copy link
Collaborator

@jaxdesmarais jaxdesmarais Nov 21, 2023

Choose a reason for hiding this comment

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

We should get input from product on what we need to support here for v1 in the demo app.

-Victoria : yes, Walter referenced docs during stand and I plan on working with Nirvan and Walter on the specs.

So right now, in demo app we only have direct, connected and managed path configured, not specific paths like CP2 or CP4. The usageType would depend on specific integration patterns.

I think specs for server side inputs like auth headers and payee info should be made for each integration types in another PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes, Walter referenced docs during stand and I plan on working with Nirvan and Walter on the specs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So right now, in demo app we only have direct, connected and managed path configured, not specific paths like CP2 or CP4. The usageType would depend on specific integration patterns.

I think specs for server side inputs like auth headers and payee info should be made for each integration types in another PR.

customer: customer
)
let paypal = VaultPayPal(attributes: attributes, experienceContext: ExperienceContext(returnURL: "https://example.com/returnUrl", cancelURL: "https://example.com/cancelUrl"))
Copy link
Collaborator

Choose a reason for hiding this comment

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

In your video, the redirect back to the Demo app looked fine, even though you would think the experienceContext should be a URL scheme to the app. What happens if the merchant doesn't set these URLs?

Copy link
Collaborator Author

@KunJeongPark KunJeongPark Nov 29, 2023

Choose a reason for hiding this comment

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

The endpoint throws an error without the experienceContext field for PayPal vaulting.

Copy link
Collaborator Author

@KunJeongPark KunJeongPark Nov 29, 2023

Choose a reason for hiding this comment

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

In our SDK, when we launch the ASWebSession for PayPalWeb, we construct the redirectURLString and append it as a query parameter to the checkout url :

func payPalCheckoutReturnURL(payPalCheckoutURL: URL) -> URL? {

The redirect url is included as a parameter in the url that we construct to launch the webSession.
I don't know if this is a typo or intentional but query parameter name for redirect url is strange.

So this experienceContext in create Order API is only relevant to JS SDK but the API fails nonetheless
without it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

:sigh: That's right, this is an issue with the create-order API. It doesn't accept non-https URLs.

We should add a comment to make this issue clear in the code. That basically createOrder requires an https URL, which doesn't get used by the SDK flow at all. The merchant could put https://bogus.com and things will work fine.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes. That might be an actual site.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added comments above to clarify that required experienceContext fields for PayPal vault with purchase are not used in the SDK.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't suppose there is a way for use to pass a URL on our merchants behalf so they don't need to do this step?

Copy link
Collaborator

Choose a reason for hiding this comment

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

The createOrder will just error if that URL value isn't set or is an empty string. It also errors if the string doesn't start with https. Victoria - can we make a JIRA for tracking taking up this issue with the API team?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Roger

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Created - DTPPCPSDK-1492

vaultPayPalPaymentSource = VaultPayPalPaymentSource(paypal: paypal)
}

var vaultPaymentSource: VaultPaymentSource?
if let vaultPayPalPaymentSource {
vaultPaymentSource = .paypal(vaultPayPalPaymentSource)
}

let orderRequestParams = CreateOrderParams(
applicationContext: nil,
intent: intent,
purchaseUnits: [PurchaseUnit(amount: amountRequest)]
purchaseUnits: [PurchaseUnit(amount: amountRequest)],
paymentSource: vaultPaymentSource
)

do {
Expand Down