-
Notifications
You must be signed in to change notification settings - Fork 215
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
feat(payment): PAYPAL-4867 POC of headless wallet buttons #2742
base: master
Are you sure you want to change the base?
Conversation
{ | ||
"inputPath": "packages/*/src/index.ts", | ||
"outputPath": "packages/core/src/generated/checkout-headless-button-strategies.ts", | ||
"memberPattern": "^create.+HeadlessButtonStrategy$" |
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.
Need to think about the right pattern for headless strategies. test-core
is failed because for button strategies we have the same substring (ButtonStrategy
) as for headless button strategies in memberPattern
field. For this reason, we do not have a unique token. Should be replaced with one of the following examples: HeadlessWalletStrategy
, HeadlessStrategy
or something like that
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.
Maybe we can use HeadlessWalletStrategy
here.
const jwtToken = state.config.getStorefrontJwtToken(); | ||
|
||
if (!jwtToken) { | ||
throw new MissingDataError(MissingDataErrorType.MissingPaymentToken); |
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.
It might be better to check if this error fits here, since StorefrontJWTToken is not equals to PaymentToken
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.
Yeap it is not difficult to switch it for another error. We can create a new one if we do not an appropriate
}) | ||
.then((response) => { | ||
observer.next( | ||
createAction(CartActionType.LoadCartSucceeded, response.body), |
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.
Should we update LoadCartSucceeded
with LoadCartEntitySucceeded
? So in this case we will 100% sure that this action type is belong to current request
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.
We can change the name if we wish, if necessary
|
||
const mappedLineItems: LineItemMap = { | ||
// @ts-ignore | ||
physicalItems: lineItems.physicalItems.map((item) => ({ |
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.
Should we handle digital items here as well?
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.
Yes, not only digital items. We need to parse response and map it into a data regarding to Cart
interface. I plan to make it after aligning the general functionality with Checkout team.
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 looks fine, but we need to take care of all kinds of items in future, such as custom item.
import createCheckoutButtonSelectors from './create-checkout-button-selectors'; | ||
|
||
@bind | ||
export default class CheckoutHeadlessButtonInitializer { |
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.
HeadlessCheckoutButtonInitializer
looks better to me, since CheckoutButton is a type of a button (like user may process checkout flow through the button..)
export default class CheckoutHeadlessButtonInitializer { | |
export default class HeadlessCheckoutButtonInitializer { |
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.
I will keep that in mind
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.
There is no registry v1 for headless button, so we can update file name with create-headless-checkout-button-registry.ts
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.
I will keep that in mind
@@ -1,4 +1,6 @@ | |||
export default interface CheckoutButtonInitializerOptions { | |||
host?: string; | |||
locale?: string; | |||
storefrontJwtToken?: string; | |||
siteLink?: string; |
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.
Why siteLink
should be created when we can use host
instead of it? As far as I remember host is equal to site link
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.
host
is BC Control Panel
siteLink
is the store url that we need to use making the right redirect on confirmation page
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.
For production, we may create a new interface for the headless wallet buttons.
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.
Do you mean host
is the canonical URL, i.e.: https://store-{hash}.mybigcommerce.com
, and siteUrl
is the BC Checkout's site URL (not the headless storefront site URL)?
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.
@davidchin yes, you are correct
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.
@bc-peng do you mean create a file with the similar interface to this one and name it HeadlessWalletInitializerOptions
?
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.
Yeah, keep the existing checkout button unchanged, create a new one like HeadlessWalletInitializerOptions
.
} | ||
} | ||
|
||
private async onHostedCheckoutApprove( |
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 method should be available only in Phase 2 of this project
this.onShippingOptionsChange(data), | ||
}; | ||
|
||
const hostedCheckoutCallbacks = { |
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 can be removed
const onShippingChangeCallbacks = { | ||
onShippingAddressChange: (data: ShippingAddressChangeCallbackPayload) => | ||
this.onShippingAddressChange(data), | ||
onShippingOptionsChange: (data: ShippingOptionChangeCallbackPayload) => | ||
this.onShippingOptionsChange(data), | ||
}; | ||
|
||
const hostedCheckoutCallbacks = { | ||
...onShippingChangeCallbacks, | ||
onApprove: (data: ApproveCallbackPayload, actions: ApproveCallbackActions) => | ||
this.onHostedCheckoutApprove(data, actions, methodId), | ||
}; |
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 part should be removed since it is outside of Headless WB Phase 1 scope
|
||
return new PayPalCommerceIntegrationService( | ||
createFormPoster(), | ||
createFormPoster({ host: getSiteLink() }), |
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.
Can getHost be used here instead of getSiteLink?
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.
[key: string]: CheckoutButtonStrategyFactory<CheckoutButtonStrategy>; | ||
} | ||
|
||
export default function createCheckoutHeadlessButtonStrategyRegistry( |
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 function looks like a duplicate of createCheckoutButtonStrategyRegistry
, except that the default value for checkoutButtonHeadlessStrategyFactories
parameter is different. Instead of duplicating, could we just call createCheckoutButtonStrategyRegistry
and pass it with defaultCheckoutHeadlessButtonStrategyFactories
instead? It should work for both headless and non-headless because both implement the same interface.
{ | ||
"inputPath": "packages/*/src/index.ts", | ||
"outputPath": "packages/core/src/generated/checkout-headless-button-strategies.ts", | ||
"memberPattern": "^create.+HeadlessButtonStrategy$" |
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.
Maybe we can use HeadlessWalletStrategy
here.
...response, | ||
body: { | ||
id: entityId, | ||
// @ts-ignore |
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.
Could you please explain why do we have @ts-ignore
here?
constructor(private _cartRequestSender: CartRequestSender) {} | ||
|
||
@cachableAction | ||
loadCardEntity( |
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.
Is loadCardEntity
the correct name? Or loadCart
?
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.
I will switch to your version. Thanks
|
||
const mappedLineItems: LineItemMap = { | ||
// @ts-ignore | ||
physicalItems: lineItems.physicalItems.map((item) => ({ |
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 looks fine, but we need to take care of all kinds of items in future, such as custom item.
@@ -1,4 +1,6 @@ | |||
export default interface CheckoutButtonInitializerOptions { | |||
host?: string; | |||
locale?: string; | |||
storefrontJwtToken?: string; | |||
siteLink?: string; |
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.
For production, we may create a new interface for the headless wallet buttons.
* @param options - Options for initializing the checkout button. | ||
* @returns A promise that resolves to the current state. | ||
*/ | ||
initializeHeadlessButton( |
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.
Can you explain why we don't call this method, intialize
?
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.
What do you mean we do not call this method?
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.
In other initializers, we refer to this method as initialize. I was wondering if there is a specific reason we didn't follow this pattern.
@@ -204,6 +205,17 @@ export default class DefaultPaymentIntegrationService implements PaymentIntegrat | |||
return buyNowCart; | |||
} | |||
|
|||
async loadCardEntity( |
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.
async loadCardEntity( | |
async loadCart( |
}, | ||
}; | ||
|
||
return this._requestSender |
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.
It seems to me that we're making a cross-origin Ajax request from headless storefront to BC, which then returns a session token and set as a third party cookie. AFAIK, Safari blocks third party cookies. Could you test if the setup works for Safari?
Another point is, Catalyst creates carts on the server-side. Therefore, the browser does not have BC session cookie required to retrieve carts directly from the browser from BC. Could you also test if the setup works for Catalyst?
I believe the solution to both of these problems is to proxy GQL calls through Catalyst or a server-side middleware for non-Catalyst headless storefronts.
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.
@davidchin we were aware about these kind of specific behaviour related cart creation process.
If cart was created by server-side we are not able to get any information by cart_id
from browser, therefore customer have to make a cart creation request from browser in order to receive cookies. This point will be mentioned in documentation of headless wallet button.
For Safari's case we can avoid blocking of third party cookies by enabling/disabling the appropriate settings.
But Safari blocking it by default, we should think about a workaround
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.
For Safari's case we can avoid blocking of third party cookies by enabling/disabling the appropriate settings.
Yes, but this is not a setting we have control over, as we can't force shoppers to relax their privacy setting. I think this issue is a blocker. If we can't make it work for Safari by default, we need to explore alternative options.
This point will be mentioned in documentation of headless wallet button.
I don't think documentation is sufficient as the solution won't be compatible with headless Catalyst, which creates carts on the server side.
It seems to me that to solve both of these problems, we shouldn't be calling the GQL API directly in the browser for headless storefronts. Instead, requests need to be routed through a server-side proxy, which in most cases will be the headless storefronts.
.then(this.transformToCartResponse); | ||
} | ||
|
||
private transformToCartResponse(response: Response<LoadCartResponse>): Response<Cart> { |
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.
@davidchin @animesh1987 @bc-peng guys I would like to ask you to take a loot at this transformer much closer there are concerns I have encountered.
This transformer is going to be very large because here i am trying to transform response in an appropriate Cart interface to align with the current realisation of stored cart information.
There are a few problems from the current approach:
- it is hard to support/maintain this code in the future
- we do not have all required Cart fields information for properly transforming
- double dependency with Cart interface
I would like to suggest creating a new Headless Cart reducer for avoiding bindings to Cart interface and all things above. What do you think?
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.
Thanks for bringing this up, my suggestion is that we should not have this in the reducer since that's not the purpose of a reducer. I see this is to transform the response from the loadCart API to a cart here in request sender.
Maybe we can have something similar to this and all transform logic can be placed there.
Maybe you can also think of moving the transform method in the action creator and not in the request sender.
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.
@animesh1987 Unfortunately we do not have all required information from response for properly transformation to Cart. For example we are not able to get coupons
, customerId
, email
from GraphQL request but these fields are required in Cart. Can we just set default parameters for them in this case for properly transformation?
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.
@animesh1987 Why I suggest creating a new reducer is because we can more easily manage this information. This reducer will only take the necessary information for Headless Wallet Buttons strategies
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.
Hey @bc-nick what is the GQL endpoint we are calling here? Maybe we can call checkout GQL endpoint for the information you need once you have a cart.
What?
POC of Headless Wallet Buttons
Why?
According to the purpose of the project
Testing / Proof
headless_demo_1.mov
@bigcommerce/team-checkout @bigcommerce/team-payments