Skip to content

Commit e511d82

Browse files
committed
feat(payment): PAYPAL-4935 added CartActionCreator for handling/storing cart information
1 parent f39f018 commit e511d82

12 files changed

+550
-1
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { createAction, createErrorAction, ThunkAction } from '@bigcommerce/data-store';
2+
import { Observable, Observer } from 'rxjs';
3+
4+
import { RequestOptions } from '@bigcommerce/checkout-sdk/payment-integration-api';
5+
6+
import { InternalCheckoutSelectors } from '../checkout';
7+
import { cachableAction } from '../common/data-store';
8+
import ActionOptions from '../common/data-store/action-options';
9+
10+
import { CartActionType, LoadCartAction } from './cart-actions';
11+
import CartRequestSender from './cart-request-sender';
12+
13+
export default class CartActionCreator {
14+
constructor(private _cartRequestSender: CartRequestSender) {}
15+
16+
@cachableAction
17+
loadCard(
18+
cartId: string,
19+
options?: RequestOptions & ActionOptions,
20+
): ThunkAction<LoadCartAction, InternalCheckoutSelectors> {
21+
return (store) => {
22+
return Observable.create((observer: Observer<LoadCartAction>) => {
23+
const state = store.getState();
24+
const host = state.config.getHost();
25+
26+
observer.next(createAction(CartActionType.LoadCartRequested, undefined));
27+
28+
this._cartRequestSender
29+
.loadCard(cartId, host, options)
30+
.then((response) => {
31+
observer.next(
32+
createAction(CartActionType.LoadCartSucceeded, response.body),
33+
);
34+
observer.complete();
35+
})
36+
.catch((response) => {
37+
observer.error(createErrorAction(CartActionType.LoadCartFailed, response));
38+
});
39+
});
40+
};
41+
}
42+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Action } from '@bigcommerce/data-store';
2+
3+
import Cart from './cart';
4+
5+
export enum CartActionType {
6+
LoadCartRequested = 'LOAD_CART_REQUESTED',
7+
LoadCartSucceeded = 'LOAD_CART_SUCCEEDED',
8+
LoadCartFailed = 'LOAD_CART_FAILED',
9+
}
10+
11+
export type LoadCartAction =
12+
| LoadCartRequestedAction
13+
| LoadCartSucceededAction
14+
| LoadCartFailedAction;
15+
16+
export interface LoadCartRequestedAction extends Action {
17+
type: CartActionType.LoadCartRequested;
18+
}
19+
20+
export interface LoadCartSucceededAction extends Action<Cart> {
21+
type: CartActionType.LoadCartSucceeded;
22+
}
23+
24+
export interface LoadCartFailedAction extends Action<Error> {
25+
type: CartActionType.LoadCartFailed;
26+
}

packages/core/src/cart/cart-reducer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import { ConsignmentAction, ConsignmentActionType } from '../shipping';
1414

1515
import Cart from './cart';
16+
import { CartActionType, LoadCartAction } from './cart-actions';
1617
import CartState, { CartErrorsState, CartStatusesState, DEFAULT_STATE } from './cart-state';
1718

