From c723c0cf6a061fcc23073d0d302e979ca952c0ca Mon Sep 17 00:00:00 2001 From: bc-nick Date: Wed, 5 Mar 2025 19:13:22 +0100 Subject: [PATCH 1/8] feat(payment): PAYPAL-4935 added CartActionCreator for handling/storing cart information --- packages/core/src/cart/cart-action-creator.ts | 42 ++++++ packages/core/src/cart/cart-actions.ts | 26 ++++ packages/core/src/cart/cart-reducer.ts | 7 +- packages/core/src/cart/cart-request-sender.ts | 39 +++++ .../headless-cart-request-response.ts | 7 + .../src/cart/headless-cart/headless-cart.ts | 133 ++++++++++++++++++ packages/core/src/cart/headless-cart/index.ts | 3 + .../headless-cart/map-to-cart-line-item.ts | 62 ++++++++ .../headless-cart/map-to-cart-line-items.ts | 53 +++++++ .../src/cart/headless-cart/map-to-cart.ts | 48 +++++++ .../headless-cart/mocks/headless-cart.mock.ts | 132 +++++++++++++++++ packages/core/src/cart/index.ts | 1 + 12 files changed, 552 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/cart/cart-action-creator.ts create mode 100644 packages/core/src/cart/cart-actions.ts create mode 100644 packages/core/src/cart/headless-cart/headless-cart-request-response.ts create mode 100644 packages/core/src/cart/headless-cart/headless-cart.ts create mode 100644 packages/core/src/cart/headless-cart/index.ts create mode 100644 packages/core/src/cart/headless-cart/map-to-cart-line-item.ts create mode 100644 packages/core/src/cart/headless-cart/map-to-cart-line-items.ts create mode 100644 packages/core/src/cart/headless-cart/map-to-cart.ts create mode 100644 packages/core/src/cart/headless-cart/mocks/headless-cart.mock.ts diff --git a/packages/core/src/cart/cart-action-creator.ts b/packages/core/src/cart/cart-action-creator.ts new file mode 100644 index 0000000000..e5d74137d9 --- /dev/null +++ b/packages/core/src/cart/cart-action-creator.ts @@ -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 { + return (store) => { + return Observable.create((observer: Observer) => { + 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)); + }); + }); + }; + } +} diff --git a/packages/core/src/cart/cart-actions.ts b/packages/core/src/cart/cart-actions.ts new file mode 100644 index 0000000000..c7e95a9b42 --- /dev/null +++ b/packages/core/src/cart/cart-actions.ts @@ -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 { + type: CartActionType.LoadCartSucceeded; +} + +export interface LoadCartFailedAction extends Action { + type: CartActionType.LoadCartFailed; +} diff --git a/packages/core/src/cart/cart-reducer.ts b/packages/core/src/cart/cart-reducer.ts index e86545ebab..68bfeddf05 100644 --- a/packages/core/src/cart/cart-reducer.ts +++ b/packages/core/src/cart/cart-reducer.ts @@ -13,6 +13,7 @@ import { import { ConsignmentAction, ConsignmentActionType } from '../shipping'; import Cart from './cart'; +import { CartActionType, LoadCartAction } from './cart-actions'; import CartState, { CartErrorsState, CartStatusesState, DEFAULT_STATE } from './cart-state'; export default function cartReducer(state: CartState = DEFAULT_STATE, action: Action): CartState { @@ -32,7 +33,8 @@ function dataReducer( | CheckoutAction | ConsignmentAction | CouponAction - | GiftCertificateAction, + | GiftCertificateAction + | LoadCartAction, ): Cart | undefined { switch (action.type) { case BillingAddressActionType.UpdateBillingAddressSucceeded: @@ -48,6 +50,9 @@ function dataReducer( case GiftCertificateActionType.RemoveGiftCertificateSucceeded: return objectMerge(data, action.payload && action.payload.cart); + case CartActionType.LoadCartSucceeded: + return objectMerge(data, action.payload && action.payload); + default: return data; } diff --git a/packages/core/src/cart/cart-request-sender.ts b/packages/core/src/cart/cart-request-sender.ts index ad4e54de45..e4f53c85fe 100644 --- a/packages/core/src/cart/cart-request-sender.ts +++ b/packages/core/src/cart/cart-request-sender.ts @@ -4,6 +4,8 @@ import { BuyNowCartRequestBody, Cart } from '@bigcommerce/checkout-sdk/payment-i import { ContentType, RequestOptions, SDK_VERSION_HEADERS } from '../common/http-request'; +import { HeadlessCartRequestResponse, mapToCart } from './headless-cart'; + export default class CartRequestSender { constructor(private _requestSender: RequestSender) {} @@ -19,4 +21,41 @@ export default class CartRequestSender { return this._requestSender.post(url, { body, headers, timeout }); } + + async loadCard( + cartId: string, + host?: string, + options?: RequestOptions, + ): Promise> { + const path = 'cart-information'; + const url = host ? `${host}/${path}` : `/${path}`; + + const requestOptions: RequestOptions = { + ...options, + params: { + cartId, + }, + }; + + return this._requestSender + .get(url, { + ...requestOptions, + }) + .then(this.transformToCartResponse); + } + + private transformToCartResponse( + response: Response, + ): Response { + const { + body: { + data: { site }, + }, + } = response; + + return { + ...response, + body: mapToCart(site), + }; + } } diff --git a/packages/core/src/cart/headless-cart/headless-cart-request-response.ts b/packages/core/src/cart/headless-cart/headless-cart-request-response.ts new file mode 100644 index 0000000000..7fa21acc32 --- /dev/null +++ b/packages/core/src/cart/headless-cart/headless-cart-request-response.ts @@ -0,0 +1,7 @@ +import HeadlessCartResponse from './headless-cart'; + +export interface HeadlessCartRequestResponse { + data: { + site: HeadlessCartResponse; + }; +} diff --git a/packages/core/src/cart/headless-cart/headless-cart.ts b/packages/core/src/cart/headless-cart/headless-cart.ts new file mode 100644 index 0000000000..b716ebaf1d --- /dev/null +++ b/packages/core/src/cart/headless-cart/headless-cart.ts @@ -0,0 +1,133 @@ +interface BaseFieldFragment { + value: number; +} + +export interface HeadlessLineItem { + name: string; + entityId: string; + quantity: number; + productEntityId: number; + brand: string; + couponAmount: { + value: number; + }; + discountedAmount: { + value: number; + }; + discounts: Array<{ + discountedAmount: { + value: number; + }; + entityId: string; + }>; + extendedListPrice: { + value: number; + }; + extendedSalePrice: { + value: number; + }; + imageUrl: string; + isTaxable: boolean; + listPrice: { + value: number; + }; + originalPrice: { + value: number; + }; + salePrice: { + value: number; + }; + sku: string; + url: string; + variantEntityId: number; + selectedOptions: Array<{ + __typename: string; + value: string; + valueEntityId: number; + entityId: number; + name: string; + }>; +} + +interface HeadlessPhysicalItem extends HeadlessLineItem { + isShippingRequired: boolean; + giftWrapping?: { + amount: { + value: number; + }; + message: string; + name: string; + } | null; +} + +interface HeadlessDigitalItem extends HeadlessLineItem { + downloadFileUrls: string[]; + downloadPageUrl: string; + downloadSize: string; +} + +export interface HeadlessCustomItem { + entityId: string; + listPrice: BaseFieldFragment; + extendedListPrice: BaseFieldFragment; + name: string; + quantity: number; + sku: string; +} + +export interface HeadlessGiftCertificates { + amount: BaseFieldFragment; + name: string; + theme: string; + entityId: string | number; + isTaxable: boolean; + message: string; + sender: { + email: string; + name: string; + }; + recipient: { + email: string; + name: string; + }; +} + +export interface HeadlessLineItems { + physicalItems: HeadlessPhysicalItem[]; + digitalItems: HeadlessDigitalItem[]; + customItems: HeadlessCustomItem[]; + giftCertificates?: HeadlessGiftCertificates[]; +} + +export default interface HeadlessCartResponse { + cart?: { + amount: BaseFieldFragment; // cart amount; + baseAmount: BaseFieldFragment; + entityId: string; + id: string; + createdAt: { + utc: string; + }; + updatedAt: { + utc: string; + }; + discounts: Array<{ + discountedAmount: BaseFieldFragment; + entityId: string; + }>; + discountedAmount: BaseFieldFragment; + isTaxIncluded: boolean; + currencyCode: string; + lineItems: HeadlessLineItems; + }; + checkout?: { + coupons: Array<{ + entityId: string; + code: string; + couponType: string; + discountedAmount: { + value: number; + }; + }>; + }; +} diff --git a/packages/core/src/cart/headless-cart/index.ts b/packages/core/src/cart/headless-cart/index.ts new file mode 100644 index 0000000000..d25cf835df --- /dev/null +++ b/packages/core/src/cart/headless-cart/index.ts @@ -0,0 +1,3 @@ +export { default as HeadlessCartResponse } from './headless-cart'; +export { default as mapToCart } from './map-to-cart'; +export { HeadlessCartRequestResponse } from './headless-cart-request-response'; diff --git a/packages/core/src/cart/headless-cart/map-to-cart-line-item.ts b/packages/core/src/cart/headless-cart/map-to-cart-line-item.ts new file mode 100644 index 0000000000..f6b920c4a5 --- /dev/null +++ b/packages/core/src/cart/headless-cart/map-to-cart-line-item.ts @@ -0,0 +1,62 @@ +import { LineItem } from '../line-item'; + +import { HeadlessLineItem } from './headless-cart'; + +export default function mapToLineItem(lineItem: HeadlessLineItem): LineItem { + const { + entityId, + name, + quantity, + productEntityId, + brand, + couponAmount, + discountedAmount, + discounts, + extendedListPrice, + extendedSalePrice, + imageUrl, + isTaxable, + listPrice, + salePrice, + sku, + url, + variantEntityId, + selectedOptions, + } = lineItem; + + return { + id: entityId, + name, + quantity, + productId: productEntityId, + brand, + couponAmount: couponAmount.value, + discountAmount: discountedAmount.value, + discounts: discounts.map((discount) => ({ + discountedAmount: discount.discountedAmount.value, + // TODO:: discount item does not have name field in response body when making request using REST API, but there is name in interface, for a while set name as entityID + name: discount.entityId, + })), + extendedListPrice: extendedListPrice.value, + extendedSalePrice: extendedSalePrice.value, + imageUrl, + isTaxable, + listPrice: listPrice.value, + salePrice: salePrice.value, + sku, + url, + variantId: variantEntityId, + options: selectedOptions?.map((option) => ({ + name: option.name, + nameId: option.entityId, + value: option.value, + valueId: option.valueEntityId, + })), + + // TODO:: we do not have any information regarding to fields below in the GraphQL Storefront doc + addedByPromotion: false, + comparisonPrice: 0, + extendedComparisonPrice: 0, + retailPrice: 0, + }; +} diff --git a/packages/core/src/cart/headless-cart/map-to-cart-line-items.ts b/packages/core/src/cart/headless-cart/map-to-cart-line-items.ts new file mode 100644 index 0000000000..558efc8887 --- /dev/null +++ b/packages/core/src/cart/headless-cart/map-to-cart-line-items.ts @@ -0,0 +1,53 @@ +import { LineItemMap } from '../index'; + +import { HeadlessLineItems } from './headless-cart'; +import mapToLineItem from './map-to-cart-line-item'; + +export default function mapToCartLineItems(lineItems: HeadlessLineItems): LineItemMap { + const { + physicalItems = [], + digitalItems = [], + giftCertificates = [], + customItems = [], + } = lineItems; + + return { + physicalItems: physicalItems.map(({ isShippingRequired, giftWrapping, ...item }) => ({ + isShippingRequired, + giftWrapping: giftWrapping + ? { + message: giftWrapping.message, + name: giftWrapping.name, + amount: giftWrapping.amount.value, + } + : undefined, + ...mapToLineItem(item), + })), + digitalItems: digitalItems.map( + ({ downloadFileUrls = [], downloadPageUrl = '', downloadSize = '', ...item }) => ({ + downloadFileUrls, + downloadPageUrl, + downloadSize, + ...mapToLineItem(item), + }), + ), + giftCertificates: giftCertificates.map((item) => ({ + id: item.entityId, + name: item.name, + theme: item.theme, + amount: item.amount.value, + taxable: item.isTaxable, + sender: item.sender, + recipient: item.recipient, + message: item.message, + })), + customItems: customItems.map((item) => ({ + id: item.entityId, + listPrice: item.listPrice.value, + extendedListPrice: item.extendedListPrice.value, + name: item.name, + quantity: item.quantity, + sku: item.sku, + })), + }; +} diff --git a/packages/core/src/cart/headless-cart/map-to-cart.ts b/packages/core/src/cart/headless-cart/map-to-cart.ts new file mode 100644 index 0000000000..5b86471a6d --- /dev/null +++ b/packages/core/src/cart/headless-cart/map-to-cart.ts @@ -0,0 +1,48 @@ +import { Cart } from '@bigcommerce/checkout-sdk/payment-integration-api'; + +import mapToCartLineItems from './map-to-cart-line-items'; + +import { HeadlessCartResponse } from './'; + +export default function mapToCart(headlessCartResponse: HeadlessCartResponse): Cart | undefined { + const { cart, checkout } = headlessCartResponse; + + if (!cart || !checkout) { + return; + } + + return { + id: cart.entityId, + baseAmount: cart.baseAmount.value, + cartAmount: cart.amount.value, + discounts: cart.discounts.map((discount) => ({ + id: discount.entityId, + discountedAmount: discount.discountedAmount.value, + })), + isTaxIncluded: cart.isTaxIncluded, + lineItems: mapToCartLineItems(cart.lineItems), + currency: { + code: cart.currencyCode, + // TODO:: we do not have any information regarding to fields below (name, symbol, decimalPlaces) in the GraphQL Storefront doc (https://developer.bigcommerce.com/docs/storefront/cart-checkout/guide/graphql-storefront) + name: '', + symbol: '', + decimalPlaces: 2, + }, + createdTime: cart.createdAt.utc, + updatedTime: cart.updatedAt.utc, + discountAmount: cart.discountedAmount.value, + + coupons: checkout.coupons.map((item) => ({ + id: item.entityId, + code: item.code, + couponType: item.couponType, + discountedAmount: item.discountedAmount.value, + // TODO:: there is no info about displayName field + displayName: '', + })), + // TODO:: information about email field can be pulled from Billing Address or Shipping Address (https://developer.bigcommerce.com/docs/storefront/cart-checkout/guide/graphql-storefront#get-checkout) + email: '', + // TODO:: there is no info about customerId field + customerId: 0, + }; +} diff --git a/packages/core/src/cart/headless-cart/mocks/headless-cart.mock.ts b/packages/core/src/cart/headless-cart/mocks/headless-cart.mock.ts new file mode 100644 index 0000000000..f118a543da --- /dev/null +++ b/packages/core/src/cart/headless-cart/mocks/headless-cart.mock.ts @@ -0,0 +1,132 @@ +import { HeadlessLineItem } from '../headless-cart'; +import { HeadlessCartRequestResponse } from '../headless-cart-request-response'; + +export function headlessLineItem(): HeadlessLineItem { + return { + discounts: [], + brand: 'null', + couponAmount: { + value: 0, + }, + discountedAmount: { + value: 0, + }, + entityId: 'e9543890-76a5-4026-bb7d-5f4bebd68b9f', + extendedListPrice: { + value: 225, + }, + extendedSalePrice: { + value: 225, + }, + imageUrl: + 'https://cdn.integration.zone/s-wbaqgkqmcy/products/86/images/286/ablebrewingsystem4.1736764752.220.290.jpg?c=1', + isTaxable: true, + listPrice: { + value: 225, + }, + name: '[Sample] Able Brewing System', + originalPrice: { + value: 225, + }, + productEntityId: 86, + quantity: 1, + salePrice: { + value: 225, + }, + selectedOptions: [ + { + name: 'Color', + entityId: 114, + __typename: 'CartSelectedMultipleChoiceOption', + value: 'Green', + valueEntityId: 102, + }, + { + name: 'Size', + entityId: 115, + __typename: 'CartSelectedMultipleChoiceOption', + value: 'Medium', + valueEntityId: 105, + }, + { + name: 'test', + entityId: 116, + __typename: 'CartSelectedMultipleChoiceOption', + value: 'test1', + valueEntityId: 107, + }, + ], + sku: 'ABS-GR-ME-TE', + url: 'https://nicktsybulko1736764704-testly-the-third.my-integration.zone/able-brewing-system', + variantEntityId: 89, + }; +} + +export function getHeadlessCartResponse(): HeadlessCartRequestResponse { + return { + data: { + site: { + cart: { + amount: { + value: 225, + }, + baseAmount: { + value: 225, + }, + createdAt: { + utc: '2025-01-13T19:08:43Z', + }, + updatedAt: { + utc: '2025-01-13T19:46:00Z', + }, + currencyCode: 'USD', + discountedAmount: { + value: 0, + }, + discounts: [ + { + discountedAmount: { + value: 0, + }, + entityId: 'e9543890-76a5-4026-bb7d-5f4bebd68b9f', + }, + ], + entityId: 'b20deef40f9699e48671bbc3fef6ca44dc80e3c7', + id: 'Q2FydDozOWE5NmQ2Yy1mNDRjLTQxZTItOTFlMy0yNTcxMDQ0Yzk1Njc=', + isTaxIncluded: false, + lineItems: { + customItems: [], + digitalItems: [], + physicalItems: [getPhysicalItem()], + }, + }, + checkout: { + coupons: [ + { + entityId: '123', + code: 'I534FA46H', + couponType: 'promotion', + discountedAmount: { + value: 10, + }, + }, + ], + }, + }, + }, + }; +} + +export function getPhysicalItem() { + return { + isShippingRequired: true, + giftWrapping: { + name: 'gift', + message: 'message', + amount: { + value: 10, + }, + }, + ...headlessLineItem(), + }; +} diff --git a/packages/core/src/cart/index.ts b/packages/core/src/cart/index.ts index 02e2f02cbf..f11cfc5c50 100644 --- a/packages/core/src/cart/index.ts +++ b/packages/core/src/cart/index.ts @@ -14,6 +14,7 @@ export { default as LineItemMap } from './line-item-map'; export { default as CartComparator } from './cart-comparator'; export { default as CartRequestSender } from './cart-request-sender'; export { default as cartReducer } from './cart-reducer'; +export { default as CartActionCreator } from './cart-action-creator'; export { default as CartSelector, CartSelectorFactory, From c6f09aec96617ff3525a39c3efb2adec13b1d79d Mon Sep 17 00:00:00 2001 From: bc-nick Date: Mon, 10 Mar 2025 19:49:18 +0100 Subject: [PATCH 2/8] feat(payment): PAYPAL-4935 updates related to new requirements --- .../core/src/cart/cart-action-creator.spec.ts | 80 +++++++++++++ .../core/src/cart/cart-request-sender.spec.ts | 34 ++++++ .../src/cart/headless-cart/headless-cart.ts | 8 ++ .../map-to-cart-line-item.spec.ts | 34 ++++++ .../map-to-cart-line-items.spec.ts | 54 +++++++++ .../cart/headless-cart/map-to-cart.spec.ts | 69 +++++++++++ .../src/cart/headless-cart/map-to-cart.ts | 13 +-- .../headless-cart/mocks/headless-cart.mock.ts | 109 +++++++++--------- 8 files changed, 342 insertions(+), 59 deletions(-) create mode 100644 packages/core/src/cart/cart-action-creator.spec.ts create mode 100644 packages/core/src/cart/headless-cart/map-to-cart-line-item.spec.ts create mode 100644 packages/core/src/cart/headless-cart/map-to-cart-line-items.spec.ts create mode 100644 packages/core/src/cart/headless-cart/map-to-cart.spec.ts diff --git a/packages/core/src/cart/cart-action-creator.spec.ts b/packages/core/src/cart/cart-action-creator.spec.ts new file mode 100644 index 0000000000..4e2e6f6df6 --- /dev/null +++ b/packages/core/src/cart/cart-action-creator.spec.ts @@ -0,0 +1,80 @@ +import { createRequestSender, RequestSender } from '@bigcommerce/request-sender'; +import { from, of } from 'rxjs'; +import { catchError, toArray } from 'rxjs/operators'; + +import { Cart } from '../cart'; +import CheckoutStore from '../checkout/checkout-store'; +import { getCheckoutStoreState } from '../checkout/checkouts.mock'; +import createCheckoutStore from '../checkout/create-checkout-store'; +import { getErrorResponse, getResponse } from '../common/http-request/responses.mock'; + +import CartActionCreator from './cart-action-creator'; +import { CartActionType } from './cart-actions'; +import CartRequestSender from './cart-request-sender'; +import { getCart } from './carts.mock'; + +describe('CartActionCreator', () => { + let cartActionCreator: CartActionCreator; + let requestSender: RequestSender; + let cartRequestSender: CartRequestSender; + let store: CheckoutStore; + let cart: Cart; + + beforeEach(() => { + cart = getCart(); + requestSender = createRequestSender(); + + cartRequestSender = new CartRequestSender(requestSender); + + store = createCheckoutStore(getCheckoutStoreState()); + + jest.spyOn(cartRequestSender, 'loadCard').mockReturnValue( + Promise.resolve(getResponse(getCart())), + ); + + cartActionCreator = new CartActionCreator(cartRequestSender); + }); + + it('emits action to notify loading progress', async () => { + const actions = await from(cartActionCreator.loadCard(cart.id)(store)) + .pipe(toArray()) + .toPromise(); + + expect(cartRequestSender.loadCard).toHaveBeenCalledWith(cart.id, undefined, undefined); + + expect(actions).toEqual( + expect.arrayContaining([ + { type: CartActionType.LoadCartRequested }, + { + type: CartActionType.LoadCartSucceeded, + payload: getCart(), + }, + ]), + ); + }); + + it('emits error action if unable to load cart', async () => { + jest.spyOn(cartRequestSender, 'loadCard').mockReturnValue( + Promise.reject(getErrorResponse()), + ); + + const errorHandler = jest.fn((action) => of(action)); + + const actions = await from(cartActionCreator.loadCard(cart.id)(store)) + .pipe(catchError(errorHandler), toArray()) + .toPromise(); + + expect(cartRequestSender.loadCard).toHaveBeenCalledWith(cart.id, undefined, undefined); + + expect(actions).toEqual( + expect.arrayContaining([ + { type: CartActionType.LoadCartRequested }, + { + type: CartActionType.LoadCartFailed, + error: true, + payload: getErrorResponse(), + }, + ]), + ); + }); +}); diff --git a/packages/core/src/cart/cart-request-sender.spec.ts b/packages/core/src/cart/cart-request-sender.spec.ts index 2048571881..a58a775349 100644 --- a/packages/core/src/cart/cart-request-sender.spec.ts +++ b/packages/core/src/cart/cart-request-sender.spec.ts @@ -14,12 +14,15 @@ import BuyNowCartRequestBody from './buy-now-cart-request-body'; import Cart from './cart'; import CartRequestSender from './cart-request-sender'; import { getCart } from './carts.mock'; +import { HeadlessCartRequestResponse } from './headless-cart'; +import { getHeadlessCartResponse } from './headless-cart/mocks/headless-cart.mock'; describe('CartRequestSender', () => { let cart: Cart; let cartRequestSender: CartRequestSender; let requestSender: RequestSender; let response: Response; + let headlessResponse: Response; beforeEach(() => { requestSender = createRequestSender(); @@ -75,4 +78,35 @@ describe('CartRequestSender', () => { }); }); }); + + describe('#loadCard', () => { + const cartId = '123123'; + const host = 'https://test.com'; + + beforeEach(() => { + headlessResponse = getResponse(getHeadlessCartResponse()); + + jest.spyOn(requestSender, 'get').mockResolvedValue(headlessResponse); + }); + + it('get headless cart', async () => { + await cartRequestSender.loadCard(cartId); + + expect(requestSender.get).toHaveBeenCalledWith('/cart-information', { + params: { + cartId, + }, + }); + }); + + it('get headless cart with host url', async () => { + await cartRequestSender.loadCard(cartId, host); + + expect(requestSender.get).toHaveBeenCalledWith('https://test.com/cart-information', { + params: { + cartId, + }, + }); + }); + }); }); diff --git a/packages/core/src/cart/headless-cart/headless-cart.ts b/packages/core/src/cart/headless-cart/headless-cart.ts index b716ebaf1d..6f49d6f87c 100644 --- a/packages/core/src/cart/headless-cart/headless-cart.ts +++ b/packages/core/src/cart/headless-cart/headless-cart.ts @@ -130,4 +130,12 @@ export default interface HeadlessCartResponse { }; }>; }; + currency?: { + display: { + decimalPlaces: number; + symbol: string; + }; + name: string; + code: string; + }; } diff --git a/packages/core/src/cart/headless-cart/map-to-cart-line-item.spec.ts b/packages/core/src/cart/headless-cart/map-to-cart-line-item.spec.ts new file mode 100644 index 0000000000..4aeeb97b41 --- /dev/null +++ b/packages/core/src/cart/headless-cart/map-to-cart-line-item.spec.ts @@ -0,0 +1,34 @@ +import { omit } from 'lodash'; + +import { getCart } from '../carts.mock'; +import { LineItem } from '../line-item'; + +import mapToLineItem from './map-to-cart-line-item'; +import { headlessLineItem } from './mocks/headless-cart.mock'; + +describe('mapToLineItem', () => { + let headlessLineItemResponse: LineItem | undefined; + + beforeEach(() => { + headlessLineItemResponse = mapToLineItem(headlessLineItem()); + }); + + it('maps to line item', () => { + const { + lineItems: { + physicalItems: [firstPhysicalItem], + }, + } = getCart(); + + // TODO:: data is not yet fully compatible due to lack of information + expect(headlessLineItemResponse).toEqual({ + // omits fields that do not exist to retrieve information via GQL + ...omit(firstPhysicalItem, ['categoryNames', 'categories', 'isShippingRequired']), + // default props that are set due lack of information + addedByPromotion: false, + comparisonPrice: 0, + extendedComparisonPrice: 0, + retailPrice: 0, + }); + }); +}); diff --git a/packages/core/src/cart/headless-cart/map-to-cart-line-items.spec.ts b/packages/core/src/cart/headless-cart/map-to-cart-line-items.spec.ts new file mode 100644 index 0000000000..c8f6d3c15f --- /dev/null +++ b/packages/core/src/cart/headless-cart/map-to-cart-line-items.spec.ts @@ -0,0 +1,54 @@ +import { omit } from 'lodash'; + +import { getCart } from '../carts.mock'; +import { LineItemMap } from '../index'; + +import mapToCartLineItems from './map-to-cart-line-items'; +import { getHeadlessCartResponse } from './mocks/headless-cart.mock'; + +describe('mapToCartLinesItem', () => { + let headlessCartLineItemsResponse: LineItemMap | undefined; + + beforeEach(() => { + const { + data: { + site: { cart }, + }, + } = getHeadlessCartResponse(); + + headlessCartLineItemsResponse = mapToCartLineItems( + cart?.lineItems ?? { + physicalItems: [], + digitalItems: [], + giftCertificates: [], + customItems: [], + }, + ); + }); + + it('maps to cart line items', () => { + const { + lineItems: { + physicalItems: [firstPhysicalItem], + }, + } = getCart(); + + // TODO:: data is not yet fully compatible due to lack of information + const physicalItem = { + // omits fields that do not exist to retrieve information via GQL + ...omit(firstPhysicalItem, ['categoryNames', 'categories']), + // default props that are set due lack of information + addedByPromotion: false, + comparisonPrice: 0, + extendedComparisonPrice: 0, + retailPrice: 0, + }; + + expect(headlessCartLineItemsResponse).toEqual({ + physicalItems: [physicalItem], + digitalItems: [], + giftCertificates: [], + customItems: [], + }); + }); +}); diff --git a/packages/core/src/cart/headless-cart/map-to-cart.spec.ts b/packages/core/src/cart/headless-cart/map-to-cart.spec.ts new file mode 100644 index 0000000000..2c4fffa7b5 --- /dev/null +++ b/packages/core/src/cart/headless-cart/map-to-cart.spec.ts @@ -0,0 +1,69 @@ +import { Cart } from '@bigcommerce/checkout-sdk/payment-integration-api'; + +import { getCart } from '../carts.mock'; + +import mapToCart from './map-to-cart'; +import { getHeadlessCartResponse } from './mocks/headless-cart.mock'; + +describe('mapToCart', () => { + let headlessCartResponse: Cart | undefined; + + beforeEach(() => { + headlessCartResponse = mapToCart(getHeadlessCartResponse().data.site); + }); + + it('maps to internal cart', () => { + const cart = getCart(); + + // TODO:: data is not yet fully compatible due to lack of information + expect(headlessCartResponse).toEqual( + expect.objectContaining({ + id: cart.id, + isTaxIncluded: cart.isTaxIncluded, + discountAmount: cart.discountAmount, + baseAmount: cart.baseAmount, + cartAmount: cart.cartAmount, + createdTime: cart.createdTime, + updatedTime: cart.updatedTime, + coupons: cart.coupons.map((item) => + expect.objectContaining({ + id: item.id, + code: item.code, + couponType: item.couponType, + discountedAmount: item.discountedAmount, + }), + ), + lineItems: expect.objectContaining({ + physicalItems: cart.lineItems.physicalItems.map((item) => + expect.objectContaining({ + id: item.id, + variantId: item.variantId, + productId: item.productId, + sku: item.sku, + name: item.name, + url: item.url, + quantity: item.quantity, + brand: item.brand, + isTaxable: item.isTaxable, + imageUrl: item.imageUrl, + discounts: item.discounts, + discountAmount: item.discountAmount, + couponAmount: item.couponAmount, + listPrice: item.listPrice, + salePrice: item.salePrice, + extendedListPrice: item.extendedListPrice, + extendedSalePrice: item.extendedSalePrice, + isShippingRequired: item.isShippingRequired, + options: expect.arrayContaining(item.options || []), + // retailPrice, comparisonPrice, extendedComparisonPrice, addedByPromotion are not exist + }), + ), + }), + discounts: cart.discounts, + currency: cart.currency, + // customerId is not exist + // email is not exist + }), + ); + }); +}); diff --git a/packages/core/src/cart/headless-cart/map-to-cart.ts b/packages/core/src/cart/headless-cart/map-to-cart.ts index 5b86471a6d..017675875a 100644 --- a/packages/core/src/cart/headless-cart/map-to-cart.ts +++ b/packages/core/src/cart/headless-cart/map-to-cart.ts @@ -5,9 +5,9 @@ import mapToCartLineItems from './map-to-cart-line-items'; import { HeadlessCartResponse } from './'; export default function mapToCart(headlessCartResponse: HeadlessCartResponse): Cart | undefined { - const { cart, checkout } = headlessCartResponse; + const { cart, checkout, currency } = headlessCartResponse; - if (!cart || !checkout) { + if (!cart || !checkout || !currency) { return; } @@ -22,11 +22,10 @@ export default function mapToCart(headlessCartResponse: HeadlessCartResponse): C isTaxIncluded: cart.isTaxIncluded, lineItems: mapToCartLineItems(cart.lineItems), currency: { - code: cart.currencyCode, - // TODO:: we do not have any information regarding to fields below (name, symbol, decimalPlaces) in the GraphQL Storefront doc (https://developer.bigcommerce.com/docs/storefront/cart-checkout/guide/graphql-storefront) - name: '', - symbol: '', - decimalPlaces: 2, + code: currency.code, + name: currency.name, + symbol: currency.display.symbol, + decimalPlaces: currency.display.decimalPlaces, }, createdTime: cart.createdAt.utc, updatedTime: cart.updatedAt.utc, diff --git a/packages/core/src/cart/headless-cart/mocks/headless-cart.mock.ts b/packages/core/src/cart/headless-cart/mocks/headless-cart.mock.ts index f118a543da..b046227ea4 100644 --- a/packages/core/src/cart/headless-cart/mocks/headless-cart.mock.ts +++ b/packages/core/src/cart/headless-cart/mocks/headless-cart.mock.ts @@ -4,61 +4,46 @@ import { HeadlessCartRequestResponse } from '../headless-cart-request-response'; export function headlessLineItem(): HeadlessLineItem { return { discounts: [], - brand: 'null', + brand: 'OFS', couponAmount: { - value: 0, + value: 5, }, discountedAmount: { - value: 0, + value: 10, }, - entityId: 'e9543890-76a5-4026-bb7d-5f4bebd68b9f', + entityId: '666', extendedListPrice: { - value: 225, + value: 200, }, extendedSalePrice: { - value: 225, + value: 190, }, - imageUrl: - 'https://cdn.integration.zone/s-wbaqgkqmcy/products/86/images/286/ablebrewingsystem4.1736764752.220.290.jpg?c=1', + imageUrl: '/images/canvas-laundry-cart.jpg', isTaxable: true, listPrice: { - value: 225, + value: 200, }, - name: '[Sample] Able Brewing System', + name: 'Canvas Laundry Cart', originalPrice: { value: 225, }, - productEntityId: 86, + productEntityId: 103, quantity: 1, salePrice: { - value: 225, + value: 190, }, selectedOptions: [ { - name: 'Color', - entityId: 114, - __typename: 'CartSelectedMultipleChoiceOption', - value: 'Green', - valueEntityId: 102, - }, - { - name: 'Size', - entityId: 115, - __typename: 'CartSelectedMultipleChoiceOption', - value: 'Medium', - valueEntityId: 105, - }, - { - name: 'test', - entityId: 116, + name: 'n', + entityId: 1, __typename: 'CartSelectedMultipleChoiceOption', - value: 'test1', - valueEntityId: 107, + value: 'v', + valueEntityId: 3, }, ], - sku: 'ABS-GR-ME-TE', - url: 'https://nicktsybulko1736764704-testly-the-third.my-integration.zone/able-brewing-system', - variantEntityId: 89, + sku: 'CLC', + url: '/canvas-laundry-cart/', + variantEntityId: 71, }; } @@ -68,27 +53,27 @@ export function getHeadlessCartResponse(): HeadlessCartRequestResponse { site: { cart: { amount: { - value: 225, + value: 190, }, baseAmount: { - value: 225, + value: 200, }, createdAt: { - utc: '2025-01-13T19:08:43Z', + utc: '2018-03-06T04:41:49+00:00', }, updatedAt: { - utc: '2025-01-13T19:46:00Z', + utc: '2018-03-07T03:44:51+00:00', }, currencyCode: 'USD', discountedAmount: { - value: 0, + value: 10, }, discounts: [ { discountedAmount: { - value: 0, + value: 10, }, - entityId: 'e9543890-76a5-4026-bb7d-5f4bebd68b9f', + entityId: '12e11c8f-7dce-4da3-9413-b649533f8bad', }, ], entityId: 'b20deef40f9699e48671bbc3fef6ca44dc80e3c7', @@ -103,30 +88,50 @@ export function getHeadlessCartResponse(): HeadlessCartRequestResponse { checkout: { coupons: [ { - entityId: '123', - code: 'I534FA46H', - couponType: 'promotion', + entityId: '1', + code: 'savebig2015', + couponType: 'percentage_discount', discountedAmount: { - value: 10, + value: 5, + }, + }, + { + entityId: '4', + code: '279F507D817E3E7', + couponType: 'shipping_discount', + discountedAmount: { + value: 5, }, }, ], }, + currency: { + display: { + decimalPlaces: 2, + symbol: '$', + }, + name: 'US Dollar', + code: 'USD', + }, }, }, }; } -export function getPhysicalItem() { +export function getPhysicalItem(hasGiftWrapping?: false) { return { isShippingRequired: true, - giftWrapping: { - name: 'gift', - message: 'message', - amount: { - value: 10, - }, - }, + ...(hasGiftWrapping + ? { + giftWrapping: { + name: 'gift', + message: 'message', + amount: { + value: 10, + }, + }, + } + : {}), ...headlessLineItem(), }; } From 0ecd084b08c10fa37b18e1ddf2db4bb74618febf Mon Sep 17 00:00:00 2001 From: bc-nick Date: Wed, 12 Mar 2025 08:20:15 +0100 Subject: [PATCH 3/8] feat(payment): PAYPAL-4935 updates after review --- packages/core/src/cart/cart-action-creator.spec.ts | 12 ++++++------ packages/core/src/cart/cart-action-creator.ts | 6 +++--- packages/core/src/cart/cart-request-sender.spec.ts | 6 +++--- packages/core/src/cart/cart-request-sender.ts | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/core/src/cart/cart-action-creator.spec.ts b/packages/core/src/cart/cart-action-creator.spec.ts index 4e2e6f6df6..d05d690cba 100644 --- a/packages/core/src/cart/cart-action-creator.spec.ts +++ b/packages/core/src/cart/cart-action-creator.spec.ts @@ -28,7 +28,7 @@ describe('CartActionCreator', () => { store = createCheckoutStore(getCheckoutStoreState()); - jest.spyOn(cartRequestSender, 'loadCard').mockReturnValue( + jest.spyOn(cartRequestSender, 'loadCart').mockReturnValue( Promise.resolve(getResponse(getCart())), ); @@ -36,11 +36,11 @@ describe('CartActionCreator', () => { }); it('emits action to notify loading progress', async () => { - const actions = await from(cartActionCreator.loadCard(cart.id)(store)) + const actions = await from(cartActionCreator.loadCart(cart.id)(store)) .pipe(toArray()) .toPromise(); - expect(cartRequestSender.loadCard).toHaveBeenCalledWith(cart.id, undefined, undefined); + expect(cartRequestSender.loadCart).toHaveBeenCalledWith(cart.id, undefined, undefined); expect(actions).toEqual( expect.arrayContaining([ @@ -54,17 +54,17 @@ describe('CartActionCreator', () => { }); it('emits error action if unable to load cart', async () => { - jest.spyOn(cartRequestSender, 'loadCard').mockReturnValue( + jest.spyOn(cartRequestSender, 'loadCart').mockReturnValue( Promise.reject(getErrorResponse()), ); const errorHandler = jest.fn((action) => of(action)); - const actions = await from(cartActionCreator.loadCard(cart.id)(store)) + const actions = await from(cartActionCreator.loadCart(cart.id)(store)) .pipe(catchError(errorHandler), toArray()) .toPromise(); - expect(cartRequestSender.loadCard).toHaveBeenCalledWith(cart.id, undefined, undefined); + expect(cartRequestSender.loadCart).toHaveBeenCalledWith(cart.id, undefined, undefined); expect(actions).toEqual( expect.arrayContaining([ diff --git a/packages/core/src/cart/cart-action-creator.ts b/packages/core/src/cart/cart-action-creator.ts index e5d74137d9..5cc2c78c8f 100644 --- a/packages/core/src/cart/cart-action-creator.ts +++ b/packages/core/src/cart/cart-action-creator.ts @@ -14,19 +14,19 @@ export default class CartActionCreator { constructor(private _cartRequestSender: CartRequestSender) {} @cachableAction - loadCard( + loadCart( cartId: string, options?: RequestOptions & ActionOptions, ): ThunkAction { return (store) => { - return Observable.create((observer: Observer) => { + return new Observable((observer: Observer) => { const state = store.getState(); const host = state.config.getHost(); observer.next(createAction(CartActionType.LoadCartRequested, undefined)); this._cartRequestSender - .loadCard(cartId, host, options) + .loadCart(cartId, host, options) .then((response) => { observer.next( createAction(CartActionType.LoadCartSucceeded, response.body), diff --git a/packages/core/src/cart/cart-request-sender.spec.ts b/packages/core/src/cart/cart-request-sender.spec.ts index a58a775349..48caec5a81 100644 --- a/packages/core/src/cart/cart-request-sender.spec.ts +++ b/packages/core/src/cart/cart-request-sender.spec.ts @@ -79,7 +79,7 @@ describe('CartRequestSender', () => { }); }); - describe('#loadCard', () => { + describe('#loadCart', () => { const cartId = '123123'; const host = 'https://test.com'; @@ -90,7 +90,7 @@ describe('CartRequestSender', () => { }); it('get headless cart', async () => { - await cartRequestSender.loadCard(cartId); + await cartRequestSender.loadCart(cartId); expect(requestSender.get).toHaveBeenCalledWith('/cart-information', { params: { @@ -100,7 +100,7 @@ describe('CartRequestSender', () => { }); it('get headless cart with host url', async () => { - await cartRequestSender.loadCard(cartId, host); + await cartRequestSender.loadCart(cartId, host); expect(requestSender.get).toHaveBeenCalledWith('https://test.com/cart-information', { params: { diff --git a/packages/core/src/cart/cart-request-sender.ts b/packages/core/src/cart/cart-request-sender.ts index e4f53c85fe..54401276b5 100644 --- a/packages/core/src/cart/cart-request-sender.ts +++ b/packages/core/src/cart/cart-request-sender.ts @@ -22,7 +22,7 @@ export default class CartRequestSender { return this._requestSender.post(url, { body, headers, timeout }); } - async loadCard( + async loadCart( cartId: string, host?: string, options?: RequestOptions, From 5728185b0a8c7779a58a2d01f58aebe1980b44c4 Mon Sep 17 00:00:00 2001 From: bc-nick Date: Tue, 18 Mar 2025 16:44:54 +0100 Subject: [PATCH 4/8] feat(payment): PAYPAL-4935 updates after review --- packages/core/src/cart/cart-request-sender.ts | 10 +++++----- packages/core/src/cart/headless-cart/headless-cart.ts | 2 +- .../src/cart/headless-cart/map-to-cart-line-item.ts | 4 ++-- packages/core/src/cart/headless-cart/map-to-cart.ts | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/core/src/cart/cart-request-sender.ts b/packages/core/src/cart/cart-request-sender.ts index 54401276b5..249308a8f1 100644 --- a/packages/core/src/cart/cart-request-sender.ts +++ b/packages/core/src/cart/cart-request-sender.ts @@ -37,11 +37,11 @@ export default class CartRequestSender { }, }; - return this._requestSender - .get(url, { - ...requestOptions, - }) - .then(this.transformToCartResponse); + const response = await this._requestSender.get(url, { + ...requestOptions, + }); + + return this.transformToCartResponse(response); } private transformToCartResponse( diff --git a/packages/core/src/cart/headless-cart/headless-cart.ts b/packages/core/src/cart/headless-cart/headless-cart.ts index 6f49d6f87c..e01eeab421 100644 --- a/packages/core/src/cart/headless-cart/headless-cart.ts +++ b/packages/core/src/cart/headless-cart/headless-cart.ts @@ -101,7 +101,7 @@ export interface HeadlessLineItems { export default interface HeadlessCartResponse { cart?: { - amount: BaseFieldFragment; // cart amount; + amount: BaseFieldFragment; baseAmount: BaseFieldFragment; entityId: string; id: string; diff --git a/packages/core/src/cart/headless-cart/map-to-cart-line-item.ts b/packages/core/src/cart/headless-cart/map-to-cart-line-item.ts index f6b920c4a5..8d18535101 100644 --- a/packages/core/src/cart/headless-cart/map-to-cart-line-item.ts +++ b/packages/core/src/cart/headless-cart/map-to-cart-line-item.ts @@ -34,7 +34,7 @@ export default function mapToLineItem(lineItem: HeadlessLineItem): LineItem { discountAmount: discountedAmount.value, discounts: discounts.map((discount) => ({ discountedAmount: discount.discountedAmount.value, - // TODO:: discount item does not have name field in response body when making request using REST API, but there is name in interface, for a while set name as entityID + // Info:: discount item does not have name field in response body when making request using REST API, but there is name in interface, for a while set name as entityID name: discount.entityId, })), extendedListPrice: extendedListPrice.value, @@ -53,7 +53,7 @@ export default function mapToLineItem(lineItem: HeadlessLineItem): LineItem { valueId: option.valueEntityId, })), - // TODO:: we do not have any information regarding to fields below in the GraphQL Storefront doc + // Info:: we do not have any information regarding to fields below in the GraphQL Storefront doc addedByPromotion: false, comparisonPrice: 0, extendedComparisonPrice: 0, diff --git a/packages/core/src/cart/headless-cart/map-to-cart.ts b/packages/core/src/cart/headless-cart/map-to-cart.ts index 017675875a..9337dfd8d3 100644 --- a/packages/core/src/cart/headless-cart/map-to-cart.ts +++ b/packages/core/src/cart/headless-cart/map-to-cart.ts @@ -36,12 +36,12 @@ export default function mapToCart(headlessCartResponse: HeadlessCartResponse): C code: item.code, couponType: item.couponType, discountedAmount: item.discountedAmount.value, - // TODO:: there is no info about displayName field + // Info:: there is no info about displayName field displayName: '', })), - // TODO:: information about email field can be pulled from Billing Address or Shipping Address (https://developer.bigcommerce.com/docs/storefront/cart-checkout/guide/graphql-storefront#get-checkout) + // Info:: information about email field can be pulled from Billing Address or Shipping Address (https://developer.bigcommerce.com/docs/storefront/cart-checkout/guide/graphql-storefront#get-checkout) email: '', - // TODO:: there is no info about customerId field + // Info:: there is no info about customerId field customerId: 0, }; } From 95c18b664d12a8530d5897c5bec9a0b25ea33251 Mon Sep 17 00:00:00 2001 From: bc-nick Date: Mon, 24 Mar 2025 14:34:26 +0100 Subject: [PATCH 5/8] feat(payment): PAYPAL-4935 updates after review --- packages/core/src/cart/cart-action-creator.spec.ts | 4 ++-- packages/core/src/cart/cart-action-creator.ts | 7 ++----- packages/core/src/cart/cart-request-sender.spec.ts | 13 +------------ packages/core/src/cart/cart-request-sender.ts | 11 +++-------- .../core/src/cart/headless-cart/headless-cart.ts | 1 - .../cart/headless-cart/mocks/headless-cart.mock.ts | 1 - 6 files changed, 8 insertions(+), 29 deletions(-) diff --git a/packages/core/src/cart/cart-action-creator.spec.ts b/packages/core/src/cart/cart-action-creator.spec.ts index d05d690cba..cb688429d6 100644 --- a/packages/core/src/cart/cart-action-creator.spec.ts +++ b/packages/core/src/cart/cart-action-creator.spec.ts @@ -40,7 +40,7 @@ describe('CartActionCreator', () => { .pipe(toArray()) .toPromise(); - expect(cartRequestSender.loadCart).toHaveBeenCalledWith(cart.id, undefined, undefined); + expect(cartRequestSender.loadCart).toHaveBeenCalledWith(cart.id, undefined); expect(actions).toEqual( expect.arrayContaining([ @@ -64,7 +64,7 @@ describe('CartActionCreator', () => { .pipe(catchError(errorHandler), toArray()) .toPromise(); - expect(cartRequestSender.loadCart).toHaveBeenCalledWith(cart.id, undefined, undefined); + expect(cartRequestSender.loadCart).toHaveBeenCalledWith(cart.id, undefined); expect(actions).toEqual( expect.arrayContaining([ diff --git a/packages/core/src/cart/cart-action-creator.ts b/packages/core/src/cart/cart-action-creator.ts index 5cc2c78c8f..39b3c8ce10 100644 --- a/packages/core/src/cart/cart-action-creator.ts +++ b/packages/core/src/cart/cart-action-creator.ts @@ -18,15 +18,12 @@ export default class CartActionCreator { cartId: string, options?: RequestOptions & ActionOptions, ): ThunkAction { - return (store) => { + return () => { return new Observable((observer: Observer) => { - const state = store.getState(); - const host = state.config.getHost(); - observer.next(createAction(CartActionType.LoadCartRequested, undefined)); this._cartRequestSender - .loadCart(cartId, host, options) + .loadCart(cartId, options) .then((response) => { observer.next( createAction(CartActionType.LoadCartSucceeded, response.body), diff --git a/packages/core/src/cart/cart-request-sender.spec.ts b/packages/core/src/cart/cart-request-sender.spec.ts index 48caec5a81..8c4d5efff6 100644 --- a/packages/core/src/cart/cart-request-sender.spec.ts +++ b/packages/core/src/cart/cart-request-sender.spec.ts @@ -81,7 +81,6 @@ describe('CartRequestSender', () => { describe('#loadCart', () => { const cartId = '123123'; - const host = 'https://test.com'; beforeEach(() => { headlessResponse = getResponse(getHeadlessCartResponse()); @@ -92,17 +91,7 @@ describe('CartRequestSender', () => { it('get headless cart', async () => { await cartRequestSender.loadCart(cartId); - expect(requestSender.get).toHaveBeenCalledWith('/cart-information', { - params: { - cartId, - }, - }); - }); - - it('get headless cart with host url', async () => { - await cartRequestSender.loadCart(cartId, host); - - expect(requestSender.get).toHaveBeenCalledWith('https://test.com/cart-information', { + expect(requestSender.get).toHaveBeenCalledWith('api/wallet-buttons/cart-information', { params: { cartId, }, diff --git a/packages/core/src/cart/cart-request-sender.ts b/packages/core/src/cart/cart-request-sender.ts index 249308a8f1..ad38e5f591 100644 --- a/packages/core/src/cart/cart-request-sender.ts +++ b/packages/core/src/cart/cart-request-sender.ts @@ -22,13 +22,8 @@ export default class CartRequestSender { return this._requestSender.post(url, { body, headers, timeout }); } - async loadCart( - cartId: string, - host?: string, - options?: RequestOptions, - ): Promise> { - const path = 'cart-information'; - const url = host ? `${host}/${path}` : `/${path}`; + async loadCart(cartId: string, options?: RequestOptions): Promise> { + const path = 'api/wallet-buttons/cart-information'; const requestOptions: RequestOptions = { ...options, @@ -37,7 +32,7 @@ export default class CartRequestSender { }, }; - const response = await this._requestSender.get(url, { + const response = await this._requestSender.get(path, { ...requestOptions, }); diff --git a/packages/core/src/cart/headless-cart/headless-cart.ts b/packages/core/src/cart/headless-cart/headless-cart.ts index e01eeab421..14ae1e734f 100644 --- a/packages/core/src/cart/headless-cart/headless-cart.ts +++ b/packages/core/src/cart/headless-cart/headless-cart.ts @@ -41,7 +41,6 @@ export interface HeadlessLineItem { url: string; variantEntityId: number; selectedOptions: Array<{ - __typename: string; value: string; valueEntityId: number; entityId: number; diff --git a/packages/core/src/cart/headless-cart/mocks/headless-cart.mock.ts b/packages/core/src/cart/headless-cart/mocks/headless-cart.mock.ts index b046227ea4..c1e29bc11f 100644 --- a/packages/core/src/cart/headless-cart/mocks/headless-cart.mock.ts +++ b/packages/core/src/cart/headless-cart/mocks/headless-cart.mock.ts @@ -36,7 +36,6 @@ export function headlessLineItem(): HeadlessLineItem { { name: 'n', entityId: 1, - __typename: 'CartSelectedMultipleChoiceOption', value: 'v', valueEntityId: 3, }, From 4df438a327abd95586703c717dbeb3c3d13d62fe Mon Sep 17 00:00:00 2001 From: bc-nick Date: Mon, 24 Mar 2025 21:20:01 +0100 Subject: [PATCH 6/8] feat(payment): PAYPAL-4935 renaming --- .../core/src/cart/cart-request-sender.spec.ts | 19 ++++++++------- packages/core/src/cart/cart-request-sender.ts | 8 +++---- .../gql-cart/gql-cart-request-response.ts | 7 ++++++ .../headless-cart.ts => gql-cart/gql-cart.ts} | 24 +++++++++---------- packages/core/src/cart/gql-cart/index.ts | 3 +++ .../map-to-cart-line-item.spec.ts | 4 ++-- .../map-to-cart-line-item.ts | 4 ++-- .../map-to-cart-line-items.spec.ts | 4 ++-- .../map-to-cart-line-items.ts | 4 ++-- .../map-to-cart.spec.ts | 4 ++-- .../map-to-cart.ts | 4 ++-- .../mocks/gql-cart.mock.ts} | 10 ++++---- .../headless-cart-request-response.ts | 7 ------ packages/core/src/cart/headless-cart/index.ts | 3 --- 14 files changed, 54 insertions(+), 51 deletions(-) create mode 100644 packages/core/src/cart/gql-cart/gql-cart-request-response.ts rename packages/core/src/cart/{headless-cart/headless-cart.ts => gql-cart/gql-cart.ts} (82%) create mode 100644 packages/core/src/cart/gql-cart/index.ts rename packages/core/src/cart/{headless-cart => gql-cart}/map-to-cart-line-item.spec.ts (88%) rename packages/core/src/cart/{headless-cart => gql-cart}/map-to-cart-line-item.ts (92%) rename packages/core/src/cart/{headless-cart => gql-cart}/map-to-cart-line-items.spec.ts (93%) rename packages/core/src/cart/{headless-cart => gql-cart}/map-to-cart-line-items.ts (92%) rename packages/core/src/cart/{headless-cart => gql-cart}/map-to-cart.spec.ts (94%) rename packages/core/src/cart/{headless-cart => gql-cart}/map-to-cart.ts (91%) rename packages/core/src/cart/{headless-cart/mocks/headless-cart.mock.ts => gql-cart/mocks/gql-cart.mock.ts} (92%) delete mode 100644 packages/core/src/cart/headless-cart/headless-cart-request-response.ts delete mode 100644 packages/core/src/cart/headless-cart/index.ts diff --git a/packages/core/src/cart/cart-request-sender.spec.ts b/packages/core/src/cart/cart-request-sender.spec.ts index 8c4d5efff6..837f9ab337 100644 --- a/packages/core/src/cart/cart-request-sender.spec.ts +++ b/packages/core/src/cart/cart-request-sender.spec.ts @@ -14,15 +14,15 @@ import BuyNowCartRequestBody from './buy-now-cart-request-body'; import Cart from './cart'; import CartRequestSender from './cart-request-sender'; import { getCart } from './carts.mock'; -import { HeadlessCartRequestResponse } from './headless-cart'; -import { getHeadlessCartResponse } from './headless-cart/mocks/headless-cart.mock'; +import { GQLCartRequestResponse } from './gql-cart'; +import { getGQLCartResponse } from './gql-cart/mocks/gql-cart.mock'; describe('CartRequestSender', () => { let cart: Cart; let cartRequestSender: CartRequestSender; let requestSender: RequestSender; let response: Response; - let headlessResponse: Response; + let headlessResponse: Response; beforeEach(() => { requestSender = createRequestSender(); @@ -83,7 +83,7 @@ describe('CartRequestSender', () => { const cartId = '123123'; beforeEach(() => { - headlessResponse = getResponse(getHeadlessCartResponse()); + headlessResponse = getResponse(getGQLCartResponse()); jest.spyOn(requestSender, 'get').mockResolvedValue(headlessResponse); }); @@ -91,11 +91,14 @@ describe('CartRequestSender', () => { it('get headless cart', async () => { await cartRequestSender.loadCart(cartId); - expect(requestSender.get).toHaveBeenCalledWith('api/wallet-buttons/cart-information', { - params: { - cartId, + expect(requestSender.get).toHaveBeenCalledWith( + 'http://localhost/api/wallet-buttons/cart-information', + { + params: { + cartId, + }, }, - }); + ); }); }); }); diff --git a/packages/core/src/cart/cart-request-sender.ts b/packages/core/src/cart/cart-request-sender.ts index ad38e5f591..d84a15e43f 100644 --- a/packages/core/src/cart/cart-request-sender.ts +++ b/packages/core/src/cart/cart-request-sender.ts @@ -4,7 +4,7 @@ import { BuyNowCartRequestBody, Cart } from '@bigcommerce/checkout-sdk/payment-i import { ContentType, RequestOptions, SDK_VERSION_HEADERS } from '../common/http-request'; -import { HeadlessCartRequestResponse, mapToCart } from './headless-cart'; +import { GQLCartRequestResponse, mapToCart } from './gql-cart'; export default class CartRequestSender { constructor(private _requestSender: RequestSender) {} @@ -23,7 +23,7 @@ export default class CartRequestSender { } async loadCart(cartId: string, options?: RequestOptions): Promise> { - const path = 'api/wallet-buttons/cart-information'; + const path = `${window.location.origin}/api/wallet-buttons/cart-information`; const requestOptions: RequestOptions = { ...options, @@ -32,7 +32,7 @@ export default class CartRequestSender { }, }; - const response = await this._requestSender.get(path, { + const response = await this._requestSender.get(path, { ...requestOptions, }); @@ -40,7 +40,7 @@ export default class CartRequestSender { } private transformToCartResponse( - response: Response, + response: Response, ): Response { const { body: { diff --git a/packages/core/src/cart/gql-cart/gql-cart-request-response.ts b/packages/core/src/cart/gql-cart/gql-cart-request-response.ts new file mode 100644 index 0000000000..f1680fe712 --- /dev/null +++ b/packages/core/src/cart/gql-cart/gql-cart-request-response.ts @@ -0,0 +1,7 @@ +import GQLCartResponse from './gql-cart'; + +export interface GQLCartRequestResponse { + data: { + site: GQLCartResponse; + }; +} diff --git a/packages/core/src/cart/headless-cart/headless-cart.ts b/packages/core/src/cart/gql-cart/gql-cart.ts similarity index 82% rename from packages/core/src/cart/headless-cart/headless-cart.ts rename to packages/core/src/cart/gql-cart/gql-cart.ts index 14ae1e734f..7ebdd89b7d 100644 --- a/packages/core/src/cart/headless-cart/headless-cart.ts +++ b/packages/core/src/cart/gql-cart/gql-cart.ts @@ -2,7 +2,7 @@ interface BaseFieldFragment { value: number; } -export interface HeadlessLineItem { +export interface GQLCartLineItem { name: string; entityId: string; quantity: number; @@ -48,7 +48,7 @@ export interface HeadlessLineItem { }>; } -interface HeadlessPhysicalItem extends HeadlessLineItem { +interface GQLCartPhysicalItem extends GQLCartLineItem { isShippingRequired: boolean; giftWrapping?: { amount: { @@ -59,13 +59,13 @@ interface HeadlessPhysicalItem extends HeadlessLineItem { } | null; } -interface HeadlessDigitalItem extends HeadlessLineItem { +interface GQLCartDigitalItem extends GQLCartLineItem { downloadFileUrls: string[]; downloadPageUrl: string; downloadSize: string; } -export interface HeadlessCustomItem { +export interface GQLCartCustomItem { entityId: string; listPrice: BaseFieldFragment; extendedListPrice: BaseFieldFragment; @@ -74,7 +74,7 @@ export interface HeadlessCustomItem { sku: string; } -export interface HeadlessGiftCertificates { +export interface GQLCartGiftCertificates { amount: BaseFieldFragment; name: string; theme: string; @@ -91,14 +91,14 @@ export interface HeadlessGiftCertificates { }; } -export interface HeadlessLineItems { - physicalItems: HeadlessPhysicalItem[]; - digitalItems: HeadlessDigitalItem[]; - customItems: HeadlessCustomItem[]; - giftCertificates?: HeadlessGiftCertificates[]; +export interface GQLCartLineItems { + physicalItems: GQLCartPhysicalItem[]; + digitalItems: GQLCartDigitalItem[]; + customItems: GQLCartCustomItem[]; + giftCertificates?: GQLCartGiftCertificates[]; } -export default interface HeadlessCartResponse { +export default interface GQLCartResponse { cart?: { amount: BaseFieldFragment; baseAmount: BaseFieldFragment; @@ -117,7 +117,7 @@ export default interface HeadlessCartResponse { discountedAmount: BaseFieldFragment; isTaxIncluded: boolean; currencyCode: string; - lineItems: HeadlessLineItems; + lineItems: GQLCartLineItems; }; checkout?: { coupons: Array<{ diff --git a/packages/core/src/cart/gql-cart/index.ts b/packages/core/src/cart/gql-cart/index.ts new file mode 100644 index 0000000000..014f8cfcdf --- /dev/null +++ b/packages/core/src/cart/gql-cart/index.ts @@ -0,0 +1,3 @@ +export { default as GQLCartResponse } from './gql-cart'; +export { default as mapToCart } from './map-to-cart'; +export { GQLCartRequestResponse } from './gql-cart-request-response'; diff --git a/packages/core/src/cart/headless-cart/map-to-cart-line-item.spec.ts b/packages/core/src/cart/gql-cart/map-to-cart-line-item.spec.ts similarity index 88% rename from packages/core/src/cart/headless-cart/map-to-cart-line-item.spec.ts rename to packages/core/src/cart/gql-cart/map-to-cart-line-item.spec.ts index 4aeeb97b41..8a1461aa71 100644 --- a/packages/core/src/cart/headless-cart/map-to-cart-line-item.spec.ts +++ b/packages/core/src/cart/gql-cart/map-to-cart-line-item.spec.ts @@ -4,13 +4,13 @@ import { getCart } from '../carts.mock'; import { LineItem } from '../line-item'; import mapToLineItem from './map-to-cart-line-item'; -import { headlessLineItem } from './mocks/headless-cart.mock'; +import { gqlCartLineItem } from './mocks/gql-cart.mock'; describe('mapToLineItem', () => { let headlessLineItemResponse: LineItem | undefined; beforeEach(() => { - headlessLineItemResponse = mapToLineItem(headlessLineItem()); + headlessLineItemResponse = mapToLineItem(gqlCartLineItem()); }); it('maps to line item', () => { diff --git a/packages/core/src/cart/headless-cart/map-to-cart-line-item.ts b/packages/core/src/cart/gql-cart/map-to-cart-line-item.ts similarity index 92% rename from packages/core/src/cart/headless-cart/map-to-cart-line-item.ts rename to packages/core/src/cart/gql-cart/map-to-cart-line-item.ts index 8d18535101..f5afa87309 100644 --- a/packages/core/src/cart/headless-cart/map-to-cart-line-item.ts +++ b/packages/core/src/cart/gql-cart/map-to-cart-line-item.ts @@ -1,8 +1,8 @@ import { LineItem } from '../line-item'; -import { HeadlessLineItem } from './headless-cart'; +import { GQLCartLineItem } from './gql-cart'; -export default function mapToLineItem(lineItem: HeadlessLineItem): LineItem { +export default function mapToLineItem(lineItem: GQLCartLineItem): LineItem { const { entityId, name, diff --git a/packages/core/src/cart/headless-cart/map-to-cart-line-items.spec.ts b/packages/core/src/cart/gql-cart/map-to-cart-line-items.spec.ts similarity index 93% rename from packages/core/src/cart/headless-cart/map-to-cart-line-items.spec.ts rename to packages/core/src/cart/gql-cart/map-to-cart-line-items.spec.ts index c8f6d3c15f..2a47a0d988 100644 --- a/packages/core/src/cart/headless-cart/map-to-cart-line-items.spec.ts +++ b/packages/core/src/cart/gql-cart/map-to-cart-line-items.spec.ts @@ -4,7 +4,7 @@ import { getCart } from '../carts.mock'; import { LineItemMap } from '../index'; import mapToCartLineItems from './map-to-cart-line-items'; -import { getHeadlessCartResponse } from './mocks/headless-cart.mock'; +import { getGQLCartResponse } from './mocks/gql-cart.mock'; describe('mapToCartLinesItem', () => { let headlessCartLineItemsResponse: LineItemMap | undefined; @@ -14,7 +14,7 @@ describe('mapToCartLinesItem', () => { data: { site: { cart }, }, - } = getHeadlessCartResponse(); + } = getGQLCartResponse(); headlessCartLineItemsResponse = mapToCartLineItems( cart?.lineItems ?? { diff --git a/packages/core/src/cart/headless-cart/map-to-cart-line-items.ts b/packages/core/src/cart/gql-cart/map-to-cart-line-items.ts similarity index 92% rename from packages/core/src/cart/headless-cart/map-to-cart-line-items.ts rename to packages/core/src/cart/gql-cart/map-to-cart-line-items.ts index 558efc8887..ad91dff85b 100644 --- a/packages/core/src/cart/headless-cart/map-to-cart-line-items.ts +++ b/packages/core/src/cart/gql-cart/map-to-cart-line-items.ts @@ -1,9 +1,9 @@ import { LineItemMap } from '../index'; -import { HeadlessLineItems } from './headless-cart'; +import { GQLCartLineItems } from './gql-cart'; import mapToLineItem from './map-to-cart-line-item'; -export default function mapToCartLineItems(lineItems: HeadlessLineItems): LineItemMap { +export default function mapToCartLineItems(lineItems: GQLCartLineItems): LineItemMap { const { physicalItems = [], digitalItems = [], diff --git a/packages/core/src/cart/headless-cart/map-to-cart.spec.ts b/packages/core/src/cart/gql-cart/map-to-cart.spec.ts similarity index 94% rename from packages/core/src/cart/headless-cart/map-to-cart.spec.ts rename to packages/core/src/cart/gql-cart/map-to-cart.spec.ts index 2c4fffa7b5..9b197d33bd 100644 --- a/packages/core/src/cart/headless-cart/map-to-cart.spec.ts +++ b/packages/core/src/cart/gql-cart/map-to-cart.spec.ts @@ -3,13 +3,13 @@ import { Cart } from '@bigcommerce/checkout-sdk/payment-integration-api'; import { getCart } from '../carts.mock'; import mapToCart from './map-to-cart'; -import { getHeadlessCartResponse } from './mocks/headless-cart.mock'; +import { getGQLCartResponse } from './mocks/gql-cart.mock'; describe('mapToCart', () => { let headlessCartResponse: Cart | undefined; beforeEach(() => { - headlessCartResponse = mapToCart(getHeadlessCartResponse().data.site); + headlessCartResponse = mapToCart(getGQLCartResponse().data.site); }); it('maps to internal cart', () => { diff --git a/packages/core/src/cart/headless-cart/map-to-cart.ts b/packages/core/src/cart/gql-cart/map-to-cart.ts similarity index 91% rename from packages/core/src/cart/headless-cart/map-to-cart.ts rename to packages/core/src/cart/gql-cart/map-to-cart.ts index 9337dfd8d3..c32bfeaccd 100644 --- a/packages/core/src/cart/headless-cart/map-to-cart.ts +++ b/packages/core/src/cart/gql-cart/map-to-cart.ts @@ -2,9 +2,9 @@ import { Cart } from '@bigcommerce/checkout-sdk/payment-integration-api'; import mapToCartLineItems from './map-to-cart-line-items'; -import { HeadlessCartResponse } from './'; +import { GQLCartResponse } from './'; -export default function mapToCart(headlessCartResponse: HeadlessCartResponse): Cart | undefined { +export default function mapToCart(headlessCartResponse: GQLCartResponse): Cart | undefined { const { cart, checkout, currency } = headlessCartResponse; if (!cart || !checkout || !currency) { diff --git a/packages/core/src/cart/headless-cart/mocks/headless-cart.mock.ts b/packages/core/src/cart/gql-cart/mocks/gql-cart.mock.ts similarity index 92% rename from packages/core/src/cart/headless-cart/mocks/headless-cart.mock.ts rename to packages/core/src/cart/gql-cart/mocks/gql-cart.mock.ts index c1e29bc11f..b2cefa5b49 100644 --- a/packages/core/src/cart/headless-cart/mocks/headless-cart.mock.ts +++ b/packages/core/src/cart/gql-cart/mocks/gql-cart.mock.ts @@ -1,7 +1,7 @@ -import { HeadlessLineItem } from '../headless-cart'; -import { HeadlessCartRequestResponse } from '../headless-cart-request-response'; +import { GQLCartLineItem } from '../gql-cart'; +import { GQLCartRequestResponse } from '../gql-cart-request-response'; -export function headlessLineItem(): HeadlessLineItem { +export function gqlCartLineItem(): GQLCartLineItem { return { discounts: [], brand: 'OFS', @@ -46,7 +46,7 @@ export function headlessLineItem(): HeadlessLineItem { }; } -export function getHeadlessCartResponse(): HeadlessCartRequestResponse { +export function getGQLCartResponse(): GQLCartRequestResponse { return { data: { site: { @@ -131,6 +131,6 @@ export function getPhysicalItem(hasGiftWrapping?: false) { }, } : {}), - ...headlessLineItem(), + ...gqlCartLineItem(), }; } diff --git a/packages/core/src/cart/headless-cart/headless-cart-request-response.ts b/packages/core/src/cart/headless-cart/headless-cart-request-response.ts deleted file mode 100644 index 7fa21acc32..0000000000 --- a/packages/core/src/cart/headless-cart/headless-cart-request-response.ts +++ /dev/null @@ -1,7 +0,0 @@ -import HeadlessCartResponse from './headless-cart'; - -export interface HeadlessCartRequestResponse { - data: { - site: HeadlessCartResponse; - }; -} diff --git a/packages/core/src/cart/headless-cart/index.ts b/packages/core/src/cart/headless-cart/index.ts deleted file mode 100644 index d25cf835df..0000000000 --- a/packages/core/src/cart/headless-cart/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as HeadlessCartResponse } from './headless-cart'; -export { default as mapToCart } from './map-to-cart'; -export { HeadlessCartRequestResponse } from './headless-cart-request-response'; From 7d966d5b1460e972cc69099924c1927756918a34 Mon Sep 17 00:00:00 2001 From: bc-nick Date: Thu, 27 Mar 2025 12:20:26 +0100 Subject: [PATCH 7/8] feat(payment): PAYPAL-4935 added host --- packages/core/src/cart/cart-action-creator.spec.ts | 4 ++-- packages/core/src/cart/cart-action-creator.ts | 9 ++++++--- packages/core/src/cart/cart-request-sender.ts | 11 ++++++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/core/src/cart/cart-action-creator.spec.ts b/packages/core/src/cart/cart-action-creator.spec.ts index cb688429d6..d05d690cba 100644 --- a/packages/core/src/cart/cart-action-creator.spec.ts +++ b/packages/core/src/cart/cart-action-creator.spec.ts @@ -40,7 +40,7 @@ describe('CartActionCreator', () => { .pipe(toArray()) .toPromise(); - expect(cartRequestSender.loadCart).toHaveBeenCalledWith(cart.id, undefined); + expect(cartRequestSender.loadCart).toHaveBeenCalledWith(cart.id, undefined, undefined); expect(actions).toEqual( expect.arrayContaining([ @@ -64,7 +64,7 @@ describe('CartActionCreator', () => { .pipe(catchError(errorHandler), toArray()) .toPromise(); - expect(cartRequestSender.loadCart).toHaveBeenCalledWith(cart.id, undefined); + expect(cartRequestSender.loadCart).toHaveBeenCalledWith(cart.id, undefined, undefined); expect(actions).toEqual( expect.arrayContaining([ diff --git a/packages/core/src/cart/cart-action-creator.ts b/packages/core/src/cart/cart-action-creator.ts index 39b3c8ce10..e58391ecdd 100644 --- a/packages/core/src/cart/cart-action-creator.ts +++ b/packages/core/src/cart/cart-action-creator.ts @@ -18,12 +18,15 @@ export default class CartActionCreator { cartId: string, options?: RequestOptions & ActionOptions, ): ThunkAction { - return () => { - return new Observable((observer: Observer) => { + return (store) => { + return Observable.create((observer: Observer) => { + const state = store.getState(); + const host = state.config.getHost(); + observer.next(createAction(CartActionType.LoadCartRequested, undefined)); this._cartRequestSender - .loadCart(cartId, options) + .loadCart(cartId, host, options) .then((response) => { observer.next( createAction(CartActionType.LoadCartSucceeded, response.body), diff --git a/packages/core/src/cart/cart-request-sender.ts b/packages/core/src/cart/cart-request-sender.ts index d84a15e43f..d48f779f7e 100644 --- a/packages/core/src/cart/cart-request-sender.ts +++ b/packages/core/src/cart/cart-request-sender.ts @@ -22,8 +22,13 @@ export default class CartRequestSender { return this._requestSender.post(url, { body, headers, timeout }); } - async loadCart(cartId: string, options?: RequestOptions): Promise> { - const path = `${window.location.origin}/api/wallet-buttons/cart-information`; + async loadCart( + cartId: string, + host?: string, + options?: RequestOptions, + ): Promise> { + const path = 'api/wallet-buttons/cart-information'; + const url = host ? `${host}/${path}` : `${window.location.origin}/${path}`; const requestOptions: RequestOptions = { ...options, @@ -32,7 +37,7 @@ export default class CartRequestSender { }, }; - const response = await this._requestSender.get(path, { + const response = await this._requestSender.get(url, { ...requestOptions, }); From 479676b98461c9a18a7aef9e4942cd7236436791 Mon Sep 17 00:00:00 2001 From: bc-nick Date: Thu, 3 Apr 2025 00:31:01 +0200 Subject: [PATCH 8/8] feat(payment): PAYPAL-4935 changes related to gql logic --- .../core/src/cart/cart-action-creator.spec.ts | 31 ++- packages/core/src/cart/cart-action-creator.ts | 45 +++- .../core/src/cart/cart-request-sender.spec.ts | 69 ++++-- packages/core/src/cart/cart-request-sender.ts | 67 ++++-- .../errors/cart-retrieval-error.spec.ts | 21 ++ .../gql-cart/errors/cart-retrieval-error.ts | 10 + .../cart/gql-cart/get-cart-currency-query.ts | 18 ++ .../core/src/cart/gql-cart/get-cart-query.ts | 203 ++++++++++++++++++ .../gql-cart/gql-cart-request-response.ts | 7 - packages/core/src/cart/gql-cart/gql-cart.ts | 19 +- .../src/cart/gql-cart/gql-request-response.ts | 5 + packages/core/src/cart/gql-cart/index.ts | 4 +- .../src/cart/gql-cart/map-to-cart.spec.ts | 8 +- .../core/src/cart/gql-cart/map-to-cart.ts | 10 +- .../src/cart/gql-cart/mocks/gql-cart.mock.ts | 31 ++- .../src/common/gql-request/gql-request-url.ts | 1 + packages/core/src/common/gql-request/index.ts | 1 + packages/core/src/config/config-selector.ts | 7 + packages/core/src/config/config-state.ts | 1 + 19 files changed, 479 insertions(+), 79 deletions(-) create mode 100644 packages/core/src/cart/gql-cart/errors/cart-retrieval-error.spec.ts create mode 100644 packages/core/src/cart/gql-cart/errors/cart-retrieval-error.ts create mode 100644 packages/core/src/cart/gql-cart/get-cart-currency-query.ts create mode 100644 packages/core/src/cart/gql-cart/get-cart-query.ts delete mode 100644 packages/core/src/cart/gql-cart/gql-cart-request-response.ts create mode 100644 packages/core/src/cart/gql-cart/gql-request-response.ts create mode 100644 packages/core/src/common/gql-request/gql-request-url.ts create mode 100644 packages/core/src/common/gql-request/index.ts diff --git a/packages/core/src/cart/cart-action-creator.spec.ts b/packages/core/src/cart/cart-action-creator.spec.ts index d05d690cba..80fa325b7b 100644 --- a/packages/core/src/cart/cart-action-creator.spec.ts +++ b/packages/core/src/cart/cart-action-creator.spec.ts @@ -12,6 +12,7 @@ import CartActionCreator from './cart-action-creator'; import { CartActionType } from './cart-actions'; import CartRequestSender from './cart-request-sender'; import { getCart } from './carts.mock'; +import { getGQLCartResponse, getGQLCurrencyResponse } from './gql-cart/mocks/gql-cart.mock'; describe('CartActionCreator', () => { let cartActionCreator: CartActionCreator; @@ -29,7 +30,11 @@ describe('CartActionCreator', () => { store = createCheckoutStore(getCheckoutStoreState()); jest.spyOn(cartRequestSender, 'loadCart').mockReturnValue( - Promise.resolve(getResponse(getCart())), + Promise.resolve(getResponse(getGQLCartResponse())), + ); + + jest.spyOn(cartRequestSender, 'loadCartCurrency').mockReturnValue( + Promise.resolve(getResponse(getGQLCurrencyResponse())), ); cartActionCreator = new CartActionCreator(cartRequestSender); @@ -47,7 +52,29 @@ describe('CartActionCreator', () => { { type: CartActionType.LoadCartRequested }, { type: CartActionType.LoadCartSucceeded, - payload: getCart(), + payload: expect.objectContaining({ + id: cart.id, + currency: { + code: cart.currency.code, + name: cart.currency.name, + symbol: cart.currency.symbol, + decimalPlaces: cart.currency.decimalPlaces, + }, + lineItems: expect.objectContaining({ + physicalItems: cart.lineItems.physicalItems.map((item) => + expect.objectContaining({ + id: item.id, + variantId: item.variantId, + productId: item.productId, + sku: item.sku, + name: item.name, + url: item.url, + quantity: item.quantity, + isShippingRequired: item.isShippingRequired, + }), + ), + }), + }), }, ]), ); diff --git a/packages/core/src/cart/cart-action-creator.ts b/packages/core/src/cart/cart-action-creator.ts index e58391ecdd..eadcb586d0 100644 --- a/packages/core/src/cart/cart-action-creator.ts +++ b/packages/core/src/cart/cart-action-creator.ts @@ -1,4 +1,6 @@ import { createAction, createErrorAction, ThunkAction } from '@bigcommerce/data-store'; +import { Response } from '@bigcommerce/request-sender'; +import { merge } from 'lodash'; import { Observable, Observer } from 'rxjs'; import { RequestOptions } from '@bigcommerce/checkout-sdk/payment-integration-api'; @@ -7,8 +9,10 @@ import { InternalCheckoutSelectors } from '../checkout'; import { cachableAction } from '../common/data-store'; import ActionOptions from '../common/data-store/action-options'; +import Cart from './cart'; import { CartActionType, LoadCartAction } from './cart-actions'; import CartRequestSender from './cart-request-sender'; +import { GQLCartResponse, GQLCurrencyResponse, GQLRequestResponse, mapToCart } from './gql-cart'; export default class CartActionCreator { constructor(private _cartRequestSender: CartRequestSender) {} @@ -19,19 +23,32 @@ export default class CartActionCreator { options?: RequestOptions & ActionOptions, ): ThunkAction { return (store) => { - return Observable.create((observer: Observer) => { + return new Observable((observer: Observer) => { const state = store.getState(); - const host = state.config.getHost(); + const gqlUrl = state.config.getGQLRequestUrl(); observer.next(createAction(CartActionType.LoadCartRequested, undefined)); this._cartRequestSender - .loadCart(cartId, host, options) - .then((response) => { - observer.next( - createAction(CartActionType.LoadCartSucceeded, response.body), - ); - observer.complete(); + .loadCart(cartId, gqlUrl, options) + .then((cartResponse) => { + return this._cartRequestSender + .loadCartCurrency( + cartResponse.body.data.site.cart.currencyCode, + gqlUrl, + options, + ) + .then((currencyResponse) => { + observer.next( + createAction( + CartActionType.LoadCartSucceeded, + this.transformToCartResponse( + merge(cartResponse, currencyResponse), + ), + ), + ); + observer.complete(); + }); }) .catch((response) => { observer.error(createErrorAction(CartActionType.LoadCartFailed, response)); @@ -39,4 +56,16 @@ export default class CartActionCreator { }); }; } + + private transformToCartResponse( + response: Response>, + ): Cart { + const { + body: { + data: { site }, + }, + } = response; + + return mapToCart(site); + } } diff --git a/packages/core/src/cart/cart-request-sender.spec.ts b/packages/core/src/cart/cart-request-sender.spec.ts index 837f9ab337..554779e319 100644 --- a/packages/core/src/cart/cart-request-sender.spec.ts +++ b/packages/core/src/cart/cart-request-sender.spec.ts @@ -7,6 +7,7 @@ import { import { CartSource } from '@bigcommerce/checkout-sdk/payment-integration-api'; +import { GQL_REQUEST_URL } from '../common/gql-request'; import { ContentType, SDK_VERSION_HEADERS } from '../common/http-request'; import { getResponse } from '../common/http-request/responses.mock'; @@ -14,15 +15,18 @@ import BuyNowCartRequestBody from './buy-now-cart-request-body'; import Cart from './cart'; import CartRequestSender from './cart-request-sender'; import { getCart } from './carts.mock'; -import { GQLCartRequestResponse } from './gql-cart'; -import { getGQLCartResponse } from './gql-cart/mocks/gql-cart.mock'; +import { GQLCartResponse, GQLCurrencyResponse, GQLRequestResponse } from './gql-cart'; +import getCartCurrencyQuery from './gql-cart/get-cart-currency-query'; +import getCartQuery from './gql-cart/get-cart-query'; +import { getGQLCartResponse, getGQLCurrencyResponse } from './gql-cart/mocks/gql-cart.mock'; describe('CartRequestSender', () => { let cart: Cart; let cartRequestSender: CartRequestSender; let requestSender: RequestSender; let response: Response; - let headlessResponse: Response; + let gqlResponse: Response>; + let gqlCurrencyResponse: Response>; beforeEach(() => { requestSender = createRequestSender(); @@ -81,24 +85,63 @@ describe('CartRequestSender', () => { describe('#loadCart', () => { const cartId = '123123'; + const gqlUrl = 'https://test.com/graphql'; beforeEach(() => { - headlessResponse = getResponse(getGQLCartResponse()); + gqlResponse = getResponse(getGQLCartResponse()); - jest.spyOn(requestSender, 'get').mockResolvedValue(headlessResponse); + jest.spyOn(requestSender, 'post').mockResolvedValue(gqlResponse); }); - it('get headless cart', async () => { + it('get gql cart', async () => { await cartRequestSender.loadCart(cartId); - expect(requestSender.get).toHaveBeenCalledWith( - 'http://localhost/api/wallet-buttons/cart-information', - { - params: { - cartId, - }, + expect(requestSender.post).toHaveBeenCalledWith(GQL_REQUEST_URL, { + body: { + query: getCartQuery(cartId), + }, + }); + }); + + it('get gql cart with graphql url', async () => { + await cartRequestSender.loadCart(cartId, gqlUrl); + + expect(requestSender.post).toHaveBeenCalledWith('https://test.com/graphql', { + body: { + query: getCartQuery(cartId), + }, + }); + }); + }); + + describe('#loadCartCurrency', () => { + const currencyCode = 'USD'; + const gqlUrl = 'https://test.com/graphql'; + + beforeEach(() => { + gqlCurrencyResponse = getResponse(getGQLCurrencyResponse()); + + jest.spyOn(requestSender, 'post').mockResolvedValue(gqlCurrencyResponse); + }); + + it('get gql cart currency', async () => { + await cartRequestSender.loadCartCurrency(currencyCode); + + expect(requestSender.post).toHaveBeenCalledWith(GQL_REQUEST_URL, { + body: { + query: getCartCurrencyQuery(currencyCode), }, - ); + }); + }); + + it('get gql cart currency with host url', async () => { + await cartRequestSender.loadCartCurrency(currencyCode, gqlUrl); + + expect(requestSender.post).toHaveBeenCalledWith('https://test.com/graphql', { + body: { + query: getCartCurrencyQuery(currencyCode), + }, + }); }); }); }); diff --git a/packages/core/src/cart/cart-request-sender.ts b/packages/core/src/cart/cart-request-sender.ts index d48f779f7e..49a1c2c8f3 100644 --- a/packages/core/src/cart/cart-request-sender.ts +++ b/packages/core/src/cart/cart-request-sender.ts @@ -2,9 +2,18 @@ import { RequestSender, Response } from '@bigcommerce/request-sender'; import { BuyNowCartRequestBody, Cart } from '@bigcommerce/checkout-sdk/payment-integration-api'; +import { GQL_REQUEST_URL } from '../common/gql-request'; import { ContentType, RequestOptions, SDK_VERSION_HEADERS } from '../common/http-request'; -import { GQLCartRequestResponse, mapToCart } from './gql-cart'; +import { + GQLCartResponse, + GQLCurrencyResponse, + GQLRequestOptions, + GQLRequestResponse, +} from './gql-cart'; +import CartRetrievalError from './gql-cart/errors/cart-retrieval-error'; +import getCartCurrencyQuery from './gql-cart/get-cart-currency-query'; +import getCartQuery from './gql-cart/get-cart-query'; export default class CartRequestSender { constructor(private _requestSender: RequestSender) {} @@ -22,40 +31,52 @@ export default class CartRequestSender { return this._requestSender.post(url, { body, headers, timeout }); } - async loadCart( - cartId: string, - host?: string, - options?: RequestOptions, - ): Promise> { - const path = 'api/wallet-buttons/cart-information'; - const url = host ? `${host}/${path}` : `${window.location.origin}/${path}`; + async loadCart(cartId: string, gqlUrl?: string, options?: RequestOptions) { + const url = gqlUrl ?? GQL_REQUEST_URL; - const requestOptions: RequestOptions = { + const requestOptions: GQLRequestOptions = { ...options, - params: { - cartId, + body: { + query: getCartQuery(cartId), }, }; - const response = await this._requestSender.get(url, { + const response = await this._requestSender.post>(url, { ...requestOptions, }); - return this.transformToCartResponse(response); + if (!response.body.data.site.cart) { + throw new CartRetrievalError( + `Could not retrieve cart information by cartId: ${cartId}`, + ); + } + + return response; } - private transformToCartResponse( - response: Response, - ): Response { - const { + async loadCartCurrency(currencyCode: string, gqlUrl?: string, options?: RequestOptions) { + const url = gqlUrl ?? GQL_REQUEST_URL; + + const requestOptions: GQLRequestOptions = { + ...options, body: { - data: { site }, + query: getCartCurrencyQuery(currencyCode), }, - } = response; - - return { - ...response, - body: mapToCart(site), }; + + const response = await this._requestSender.post>( + url, + { + ...requestOptions, + }, + ); + + if (!response.body.data.site.currency) { + throw new CartRetrievalError( + `Could not retrieve currency information by currencyCode: ${currencyCode}`, + ); + } + + return response; } } diff --git a/packages/core/src/cart/gql-cart/errors/cart-retrieval-error.spec.ts b/packages/core/src/cart/gql-cart/errors/cart-retrieval-error.spec.ts new file mode 100644 index 0000000000..f603d0e26c --- /dev/null +++ b/packages/core/src/cart/gql-cart/errors/cart-retrieval-error.spec.ts @@ -0,0 +1,21 @@ +import CartRetrievalError from './cart-retrieval-error'; + +describe('init', () => { + it('sets type to cart_retrieval', () => { + const error = new CartRetrievalError(); + + expect(error.type).toBe('cart_retrieval'); + }); + + it('returns error name', () => { + const error = new CartRetrievalError(); + + expect(error.name).toBe('CartRetrievalError'); + }); + + it('sets the message as `body.title`', () => { + const error = new CartRetrievalError('test message'); + + expect(error.message).toBe('test message'); + }); +}); diff --git a/packages/core/src/cart/gql-cart/errors/cart-retrieval-error.ts b/packages/core/src/cart/gql-cart/errors/cart-retrieval-error.ts new file mode 100644 index 0000000000..f98ed8e269 --- /dev/null +++ b/packages/core/src/cart/gql-cart/errors/cart-retrieval-error.ts @@ -0,0 +1,10 @@ +import { StandardError } from '../../../common/error/errors'; + +export default class CartRetrievalError extends StandardError { + constructor(message?: string) { + super(message || 'Cart not available.'); + + this.name = 'CartRetrievalError'; + this.type = 'cart_retrieval'; + } +} diff --git a/packages/core/src/cart/gql-cart/get-cart-currency-query.ts b/packages/core/src/cart/gql-cart/get-cart-currency-query.ts new file mode 100644 index 0000000000..40197f7766 --- /dev/null +++ b/packages/core/src/cart/gql-cart/get-cart-currency-query.ts @@ -0,0 +1,18 @@ +const getCartCurrencyQuery = (currencyCode: string) => { + return ` + query Currency { + site { + currency(currencyCode: ${currencyCode}) { + display { + decimalPlaces + symbol + } + name + code + } + } + } + `; +}; + +export default getCartCurrencyQuery; diff --git a/packages/core/src/cart/gql-cart/get-cart-query.ts b/packages/core/src/cart/gql-cart/get-cart-query.ts new file mode 100644 index 0000000000..5872c323b9 --- /dev/null +++ b/packages/core/src/cart/gql-cart/get-cart-query.ts @@ -0,0 +1,203 @@ +const getCartQuery = (cartId: string) => { + return ` + query GetCartQuery { + site { + cart(entityId: "${cartId}") { + entityId + isTaxIncluded + currencyCode + id + updatedAt { + utc + } + createdAt { + utc + } + discounts { + entityId + discountedAmount { + currencyCode + value + } + } + baseAmount { + currencyCode + value + } + lineItems { + totalQuantity + customItems { + entityId + extendedListPrice { + value + } + listPrice { + value + } + name + quantity + sku + } + giftCertificates { + amount { + value + } + recipient { + email + name + } + sender { + email + name + } + theme + entityId + isTaxable + message + name + } + physicalItems { + name + brand + imageUrl + entityId + quantity + productEntityId + variantEntityId + couponAmount { + value + currencyCode + } + discountedAmount { + value + } + discounts { + discountedAmount { + value + } + entityId + } + extendedListPrice { + currencyCode + value + } + extendedSalePrice { + currencyCode + value + } + giftWrapping { + amount { + value + } + message + name + } + listPrice { + value + } + originalPrice { + value + } + salePrice { + value + } + sku + url + isShippingRequired + isTaxable + selectedOptions { + entityId + name + ... on CartSelectedMultipleChoiceOption { + value + valueEntityId + } + ... on CartSelectedCheckboxOption { + value + valueEntityId + } + ... on CartSelectedNumberFieldOption { + number + } + ... on CartSelectedMultiLineTextFieldOption { + text + } + ... on CartSelectedTextFieldOption { + text + } + ... on CartSelectedDateFieldOption { + date { + utc + } + } + } + } + digitalItems { + name + brand + imageUrl + entityId + quantity + productEntityId + variantEntityId + extendedListPrice { + currencyCode + value + } + extendedSalePrice { + currencyCode + value + } + selectedOptions { + entityId + name + ... on CartSelectedMultipleChoiceOption { + value + valueEntityId + } + ... on CartSelectedCheckboxOption { + value + valueEntityId + } + ... on CartSelectedNumberFieldOption { + number + } + ... on CartSelectedMultiLineTextFieldOption { + text + } + ... on CartSelectedTextFieldOption { + text + } + ... on CartSelectedDateFieldOption { + date { + utc + } + } + } + } + } + amount { + currencyCode + value + } + discountedAmount { + currencyCode + value + } + } + checkout(entityId: "${cartId}") { + coupons { + entityId + code + couponType + discountedAmount { + currencyCode + value + } + } + } + } + }`; +}; + +export default getCartQuery; diff --git a/packages/core/src/cart/gql-cart/gql-cart-request-response.ts b/packages/core/src/cart/gql-cart/gql-cart-request-response.ts deleted file mode 100644 index f1680fe712..0000000000 --- a/packages/core/src/cart/gql-cart/gql-cart-request-response.ts +++ /dev/null @@ -1,7 +0,0 @@ -import GQLCartResponse from './gql-cart'; - -export interface GQLCartRequestResponse { - data: { - site: GQLCartResponse; - }; -} diff --git a/packages/core/src/cart/gql-cart/gql-cart.ts b/packages/core/src/cart/gql-cart/gql-cart.ts index 7ebdd89b7d..246f66d3bb 100644 --- a/packages/core/src/cart/gql-cart/gql-cart.ts +++ b/packages/core/src/cart/gql-cart/gql-cart.ts @@ -1,7 +1,15 @@ +import { RequestOptions } from '@bigcommerce/request-sender'; + interface BaseFieldFragment { value: number; } +export interface GQLRequestOptions extends RequestOptions { + body: { + query: string; + }; +} + export interface GQLCartLineItem { name: string; entityId: string; @@ -98,8 +106,8 @@ export interface GQLCartLineItems { giftCertificates?: GQLCartGiftCertificates[]; } -export default interface GQLCartResponse { - cart?: { +export interface GQLCartResponse { + cart: { amount: BaseFieldFragment; baseAmount: BaseFieldFragment; entityId: string; @@ -119,7 +127,7 @@ export default interface GQLCartResponse { currencyCode: string; lineItems: GQLCartLineItems; }; - checkout?: { + checkout: { coupons: Array<{ entityId: string; code: string; @@ -129,7 +137,10 @@ export default interface GQLCartResponse { }; }>; }; - currency?: { +} + +export interface GQLCurrencyResponse { + currency: { display: { decimalPlaces: number; symbol: string; diff --git a/packages/core/src/cart/gql-cart/gql-request-response.ts b/packages/core/src/cart/gql-cart/gql-request-response.ts new file mode 100644 index 0000000000..9244d8445a --- /dev/null +++ b/packages/core/src/cart/gql-cart/gql-request-response.ts @@ -0,0 +1,5 @@ +export interface GQLRequestResponse { + data: { + site: T; + }; +} diff --git a/packages/core/src/cart/gql-cart/index.ts b/packages/core/src/cart/gql-cart/index.ts index 014f8cfcdf..fc31eb0265 100644 --- a/packages/core/src/cart/gql-cart/index.ts +++ b/packages/core/src/cart/gql-cart/index.ts @@ -1,3 +1,3 @@ -export { default as GQLCartResponse } from './gql-cart'; +export { GQLCartResponse, GQLRequestOptions, GQLCurrencyResponse } from './gql-cart'; export { default as mapToCart } from './map-to-cart'; -export { GQLCartRequestResponse } from './gql-cart-request-response'; +export { GQLRequestResponse } from './gql-request-response'; diff --git a/packages/core/src/cart/gql-cart/map-to-cart.spec.ts b/packages/core/src/cart/gql-cart/map-to-cart.spec.ts index 9b197d33bd..28ad25983e 100644 --- a/packages/core/src/cart/gql-cart/map-to-cart.spec.ts +++ b/packages/core/src/cart/gql-cart/map-to-cart.spec.ts @@ -1,15 +1,19 @@ +import { merge } from 'lodash'; + import { Cart } from '@bigcommerce/checkout-sdk/payment-integration-api'; import { getCart } from '../carts.mock'; import mapToCart from './map-to-cart'; -import { getGQLCartResponse } from './mocks/gql-cart.mock'; +import { getGQLCartResponse, getGQLCurrencyResponse } from './mocks/gql-cart.mock'; describe('mapToCart', () => { let headlessCartResponse: Cart | undefined; beforeEach(() => { - headlessCartResponse = mapToCart(getGQLCartResponse().data.site); + headlessCartResponse = mapToCart( + merge(getGQLCartResponse().data.site, getGQLCurrencyResponse().data.site), + ); }); it('maps to internal cart', () => { diff --git a/packages/core/src/cart/gql-cart/map-to-cart.ts b/packages/core/src/cart/gql-cart/map-to-cart.ts index c32bfeaccd..35c00f627a 100644 --- a/packages/core/src/cart/gql-cart/map-to-cart.ts +++ b/packages/core/src/cart/gql-cart/map-to-cart.ts @@ -2,14 +2,10 @@ import { Cart } from '@bigcommerce/checkout-sdk/payment-integration-api'; import mapToCartLineItems from './map-to-cart-line-items'; -import { GQLCartResponse } from './'; +import { GQLCartResponse, GQLCurrencyResponse } from './'; -export default function mapToCart(headlessCartResponse: GQLCartResponse): Cart | undefined { - const { cart, checkout, currency } = headlessCartResponse; - - if (!cart || !checkout || !currency) { - return; - } +export default function mapToCart(response: GQLCartResponse & GQLCurrencyResponse): Cart { + const { cart, checkout, currency } = response; return { id: cart.entityId, diff --git a/packages/core/src/cart/gql-cart/mocks/gql-cart.mock.ts b/packages/core/src/cart/gql-cart/mocks/gql-cart.mock.ts index b2cefa5b49..1ead9a46ba 100644 --- a/packages/core/src/cart/gql-cart/mocks/gql-cart.mock.ts +++ b/packages/core/src/cart/gql-cart/mocks/gql-cart.mock.ts @@ -1,5 +1,5 @@ -import { GQLCartLineItem } from '../gql-cart'; -import { GQLCartRequestResponse } from '../gql-cart-request-response'; +import { GQLCartLineItem, GQLCartResponse, GQLCurrencyResponse } from '../gql-cart'; +import { GQLRequestResponse } from '../gql-request-response'; export function gqlCartLineItem(): GQLCartLineItem { return { @@ -46,7 +46,7 @@ export function gqlCartLineItem(): GQLCartLineItem { }; } -export function getGQLCartResponse(): GQLCartRequestResponse { +export function getGQLCartResponse(): GQLRequestResponse { return { data: { site: { @@ -104,14 +104,6 @@ export function getGQLCartResponse(): GQLCartRequestResponse { }, ], }, - currency: { - display: { - decimalPlaces: 2, - symbol: '$', - }, - name: 'US Dollar', - code: 'USD', - }, }, }, }; @@ -134,3 +126,20 @@ export function getPhysicalItem(hasGiftWrapping?: false) { ...gqlCartLineItem(), }; } + +export function getGQLCurrencyResponse(): GQLRequestResponse { + return { + data: { + site: { + currency: { + display: { + decimalPlaces: 2, + symbol: '$', + }, + name: 'US Dollar', + code: 'USD', + }, + }, + }, + }; +} diff --git a/packages/core/src/common/gql-request/gql-request-url.ts b/packages/core/src/common/gql-request/gql-request-url.ts new file mode 100644 index 0000000000..cbcd0a149e --- /dev/null +++ b/packages/core/src/common/gql-request/gql-request-url.ts @@ -0,0 +1 @@ +export const GQL_REQUEST_URL = `${window.location.origin}/wallet-buttons`; diff --git a/packages/core/src/common/gql-request/index.ts b/packages/core/src/common/gql-request/index.ts new file mode 100644 index 0000000000..b75b6d7043 --- /dev/null +++ b/packages/core/src/common/gql-request/index.ts @@ -0,0 +1 @@ +export { GQL_REQUEST_URL } from './gql-request-url'; diff --git a/packages/core/src/config/config-selector.ts b/packages/core/src/config/config-selector.ts index b1f94f4244..67f8120d85 100644 --- a/packages/core/src/config/config-selector.ts +++ b/packages/core/src/config/config-selector.ts @@ -16,6 +16,7 @@ export default interface ConfigSelector { getContextConfig(): ContextConfig | undefined; getExternalSource(): string | undefined; getHost(): string | undefined; + getGQLRequestUrl(): string | undefined; getLocale(): string | undefined; getVariantIdentificationToken(): string | undefined; getLoadError(): Error | undefined; @@ -90,6 +91,11 @@ export function createConfigSelectorFactory(): ConfigSelectorFactory { (data) => () => data, ); + const getGQLRequestUrl = createSelector( + (state: ConfigState) => state.meta?.gqlRequestUrl, + (data) => () => data, + ); + const getLocale = createSelector( (state: ConfigState) => state.meta?.locale, (data) => () => data, @@ -120,6 +126,7 @@ export function createConfigSelectorFactory(): ConfigSelectorFactory { getContextConfig: getContextConfig(state), getExternalSource: getExternalSource(state), getHost: getHost(state), + getGQLRequestUrl: getGQLRequestUrl(state), getLocale: getLocale(state), getVariantIdentificationToken: getVariantIdentificationToken(state), getLoadError: getLoadError(state), diff --git a/packages/core/src/config/config-state.ts b/packages/core/src/config/config-state.ts index 15ded51e82..336bc1b942 100644 --- a/packages/core/src/config/config-state.ts +++ b/packages/core/src/config/config-state.ts @@ -12,6 +12,7 @@ export interface ConfigMetaState { variantIdentificationToken?: string; host?: string; locale?: string; + gqlRequestUrl?: string; } export interface ConfigErrorsState {