-
Notifications
You must be signed in to change notification settings - Fork 216
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 all commits
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 { createHeadlessCheckoutWalletInitializer } from '../checkout-buttons'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
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 { CartActionType, LoadCartAction } from './cart-actions'; | ||
import CartRequestSender from './cart-request-sender'; | ||
|
||
export default class CartActionCreator { | ||
constructor(private _cartRequestSender: CartRequestSender) {} | ||
|
||
@cachableAction | ||
loadCard( | ||
cartId: string, | ||
options?: RequestOptions & ActionOptions, | ||
): ThunkAction<LoadCartAction, InternalCheckoutSelectors> { | ||
return (store) => { | ||
return Observable.create((observer: Observer<LoadCartAction>) => { | ||
const state = store.getState(); | ||
const host = state.config.getHost(); | ||
|
||
observer.next(createAction(CartActionType.LoadCartRequested, undefined)); | ||
|
||
this._cartRequestSender | ||
.loadCard(cartId, host, options) | ||
.then((response) => { | ||
observer.next( | ||
createAction(CartActionType.LoadCartSucceeded, response.body), | ||
); | ||
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,23 @@ import { BuyNowCartRequestBody, Cart } from '@bigcommerce/checkout-sdk/payment-i | |
|
||
import { ContentType, RequestOptions, SDK_VERSION_HEADERS } from '../common/http-request'; | ||
|
||
import { LineItemMap } from './index'; | ||
|
||
interface LoadCartResponse { | ||
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 +36,53 @@ export default class CartRequestSender { | |
|
||
return this._requestSender.post(url, { body, headers, timeout }); | ||
} | ||
|
||
loadCard(cartId: string, host?: string, options?: RequestOptions): Promise<Response<Cart>> { | ||
const path = 'cart-information'; | ||
const url = host ? `${host}/${path}` : `/${path}`; | ||
|
||
const requestOptions: RequestOptions = { | ||
...options, | ||
params: { | ||
cartId, | ||
}, | ||
}; | ||
|
||
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. |
||
.get<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: { amount, entityId, lineItems }, | ||
} = response; | ||
|
||
const mappedLineItems: LineItemMap = { | ||
// TODO:: add all missing fields | ||
// eslint-disable-next-line | ||
// @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, | ||
// eslint-disable-next-line | ||
// @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 |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { createRequestSender } from '@bigcommerce/request-sender'; | ||
|
||
import { createCheckoutStore } from '../checkout'; | ||
import { ConfigState } from '../config'; | ||
import * as defaultCheckoutHeadlessWalletStrategyFactories from '../generated/checkout-headless-wallet-strategies'; | ||
import { PaymentMethodActionCreator, PaymentMethodRequestSender } from '../payment'; | ||
import { createPaymentIntegrationService } from '../payment-integration'; | ||
|
||
import createCheckoutButtonStrategyRegistryV2 from './create-checkout-button-registry-v2'; | ||
import HeadlessCheckoutWalletInitializer from './headless-checkout-wallet-initializer'; | ||
import HeadlessCheckoutWalletInitializerOptions from './headless-checkout-wallet-initializer-options'; | ||
import HeadlessCheckoutWalletStrategyActionCreator from './headless-checkout-wallet-strategy-action-creator'; | ||
|
||
export default function createHeadlessCheckoutWalletInitializer( | ||
options?: HeadlessCheckoutWalletInitializerOptions, | ||
): HeadlessCheckoutWalletInitializer { | ||
const { host, locale = 'en' } = options ?? {}; | ||
|
||
const config: ConfigState = { | ||
meta: { | ||
host, | ||
locale, | ||
}, | ||
errors: {}, | ||
statuses: {}, | ||
}; | ||
|
||
const store = createCheckoutStore({ config }); | ||
const requestSender = createRequestSender({ host }); | ||
const paymentIntegrationService = createPaymentIntegrationService(store); | ||
const registryV2 = createCheckoutButtonStrategyRegistryV2( | ||
paymentIntegrationService, | ||
defaultCheckoutHeadlessWalletStrategyFactories, | ||
); | ||
|
||
return new HeadlessCheckoutWalletInitializer( | ||
store, | ||
new HeadlessCheckoutWalletStrategyActionCreator( | ||
registryV2, | ||
new PaymentMethodActionCreator(new PaymentMethodRequestSender(requestSender)), | ||
), | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export default interface HeadlessCheckoutWalletInitializerOptions { | ||
host?: string; | ||
locale?: 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.
Should we update
LoadCartSucceeded
withLoadCartEntitySucceeded
? So in this case we will 100% sure that this action type is belong to current requestThere 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