1819
export default function cartReducer(state: CartState = DEFAULT_STATE, action: Action): CartState {
@@ -32,7 +33,8 @@ function dataReducer(
3233
| CheckoutAction
3334
| ConsignmentAction
3435
| CouponAction
35-
| GiftCertificateAction,
36+
| GiftCertificateAction
37+
| LoadCartAction,
3638
): Cart | undefined {
3739
switch (action.type) {
3840
case BillingAddressActionType.UpdateBillingAddressSucceeded:
@@ -48,6 +50,9 @@ function dataReducer(
4850
case GiftCertificateActionType.RemoveGiftCertificateSucceeded:
4951
return objectMerge(data, action.payload && action.payload.cart);
5052

53+
case CartActionType.LoadCartSucceeded:
54+
return objectMerge(data, action.payload && action.payload);
55+
5156
default:
5257
return data;
5358
}

packages/core/src/cart/cart-request-sender.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { BuyNowCartRequestBody, Cart } from '@bigcommerce/checkout-sdk/payment-i
44

55
import { ContentType, RequestOptions, SDK_VERSION_HEADERS } from '../common/http-request';
66

7+
import { HeadlessCartRequestResponse, mapToCart } from './headless-cart';
8+
79
export default class CartRequestSender {
810
constructor(private _requestSender: RequestSender) {}
911

@@ -19,4 +21,41 @@ export default class CartRequestSender {
1921

2022
return this._requestSender.post(url, { body, headers, timeout });
2123
}
24+
25+
async loadCard(
26+
cartId: string,
27+
host?: string,
28+
options?: RequestOptions,
29+
): Promise<Response<Cart>> {
30+
const path = 'cart-information';
31+
const url = host ? `${host}/${path}` : `/${path}`;
32+
33+
const requestOptions: RequestOptions = {
34+
...options,
35+
params: {
36+
cartId,
37+
},
38+
};
39+
40+
return this._requestSender
41+
.get<HeadlessCartRequestResponse>(url, {
42+
...requestOptions,
43+
})
44+
.then(this.transformToCartResponse);
45+
}
46+
47+
private transformToCartResponse(
48+
response: Response<HeadlessCartRequestResponse>,
49+
): Response<Cart> {
50+
const {
51+
body: {
52+
data: { site },
53+
},
54+
} = response;
55+
56+
return {
57+
...response,
58+
body: mapToCart(site),
59+
};
60+
}
2261
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import HeadlessCartResponse from './headless-cart';
2+
3+
export interface HeadlessCartRequestResponse {
4+
data: {
5+
site: HeadlessCartResponse;
6+
};
7+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
interface BaseFieldFragment {
2+
value: number;
3+
}
4+
5+
export interface HeadlessLineItem {
6+
name: string;
7+
entityId: string;
8+
quantity: number;
9+
productEntityId: number;
10+
brand: string;
11+
// parentEntityId: number;
12+
couponAmount: {
13+
value: number;
14+
};
15+
discountedAmount: {
16+
value: number;
17+
};
18+
discounts: Array<{
19+
discountedAmount: {
20+
value: number;
21+
};
22+
entityId: string;
23+
}>;
24+
extendedListPrice: {
25+
value: number;
26+
};
27+
extendedSalePrice: {
28+
value: number;
29+
};
30+
imageUrl: string;
31+
isTaxable: boolean;
32+
listPrice: {
33+
value: number;
34+
};
35+
originalPrice: {
36+
value: number;
37+
};
38+
salePrice: {
39+
value: number;
40+
};
41+
sku: string;
42+
url: string;
43+
variantEntityId: number;
44+
selectedOptions: Array<{
45+
__typename: string;
46+
value: string;
47+
valueEntityId: number;
48+
entityId: number;
49+
name: string;
50+
}>;
51+
}
52+
53+
interface HeadlessPhysicalItem extends HeadlessLineItem {
54+
isShippingRequired: boolean;
55+
giftWrapping?: {
56+
amount: {
57+
value: number;
58+
};
59+
message: string;
60+
name: string;
61+
} | null;
62+
}
63+
64+
interface HeadlessDigitalItem extends HeadlessLineItem {
65+
// these fields can be optional because I did not see them in response
66+
downloadFileUrls: string[];
67+
downloadPageUrl: string;
68+
downloadSize: string;
69+
}
70+
71+
export interface HeadlessCustomItem {
72+
entityId: string;
73+
listPrice: BaseFieldFragment;
74+
extendedListPrice: BaseFieldFragment;
75+
name: string;
76+
quantity: number;
77+
sku: string;
78+
}
79+
80+
export interface HeadlessGiftCertificates {
81+
amount: BaseFieldFragment;
82+
name: string;
83+
theme: string;
84+
entityId: string | number;
85+
isTaxable: boolean;
86+
message: string;
87+
sender: {
88+
email: string;
89+
name: string;
90+
};
91+
recipient: {
92+
email: string;
93+
name: string;
94+
};
95+
}
96+
97+
export interface HeadlessLineItems {
98+
physicalItems: HeadlessPhysicalItem[];
99+
digitalItems: HeadlessDigitalItem[];
100+
customItems: HeadlessCustomItem[];
101+
giftCertificates?: HeadlessGiftCertificates[];
102+
}
103+
104+
export default interface HeadlessCartResponse {
105+
cart: {
106+
amount: BaseFieldFragment; // cart amount;
107+
baseAmount: BaseFieldFragment;
108+
entityId: string;
109+
id: string;
110+
createdAt: {
111+
utc: string;
112+
};
113+
updatedAt: {
114+
utc: string;
115+
};
116+
discounts: Array<{
117+
discountedAmount: BaseFieldFragment;
118+
entityId: string;
119+
}>;
120+
discountedAmount: BaseFieldFragment;
121+
isTaxIncluded: boolean;
122+
currencyCode: string;
123+
lineItems: HeadlessLineItems;
124+
};
125+
checkout: {
126+
coupons: Array<{
127+
entityId: string;
128+
code: string;
129+
couponType: string;
130+
discountedAmount: {
131+
value: number;
132+
};
133+
}>;
134+
};
135+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default as HeadlessCartResponse } from './headless-cart';
2+
export { default as mapToCart } from './map-to-cart';
3+
export { HeadlessCartRequestResponse } from './headless-cart-request-response';
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { LineItem } from '../line-item';
2+
3+
import { HeadlessLineItem } from './headless-cart';
4+
5+
export default function mapToLineItem(lineItem: HeadlessLineItem): LineItem {
6+
const {
7+
entityId,
8+
name,
9+
quantity,
10+
productEntityId,
11+
brand,
12+
couponAmount,
13+
discountedAmount,
14+
discounts,
15+
extendedListPrice,
16+
extendedSalePrice,
17+
imageUrl,
18+
isTaxable,
19+
listPrice,
20+
salePrice,
21+
sku,
22+
url,
23+
variantEntityId,
24+
selectedOptions,
25+
} = lineItem;
26+
27+
return {
28+
id: entityId,
29+
name,
30+
quantity,
31+
productId: productEntityId,
32+
brand,
33+
couponAmount: couponAmount.value,
34+
discountAmount: discountedAmount.value,
35+
discounts: discounts.map((discount) => ({
36+
discountedAmount: discount.discountedAmount.value,
37+
// 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
38+
name: discount.entityId,
39+
})),
40+
extendedListPrice: extendedListPrice.value,
41+
extendedSalePrice: extendedSalePrice.value,
42+
imageUrl,
43+
isTaxable,
44+
listPrice: listPrice.value,
45+
salePrice: salePrice.value,
46+
sku,
47+
url,
48+
variantId: variantEntityId,
49+
options: selectedOptions?.map((option) => ({
50+
name: option.name,
51+
nameId: option.entityId,
52+
value: option.value,
53+
valueId: option.valueEntityId,
54+
})),
55+
56+
// TODO:: we do not have any information regarding to fields below in the GraphQL Storefront doc
57+
addedByPromotion: false,
58+
comparisonPrice: 0,
59+
extendedComparisonPrice: 0,
60+
retailPrice: 0,
61+
};
62+
}

0 commit comments

Comments
 (0)