shop-client 3.9.0 → 3.9.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/README.md +65 -0
- package/dist/ai/enrich.d.ts +93 -0
- package/dist/ai/enrich.js +25 -0
- package/dist/checkout.js +6 -0
- package/dist/chunk-2MF53V33.js +196 -0
- package/dist/chunk-CN7L3BHG.js +147 -0
- package/dist/chunk-CXUCPK6X.js +460 -0
- package/dist/chunk-DJQEZNHG.js +233 -0
- package/dist/chunk-MOBWPEY4.js +420 -0
- package/dist/chunk-QUDGES3A.js +195 -0
- package/dist/chunk-RR6YTQWP.js +90 -0
- package/dist/chunk-VPPCOJC3.js +865 -0
- package/dist/collections.d.ts +2 -1
- package/dist/collections.js +8 -0
- package/dist/index.d.ts +7 -84
- package/dist/index.js +753 -0
- package/dist/products.d.ts +2 -1
- package/dist/products.js +8 -0
- package/dist/store.d.ts +53 -1
- package/dist/store.js +9 -0
- package/dist/{store-iQARl6J3.d.ts → types-luPg5O08.d.ts} +1 -208
- package/dist/utils/detect-country.d.ts +32 -0
- package/dist/utils/detect-country.js +6 -0
- package/dist/utils/func.d.ts +61 -0
- package/dist/utils/func.js +24 -0
- package/dist/utils/rate-limit.js +10 -0
- package/package.json +16 -3
- package/dist/checkout.mjs +0 -1
- package/dist/chunk-6GPWNCDO.mjs +0 -130
- package/dist/chunk-EJO5U4BT.mjs +0 -2
- package/dist/chunk-FFKWCNLU.mjs +0 -1
- package/dist/chunk-KYLPIEU3.mjs +0 -2
- package/dist/chunk-MB2INNNP.mjs +0 -1
- package/dist/chunk-MI7754VX.mjs +0 -2
- package/dist/chunk-SZQPMLZG.mjs +0 -1
- package/dist/collections.mjs +0 -1
- package/dist/enrich-OZHBXKK6.mjs +0 -1
- package/dist/index.mjs +0 -2
- package/dist/products.mjs +0 -1
- package/dist/store.mjs +0 -1
- package/dist/utils/rate-limit.mjs +0 -1
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import {
|
|
2
|
+
rateLimitedFetch
|
|
3
|
+
} from "./chunk-2MF53V33.js";
|
|
4
|
+
import {
|
|
5
|
+
formatPrice
|
|
6
|
+
} from "./chunk-CN7L3BHG.js";
|
|
7
|
+
|
|
8
|
+
// src/collections.ts
|
|
9
|
+
import { filter, isNonNullish } from "remeda";
|
|
10
|
+
function createCollectionOperations(baseUrl, storeDomain, fetchCollections, collectionsDto, fetchPaginatedProductsFromCollection, getStoreInfo, findCollection) {
|
|
11
|
+
const cacheExpiryMs = 5 * 60 * 1e3;
|
|
12
|
+
const findCache = /* @__PURE__ */ new Map();
|
|
13
|
+
const getCached = (key) => {
|
|
14
|
+
const entry = findCache.get(key);
|
|
15
|
+
if (!entry) return void 0;
|
|
16
|
+
if (Date.now() - entry.ts < cacheExpiryMs) return entry.value;
|
|
17
|
+
findCache.delete(key);
|
|
18
|
+
return void 0;
|
|
19
|
+
};
|
|
20
|
+
const setCached = (key, value) => {
|
|
21
|
+
findCache.set(key, { ts: Date.now(), value });
|
|
22
|
+
};
|
|
23
|
+
function applyCurrencyOverride(product, currency) {
|
|
24
|
+
var _a, _b, _c, _d, _e, _f;
|
|
25
|
+
const priceMin = (_b = (_a = product.priceMin) != null ? _a : product.price) != null ? _b : 0;
|
|
26
|
+
const priceMax = (_d = (_c = product.priceMax) != null ? _c : product.price) != null ? _d : 0;
|
|
27
|
+
const compareAtMin = (_f = (_e = product.compareAtPriceMin) != null ? _e : product.compareAtPrice) != null ? _f : 0;
|
|
28
|
+
return {
|
|
29
|
+
...product,
|
|
30
|
+
currency,
|
|
31
|
+
localizedPricing: {
|
|
32
|
+
currency,
|
|
33
|
+
priceFormatted: formatPrice(priceMin, currency),
|
|
34
|
+
priceMinFormatted: formatPrice(priceMin, currency),
|
|
35
|
+
priceMaxFormatted: formatPrice(priceMax, currency),
|
|
36
|
+
compareAtPriceFormatted: formatPrice(compareAtMin, currency)
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function maybeOverrideProductsCurrency(products, currency) {
|
|
41
|
+
if (!products || !currency) return products;
|
|
42
|
+
return products.map((p) => applyCurrencyOverride(p, currency));
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
/**
|
|
46
|
+
* Fetches collections with pagination support.
|
|
47
|
+
*
|
|
48
|
+
* @param options - Pagination options
|
|
49
|
+
* @param options.page - Page number (default: 1)
|
|
50
|
+
* @param options.limit - Number of collections per page (default: 10, max: 250)
|
|
51
|
+
*
|
|
52
|
+
* @returns {Promise<Collection[] | null>} Collections for the requested page, or null on error
|
|
53
|
+
*/
|
|
54
|
+
paginated: async (options) => {
|
|
55
|
+
var _a, _b;
|
|
56
|
+
const page = (_a = options == null ? void 0 : options.page) != null ? _a : 1;
|
|
57
|
+
const limit = (_b = options == null ? void 0 : options.limit) != null ? _b : 10;
|
|
58
|
+
if (page < 1 || limit < 1 || limit > 250) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"Invalid pagination parameters: page must be >= 1, limit must be between 1 and 250"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const collections = await fetchCollections(page, limit);
|
|
65
|
+
return collections != null ? collections : null;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(
|
|
68
|
+
"Failed to fetch paginated collections:",
|
|
69
|
+
storeDomain,
|
|
70
|
+
error
|
|
71
|
+
);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
/**
|
|
76
|
+
* Fetches all collections from the store across all pages.
|
|
77
|
+
*
|
|
78
|
+
* @returns {Promise<Collection[]>} Array of all collections
|
|
79
|
+
*
|
|
80
|
+
* @throws {Error} When there's a network error or API failure
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const shop = new ShopClient('https://exampleshop.com');
|
|
85
|
+
* const allCollections = await shop.collections.all();
|
|
86
|
+
*
|
|
87
|
+
* console.log(`Found ${allCollections.length} collections`);
|
|
88
|
+
* allCollections.forEach(collection => {
|
|
89
|
+
* console.log(collection.title, collection.handle);
|
|
90
|
+
* });
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
all: async () => {
|
|
94
|
+
const limit = 250;
|
|
95
|
+
const allCollections = [];
|
|
96
|
+
async function fetchAll() {
|
|
97
|
+
let currentPage = 1;
|
|
98
|
+
while (true) {
|
|
99
|
+
const collections = await fetchCollections(currentPage, limit);
|
|
100
|
+
if (!collections || collections.length === 0 || collections.length < limit) {
|
|
101
|
+
if (!collections) {
|
|
102
|
+
console.warn(
|
|
103
|
+
"fetchCollections returned null, treating as empty array."
|
|
104
|
+
);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
if (collections && collections.length > 0) {
|
|
108
|
+
allCollections.push(...collections);
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
allCollections.push(...collections);
|
|
113
|
+
currentPage++;
|
|
114
|
+
}
|
|
115
|
+
return allCollections;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const collections = await fetchAll();
|
|
119
|
+
return collections || [];
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error("Failed to fetch all collections:", storeDomain, error);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
/**
|
|
126
|
+
* Finds a specific collection by its handle.
|
|
127
|
+
*
|
|
128
|
+
* @param collectionHandle - The collection handle (URL slug) to search for
|
|
129
|
+
*
|
|
130
|
+
* @returns {Promise<Collection | null>} The collection if found, null if not found
|
|
131
|
+
*
|
|
132
|
+
* @throws {Error} When the handle is invalid or there's a network error
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```typescript
|
|
136
|
+
* const shop = new ShopClient('https://example.myshopify.com');
|
|
137
|
+
* const collection = await shop.collections.find('summer-collection');
|
|
138
|
+
* if (collection) {
|
|
139
|
+
* console.log(collection.title); // "Summer Collection"
|
|
140
|
+
* }
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
find: async (collectionHandle) => {
|
|
144
|
+
var _a, _b;
|
|
145
|
+
if (!collectionHandle || typeof collectionHandle !== "string") {
|
|
146
|
+
throw new Error("Collection handle is required and must be a string");
|
|
147
|
+
}
|
|
148
|
+
const sanitizedHandle = collectionHandle.trim().replace(/[^a-zA-Z0-9\-_]/g, "");
|
|
149
|
+
if (!sanitizedHandle) {
|
|
150
|
+
throw new Error("Invalid collection handle format");
|
|
151
|
+
}
|
|
152
|
+
if (sanitizedHandle.length > 255) {
|
|
153
|
+
throw new Error("Collection handle is too long");
|
|
154
|
+
}
|
|
155
|
+
const cached = getCached(sanitizedHandle);
|
|
156
|
+
if (typeof cached !== "undefined") {
|
|
157
|
+
return cached;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const url = `${baseUrl}collections/${encodeURIComponent(sanitizedHandle)}.json`;
|
|
161
|
+
const response = await rateLimitedFetch(url, {
|
|
162
|
+
rateLimitClass: "collections:single"
|
|
163
|
+
});
|
|
164
|
+
if (!response.ok) {
|
|
165
|
+
if (response.status === 404) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
169
|
+
}
|
|
170
|
+
const result = await response.json();
|
|
171
|
+
let collectionImage = result.collection.image;
|
|
172
|
+
if (!collectionImage) {
|
|
173
|
+
const collectionProduct = (_a = await fetchPaginatedProductsFromCollection(
|
|
174
|
+
result.collection.handle,
|
|
175
|
+
{
|
|
176
|
+
limit: 1,
|
|
177
|
+
page: 1
|
|
178
|
+
}
|
|
179
|
+
)) == null ? void 0 : _a.at(0);
|
|
180
|
+
const collectionProductImage = (_b = collectionProduct == null ? void 0 : collectionProduct.images) == null ? void 0 : _b[0];
|
|
181
|
+
if (collectionProduct && collectionProductImage) {
|
|
182
|
+
collectionImage = {
|
|
183
|
+
id: collectionProductImage.id,
|
|
184
|
+
src: collectionProductImage.src,
|
|
185
|
+
alt: collectionProductImage.alt || collectionProduct.title,
|
|
186
|
+
created_at: collectionProductImage.createdAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const collectionData = collectionsDto([
|
|
191
|
+
{
|
|
192
|
+
...result.collection,
|
|
193
|
+
image: collectionImage
|
|
194
|
+
}
|
|
195
|
+
]);
|
|
196
|
+
const coll = collectionData[0] || null;
|
|
197
|
+
setCached(sanitizedHandle, coll);
|
|
198
|
+
if ((coll == null ? void 0 : coll.handle) && coll.handle !== sanitizedHandle) {
|
|
199
|
+
setCached(coll.handle, coll);
|
|
200
|
+
}
|
|
201
|
+
return coll;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (error instanceof Error) {
|
|
204
|
+
console.error(
|
|
205
|
+
`Error fetching collection ${sanitizedHandle}:`,
|
|
206
|
+
baseUrl,
|
|
207
|
+
error.message
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
/**
|
|
214
|
+
* Fetches collections that are showcased/featured on the store's homepage.
|
|
215
|
+
*
|
|
216
|
+
* @returns {Promise<Collection[]>} Array of showcased collections found on the homepage
|
|
217
|
+
*
|
|
218
|
+
* @throws {Error} When there's a network error or API failure
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* ```typescript
|
|
222
|
+
* const shop = new ShopClient('https://exampleshop.com');
|
|
223
|
+
* const showcasedCollections = await shop.collections.showcased();
|
|
224
|
+
*
|
|
225
|
+
* console.log(`Found ${showcasedCollections.length} showcased collections`);
|
|
226
|
+
* showcasedCollections.forEach(collection => {
|
|
227
|
+
* console.log(`Featured: ${collection.title} - ${collection.productsCount} products`);
|
|
228
|
+
* });
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
showcased: async () => {
|
|
232
|
+
const storeInfo = await getStoreInfo();
|
|
233
|
+
const collections = await Promise.all(
|
|
234
|
+
storeInfo.showcase.collections.map(
|
|
235
|
+
(collectionHandle) => findCollection(collectionHandle)
|
|
236
|
+
)
|
|
237
|
+
);
|
|
238
|
+
return filter(collections, isNonNullish);
|
|
239
|
+
},
|
|
240
|
+
products: {
|
|
241
|
+
/**
|
|
242
|
+
* Fetches products from a specific collection with pagination support.
|
|
243
|
+
*
|
|
244
|
+
* @param collectionHandle - The collection handle to fetch products from
|
|
245
|
+
* @param options - Pagination options
|
|
246
|
+
* @param options.page - Page number (default: 1)
|
|
247
|
+
* @param options.limit - Number of products per page (default: 250, max: 250)
|
|
248
|
+
*
|
|
249
|
+
* @returns {Promise<Product[] | null>} Array of products from the collection or null if error occurs
|
|
250
|
+
*
|
|
251
|
+
* @throws {Error} When the collection handle is invalid or there's a network error
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```typescript
|
|
255
|
+
* const shop = new ShopClient('https://example.myshopify.com');
|
|
256
|
+
*
|
|
257
|
+
* // Get first page of products from a collection
|
|
258
|
+
* const products = await shop.collections.products.paginated('summer-collection');
|
|
259
|
+
*
|
|
260
|
+
* // Get second page with custom limit
|
|
261
|
+
* const moreProducts = await shop.collections.products.paginated(
|
|
262
|
+
* 'summer-collection',
|
|
263
|
+
* { page: 2, limit: 50 }
|
|
264
|
+
* );
|
|
265
|
+
* ```
|
|
266
|
+
*/
|
|
267
|
+
paginated: async (collectionHandle, options) => {
|
|
268
|
+
var _a, _b;
|
|
269
|
+
if (!collectionHandle || typeof collectionHandle !== "string") {
|
|
270
|
+
throw new Error("Collection handle is required and must be a string");
|
|
271
|
+
}
|
|
272
|
+
const sanitizedHandle = collectionHandle.trim().replace(/[^a-zA-Z0-9\-_]/g, "");
|
|
273
|
+
if (!sanitizedHandle) {
|
|
274
|
+
throw new Error("Invalid collection handle format");
|
|
275
|
+
}
|
|
276
|
+
if (sanitizedHandle.length > 255) {
|
|
277
|
+
throw new Error("Collection handle is too long");
|
|
278
|
+
}
|
|
279
|
+
const page = (_a = options == null ? void 0 : options.page) != null ? _a : 1;
|
|
280
|
+
const limit = (_b = options == null ? void 0 : options.limit) != null ? _b : 250;
|
|
281
|
+
if (page < 1 || limit < 1 || limit > 250) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
"Invalid pagination parameters: page must be >= 1, limit must be between 1 and 250"
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
const products = await fetchPaginatedProductsFromCollection(
|
|
287
|
+
sanitizedHandle,
|
|
288
|
+
{ page, limit }
|
|
289
|
+
);
|
|
290
|
+
return maybeOverrideProductsCurrency(products, options == null ? void 0 : options.currency);
|
|
291
|
+
},
|
|
292
|
+
/**
|
|
293
|
+
* Fetches all products from a specific collection.
|
|
294
|
+
*
|
|
295
|
+
* @param collectionHandle - The collection handle to fetch products from
|
|
296
|
+
*
|
|
297
|
+
* @returns {Promise<Product[] | null>} Array of all products from the collection or null if error occurs
|
|
298
|
+
*
|
|
299
|
+
* @throws {Error} When the collection handle is invalid or there's a network error
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* ```typescript
|
|
303
|
+
* const shop = new ShopClient('https://exampleshop.com');
|
|
304
|
+
* const allProducts = await shop.collections.products.all('summer-collection');
|
|
305
|
+
*
|
|
306
|
+
* if (allProducts) {
|
|
307
|
+
* console.log(`Found ${allProducts.length} products in the collection`);
|
|
308
|
+
* allProducts.forEach(product => {
|
|
309
|
+
* console.log(`${product.title} - $${product.price}`);
|
|
310
|
+
* });
|
|
311
|
+
* }
|
|
312
|
+
* ```
|
|
313
|
+
*/
|
|
314
|
+
all: async (collectionHandle, options) => {
|
|
315
|
+
if (!collectionHandle || typeof collectionHandle !== "string") {
|
|
316
|
+
throw new Error("Collection handle is required and must be a string");
|
|
317
|
+
}
|
|
318
|
+
const sanitizedHandle = collectionHandle.trim().replace(/[^a-zA-Z0-9\-_]/g, "");
|
|
319
|
+
if (!sanitizedHandle) {
|
|
320
|
+
throw new Error("Invalid collection handle format");
|
|
321
|
+
}
|
|
322
|
+
if (sanitizedHandle.length > 255) {
|
|
323
|
+
throw new Error("Collection handle is too long");
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const limit = 250;
|
|
327
|
+
const allProducts = [];
|
|
328
|
+
let currentPage = 1;
|
|
329
|
+
while (true) {
|
|
330
|
+
const products = await fetchPaginatedProductsFromCollection(
|
|
331
|
+
sanitizedHandle,
|
|
332
|
+
{
|
|
333
|
+
page: currentPage,
|
|
334
|
+
limit
|
|
335
|
+
}
|
|
336
|
+
);
|
|
337
|
+
if (!products || products.length === 0 || products.length < limit) {
|
|
338
|
+
if (products && products.length > 0) {
|
|
339
|
+
allProducts.push(...products);
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
allProducts.push(...products);
|
|
344
|
+
currentPage++;
|
|
345
|
+
}
|
|
346
|
+
return maybeOverrideProductsCurrency(allProducts, options == null ? void 0 : options.currency);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.error(
|
|
349
|
+
`Error fetching all products for collection ${sanitizedHandle}:`,
|
|
350
|
+
baseUrl,
|
|
351
|
+
error
|
|
352
|
+
);
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
/**
|
|
357
|
+
* Fetches all product slugs from a specific collection.
|
|
358
|
+
*
|
|
359
|
+
* @param collectionHandle - The collection handle to fetch product slugs from
|
|
360
|
+
*
|
|
361
|
+
* @returns {Promise<string[] | null>} Array of product slugs from the collection or null if error occurs
|
|
362
|
+
*
|
|
363
|
+
* @throws {Error} When the collection handle is invalid or there's a network error
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```typescript
|
|
367
|
+
* const shop = new ShopClient('https://exampleshop.com');
|
|
368
|
+
* const productSlugs = await shop.collections.products.slugs('summer-collection');
|
|
369
|
+
* console.log(productSlugs);
|
|
370
|
+
* ```
|
|
371
|
+
*/
|
|
372
|
+
slugs: async (collectionHandle) => {
|
|
373
|
+
if (!collectionHandle || typeof collectionHandle !== "string") {
|
|
374
|
+
throw new Error("Collection handle is required and must be a string");
|
|
375
|
+
}
|
|
376
|
+
const sanitizedHandle = collectionHandle.trim().replace(/[^a-zA-Z0-9\-_]/g, "");
|
|
377
|
+
if (!sanitizedHandle) {
|
|
378
|
+
throw new Error("Invalid collection handle format");
|
|
379
|
+
}
|
|
380
|
+
if (sanitizedHandle.length > 255) {
|
|
381
|
+
throw new Error("Collection handle is too long");
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
const limit = 250;
|
|
385
|
+
const slugs = [];
|
|
386
|
+
let currentPage = 1;
|
|
387
|
+
while (true) {
|
|
388
|
+
const products = await fetchPaginatedProductsFromCollection(
|
|
389
|
+
sanitizedHandle,
|
|
390
|
+
{
|
|
391
|
+
page: currentPage,
|
|
392
|
+
limit
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
if (!products || products.length === 0 || products.length < limit) {
|
|
396
|
+
if (products && products.length > 0) {
|
|
397
|
+
slugs.push(...products.map((p) => p.slug));
|
|
398
|
+
}
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
slugs.push(...products.map((p) => p.slug));
|
|
402
|
+
currentPage++;
|
|
403
|
+
}
|
|
404
|
+
return slugs;
|
|
405
|
+
} catch (error) {
|
|
406
|
+
console.error(
|
|
407
|
+
`Error fetching product slugs for collection ${sanitizedHandle}:`,
|
|
408
|
+
baseUrl,
|
|
409
|
+
error
|
|
410
|
+
);
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export {
|
|
419
|
+
createCollectionOperations
|
|
420
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// src/utils/detect-country.ts
|
|
2
|
+
var COUNTRY_CODES = {
|
|
3
|
+
"+1": "US",
|
|
4
|
+
// United States (primary) / Canada also uses +1
|
|
5
|
+
"+44": "GB",
|
|
6
|
+
// United Kingdom
|
|
7
|
+
"+61": "AU",
|
|
8
|
+
// Australia
|
|
9
|
+
"+65": "SG",
|
|
10
|
+
// Singapore
|
|
11
|
+
"+91": "IN",
|
|
12
|
+
// India
|
|
13
|
+
"+81": "JP",
|
|
14
|
+
// Japan
|
|
15
|
+
"+49": "DE",
|
|
16
|
+
// Germany
|
|
17
|
+
"+33": "FR",
|
|
18
|
+
// France
|
|
19
|
+
"+971": "AE",
|
|
20
|
+
// United Arab Emirates
|
|
21
|
+
"+39": "IT",
|
|
22
|
+
// Italy
|
|
23
|
+
"+34": "ES",
|
|
24
|
+
// Spain
|
|
25
|
+
"+82": "KR",
|
|
26
|
+
// South Korea
|
|
27
|
+
"+55": "BR",
|
|
28
|
+
// Brazil
|
|
29
|
+
"+62": "ID",
|
|
30
|
+
// Indonesia
|
|
31
|
+
"+92": "PK",
|
|
32
|
+
// Pakistan
|
|
33
|
+
"+7": "RU"
|
|
34
|
+
// Russia
|
|
35
|
+
};
|
|
36
|
+
var CURRENCY_CODE_TO_COUNTRY = {
|
|
37
|
+
INR: "IN",
|
|
38
|
+
USD: "US",
|
|
39
|
+
CAD: "CA",
|
|
40
|
+
AUD: "AU",
|
|
41
|
+
GBP: "GB",
|
|
42
|
+
EUR: "EU",
|
|
43
|
+
AED: "AE",
|
|
44
|
+
KRW: "KR",
|
|
45
|
+
JPY: "JP"
|
|
46
|
+
};
|
|
47
|
+
function scoreCountry(countryScores, country, weight, reason) {
|
|
48
|
+
if (!country) return;
|
|
49
|
+
if (!countryScores[country])
|
|
50
|
+
countryScores[country] = { score: 0, reasons: [] };
|
|
51
|
+
countryScores[country].score += weight;
|
|
52
|
+
countryScores[country].reasons.push(reason);
|
|
53
|
+
}
|
|
54
|
+
async function detectShopCountry(html) {
|
|
55
|
+
var _a;
|
|
56
|
+
const countryScores = {};
|
|
57
|
+
let detectedCurrencyCode;
|
|
58
|
+
const currencyJsonMatch = html.match(/Shopify\.currency\s*=\s*(\{[^}]*\})/);
|
|
59
|
+
if (currencyJsonMatch) {
|
|
60
|
+
try {
|
|
61
|
+
const raw = currencyJsonMatch[1];
|
|
62
|
+
const obj = JSON.parse(raw || "{}");
|
|
63
|
+
const activeCode = typeof (obj == null ? void 0 : obj.active) === "string" ? obj.active.toUpperCase() : void 0;
|
|
64
|
+
const iso = activeCode ? CURRENCY_CODE_TO_COUNTRY[activeCode] : void 0;
|
|
65
|
+
if (activeCode) {
|
|
66
|
+
detectedCurrencyCode = activeCode;
|
|
67
|
+
}
|
|
68
|
+
if (typeof iso === "string") {
|
|
69
|
+
scoreCountry(countryScores, iso, 0.8, "Shopify.currency.active");
|
|
70
|
+
}
|
|
71
|
+
} catch (_error) {
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
const currencyActiveAssignMatch = html.match(
|
|
75
|
+
/Shopify\.currency\.active\s*=\s*['"]([A-Za-z]{3})['"]/i
|
|
76
|
+
);
|
|
77
|
+
if (currencyActiveAssignMatch) {
|
|
78
|
+
const captured = currencyActiveAssignMatch[1];
|
|
79
|
+
const code = typeof captured === "string" ? captured.toUpperCase() : void 0;
|
|
80
|
+
const iso = code ? CURRENCY_CODE_TO_COUNTRY[code] : void 0;
|
|
81
|
+
if (code) {
|
|
82
|
+
detectedCurrencyCode = code;
|
|
83
|
+
}
|
|
84
|
+
if (typeof iso === "string") {
|
|
85
|
+
scoreCountry(countryScores, iso, 0.8, "Shopify.currency.active");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const shopifyCountryMatch = html.match(
|
|
90
|
+
/Shopify\.country\s*=\s*['"]([A-Za-z]{2})['"]/i
|
|
91
|
+
);
|
|
92
|
+
if (shopifyCountryMatch) {
|
|
93
|
+
const captured = shopifyCountryMatch[1];
|
|
94
|
+
const iso = typeof captured === "string" ? captured.toUpperCase() : void 0;
|
|
95
|
+
if (typeof iso === "string") {
|
|
96
|
+
scoreCountry(countryScores, iso, 1, "Shopify.country");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const phones = html.match(/\+\d{1,3}[\s\-()0-9]{5,}/g);
|
|
100
|
+
if (phones) {
|
|
101
|
+
for (const phone of phones) {
|
|
102
|
+
const prefix = (_a = phone.match(/^\+\d{1,3}/)) == null ? void 0 : _a[0];
|
|
103
|
+
if (prefix && COUNTRY_CODES[prefix])
|
|
104
|
+
scoreCountry(
|
|
105
|
+
countryScores,
|
|
106
|
+
COUNTRY_CODES[prefix],
|
|
107
|
+
0.8,
|
|
108
|
+
`phone prefix ${prefix}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const jsonLdRegex = /<script[^>]+application\/ld\+json[^>]*>(.*?)<\/script>/g;
|
|
113
|
+
let jsonLdMatch = jsonLdRegex.exec(html);
|
|
114
|
+
while (jsonLdMatch !== null) {
|
|
115
|
+
try {
|
|
116
|
+
const json = jsonLdMatch[1];
|
|
117
|
+
if (!json) {
|
|
118
|
+
} else {
|
|
119
|
+
const raw = JSON.parse(json);
|
|
120
|
+
const collectAddressCountries = (node, results = []) => {
|
|
121
|
+
if (Array.isArray(node)) {
|
|
122
|
+
for (const item of node) collectAddressCountries(item, results);
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
125
|
+
if (node && typeof node === "object") {
|
|
126
|
+
const obj = node;
|
|
127
|
+
const address = obj.address;
|
|
128
|
+
if (address && typeof address === "object") {
|
|
129
|
+
const country = address.addressCountry;
|
|
130
|
+
if (typeof country === "string") results.push(country);
|
|
131
|
+
}
|
|
132
|
+
const graph = obj["@graph"];
|
|
133
|
+
if (graph) collectAddressCountries(graph, results);
|
|
134
|
+
}
|
|
135
|
+
return results;
|
|
136
|
+
};
|
|
137
|
+
const countries = collectAddressCountries(raw);
|
|
138
|
+
for (const country of countries) {
|
|
139
|
+
scoreCountry(countryScores, country, 1, "JSON-LD addressCountry");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch (_error) {
|
|
143
|
+
}
|
|
144
|
+
jsonLdMatch = jsonLdRegex.exec(html);
|
|
145
|
+
}
|
|
146
|
+
const footerMatch = html.match(/<footer[^>]*>(.*?)<\/footer>/i);
|
|
147
|
+
if (footerMatch) {
|
|
148
|
+
const footerTextGroup = footerMatch[1];
|
|
149
|
+
const footerText = footerTextGroup ? footerTextGroup.toLowerCase() : "";
|
|
150
|
+
const countryNameToISO = {
|
|
151
|
+
india: "IN",
|
|
152
|
+
"united states": "US",
|
|
153
|
+
canada: "CA",
|
|
154
|
+
australia: "AU",
|
|
155
|
+
"united kingdom": "GB",
|
|
156
|
+
britain: "GB",
|
|
157
|
+
uk: "GB",
|
|
158
|
+
japan: "JP",
|
|
159
|
+
"south korea": "KR",
|
|
160
|
+
korea: "KR",
|
|
161
|
+
germany: "DE",
|
|
162
|
+
france: "FR",
|
|
163
|
+
italy: "IT",
|
|
164
|
+
spain: "ES",
|
|
165
|
+
brazil: "BR",
|
|
166
|
+
russia: "RU",
|
|
167
|
+
singapore: "SG",
|
|
168
|
+
indonesia: "ID",
|
|
169
|
+
pakistan: "PK"
|
|
170
|
+
};
|
|
171
|
+
for (const [countryName, isoCode] of Object.entries(countryNameToISO)) {
|
|
172
|
+
if (footerText.includes(countryName))
|
|
173
|
+
scoreCountry(countryScores, isoCode, 0.4, "footer mention");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const sorted = Object.entries(countryScores).sort(
|
|
177
|
+
(a, b) => b[1].score - a[1].score
|
|
178
|
+
);
|
|
179
|
+
const best = sorted[0];
|
|
180
|
+
return best ? {
|
|
181
|
+
country: best[0],
|
|
182
|
+
confidence: Math.min(1, best[1].score / 2),
|
|
183
|
+
signals: best[1].reasons,
|
|
184
|
+
currencyCode: detectedCurrencyCode
|
|
185
|
+
} : {
|
|
186
|
+
country: "Unknown",
|
|
187
|
+
confidence: 0,
|
|
188
|
+
signals: [],
|
|
189
|
+
currencyCode: detectedCurrencyCode
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export {
|
|
194
|
+
detectShopCountry
|
|
195
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// src/checkout.ts
|
|
2
|
+
function createCheckoutOperations(baseUrl) {
|
|
3
|
+
return {
|
|
4
|
+
/**
|
|
5
|
+
* Creates a Shopify checkout URL with pre-filled customer information and cart items.
|
|
6
|
+
*
|
|
7
|
+
* @param params - Checkout parameters
|
|
8
|
+
* @param params.email - Customer's email address (must be valid email format)
|
|
9
|
+
* @param params.items - Array of products to add to cart
|
|
10
|
+
* @param params.items[].productVariantId - Shopify product variant ID
|
|
11
|
+
* @param params.items[].quantity - Quantity as string (must be positive number)
|
|
12
|
+
* @param params.address - Customer's shipping address
|
|
13
|
+
* @param params.address.firstName - Customer's first name
|
|
14
|
+
* @param params.address.lastName - Customer's last name
|
|
15
|
+
* @param params.address.address1 - Street address
|
|
16
|
+
* @param params.address.city - City name
|
|
17
|
+
* @param params.address.zip - Postal/ZIP code
|
|
18
|
+
* @param params.address.country - Country name
|
|
19
|
+
* @param params.address.province - State/Province name
|
|
20
|
+
* @param params.address.phone - Phone number
|
|
21
|
+
*
|
|
22
|
+
* @returns {string} Complete Shopify checkout URL with pre-filled information
|
|
23
|
+
*
|
|
24
|
+
* @throws {Error} When email is invalid, items array is empty, or required address fields are missing
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const shop = new ShopClient('https://exampleshop.com');
|
|
29
|
+
* const checkoutUrl = await shop.checkout.create([
|
|
30
|
+
* { variantId: '123', quantity: 2 },
|
|
31
|
+
* { variantId: '456', quantity: 1 }
|
|
32
|
+
* ]);
|
|
33
|
+
* console.log(checkoutUrl);
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
createUrl: ({
|
|
37
|
+
email,
|
|
38
|
+
items,
|
|
39
|
+
address
|
|
40
|
+
}) => {
|
|
41
|
+
if (!email || !email.includes("@")) {
|
|
42
|
+
throw new Error("Invalid email address");
|
|
43
|
+
}
|
|
44
|
+
if (!items || items.length === 0) {
|
|
45
|
+
throw new Error("Items array cannot be empty");
|
|
46
|
+
}
|
|
47
|
+
for (const item of items) {
|
|
48
|
+
if (!item.productVariantId || !item.quantity) {
|
|
49
|
+
throw new Error("Each item must have productVariantId and quantity");
|
|
50
|
+
}
|
|
51
|
+
const qty = Number.parseInt(item.quantity, 10);
|
|
52
|
+
if (Number.isNaN(qty) || qty <= 0) {
|
|
53
|
+
throw new Error("Quantity must be a positive number");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const requiredFields = [
|
|
57
|
+
"firstName",
|
|
58
|
+
"lastName",
|
|
59
|
+
"address1",
|
|
60
|
+
"city",
|
|
61
|
+
"zip",
|
|
62
|
+
"country"
|
|
63
|
+
];
|
|
64
|
+
for (const field of requiredFields) {
|
|
65
|
+
if (!address[field]) {
|
|
66
|
+
throw new Error(`Address field '${field}' is required`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const cartPath = items.map(
|
|
70
|
+
(item) => `${encodeURIComponent(item.productVariantId)}:${encodeURIComponent(item.quantity)}`
|
|
71
|
+
).join(",");
|
|
72
|
+
const params = new URLSearchParams({
|
|
73
|
+
"checkout[email]": email,
|
|
74
|
+
"checkout[shipping_address][first_name]": address.firstName,
|
|
75
|
+
"checkout[shipping_address][last_name]": address.lastName,
|
|
76
|
+
"checkout[shipping_address][address1]": address.address1,
|
|
77
|
+
"checkout[shipping_address][city]": address.city,
|
|
78
|
+
"checkout[shipping_address][zip]": address.zip,
|
|
79
|
+
"checkout[shipping_address][country]": address.country,
|
|
80
|
+
"checkout[shipping_address][province]": address.province,
|
|
81
|
+
"checkout[shipping_address][phone]": address.phone
|
|
82
|
+
});
|
|
83
|
+
return `${baseUrl}cart/${cartPath}?${params.toString()}`;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
createCheckoutOperations
|
|
90
|
+
};
|