@tapcart/mobile-components 0.16.0 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/hooks/use-nosto-recommendation.d.ts +4 -0
- package/dist/components/hooks/use-nosto-recommendation.d.ts.map +1 -1
- package/dist/components/hooks/use-nosto-recommendation.js +116 -23
- package/dist/components/hooks/use-nosto-recommendation.queries.d.ts +0 -1
- package/dist/components/hooks/use-nosto-recommendation.queries.d.ts.map +1 -1
- package/dist/components/hooks/use-nosto-recommendation.queries.js +0 -3
- package/dist/components/hooks/use-nosto-recommendation.test.js +168 -171
- package/dist/components/hooks/use-order-details.d.ts +23 -0
- package/dist/components/hooks/use-order-details.d.ts.map +1 -1
- package/dist/components/hooks/use-order-details.js +59 -11
- package/dist/components/hooks/use-products.d.ts.map +1 -1
- package/dist/components/hooks/use-products.js +24 -4
- package/dist/components/hooks/use-products.test.d.ts +2 -0
- package/dist/components/hooks/use-products.test.d.ts.map +1 -0
- package/dist/components/hooks/use-products.test.js +129 -0
- package/dist/components/libs/cache/ProductsLocalStorage.d.ts.map +1 -1
- package/dist/components/libs/cache/ProductsLocalStorage.js +32 -0
- package/dist/components/libs/cache/ProductsLocalStorage.test.d.ts +2 -0
- package/dist/components/libs/cache/ProductsLocalStorage.test.d.ts.map +1 -0
- package/dist/components/libs/cache/ProductsLocalStorage.test.js +110 -0
- package/dist/components/ui/drawer.d.ts +3 -2
- package/dist/components/ui/drawer.d.ts.map +1 -1
- package/dist/components/ui/drawer.js +3 -3
- package/dist/tests/use-order-details.test.js +84 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-products.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-products.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAK1C,KAAK,GAAG,GAAG,MAAM,CAAA;AACjB,KAAK,cAAc,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAA;AACxD,KAAK,gBAAgB,GAAG;IACtB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,OAAO,EAAE,GAAG,CAAA;IACZ,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;IAC1E,UAAU,CAAC,EAAE,cAAc,EAAE,CAAA;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC,CAAA;AACD,KAAK,iBAAiB,GAAG;IACvB,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,KAAK,EAAE,GAAG,CAAA;IACV,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,OAAO,CAAA;IACrB,YAAY,EAAE,CAAC,OAAO,EAAE,OAAO,KAC3B;QACE,EAAE,EAAE,MAAM,CAAA;QACV,MAAM,EAAE,MAAM,CAAA;KACf,GACD,SAAS,CAAA;CACd,CAAA;AAqDD,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,GAAG,iBAAiB,
|
|
1
|
+
{"version":3,"file":"use-products.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-products.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAK1C,KAAK,GAAG,GAAG,MAAM,CAAA;AACjB,KAAK,cAAc,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAA;AACxD,KAAK,gBAAgB,GAAG;IACtB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,OAAO,EAAE,GAAG,CAAA;IACZ,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;IAC1E,UAAU,CAAC,EAAE,cAAc,EAAE,CAAA;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC,CAAA;AACD,KAAK,iBAAiB,GAAG;IACvB,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,KAAK,EAAE,GAAG,CAAA;IACV,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,OAAO,CAAA;IACrB,YAAY,EAAE,CAAC,OAAO,EAAE,OAAO,KAC3B;QACE,EAAE,EAAE,MAAM,CAAA;QACV,MAAM,EAAE,MAAM,CAAA;KACf,GACD,SAAS,CAAA;CACd,CAAA;AAqDD,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,GAAG,iBAAiB,CAqL7E"}
|
|
@@ -108,16 +108,36 @@ export function useProducts(props) {
|
|
|
108
108
|
const productsArray = Array.isArray(data) ? data : data.products || [];
|
|
109
109
|
const cachedProducts = productsArray
|
|
110
110
|
.filter((product) => !!product)
|
|
111
|
-
.map((product) =>
|
|
112
|
-
productsLocalStorage.setCacheItem(product);
|
|
113
|
-
return product;
|
|
114
|
-
});
|
|
111
|
+
.map((product) => productsLocalStorage.setCacheItem(product) || product);
|
|
115
112
|
// Return either just the products array or an object with spread contents and overridden products
|
|
116
113
|
return Array.isArray(data)
|
|
117
114
|
? cachedProducts
|
|
118
115
|
: Object.assign(Object.assign({}, data), { products: cachedProducts });
|
|
119
116
|
}));
|
|
120
117
|
const fetcher = (props === null || props === void 0 ? void 0 : props.fetcher) || defaultFetcher;
|
|
118
|
+
// Reset local product state when the requested product identity changes so
|
|
119
|
+
// stale or partial state from a previously viewed product cannot carry over.
|
|
120
|
+
const requestedProductKey = React.useMemo(() => {
|
|
121
|
+
var _a, _b, _c;
|
|
122
|
+
return JSON.stringify({
|
|
123
|
+
productIds: (_a = props === null || props === void 0 ? void 0 : props.productIds) !== null && _a !== void 0 ? _a : [],
|
|
124
|
+
productHandles: (_b = props === null || props === void 0 ? void 0 : props.productHandles) !== null && _b !== void 0 ? _b : [],
|
|
125
|
+
variantIds: (_c = props === null || props === void 0 ? void 0 : props.variantIds) !== null && _c !== void 0 ? _c : [],
|
|
126
|
+
});
|
|
127
|
+
}, [props === null || props === void 0 ? void 0 : props.productIds, props === null || props === void 0 ? void 0 : props.productHandles, props === null || props === void 0 ? void 0 : props.variantIds]);
|
|
128
|
+
const hasMountedRef = React.useRef(false);
|
|
129
|
+
React.useEffect(() => {
|
|
130
|
+
if (!hasMountedRef.current) {
|
|
131
|
+
hasMountedRef.current = true;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
setProductResponse(productsLocalStorage.getCacheItems({
|
|
135
|
+
productIds: props === null || props === void 0 ? void 0 : props.productIds,
|
|
136
|
+
productHandles: props === null || props === void 0 ? void 0 : props.productHandles,
|
|
137
|
+
}));
|
|
138
|
+
setIsProductResponseReady(false);
|
|
139
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
140
|
+
}, [requestedProductKey]);
|
|
121
141
|
const { data, error, isLoading } = useSWR(url ? [url, body] : null, ([url, body]) => fetcher(url, body));
|
|
122
142
|
const cacheProduct = (product) => {
|
|
123
143
|
const cachedProduct = productsLocalStorage.setCacheItem(product);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-products.test.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-products.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
11
|
+
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
12
|
+
import { SWRConfig } from "swr";
|
|
13
|
+
import ProductsLocalStorage from "../libs/cache/ProductsLocalStorage";
|
|
14
|
+
import { useProducts } from "./use-products";
|
|
15
|
+
const productId = "gid://shopify/Product/1";
|
|
16
|
+
const hydratedSellingPlanGroups = {
|
|
17
|
+
edges: [
|
|
18
|
+
{
|
|
19
|
+
node: {
|
|
20
|
+
id: "group-1",
|
|
21
|
+
sellingPlans: {
|
|
22
|
+
edges: [
|
|
23
|
+
{
|
|
24
|
+
node: {
|
|
25
|
+
id: "plan-1",
|
|
26
|
+
priceAdjustments: [
|
|
27
|
+
{ adjustmentValue: { adjustmentPercentage: 10 } },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
const wrapper = ({ children }) => (_jsx(SWRConfig, Object.assign({ value: {
|
|
38
|
+
provider: () => new Map(),
|
|
39
|
+
dedupingInterval: 0,
|
|
40
|
+
} }, { children: children })));
|
|
41
|
+
describe("useProducts", () => {
|
|
42
|
+
let originalFetch;
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
originalFetch = global.fetch;
|
|
45
|
+
localStorage.clear();
|
|
46
|
+
jest.restoreAllMocks();
|
|
47
|
+
});
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
global.fetch = originalFetch;
|
|
50
|
+
});
|
|
51
|
+
it("renders the merged cached product when a fetch returns partial selling plans", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
52
|
+
var _a;
|
|
53
|
+
const cache = new ProductsLocalStorage();
|
|
54
|
+
cache.setCacheItem({
|
|
55
|
+
id: productId,
|
|
56
|
+
handle: "subscription-product",
|
|
57
|
+
title: "Cached product",
|
|
58
|
+
sellingPlanGroups: hydratedSellingPlanGroups,
|
|
59
|
+
});
|
|
60
|
+
const fetchMock = jest.fn().mockResolvedValue({
|
|
61
|
+
json: jest.fn().mockResolvedValue([
|
|
62
|
+
{
|
|
63
|
+
id: productId,
|
|
64
|
+
handle: "subscription-product",
|
|
65
|
+
title: "Network product",
|
|
66
|
+
sellingPlanGroups: {
|
|
67
|
+
edges: [
|
|
68
|
+
{
|
|
69
|
+
node: {
|
|
70
|
+
id: "group-1",
|
|
71
|
+
sellingPlans: {
|
|
72
|
+
edges: [{ node: { id: "plan-1" } }],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
]),
|
|
80
|
+
});
|
|
81
|
+
global.fetch = fetchMock;
|
|
82
|
+
const { result } = renderHook(() => useProducts({
|
|
83
|
+
baseURL: "https://example.test",
|
|
84
|
+
productIds: [productId],
|
|
85
|
+
}), { wrapper });
|
|
86
|
+
yield waitFor(() => expect(fetchMock).toHaveBeenCalled());
|
|
87
|
+
yield waitFor(() => { var _a; return expect((_a = result.current.products[0]) === null || _a === void 0 ? void 0 : _a.title).toBe("Network product"); });
|
|
88
|
+
expect((_a = result.current.products[0]) === null || _a === void 0 ? void 0 : _a.sellingPlanGroups).toEqual(hydratedSellingPlanGroups);
|
|
89
|
+
}));
|
|
90
|
+
it("clears the previous product while a new product identity resolves", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
|
+
let resolveProductB = () => { };
|
|
92
|
+
const productBResponse = new Promise((resolve) => {
|
|
93
|
+
resolveProductB = resolve;
|
|
94
|
+
});
|
|
95
|
+
const fetcher = jest
|
|
96
|
+
.fn()
|
|
97
|
+
.mockResolvedValueOnce([
|
|
98
|
+
{
|
|
99
|
+
id: "gid://shopify/Product/A",
|
|
100
|
+
handle: "product-a",
|
|
101
|
+
title: "Product A",
|
|
102
|
+
},
|
|
103
|
+
])
|
|
104
|
+
.mockReturnValueOnce(productBResponse);
|
|
105
|
+
const { result, rerender } = renderHook(({ productId }) => useProducts({
|
|
106
|
+
baseURL: "https://example.test",
|
|
107
|
+
productIds: [productId],
|
|
108
|
+
fetcher,
|
|
109
|
+
}), {
|
|
110
|
+
initialProps: { productId: "gid://shopify/Product/A" },
|
|
111
|
+
wrapper,
|
|
112
|
+
});
|
|
113
|
+
yield waitFor(() => { var _a; return expect((_a = result.current.products[0]) === null || _a === void 0 ? void 0 : _a.title).toBe("Product A"); });
|
|
114
|
+
rerender({ productId: "gid://shopify/Product/B" });
|
|
115
|
+
expect(result.current.products).toEqual([]);
|
|
116
|
+
expect(result.current.isLoading).toBe(true);
|
|
117
|
+
yield act(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
118
|
+
resolveProductB([
|
|
119
|
+
{
|
|
120
|
+
id: "gid://shopify/Product/B",
|
|
121
|
+
handle: "product-b",
|
|
122
|
+
title: "Product B",
|
|
123
|
+
},
|
|
124
|
+
]);
|
|
125
|
+
yield productBResponse;
|
|
126
|
+
}));
|
|
127
|
+
yield waitFor(() => { var _a; return expect((_a = result.current.products[0]) === null || _a === void 0 ? void 0 : _a.title).toBe("Product B"); });
|
|
128
|
+
}));
|
|
129
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProductsLocalStorage.d.ts","sourceRoot":"","sources":["../../../../components/libs/cache/ProductsLocalStorage.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"ProductsLocalStorage.d.ts","sourceRoot":"","sources":["../../../../components/libs/cache/ProductsLocalStorage.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,MAAM,kBAAkB,CAAA;AA0B7C,KAAK,aAAa,GAAG;IACnB,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,EAAE,EAAE,MAAM,CAAA;IACV,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CACnB,CAAA;AAID,MAAM,CAAC,OAAO,OAAO,oBAAqB,SAAQ,cAAc;IAC9D,OAAO,EAAE,MAAM,CAA2B;IAC1C,WAAW,EAAE,MAAM,CAAe;IAElC,aAAa,CAAC,EAAE,EAAE,MAAM;IAIxB,iBAAiB,CAAC,MAAM,EAAE,MAAM;;IAShC,QAAQ,IAAI,aAAa,EAAE;IAiB3B,YAAY,CAAC,EACX,EAAE,EACF,MAAM,GACP,EAAE;QACD,EAAE,CAAC,EAAE,MAAM,CAAA;QACX,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,GAAG,MAAM,GAAG,IAAI;IAgBjB,aAAa,CAAC,QAAQ,EAAE;QACtB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;QACrB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;KAC1B,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE;IAYrB,YAAY,CACV,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,EACvC,KAAK,CAAC,EAAE,aAAa,EAAE,EACvB,YAAY,GAAE,OAAc;gBAFT,MAAM;YAAM,MAAM;;IA6EvC,aAAa,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE;;;;IASrD,eAAe,CAAC,WAAW,GAAE,MAAU;IAWvC,UAAU;IAQV,eAAe,CAAC,CAAC,EAAE,GAAG;CASvB"}
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import AppStudioCache from "./AppStudioCache";
|
|
2
2
|
import { getProductGidsFromIds } from "../../../lib/utils";
|
|
3
|
+
// Normalizes a Shopify connection ({ edges: [...] }) or a plain array into an array.
|
|
4
|
+
const normalizeConnection = (value) => Array.isArray(value) ? value : Array.isArray(value === null || value === void 0 ? void 0 : value.edges) ? value.edges : [];
|
|
5
|
+
const isConnection = (value) => Array.isArray(value) || Array.isArray(value === null || value === void 0 ? void 0 : value.edges);
|
|
6
|
+
// Empty connections are authoritative. Non-empty connections are complete only
|
|
7
|
+
// when every nested plan includes the priceAdjustments field requested by PDPs.
|
|
8
|
+
const hasCompleteSellingPlanData = (groups) => {
|
|
9
|
+
if (!isConnection(groups))
|
|
10
|
+
return false;
|
|
11
|
+
return normalizeConnection(groups).every((group) => {
|
|
12
|
+
var _a;
|
|
13
|
+
const groupNode = (_a = group === null || group === void 0 ? void 0 : group.node) !== null && _a !== void 0 ? _a : group;
|
|
14
|
+
if (!isConnection(groupNode === null || groupNode === void 0 ? void 0 : groupNode.sellingPlans))
|
|
15
|
+
return false;
|
|
16
|
+
return normalizeConnection(groupNode.sellingPlans).every((plan) => {
|
|
17
|
+
var _a;
|
|
18
|
+
const planNode = (_a = plan === null || plan === void 0 ? void 0 : plan.node) !== null && _a !== void 0 ? _a : plan;
|
|
19
|
+
return Array.isArray(planNode === null || planNode === void 0 ? void 0 : planNode.priceAdjustments);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
const FRESH_MS = 60 * 1000;
|
|
3
24
|
export default class ProductsLocalStorage extends AppStudioCache {
|
|
4
25
|
getIdCacheKey(id) {
|
|
5
26
|
return `${this.cachePrefix}-data-${id}`;
|
|
@@ -68,6 +89,17 @@ export default class ProductsLocalStorage extends AppStudioCache {
|
|
|
68
89
|
if (cachedProduct) {
|
|
69
90
|
// merge products, keeping populated arrays from cached product
|
|
70
91
|
productToCache = Object.entries(product).reduce((merged, [key, value]) => {
|
|
92
|
+
var _a;
|
|
93
|
+
// Keep the more-complete cached sellingPlanGroups when the incoming
|
|
94
|
+
// payload is partial/unhydrated (missing nested priceAdjustments),
|
|
95
|
+
// e.g. from PLP or carousel fetches. Prevents a shallow payload from
|
|
96
|
+
// poisoning a fully hydrated PDP record.
|
|
97
|
+
if (key === "sellingPlanGroups" &&
|
|
98
|
+
hasCompleteSellingPlanData(cachedProduct[key]) &&
|
|
99
|
+
!hasCompleteSellingPlanData(value) &&
|
|
100
|
+
Date.now() - ((_a = cachedProduct.ct) !== null && _a !== void 0 ? _a : 0) < FRESH_MS) {
|
|
101
|
+
return Object.assign(Object.assign({}, merged), { [key]: cachedProduct[key] });
|
|
102
|
+
}
|
|
71
103
|
// Keep non-empty arrays from cached product if current product has empty array
|
|
72
104
|
if (Array.isArray(value) &&
|
|
73
105
|
value.length === 0 &&
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductsLocalStorage.test.d.ts","sourceRoot":"","sources":["../../../../components/libs/cache/ProductsLocalStorage.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import ProductsLocalStorage from "./ProductsLocalStorage";
|
|
2
|
+
const productId = "gid://shopify/Product/1";
|
|
3
|
+
const sellingPlanGroups = (price) => ({
|
|
4
|
+
edges: [
|
|
5
|
+
{
|
|
6
|
+
node: {
|
|
7
|
+
id: "group-1",
|
|
8
|
+
sellingPlans: {
|
|
9
|
+
edges: [
|
|
10
|
+
{
|
|
11
|
+
node: {
|
|
12
|
+
id: "plan-1",
|
|
13
|
+
priceAdjustments: [{ adjustmentValue: { price } }],
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
const product = (groups) => ({
|
|
23
|
+
id: productId,
|
|
24
|
+
handle: "subscription-product",
|
|
25
|
+
sellingPlanGroups: groups,
|
|
26
|
+
});
|
|
27
|
+
describe("ProductsLocalStorage selling plan merges", () => {
|
|
28
|
+
let storage;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
localStorage.clear();
|
|
31
|
+
storage = new ProductsLocalStorage();
|
|
32
|
+
});
|
|
33
|
+
it("keeps hydrated selling plans when an incoming payload is partial", () => {
|
|
34
|
+
storage.setCacheItem(product(sellingPlanGroups("10.00")));
|
|
35
|
+
const merged = storage.setCacheItem(product({
|
|
36
|
+
edges: [
|
|
37
|
+
{
|
|
38
|
+
node: {
|
|
39
|
+
id: "group-1",
|
|
40
|
+
sellingPlans: {
|
|
41
|
+
edges: [{ node: { id: "plan-1" } }],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
}));
|
|
47
|
+
expect(merged.sellingPlanGroups.edges[0].node.sellingPlans.edges[0].node
|
|
48
|
+
.priceAdjustments).toEqual([{ adjustmentValue: { price: "10.00" } }]);
|
|
49
|
+
});
|
|
50
|
+
it("keeps hydrated selling plans when only some incoming plans are hydrated", () => {
|
|
51
|
+
storage.setCacheItem(product(sellingPlanGroups("10.00")));
|
|
52
|
+
const merged = storage.setCacheItem(product({
|
|
53
|
+
edges: [
|
|
54
|
+
{
|
|
55
|
+
node: {
|
|
56
|
+
id: "group-1",
|
|
57
|
+
sellingPlans: {
|
|
58
|
+
edges: [
|
|
59
|
+
{
|
|
60
|
+
node: {
|
|
61
|
+
id: "plan-1",
|
|
62
|
+
priceAdjustments: [
|
|
63
|
+
{ adjustmentValue: { price: "12.00" } },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{ node: { id: "plan-2" } },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
}));
|
|
74
|
+
expect(merged.sellingPlanGroups.edges[0].node.sellingPlans.edges).toHaveLength(1);
|
|
75
|
+
expect(merged.sellingPlanGroups.edges[0].node.sellingPlans.edges[0].node
|
|
76
|
+
.priceAdjustments).toEqual([{ adjustmentValue: { price: "10.00" } }]);
|
|
77
|
+
});
|
|
78
|
+
it("accepts an authoritative empty selling plan list", () => {
|
|
79
|
+
storage.setCacheItem(product(sellingPlanGroups("10.00")));
|
|
80
|
+
const merged = storage.setCacheItem(product({ edges: [] }));
|
|
81
|
+
expect(merged.sellingPlanGroups).toEqual({ edges: [] });
|
|
82
|
+
});
|
|
83
|
+
it("replaces cached selling plans with a complete incoming payload", () => {
|
|
84
|
+
storage.setCacheItem(product(sellingPlanGroups("10.00")));
|
|
85
|
+
const merged = storage.setCacheItem(product(sellingPlanGroups("12.00")));
|
|
86
|
+
expect(merged.sellingPlanGroups.edges[0].node.sellingPlans.edges[0].node
|
|
87
|
+
.priceAdjustments).toEqual([{ adjustmentValue: { price: "12.00" } }]);
|
|
88
|
+
});
|
|
89
|
+
it("lets a partial payload win once the cached entry is stale", () => {
|
|
90
|
+
const nowSpy = jest.spyOn(Date, "now");
|
|
91
|
+
nowSpy.mockReturnValue(1000000);
|
|
92
|
+
storage.setCacheItem(product(sellingPlanGroups("10.00")));
|
|
93
|
+
nowSpy.mockReturnValue(1000000 + 61 * 1000);
|
|
94
|
+
const merged = storage.setCacheItem(product({
|
|
95
|
+
edges: [
|
|
96
|
+
{
|
|
97
|
+
node: {
|
|
98
|
+
id: "group-1",
|
|
99
|
+
sellingPlans: {
|
|
100
|
+
edges: [{ node: { id: "plan-1" } }],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
}));
|
|
106
|
+
expect(merged.sellingPlanGroups.edges[0].node.sellingPlans.edges[0].node
|
|
107
|
+
.priceAdjustments).toBeUndefined();
|
|
108
|
+
nowSpy.mockRestore();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -33,11 +33,12 @@ declare const DrawerContentBase: React.ForwardRefExoticComponent<Omit<React.Deta
|
|
|
33
33
|
containerRef?: HTMLElement | undefined;
|
|
34
34
|
maxHeight?: string | undefined;
|
|
35
35
|
} & React.RefAttributes<HTMLDivElement>>;
|
|
36
|
+
declare const DrawerTitle: React.ForwardRefExoticComponent<Omit<import("@radix-ui/react-dialog").DialogTitleProps & React.RefAttributes<HTMLHeadingElement>, "ref"> & React.RefAttributes<HTMLHeadingElement>>;
|
|
36
37
|
declare const DrawerFooter: {
|
|
37
38
|
({ className, ...props }: React.HTMLAttributes<HTMLDivElement>): import("react/jsx-runtime").JSX.Element;
|
|
38
39
|
displayName: string;
|
|
39
40
|
};
|
|
40
|
-
declare const DrawerDescription: React.ForwardRefExoticComponent<Omit<
|
|
41
|
+
declare const DrawerDescription: React.ForwardRefExoticComponent<Omit<import("@radix-ui/react-dialog").DialogDescriptionProps & React.RefAttributes<HTMLParagraphElement>, "ref"> & React.RefAttributes<HTMLParagraphElement>>;
|
|
41
42
|
type DrawerHeaderProps = {
|
|
42
43
|
iconLeftName?: string;
|
|
43
44
|
iconRightName?: string;
|
|
@@ -46,5 +47,5 @@ type DrawerHeaderProps = {
|
|
|
46
47
|
};
|
|
47
48
|
declare const DrawerHeader: ({ title, iconLeftName, iconRightName, onCloseClick, }: DrawerHeaderProps) => import("react/jsx-runtime").JSX.Element;
|
|
48
49
|
declare const DrawerContent: ({ children, className, ...props }: React.HTMLAttributes<HTMLDivElement>) => import("react/jsx-runtime").JSX.Element;
|
|
49
|
-
export { Drawer, DrawerPortal, DrawerOverlay, DrawerTrigger, DrawerClose, DrawerContentBase, DrawerContent, DrawerFooter, DrawerDescription, DrawerHeader, };
|
|
50
|
+
export { Drawer, DrawerPortal, DrawerOverlay, DrawerTrigger, DrawerClose, DrawerContentBase, DrawerContent, DrawerFooter, DrawerTitle, DrawerDescription, DrawerHeader, };
|
|
50
51
|
//# sourceMappingURL=drawer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drawer.d.ts","sourceRoot":"","sources":["../../../components/ui/drawer.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,MAAM,IAAI,eAAe,EAAE,MAAM,MAAM,CAAA;AAKhD,KAAK,kBAAkB,GAAG,KAAK,CAAC,wBAAwB,CAAC,QAAQ,CAAC,GAAG;IACnE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,KAAK,iBAAiB,GAAG;IACvB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,YAAY,CAAC,EAAE,WAAW,CAAA;CAC3B,CAAA;AAED,KAAK,gBAAgB,GAAG,KAAK,CAAC,wBAAwB,CAAC,QAAQ,CAAC,GAAG;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,QAAA,MAAM,aAAa,8BAA0D,CAAA;AAG7E,QAAA,MAAM,YAAY;iCAAgC,iBAAiB;;CAgBlE,CAAA;AAGD,QAAA,MAAM,WAAW,4BAAsD,CAAA;AAGvE,KAAK,WAAW,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,GAAG;IACrE,YAAY,CAAC,EAAE,WAAW,CAAA;CAC3B,CAAA;AAED,QAAA,MAAM,MAAM;0CAAyC,WAAW;;CAS/D,CAAA;AAOD,QAAA,MAAM,aAAa;;wCAYlB,CAAA;AAWD,QAAA,MAAM,iBAAiB;;;;;;wCA2CtB,CAAA;
|
|
1
|
+
{"version":3,"file":"drawer.d.ts","sourceRoot":"","sources":["../../../components/ui/drawer.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,MAAM,IAAI,eAAe,EAAE,MAAM,MAAM,CAAA;AAKhD,KAAK,kBAAkB,GAAG,KAAK,CAAC,wBAAwB,CAAC,QAAQ,CAAC,GAAG;IACnE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,KAAK,iBAAiB,GAAG;IACvB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,YAAY,CAAC,EAAE,WAAW,CAAA;CAC3B,CAAA;AAED,KAAK,gBAAgB,GAAG,KAAK,CAAC,wBAAwB,CAAC,QAAQ,CAAC,GAAG;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,QAAA,MAAM,aAAa,8BAA0D,CAAA;AAG7E,QAAA,MAAM,YAAY;iCAAgC,iBAAiB;;CAgBlE,CAAA;AAGD,QAAA,MAAM,WAAW,4BAAsD,CAAA;AAGvE,KAAK,WAAW,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,GAAG;IACrE,YAAY,CAAC,EAAE,WAAW,CAAA;CAC3B,CAAA;AAED,QAAA,MAAM,MAAM;0CAAyC,WAAW;;CAS/D,CAAA;AAOD,QAAA,MAAM,aAAa;;wCAYlB,CAAA;AAWD,QAAA,MAAM,iBAAiB;;;;;;wCA2CtB,CAAA;AAWD,QAAA,MAAM,WAAW,qLAYf,CAAA;AAGF,QAAA,MAAM,YAAY;8BAGf,MAAM,cAAc,CAAC,cAAc,CAAC;;CAQtC,CAAA;AAGD,QAAA,MAAM,iBAAiB,+LASrB,CAAA;AAGF,KAAK,iBAAiB,GAAG;IACvB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAA;CACjD,CAAA;AAED,QAAA,MAAM,YAAY,0DAKf,iBAAiB,4CA8BnB,CAAA;AAED,QAAA,MAAM,aAAa,sCAIhB,MAAM,cAAc,CAAC,cAAc,CAAC,4CAMtC,CAAA;AAED,OAAO,EACL,MAAM,EACN,YAAY,EACZ,aAAa,EACb,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,aAAa,EACb,YAAY,EACZ,WAAW,EACX,iBAAiB,EACjB,YAAY,GACb,CAAA"}
|
|
@@ -71,9 +71,9 @@ const DrawerFooter = (_a) => {
|
|
|
71
71
|
DrawerFooter.displayName = "DrawerFooter";
|
|
72
72
|
const DrawerDescription = React.forwardRef((_a, ref) => {
|
|
73
73
|
var { className } = _a, props = __rest(_a, ["className"]);
|
|
74
|
-
return (_jsx(
|
|
74
|
+
return (_jsx(DrawerPrimitive.Description, Object.assign({ ref: ref, className: cn("text-sm text-muted-foreground", className) }, props)));
|
|
75
75
|
});
|
|
76
|
-
DrawerDescription.displayName =
|
|
76
|
+
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
|
|
77
77
|
const DrawerHeader = ({ title, iconLeftName, iconRightName, onCloseClick, }) => {
|
|
78
78
|
return (_jsxs(DrawerHeaderBase, Object.assign({ className: "p-4 text-center flex justify-between" }, { children: [iconLeftName ? (_jsx(DrawerClose, Object.assign({ className: "h-6 w-6 p-0", onClick: onCloseClick }, { children: _jsx(Icon, { name: iconLeftName, color: "coreColors-secondaryIcon", className: "h-6 w-6 p-0", size: "md" }) }))) : (_jsx("div", { className: "w-6" })), _jsx(DrawerTitle, { children: title }), iconRightName ? (_jsx(DrawerClose, Object.assign({ className: "h-6 w-6 p-0", onClick: onCloseClick }, { children: _jsx(Icon, { name: iconRightName, color: "coreColors-secondaryIcon", className: "h-6 w-6 p-0", size: "md" }) }))) : (_jsx("div", { className: "w-6" }))] })));
|
|
79
79
|
};
|
|
@@ -81,4 +81,4 @@ const DrawerContent = (_a) => {
|
|
|
81
81
|
var { children, className } = _a, props = __rest(_a, ["children", "className"]);
|
|
82
82
|
return (_jsx("div", Object.assign({ className: cn("h-full overflow-scroll", className) }, props, { children: children })));
|
|
83
83
|
};
|
|
84
|
-
export { Drawer, DrawerPortal, DrawerOverlay, DrawerTrigger, DrawerClose, DrawerContentBase, DrawerContent, DrawerFooter, DrawerDescription, DrawerHeader, };
|
|
84
|
+
export { Drawer, DrawerPortal, DrawerOverlay, DrawerTrigger, DrawerClose, DrawerContentBase, DrawerContent, DrawerFooter, DrawerTitle, DrawerDescription, DrawerHeader, };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { transformOrderDetails } from "../components/hooks/use-order-details";
|
|
1
|
+
import { transformOrderDetails, getEffectiveLineQuantity, } from "../components/hooks/use-order-details";
|
|
2
2
|
// jsdom does not implement crypto.randomUUID, which transformOrderDetails
|
|
3
3
|
// uses for line uniqueIds
|
|
4
4
|
beforeAll(() => {
|
|
@@ -63,3 +63,86 @@ describe("transformOrderDetails — Shopify Order Editing (currentQuantity)", ()
|
|
|
63
63
|
expect(orderDetails.cart.lines[0].quantity).toBe(1);
|
|
64
64
|
});
|
|
65
65
|
});
|
|
66
|
+
describe("transformOrderDetails — New Customer Account API (no currentQuantity)", () => {
|
|
67
|
+
// Modeled on real prod data (Pink Palm Puff order #PPP95701331): the
|
|
68
|
+
// Customer Account API LineItem has NO `currentQuantity` — a line removed via
|
|
69
|
+
// Order Editing keeps `quantity: 1` and signals removal only via
|
|
70
|
+
// `refundableQuantity: 0`. The kept line has `refundableQuantity: 1`.
|
|
71
|
+
const editedOrder = {
|
|
72
|
+
id: "gid://shopify/Order/10929524212006",
|
|
73
|
+
currency: "USD",
|
|
74
|
+
edited: true,
|
|
75
|
+
financialStatus: "PAID",
|
|
76
|
+
refunds: [],
|
|
77
|
+
totalRefunded: { amount: "0.0", currencyCode: "USD" },
|
|
78
|
+
totalAmount: "63.88",
|
|
79
|
+
lineItems: [
|
|
80
|
+
{
|
|
81
|
+
title: "Couture Carryall in Blush Bay (Reversible)",
|
|
82
|
+
quantity: 1,
|
|
83
|
+
refundableQuantity: 0,
|
|
84
|
+
price: { amount: "99.0", currencyCode: "USD" },
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
title: "Malibu Slippers in Blush Bay",
|
|
88
|
+
quantity: 1,
|
|
89
|
+
refundableQuantity: 1,
|
|
90
|
+
price: { amount: "59.0", currencyCode: "USD" },
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
it("hides items removed via order editing on an edited order", () => {
|
|
95
|
+
const { orderDetails, checkoutData } = transformOrderDetails(editedOrder);
|
|
96
|
+
expect(orderDetails.cart.lines).toHaveLength(1);
|
|
97
|
+
expect(orderDetails.cart.lines[0].title).toBe("Malibu Slippers in Blush Bay");
|
|
98
|
+
expect(checkoutData.lineItems).toHaveLength(1);
|
|
99
|
+
expect(checkoutData.lineItems[0].title).toBe("Malibu Slippers in Blush Bay");
|
|
100
|
+
});
|
|
101
|
+
it("does NOT hide items on an UN-edited fully-refunded order", () => {
|
|
102
|
+
// Real prod data: cancelled+refunded order #PPP95700931 — edited:false,
|
|
103
|
+
// both lines refundableQuantity:0. These must still display in full.
|
|
104
|
+
const refundedOrder = Object.assign(Object.assign({}, editedOrder), { id: "gid://shopify/Order/10929514119462", edited: false, lineItems: editedOrder.lineItems.map((li) => (Object.assign(Object.assign({}, li), { refundableQuantity: 0 }))) });
|
|
105
|
+
const { orderDetails } = transformOrderDetails(refundedOrder);
|
|
106
|
+
expect(orderDetails.cart.lines).toHaveLength(2);
|
|
107
|
+
});
|
|
108
|
+
it("does NOT hide items on an EDITED order that also has a refund/return", () => {
|
|
109
|
+
// `edited` is sticky in Shopify and `refundableQuantity` drops on refunds
|
|
110
|
+
// too, so an edited-then-returned order must NOT collapse: fall back to
|
|
111
|
+
// ordered quantity rather than blank the order / hide bought items.
|
|
112
|
+
const editedAndRefunded = Object.assign(Object.assign({}, editedOrder), { financialStatus: "REFUNDED", totalRefunded: { amount: "171.06", currencyCode: "USD" }, refunds: [{ id: "gid://shopify/Refund/1", createdAt: "2026-07-08" }], lineItems: editedOrder.lineItems.map((li) => (Object.assign(Object.assign({}, li), { refundableQuantity: 0 }))) });
|
|
113
|
+
const { orderDetails } = transformOrderDetails(editedAndRefunded);
|
|
114
|
+
expect(orderDetails.cart.lines).toHaveLength(2);
|
|
115
|
+
});
|
|
116
|
+
it("reduces (not hides) a quantity edited down on an edited order", () => {
|
|
117
|
+
const { orderDetails } = transformOrderDetails(Object.assign(Object.assign({}, editedOrder), { lineItems: [
|
|
118
|
+
{
|
|
119
|
+
title: "Pink Palm Pajamas",
|
|
120
|
+
quantity: 3,
|
|
121
|
+
refundableQuantity: 2,
|
|
122
|
+
price: { amount: "95.0", currencyCode: "USD" },
|
|
123
|
+
},
|
|
124
|
+
] }));
|
|
125
|
+
expect(orderDetails.cart.lines).toHaveLength(1);
|
|
126
|
+
expect(orderDetails.cart.lines[0].quantity).toBe(2);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
describe("getEffectiveLineQuantity", () => {
|
|
130
|
+
it("prefers currentQuantity when present (Storefront API)", () => {
|
|
131
|
+
expect(getEffectiveLineQuantity({ edited: true }, { currentQuantity: 0, quantity: 1, refundableQuantity: 1 })).toBe(0);
|
|
132
|
+
});
|
|
133
|
+
it("uses refundableQuantity on edited orders without currentQuantity", () => {
|
|
134
|
+
expect(getEffectiveLineQuantity({ edited: true }, { quantity: 1, refundableQuantity: 0 })).toBe(0);
|
|
135
|
+
});
|
|
136
|
+
it("uses quantity on un-edited orders even when refundableQuantity differs", () => {
|
|
137
|
+
expect(getEffectiveLineQuantity({ edited: false }, { quantity: 3, refundableQuantity: 0 })).toBe(3);
|
|
138
|
+
});
|
|
139
|
+
it("ignores refundableQuantity on an edited order with a refund (refunds array)", () => {
|
|
140
|
+
expect(getEffectiveLineQuantity({ edited: true, refunds: [{ id: "r1" }] }, { quantity: 1, refundableQuantity: 0 })).toBe(1);
|
|
141
|
+
});
|
|
142
|
+
it("ignores refundableQuantity on an edited order with a refund (financialStatus)", () => {
|
|
143
|
+
expect(getEffectiveLineQuantity({ edited: true, financialStatus: "PARTIALLY_REFUNDED" }, { quantity: 3, refundableQuantity: 1 })).toBe(3);
|
|
144
|
+
});
|
|
145
|
+
it("ignores refundableQuantity on an edited order with totalRefunded > 0", () => {
|
|
146
|
+
expect(getEffectiveLineQuantity({ edited: true, totalRefunded: { amount: "50.0" } }, { quantity: 2, refundableQuantity: 0 })).toBe(2);
|
|
147
|
+
});
|
|
148
|
+
});
|