-
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?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export { createTimeout } from '@bigcommerce/request-sender'; | ||
|
||
export { createCheckoutHeadlessButtonInitializer } from '../checkout-buttons'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { createAction, createErrorAction, ThunkAction } from '@bigcommerce/data-store'; | ||
import { Observable, Observer } from 'rxjs'; | ||
|
||
import { RequestOptions } from '@bigcommerce/checkout-sdk/payment-integration-api'; | ||
|
||
import { InternalCheckoutSelectors } from '../checkout'; | ||
import { cachableAction } from '../common/data-store'; | ||
import ActionOptions from '../common/data-store/action-options'; | ||
import { MissingDataError, MissingDataErrorType } from '../common/error/errors'; | ||
|
||
import { CartActionType, LoadCartAction } from './cart-actions'; | ||
import CartRequestSender from './cart-request-sender'; | ||
|
||
export default class CartActionCreator { | ||
constructor(private _cartRequestSender: CartRequestSender) {} | ||
|
||
@cachableAction | ||
loadCardEntity( | ||
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 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. I will switch to your version. Thanks |
||
cartId: string, | ||
options?: RequestOptions & ActionOptions, | ||
): ThunkAction<LoadCartAction, InternalCheckoutSelectors> { | ||
return (store) => { | ||
return Observable.create((observer: Observer<LoadCartAction>) => { | ||
const state = store.getState(); | ||
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 commentThe 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 commentThe 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 |
||
} | ||
|
||
observer.next(createAction(CartActionType.LoadCartRequested, undefined)); | ||
|
||
this._cartRequestSender | ||
.loadCardEntity(cartId, { | ||
headers: { | ||
Authorization: `Bearer ${jwtToken}`, | ||
'Content-Type': 'application/json', | ||
}, | ||
...options, | ||
}) | ||
.then((response) => { | ||
observer.next( | ||
createAction(CartActionType.LoadCartSucceeded, response.body), | ||
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. Should we update 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 can change the name if we wish, if necessary |
||
); | ||
observer.complete(); | ||
}) | ||
.catch((response) => { | ||
observer.error(createErrorAction(CartActionType.LoadCartFailed, response)); | ||
}); | ||
}); | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { Action } from '@bigcommerce/data-store'; | ||
|
||
import Cart from './cart'; | ||
|
||
export enum CartActionType { | ||
LoadCartRequested = 'LOAD_CART_REQUESTED', | ||
LoadCartSucceeded = 'LOAD_CART_SUCCEEDED', | ||
LoadCartFailed = 'LOAD_CART_FAILED', | ||
} | ||
|
||
export type LoadCartAction = | ||
| LoadCartRequestedAction | ||
| LoadCartSucceededAction | ||
| LoadCartFailedAction; | ||
|
||
export interface LoadCartRequestedAction extends Action { | ||
type: CartActionType.LoadCartRequested; | ||
} | ||
|
||
export interface LoadCartSucceededAction extends Action<Cart> { | ||
type: CartActionType.LoadCartSucceeded; | ||
} | ||
|
||
export interface LoadCartFailedAction extends Action<Error> { | ||
type: CartActionType.LoadCartFailed; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,34 @@ import { BuyNowCartRequestBody, Cart } from '@bigcommerce/checkout-sdk/payment-i | |
|
||
import { ContentType, RequestOptions, SDK_VERSION_HEADERS } from '../common/http-request'; | ||
|
||
import { LineItemMap } from './index'; | ||
|
||
interface LoadCartRequestOptions extends RequestOptions { | ||
body?: { query: string }; | ||
headers: { Authorization: string; [key: string]: string }; | ||
} | ||
|
||
interface LoadCartResponse { | ||
data: { | ||
site: { | ||
cart: { | ||
amount: { | ||
currencyCode: string; | ||
}; | ||
entityId: string; | ||
lineItems: { | ||
physicalItems: Array<{ | ||
name: string; | ||
entityId: string; | ||
quantity: string; | ||
productEntityId: string; | ||
}>; | ||
}; | ||
}; | ||
}; | ||
}; | ||
} | ||
|
||
export default class CartRequestSender { | ||
constructor(private _requestSender: RequestSender) {} | ||
|
||
|
@@ -19,4 +47,136 @@ export default class CartRequestSender { | |
|
||
return this._requestSender.post(url, { body, headers, timeout }); | ||
} | ||
|
||
loadCardEntity(cartId: string, options: LoadCartRequestOptions): Promise<Response<Cart>> { | ||
const url = `/graphql`; | ||
|
||
const graphQLQuery = ` | ||
query { | ||
site { | ||
cart(entityId: "${cartId}") { | ||
currencyCode | ||
entityId | ||
id | ||
isTaxIncluded | ||
discounts { | ||
entityId | ||
discountedAmount { | ||
currencyCode | ||
value | ||
} | ||
} | ||
discountedAmount { | ||
currencyCode | ||
value | ||
} | ||
baseAmount { | ||
currencyCode | ||
value | ||
} | ||
amount { | ||
currencyCode | ||
value | ||
} | ||
lineItems { | ||
physicalItems { | ||
brand | ||
couponAmount { | ||
value | ||
} | ||
discountedAmount { | ||
value | ||
} | ||
discounts { | ||
discountedAmount { | ||
value | ||
} | ||
entityId | ||
} | ||
extendedListPrice { | ||
value | ||
} | ||
extendedSalePrice { | ||
value | ||
} | ||
giftWrapping { | ||
amount { | ||
value | ||
} | ||
message | ||
name | ||
} | ||
isShippingRequired | ||
isTaxable | ||
listPrice { | ||
value | ||
} | ||
name | ||
originalPrice { | ||
value | ||
} | ||
entityId | ||
quantity | ||
salePrice { | ||
value | ||
} | ||
sku | ||
url | ||
} | ||
} | ||
} | ||
} | ||
}`; | ||
|
||
const requestOptions: LoadCartRequestOptions = { | ||
...options, | ||
headers: { | ||
...options.headers, | ||
'Content-Type': 'application/json', | ||
}, | ||
body: { | ||
query: graphQLQuery, | ||
}, | ||
}; | ||
|
||
return this._requestSender | ||
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. 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 commentThe 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. For Safari's case we can avoid blocking of third party cookies by enabling/disabling the appropriate settings. 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.
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.
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. |
||
.post<LoadCartResponse>(url, { | ||
...requestOptions, | ||
}) | ||
.then(this.transformToCartResponse); | ||
} | ||
|
||
private transformToCartResponse(response: Response<LoadCartResponse>): Response<Cart> { | ||
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. @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:
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 commentThe 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 commentThe 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 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. @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 commentThe 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. |
||
const { | ||
body: { | ||
data: { | ||
site: { | ||
cart: { amount, entityId, lineItems }, | ||
}, | ||
}, | ||
}, | ||
} = response; | ||
|
||
const mappedLineItems: LineItemMap = { | ||
// @ts-ignore | ||
physicalItems: lineItems.physicalItems.map((item) => ({ | ||
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. Should we handle digital items here as well? 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. Yes, not only digital items. We need to parse response and map it into a data regarding to 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. This looks fine, but we need to take care of all kinds of items in future, such as custom item. |
||
id: item.entityId, | ||
name: item.name, | ||
quantity: item.quantity, | ||
productId: item.productEntityId, | ||
})), | ||
}; | ||
|
||
return { | ||
...response, | ||
body: { | ||
id: entityId, | ||
// @ts-ignore | ||
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. Could you please explain why do we have |
||
currency: { | ||
code: amount.currencyCode, | ||
}, | ||
lineItems: mappedLineItems, | ||
}, | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more. Why 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.
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. 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 commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean 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. @davidchin yes, you are correct 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. @bc-peng do you mean create a file with the similar interface to this one and name it 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. Yeah, keep the existing checkout button unchanged, create a new one like |
||
} |
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 inmemberPattern
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 thatThere 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.