brainerce 1.20.2 → 1.23.11
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 +4662 -4506
- package/dist/index.d.mts +1134 -57
- package/dist/index.d.ts +1134 -57
- package/dist/index.js +626 -32
- package/dist/index.mjs +624 -32
- package/package.json +76 -76
package/dist/index.js
CHANGED
|
@@ -51,10 +51,12 @@ __export(index_exports, {
|
|
|
51
51
|
getStockStatus: () => getStockStatus,
|
|
52
52
|
getVariantOptions: () => getVariantOptions,
|
|
53
53
|
getVariantPrice: () => getVariantPrice,
|
|
54
|
+
isAllowedPaymentUrl: () => isAllowedPaymentUrl,
|
|
54
55
|
isCouponApplicableToProduct: () => isCouponApplicableToProduct,
|
|
55
56
|
isHtmlDescription: () => isHtmlDescription,
|
|
56
57
|
isWebhookEventType: () => isWebhookEventType,
|
|
57
58
|
parseWebhookEvent: () => parseWebhookEvent,
|
|
59
|
+
safePaymentRedirect: () => safePaymentRedirect,
|
|
58
60
|
verifyWebhook: () => verifyWebhook
|
|
59
61
|
});
|
|
60
62
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -176,7 +178,7 @@ function isDevGuardsEnabled() {
|
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
// src/version.ts
|
|
179
|
-
var SDK_VERSION = "1.
|
|
181
|
+
var SDK_VERSION = "1.21.0";
|
|
180
182
|
|
|
181
183
|
// src/client.ts
|
|
182
184
|
var DEFAULT_BASE_URL = "https://api.brainerce.com";
|
|
@@ -202,27 +204,87 @@ var BrainerceClient = class {
|
|
|
202
204
|
* This is needed because Stripe redirects lose in-memory state.
|
|
203
205
|
*/
|
|
204
206
|
this.ACTIVE_CHECKOUT_KEY = "brainerce_active_checkout";
|
|
207
|
+
// -------------------- Contact Forms (schema) --------------------
|
|
208
|
+
/**
|
|
209
|
+
* List active contact forms configured for the store.
|
|
210
|
+
*
|
|
211
|
+
* Storefront (public) mode only. Useful when your site has multiple
|
|
212
|
+
* forms (e.g. "main", "newsletter", "whatsapp_prechat") and you need
|
|
213
|
+
* to pick the right one at render time.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```typescript
|
|
217
|
+
* const forms = await brainerce.contactForms.list();
|
|
218
|
+
* // → [{ key: 'main', name: 'Main Contact', isDefault: true }, ...]
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
this.contactForms = {
|
|
222
|
+
list: async () => {
|
|
223
|
+
if (this.isVibeCodedMode()) {
|
|
224
|
+
return this.vibeCodedRequest("GET", "/contact-forms");
|
|
225
|
+
}
|
|
226
|
+
return this.storefrontRequest("GET", "/contact-forms");
|
|
227
|
+
},
|
|
228
|
+
/**
|
|
229
|
+
* Fetch the full schema for a single contact form, including all
|
|
230
|
+
* visible fields + their localized labels/placeholders/help text.
|
|
231
|
+
*
|
|
232
|
+
* `locale` is the storefront locale at render time (e.g. `"he"`).
|
|
233
|
+
* Falls back to the store's default language when omitted.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```typescript
|
|
237
|
+
* const form = await brainerce.contactForms.get('main', 'he');
|
|
238
|
+
* // Render form.fields; submit via brainerce.createInquiry({ formKey: 'main', fields })
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
get: async (formKey = "main", locale) => {
|
|
242
|
+
const query = locale ? { locale } : void 0;
|
|
243
|
+
if (this.isVibeCodedMode()) {
|
|
244
|
+
return this.vibeCodedRequest(
|
|
245
|
+
"GET",
|
|
246
|
+
`/contact-forms/${encodeURIComponent(formKey)}`,
|
|
247
|
+
void 0,
|
|
248
|
+
query
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return this.storefrontRequest(
|
|
252
|
+
"GET",
|
|
253
|
+
`/contact-forms/${encodeURIComponent(formKey)}`,
|
|
254
|
+
void 0,
|
|
255
|
+
query
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
205
259
|
// -------------------- Local Cart (Client-Side for Guests) --------------------
|
|
206
260
|
// These methods store cart data in localStorage - NO API calls!
|
|
207
261
|
// Use for guest users in vibe-coded sites
|
|
208
262
|
this.LOCAL_CART_KEY = "brainerce_cart";
|
|
209
|
-
|
|
210
|
-
|
|
263
|
+
const resolvedSalesChannelId = options.salesChannelId ?? options.connectionId;
|
|
264
|
+
if (!options.apiKey && !options.storeId && !resolvedSalesChannelId) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
"BrainerceClient: either salesChannelId, apiKey, or storeId is required"
|
|
267
|
+
);
|
|
211
268
|
}
|
|
212
269
|
if (options.apiKey && !options.apiKey.startsWith("brainerce_")) {
|
|
213
270
|
console.warn('BrainerceClient: apiKey should start with "brainerce_"');
|
|
214
271
|
}
|
|
215
|
-
if (
|
|
216
|
-
console.warn('BrainerceClient:
|
|
272
|
+
if (resolvedSalesChannelId && !resolvedSalesChannelId.startsWith("vc_")) {
|
|
273
|
+
console.warn('BrainerceClient: salesChannelId should start with "vc_"');
|
|
274
|
+
}
|
|
275
|
+
if (!options.salesChannelId && options.connectionId) {
|
|
276
|
+
console.warn(
|
|
277
|
+
"BrainerceClient: `connectionId` is deprecated \u2014 use `salesChannelId` instead. `connectionId` will be removed in SDK 2.0."
|
|
278
|
+
);
|
|
217
279
|
}
|
|
218
280
|
if (options.apiKey && typeof window !== "undefined") {
|
|
219
281
|
console.warn(
|
|
220
|
-
"BrainerceClient: WARNING - API key detected in browser environment. This is a security risk! Use
|
|
282
|
+
"BrainerceClient: WARNING - API key detected in browser environment. This is a security risk! Use salesChannelId or storeId for frontend applications."
|
|
221
283
|
);
|
|
222
284
|
}
|
|
223
285
|
this.apiKey = options.apiKey;
|
|
224
286
|
this.storeId = options.storeId;
|
|
225
|
-
this.connectionId =
|
|
287
|
+
this.connectionId = resolvedSalesChannelId;
|
|
226
288
|
let resolvedBase = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
227
289
|
if (resolvedBase.startsWith("/") && typeof window !== "undefined" && window.location?.origin) {
|
|
228
290
|
resolvedBase = window.location.origin + resolvedBase;
|
|
@@ -309,11 +371,17 @@ var BrainerceClient = class {
|
|
|
309
371
|
}
|
|
310
372
|
// -------------------- Mode Detection --------------------
|
|
311
373
|
/**
|
|
312
|
-
* Check if client is in
|
|
374
|
+
* Check if client is in sales-channel mode (using salesChannelId / legacy connectionId).
|
|
313
375
|
*/
|
|
314
|
-
|
|
376
|
+
isSalesChannelMode() {
|
|
315
377
|
return !!this.connectionId && !this.apiKey;
|
|
316
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* @deprecated Use `isSalesChannelMode()` instead. Kept as a backwards-compatible alias.
|
|
381
|
+
*/
|
|
382
|
+
isVibeCodedMode() {
|
|
383
|
+
return this.isSalesChannelMode();
|
|
384
|
+
}
|
|
317
385
|
/**
|
|
318
386
|
* Check if client is in storefront mode (using storeId)
|
|
319
387
|
*/
|
|
@@ -625,6 +693,18 @@ var BrainerceClient = class {
|
|
|
625
693
|
const categories = Array.isArray(params?.categories) ? params.categories.join(",") : params?.categories;
|
|
626
694
|
const brands = Array.isArray(params?.brands) ? params.brands.join(",") : params?.brands;
|
|
627
695
|
const tags = Array.isArray(params?.tags) ? params.tags.join(",") : params?.tags;
|
|
696
|
+
let metafields;
|
|
697
|
+
if (params?.metafields && Object.keys(params.metafields).length > 0) {
|
|
698
|
+
const normalized = {};
|
|
699
|
+
for (const [key, raw] of Object.entries(params.metafields)) {
|
|
700
|
+
const arr = Array.isArray(raw) ? raw : [raw];
|
|
701
|
+
const strings = arr.map((v) => String(v)).filter(Boolean);
|
|
702
|
+
if (strings.length > 0) normalized[key] = strings;
|
|
703
|
+
}
|
|
704
|
+
if (Object.keys(normalized).length > 0) {
|
|
705
|
+
metafields = JSON.stringify(normalized);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
628
708
|
const queryParams = {
|
|
629
709
|
page: params?.page,
|
|
630
710
|
limit: params?.limit,
|
|
@@ -635,6 +715,7 @@ var BrainerceClient = class {
|
|
|
635
715
|
tags,
|
|
636
716
|
minPrice: params?.minPrice,
|
|
637
717
|
maxPrice: params?.maxPrice,
|
|
718
|
+
metafields,
|
|
638
719
|
sortBy: params?.sortBy,
|
|
639
720
|
sortOrder: params?.sortOrder,
|
|
640
721
|
// Admin-only params
|
|
@@ -1564,6 +1645,58 @@ var BrainerceClient = class {
|
|
|
1564
1645
|
async getCustomerByEmail(email) {
|
|
1565
1646
|
return this.request("GET", "/api/v1/customers/by-email", void 0, { email });
|
|
1566
1647
|
}
|
|
1648
|
+
/**
|
|
1649
|
+
* List a customer's saved payment methods (vaulted cards).
|
|
1650
|
+
*
|
|
1651
|
+
* Returns display-only metadata — last4, brand, expiry, default flag,
|
|
1652
|
+
* status. The underlying provider token is encrypted at rest and
|
|
1653
|
+
* NEVER returned through this API.
|
|
1654
|
+
*
|
|
1655
|
+
* Apps mode requires `customers:read` and `payments:read` scopes.
|
|
1656
|
+
*
|
|
1657
|
+
* @param storeId - The store this customer belongs to
|
|
1658
|
+
* @param customerId - The customer's ID
|
|
1659
|
+
* @returns Array of saved payment methods
|
|
1660
|
+
*
|
|
1661
|
+
* @example
|
|
1662
|
+
* ```typescript
|
|
1663
|
+
* const methods = await client.listSavedPaymentMethods(storeId, customerId);
|
|
1664
|
+
* methods.forEach((m) => {
|
|
1665
|
+
* console.log(`${m.brand} ending in ${m.last4} (expires ${m.expMonth}/${m.expYear})`);
|
|
1666
|
+
* });
|
|
1667
|
+
* ```
|
|
1668
|
+
*/
|
|
1669
|
+
async listSavedPaymentMethods(storeId, customerId) {
|
|
1670
|
+
return this.request(
|
|
1671
|
+
"GET",
|
|
1672
|
+
`/api/stores/${storeId}/customers/${customerId}/payment-methods`
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Remove a customer's saved payment method.
|
|
1677
|
+
*
|
|
1678
|
+
* Hard-deletes the row. The provider may still hold the underlying
|
|
1679
|
+
* token internally — we don't issue a delete-at-provider call because
|
|
1680
|
+
* not every provider supports it. From the platform's perspective the
|
|
1681
|
+
* token is gone; subsequent charges will fail.
|
|
1682
|
+
*
|
|
1683
|
+
* Apps mode requires `customers:write` scope.
|
|
1684
|
+
*
|
|
1685
|
+
* @param storeId - The store this customer belongs to
|
|
1686
|
+
* @param customerId - The customer's ID
|
|
1687
|
+
* @param paymentMethodId - The saved payment method ID to remove
|
|
1688
|
+
*
|
|
1689
|
+
* @example
|
|
1690
|
+
* ```typescript
|
|
1691
|
+
* await client.removeSavedPaymentMethod(storeId, customerId, methodId);
|
|
1692
|
+
* ```
|
|
1693
|
+
*/
|
|
1694
|
+
async removeSavedPaymentMethod(storeId, customerId, paymentMethodId) {
|
|
1695
|
+
return this.request(
|
|
1696
|
+
"DELETE",
|
|
1697
|
+
`/api/stores/${storeId}/customers/${customerId}/payment-methods/${paymentMethodId}`
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1567
1700
|
/**
|
|
1568
1701
|
* Login an existing customer (returns JWT token)
|
|
1569
1702
|
* Works in vibe-coded, storefront, and admin mode
|
|
@@ -2070,7 +2203,7 @@ var BrainerceClient = class {
|
|
|
2070
2203
|
* per IP on the server. Include an empty `honeypot` field on your form
|
|
2071
2204
|
* and do NOT send it — bots that auto-fill every input will be rejected.
|
|
2072
2205
|
*
|
|
2073
|
-
*
|
|
2206
|
+
* **Legacy shape (kept working forever):**
|
|
2074
2207
|
* ```typescript
|
|
2075
2208
|
* await brainerce.createInquiry({
|
|
2076
2209
|
* name: 'Jane Doe',
|
|
@@ -2080,6 +2213,16 @@ var BrainerceClient = class {
|
|
|
2080
2213
|
* phone: '+1-555-0100',
|
|
2081
2214
|
* });
|
|
2082
2215
|
* ```
|
|
2216
|
+
*
|
|
2217
|
+
* **Phase 2 (multi-form / custom fields):**
|
|
2218
|
+
* ```typescript
|
|
2219
|
+
* const form = await brainerce.contactForms.get('newsletter');
|
|
2220
|
+
* await brainerce.createInquiry({
|
|
2221
|
+
* formKey: 'newsletter',
|
|
2222
|
+
* fields: { email: 'jane@example.com', source: 'homepage' },
|
|
2223
|
+
* locale: 'he',
|
|
2224
|
+
* });
|
|
2225
|
+
* ```
|
|
2083
2226
|
*/
|
|
2084
2227
|
async createInquiry(input) {
|
|
2085
2228
|
if (this.isVibeCodedMode()) {
|
|
@@ -2406,6 +2549,82 @@ var BrainerceClient = class {
|
|
|
2406
2549
|
"cart"
|
|
2407
2550
|
);
|
|
2408
2551
|
}
|
|
2552
|
+
/**
|
|
2553
|
+
* Recalculate cart totals against current product/variant prices and
|
|
2554
|
+
* discount rules. Idempotent — does NOT mutate the per-item snapshot
|
|
2555
|
+
* `unitPrice`. The returned cart's `hasPriceChanges` / `hasUnavailableItems`
|
|
2556
|
+
* flags reflect the live state. Call this on cart load if you want to
|
|
2557
|
+
* surface drift to the customer before they reach checkout.
|
|
2558
|
+
*
|
|
2559
|
+
* @example
|
|
2560
|
+
* ```typescript
|
|
2561
|
+
* const cart = await client.recalculateCart('cart_123');
|
|
2562
|
+
* if (cart.hasPriceChanges) {
|
|
2563
|
+
* // show "prices have changed" banner
|
|
2564
|
+
* }
|
|
2565
|
+
* ```
|
|
2566
|
+
*/
|
|
2567
|
+
async recalculateCart(cartId) {
|
|
2568
|
+
if (cartId === this.VIRTUAL_LOCAL_CART_ID) {
|
|
2569
|
+
return this.withGuards(this.localCartToCart(this.getLocalCart()), "cart");
|
|
2570
|
+
}
|
|
2571
|
+
if (this.isVibeCodedMode()) {
|
|
2572
|
+
return this.withGuards(
|
|
2573
|
+
this.vibeCodedRequest("POST", `/cart/${cartId}/recalculate`),
|
|
2574
|
+
"cart"
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2577
|
+
if (this.storeId && !this.apiKey) {
|
|
2578
|
+
return this.withGuards(
|
|
2579
|
+
this.storefrontRequest("POST", `/cart/${cartId}/recalculate`),
|
|
2580
|
+
"cart"
|
|
2581
|
+
);
|
|
2582
|
+
}
|
|
2583
|
+
return this.withGuards(
|
|
2584
|
+
this.adminRequest("POST", `/api/v1/cart/${cartId}/recalculate`),
|
|
2585
|
+
"cart"
|
|
2586
|
+
);
|
|
2587
|
+
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Refresh per-item `unitPrice` snapshots to current live prices. Use this
|
|
2590
|
+
* after the customer accepts new prices in the drift-reconfirm flow. The
|
|
2591
|
+
* subsequent `createCheckout` call will then succeed (it would otherwise
|
|
2592
|
+
* throw `PRICE_DRIFT`).
|
|
2593
|
+
*
|
|
2594
|
+
* @example
|
|
2595
|
+
* ```typescript
|
|
2596
|
+
* try {
|
|
2597
|
+
* await client.createCheckout(cartId);
|
|
2598
|
+
* } catch (err) {
|
|
2599
|
+
* if (err.code === 'PRICE_DRIFT') {
|
|
2600
|
+
* // ask user to confirm new prices, then:
|
|
2601
|
+
* await client.refreshCartSnapshots(cartId);
|
|
2602
|
+
* await client.createCheckout(cartId);
|
|
2603
|
+
* }
|
|
2604
|
+
* }
|
|
2605
|
+
* ```
|
|
2606
|
+
*/
|
|
2607
|
+
async refreshCartSnapshots(cartId) {
|
|
2608
|
+
if (cartId === this.VIRTUAL_LOCAL_CART_ID) {
|
|
2609
|
+
return this.withGuards(this.localCartToCart(this.getLocalCart()), "cart");
|
|
2610
|
+
}
|
|
2611
|
+
if (this.isVibeCodedMode()) {
|
|
2612
|
+
return this.withGuards(
|
|
2613
|
+
this.vibeCodedRequest("POST", `/cart/${cartId}/refresh-snapshots`),
|
|
2614
|
+
"cart"
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
if (this.storeId && !this.apiKey) {
|
|
2618
|
+
return this.withGuards(
|
|
2619
|
+
this.storefrontRequest("POST", `/cart/${cartId}/refresh-snapshots`),
|
|
2620
|
+
"cart"
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
return this.withGuards(
|
|
2624
|
+
this.adminRequest("POST", `/api/v1/cart/${cartId}/refresh-snapshots`),
|
|
2625
|
+
"cart"
|
|
2626
|
+
);
|
|
2627
|
+
}
|
|
2409
2628
|
/**
|
|
2410
2629
|
* Link a cart to the currently logged-in customer.
|
|
2411
2630
|
* Use this after customer logs in to associate their guest cart with their account.
|
|
@@ -2646,7 +2865,8 @@ var BrainerceClient = class {
|
|
|
2646
2865
|
* ```typescript
|
|
2647
2866
|
* const { bundles } = await client.getCartBundles('cart_123');
|
|
2648
2867
|
* bundles.forEach(b => {
|
|
2649
|
-
*
|
|
2868
|
+
* const names = b.offeredProducts.map(p => p.name).join(', ');
|
|
2869
|
+
* console.log(`Add ${names} and save! Was ${b.totalOriginalPrice}, now ${b.totalDiscountedPrice}`);
|
|
2650
2870
|
* });
|
|
2651
2871
|
* ```
|
|
2652
2872
|
*/
|
|
@@ -2709,24 +2929,31 @@ var BrainerceClient = class {
|
|
|
2709
2929
|
throw new BrainerceError("removeOrderBump() requires vibe-coded or storefront mode", 400);
|
|
2710
2930
|
}
|
|
2711
2931
|
/**
|
|
2712
|
-
*
|
|
2932
|
+
* Accept an N-product bundle offer: every offered product not yet in cart
|
|
2933
|
+
* is added with the bundle discount applied.
|
|
2713
2934
|
*
|
|
2714
2935
|
* @param cartId - Cart ID
|
|
2715
2936
|
* @param bundleOfferId - Bundle offer ID
|
|
2716
|
-
* @param
|
|
2937
|
+
* @param variantSelections - Optional map of `productId → variantId` for offered products with variants
|
|
2717
2938
|
* @returns Updated cart
|
|
2718
2939
|
*
|
|
2719
2940
|
* @example
|
|
2720
2941
|
* ```typescript
|
|
2721
2942
|
* const { bundles } = await client.getCartBundles('cart_123');
|
|
2722
|
-
*
|
|
2723
|
-
*
|
|
2724
|
-
*
|
|
2725
|
-
*
|
|
2943
|
+
* const bundle = bundles[0];
|
|
2944
|
+
* // Simple products only:
|
|
2945
|
+
* await client.addBundleToCart('cart_123', bundle.id);
|
|
2946
|
+
* // Some offered products have variants:
|
|
2947
|
+
* await client.addBundleToCart('cart_123', bundle.id, {
|
|
2948
|
+
* [variantProductId]: selectedVariantId,
|
|
2949
|
+
* });
|
|
2726
2950
|
* ```
|
|
2727
2951
|
*/
|
|
2728
|
-
async addBundleToCart(cartId, bundleOfferId,
|
|
2729
|
-
const body = {
|
|
2952
|
+
async addBundleToCart(cartId, bundleOfferId, variantSelections) {
|
|
2953
|
+
const body = {
|
|
2954
|
+
bundleOfferId,
|
|
2955
|
+
...variantSelections && Object.keys(variantSelections).length > 0 ? { variantSelections } : {}
|
|
2956
|
+
};
|
|
2730
2957
|
if (this.isVibeCodedMode()) {
|
|
2731
2958
|
return this.vibeCodedRequest("POST", `/cart/${cartId}/bundle`, body);
|
|
2732
2959
|
}
|
|
@@ -2842,6 +3069,9 @@ var BrainerceClient = class {
|
|
|
2842
3069
|
couponCode: null,
|
|
2843
3070
|
items: [],
|
|
2844
3071
|
itemCount: 0,
|
|
3072
|
+
hasPriceChanges: false,
|
|
3073
|
+
hasUnavailableItems: false,
|
|
3074
|
+
unavailableItemIds: [],
|
|
2845
3075
|
expiresAt: null,
|
|
2846
3076
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2847
3077
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -3078,23 +3308,21 @@ var BrainerceClient = class {
|
|
|
3078
3308
|
* ```
|
|
3079
3309
|
*/
|
|
3080
3310
|
async smartAddToCart(item) {
|
|
3311
|
+
const payload = {
|
|
3312
|
+
productId: item.productId,
|
|
3313
|
+
variantId: item.variantId,
|
|
3314
|
+
quantity: item.quantity,
|
|
3315
|
+
metadata: item.metadata,
|
|
3316
|
+
...item.selections && item.selections.length > 0 ? { selections: item.selections } : {},
|
|
3317
|
+
...item.nestedByModifierId ? { nestedByModifierId: item.nestedByModifierId } : {}
|
|
3318
|
+
};
|
|
3081
3319
|
if (this.isCustomerLoggedIn()) {
|
|
3082
3320
|
const cart = await this.getOrCreateCustomerCart();
|
|
3083
|
-
const updated = await this.addToCart(cart.id,
|
|
3084
|
-
productId: item.productId,
|
|
3085
|
-
variantId: item.variantId,
|
|
3086
|
-
quantity: item.quantity,
|
|
3087
|
-
metadata: item.metadata
|
|
3088
|
-
});
|
|
3321
|
+
const updated = await this.addToCart(cart.id, payload);
|
|
3089
3322
|
return updated;
|
|
3090
3323
|
} else {
|
|
3091
3324
|
const cart = await this.getOrCreateSessionCart();
|
|
3092
|
-
const updated = await this.addToCart(cart.id,
|
|
3093
|
-
productId: item.productId,
|
|
3094
|
-
variantId: item.variantId,
|
|
3095
|
-
quantity: item.quantity,
|
|
3096
|
-
metadata: item.metadata
|
|
3097
|
-
});
|
|
3325
|
+
const updated = await this.addToCart(cart.id, payload);
|
|
3098
3326
|
this.updateSessionCartItemCount(updated.items?.length ?? 0);
|
|
3099
3327
|
return updated;
|
|
3100
3328
|
}
|
|
@@ -4137,6 +4365,16 @@ var BrainerceClient = class {
|
|
|
4137
4365
|
variantId: item.variantId || null,
|
|
4138
4366
|
quantity: item.quantity,
|
|
4139
4367
|
unitPrice: item.price || "0",
|
|
4368
|
+
// Local carts never reach the server, so they have no live price to
|
|
4369
|
+
// compare against. Surface the snapshot as both values and flag
|
|
4370
|
+
// everything as "unchanged + available" — the migration to a server
|
|
4371
|
+
// cart on next load will run the real recalc.
|
|
4372
|
+
currentUnitPrice: item.price || "0",
|
|
4373
|
+
priceChanged: false,
|
|
4374
|
+
priceDelta: "0",
|
|
4375
|
+
priceDirection: "unchanged",
|
|
4376
|
+
isAvailable: true,
|
|
4377
|
+
unavailableReason: null,
|
|
4140
4378
|
discountAmount: "0",
|
|
4141
4379
|
promoDiscountAmount: "0",
|
|
4142
4380
|
promoSource: null,
|
|
@@ -4159,6 +4397,9 @@ var BrainerceClient = class {
|
|
|
4159
4397
|
updatedAt: item.addedAt
|
|
4160
4398
|
})),
|
|
4161
4399
|
itemCount: localCart.items.reduce((sum, i) => sum + i.quantity, 0),
|
|
4400
|
+
hasPriceChanges: false,
|
|
4401
|
+
hasUnavailableItems: false,
|
|
4402
|
+
unavailableItemIds: [],
|
|
4162
4403
|
expiresAt: null,
|
|
4163
4404
|
createdAt: localCart.updatedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
4164
4405
|
updatedAt: localCart.updatedAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -5505,6 +5746,204 @@ var BrainerceClient = class {
|
|
|
5505
5746
|
`/api/v1/attributes/${attributeId}/options/${optionId}`
|
|
5506
5747
|
);
|
|
5507
5748
|
}
|
|
5749
|
+
// -------------------- Modifier Groups (Admin) --------------------
|
|
5750
|
+
// These methods require Admin mode (apiKey).
|
|
5751
|
+
// Routes: /api/stores/:storeId/modifier-groups[/:id]
|
|
5752
|
+
// /api/stores/:storeId/modifier-groups/:groupId/modifiers[/:modifierId]
|
|
5753
|
+
// /api/stores/:storeId/products/:productId/modifier-groups[/:attachmentId]
|
|
5754
|
+
//
|
|
5755
|
+
// Server-side validation failures arrive as a structured 400 envelope:
|
|
5756
|
+
// { code: 'MODIFIER_VALIDATION_FAILED', errors: ModifierValidationError[] }
|
|
5757
|
+
// surfaced via BrainerceError.details.
|
|
5758
|
+
/**
|
|
5759
|
+
* List modifier groups in a store, paginated.
|
|
5760
|
+
* Requires Admin mode (apiKey).
|
|
5761
|
+
*
|
|
5762
|
+
* @example
|
|
5763
|
+
* ```typescript
|
|
5764
|
+
* const groups = await client.listModifierGroups('store_123', {
|
|
5765
|
+
* page: 1,
|
|
5766
|
+
* limit: 20,
|
|
5767
|
+
* search: 'pizza',
|
|
5768
|
+
* status: 'active',
|
|
5769
|
+
* });
|
|
5770
|
+
* ```
|
|
5771
|
+
*/
|
|
5772
|
+
async listModifierGroups(storeId, params) {
|
|
5773
|
+
return this.adminRequest(
|
|
5774
|
+
"GET",
|
|
5775
|
+
`/api/stores/${storeId}/modifier-groups`,
|
|
5776
|
+
void 0,
|
|
5777
|
+
params
|
|
5778
|
+
);
|
|
5779
|
+
}
|
|
5780
|
+
/**
|
|
5781
|
+
* Fetch a single modifier group (with its modifiers).
|
|
5782
|
+
* Requires Admin mode (apiKey).
|
|
5783
|
+
*
|
|
5784
|
+
* Admin fetches include `internalName`; storefront responses strip it
|
|
5785
|
+
* (PRD §5.2 invariant).
|
|
5786
|
+
*/
|
|
5787
|
+
async getModifierGroup(storeId, groupId) {
|
|
5788
|
+
return this.adminRequest(
|
|
5789
|
+
"GET",
|
|
5790
|
+
`/api/stores/${storeId}/modifier-groups/${groupId}`
|
|
5791
|
+
);
|
|
5792
|
+
}
|
|
5793
|
+
/**
|
|
5794
|
+
* Create a modifier group. Modifiers themselves are created separately via
|
|
5795
|
+
* `createModifier(storeId, groupId, …)`.
|
|
5796
|
+
* Requires Admin mode (apiKey).
|
|
5797
|
+
*
|
|
5798
|
+
* @example
|
|
5799
|
+
* ```typescript
|
|
5800
|
+
* const group = await client.createModifierGroup('store_123', {
|
|
5801
|
+
* name: 'Toppings',
|
|
5802
|
+
* internalName: 'Pizza toppings',
|
|
5803
|
+
* selectionType: 'MULTIPLE',
|
|
5804
|
+
* minSelections: 0,
|
|
5805
|
+
* maxSelections: 8,
|
|
5806
|
+
* freeQuantity: 3,
|
|
5807
|
+
* freeAllocationPolicy: 'EXPENSIVE_FREE',
|
|
5808
|
+
* });
|
|
5809
|
+
* ```
|
|
5810
|
+
*/
|
|
5811
|
+
async createModifierGroup(storeId, data) {
|
|
5812
|
+
return this.adminRequest("POST", `/api/stores/${storeId}/modifier-groups`, data);
|
|
5813
|
+
}
|
|
5814
|
+
/**
|
|
5815
|
+
* Update a modifier group's metadata or selection rules. Pass `status: 'archived'`
|
|
5816
|
+
* to soft-delete (the group becomes hidden from product attachments).
|
|
5817
|
+
* Requires Admin mode (apiKey).
|
|
5818
|
+
*/
|
|
5819
|
+
async updateModifierGroup(storeId, groupId, data) {
|
|
5820
|
+
return this.adminRequest(
|
|
5821
|
+
"PATCH",
|
|
5822
|
+
`/api/stores/${storeId}/modifier-groups/${groupId}`,
|
|
5823
|
+
data
|
|
5824
|
+
);
|
|
5825
|
+
}
|
|
5826
|
+
/**
|
|
5827
|
+
* Hard-delete a modifier group. Server rejects deletion when the group is
|
|
5828
|
+
* still attached to any product (detach the attachments first, or use
|
|
5829
|
+
* `updateModifierGroup(..., { status: 'archived' })` to keep the row).
|
|
5830
|
+
* Requires Admin mode (apiKey).
|
|
5831
|
+
*/
|
|
5832
|
+
async deleteModifierGroup(storeId, groupId) {
|
|
5833
|
+
await this.adminRequest("DELETE", `/api/stores/${storeId}/modifier-groups/${groupId}`);
|
|
5834
|
+
}
|
|
5835
|
+
/**
|
|
5836
|
+
* Create a modifier inside a group (e.g., "Olives" inside the "Toppings" group).
|
|
5837
|
+
* Requires Admin mode (apiKey).
|
|
5838
|
+
*
|
|
5839
|
+
* `priceDelta` is a decimal string. Negatives are accepted for downsell
|
|
5840
|
+
* modifiers (e.g., `"-2.00"` for "no bread"); the server still enforces
|
|
5841
|
+
* `unitPrice >= 0` at cart-line resolution and rejects negative deltas
|
|
5842
|
+
* combined with a `referencedProductId`.
|
|
5843
|
+
*
|
|
5844
|
+
* @example
|
|
5845
|
+
* ```typescript
|
|
5846
|
+
* const olives = await client.createModifier('store_123', 'mg_toppings', {
|
|
5847
|
+
* name: 'Olives',
|
|
5848
|
+
* priceDelta: '5.00',
|
|
5849
|
+
* isDefault: false,
|
|
5850
|
+
* available: true,
|
|
5851
|
+
* });
|
|
5852
|
+
* ```
|
|
5853
|
+
*/
|
|
5854
|
+
async createModifier(storeId, groupId, data) {
|
|
5855
|
+
return this.adminRequest(
|
|
5856
|
+
"POST",
|
|
5857
|
+
`/api/stores/${storeId}/modifier-groups/${groupId}/modifiers`,
|
|
5858
|
+
data
|
|
5859
|
+
);
|
|
5860
|
+
}
|
|
5861
|
+
/**
|
|
5862
|
+
* Update a modifier. Pass `status: 'archived'` to soft-delete.
|
|
5863
|
+
* Requires Admin mode (apiKey).
|
|
5864
|
+
*/
|
|
5865
|
+
async updateModifier(storeId, groupId, modifierId, data) {
|
|
5866
|
+
return this.adminRequest(
|
|
5867
|
+
"PATCH",
|
|
5868
|
+
`/api/stores/${storeId}/modifier-groups/${groupId}/modifiers/${modifierId}`,
|
|
5869
|
+
data
|
|
5870
|
+
);
|
|
5871
|
+
}
|
|
5872
|
+
/**
|
|
5873
|
+
* Hard-delete a modifier. Use `updateModifier(..., { status: 'archived' })`
|
|
5874
|
+
* if existing line items / order snapshots reference it.
|
|
5875
|
+
* Requires Admin mode (apiKey).
|
|
5876
|
+
*/
|
|
5877
|
+
async deleteModifier(storeId, groupId, modifierId) {
|
|
5878
|
+
await this.adminRequest(
|
|
5879
|
+
"DELETE",
|
|
5880
|
+
`/api/stores/${storeId}/modifier-groups/${groupId}/modifiers/${modifierId}`
|
|
5881
|
+
);
|
|
5882
|
+
}
|
|
5883
|
+
/**
|
|
5884
|
+
* Flip the sold-out toggle on a modifier. Operational endpoint kept separate
|
|
5885
|
+
* from `updateModifier` because the daily ops bar is intentionally lower
|
|
5886
|
+
* (PRD §7.1 — STAFF role once the granular `TOGGLE_MODIFIER_AVAILABILITY`
|
|
5887
|
+
* permission ships).
|
|
5888
|
+
* Requires Admin mode (apiKey).
|
|
5889
|
+
*
|
|
5890
|
+
* @example
|
|
5891
|
+
* ```typescript
|
|
5892
|
+
* // Mark the egg modifier as out of stock during a busy lunch service
|
|
5893
|
+
* await client.toggleModifierAvailability('store_123', 'mg_toppings', 'm_egg', false);
|
|
5894
|
+
* ```
|
|
5895
|
+
*/
|
|
5896
|
+
async toggleModifierAvailability(storeId, groupId, modifierId, available) {
|
|
5897
|
+
return this.adminRequest(
|
|
5898
|
+
"POST",
|
|
5899
|
+
`/api/stores/${storeId}/modifier-groups/${groupId}/modifiers/${modifierId}/availability-toggle`,
|
|
5900
|
+
{ available }
|
|
5901
|
+
);
|
|
5902
|
+
}
|
|
5903
|
+
/**
|
|
5904
|
+
* Attach a modifier group to a product. Three patterns (PRD §5.4):
|
|
5905
|
+
*
|
|
5906
|
+
* - `variantId` omitted → default attach (applies to all variants).
|
|
5907
|
+
* - `variantId` set + matching default attach already exists → variant override row.
|
|
5908
|
+
* - `variantId` set + no default attach → variant-only group.
|
|
5909
|
+
*
|
|
5910
|
+
* Override fields use `null` to mean inherit; any non-null value (including
|
|
5911
|
+
* `0` or `false`) wins. The disable-for-variant convention is `maxOverride: 0` —
|
|
5912
|
+
* the resolver returns the group with `effectiveMax=0` and the validator
|
|
5913
|
+
* silently skips it on cart add.
|
|
5914
|
+
*
|
|
5915
|
+
* Requires Admin mode (apiKey).
|
|
5916
|
+
*/
|
|
5917
|
+
async attachModifierGroup(storeId, productId, data) {
|
|
5918
|
+
return this.adminRequest(
|
|
5919
|
+
"POST",
|
|
5920
|
+
`/api/stores/${storeId}/products/${productId}/modifier-groups`,
|
|
5921
|
+
data
|
|
5922
|
+
);
|
|
5923
|
+
}
|
|
5924
|
+
/**
|
|
5925
|
+
* Update an existing attachment's overrides or position. The `modifierGroupId`
|
|
5926
|
+
* and `variantId` are immutable — to swap groups or move between default /
|
|
5927
|
+
* per-variant rows, detach and re-attach.
|
|
5928
|
+
* Requires Admin mode (apiKey).
|
|
5929
|
+
*/
|
|
5930
|
+
async updateAttachment(storeId, productId, attachmentId, data) {
|
|
5931
|
+
return this.adminRequest(
|
|
5932
|
+
"PATCH",
|
|
5933
|
+
`/api/stores/${storeId}/products/${productId}/modifier-groups/${attachmentId}`,
|
|
5934
|
+
data
|
|
5935
|
+
);
|
|
5936
|
+
}
|
|
5937
|
+
/**
|
|
5938
|
+
* Detach a modifier group from a product (or remove a per-variant override row).
|
|
5939
|
+
* Requires Admin mode (apiKey).
|
|
5940
|
+
*/
|
|
5941
|
+
async detachModifierGroup(storeId, productId, attachmentId) {
|
|
5942
|
+
await this.adminRequest(
|
|
5943
|
+
"DELETE",
|
|
5944
|
+
`/api/stores/${storeId}/products/${productId}/modifier-groups/${attachmentId}`
|
|
5945
|
+
);
|
|
5946
|
+
}
|
|
5508
5947
|
// -------------------- Shipping: Zones and Rates (Admin) --------------------
|
|
5509
5948
|
// These methods require Admin mode (apiKey)
|
|
5510
5949
|
/**
|
|
@@ -5684,7 +6123,10 @@ var BrainerceClient = class {
|
|
|
5684
6123
|
type: d.type,
|
|
5685
6124
|
required: d.required,
|
|
5686
6125
|
enumValues: d.enumValues || void 0,
|
|
5687
|
-
position: d.position
|
|
6126
|
+
position: d.position,
|
|
6127
|
+
isCustomerInput: d.isCustomerInput,
|
|
6128
|
+
appliesToAllProducts: d.appliesToAllProducts,
|
|
6129
|
+
filterable: d.filterable
|
|
5688
6130
|
}))
|
|
5689
6131
|
};
|
|
5690
6132
|
}
|
|
@@ -5740,6 +6182,107 @@ var BrainerceClient = class {
|
|
|
5740
6182
|
async deleteMetafieldDefinition(definitionId) {
|
|
5741
6183
|
await this.adminRequest("DELETE", `/api/v1/metafield-definitions/${definitionId}`);
|
|
5742
6184
|
}
|
|
6185
|
+
/**
|
|
6186
|
+
* Replace the platform publishing configuration on a metafield definition.
|
|
6187
|
+
* `publishedOn` is the source of truth for which sales channels the field
|
|
6188
|
+
* appears on; `platformMetadata` carries the per-platform mapping config
|
|
6189
|
+
* required for sync.
|
|
6190
|
+
*
|
|
6191
|
+
* Requires Admin mode (apiKey).
|
|
6192
|
+
*
|
|
6193
|
+
* @example
|
|
6194
|
+
* ```typescript
|
|
6195
|
+
* await client.setMetafieldPlatforms('def_123', {
|
|
6196
|
+
* publishedOn: ['SHOPIFY', 'WOOCOMMERCE'],
|
|
6197
|
+
* platformMetadata: {
|
|
6198
|
+
* SHOPIFY: { namespace: 'custom', key: 'warranty' },
|
|
6199
|
+
* WOOCOMMERCE: { key: '_warranty_info' },
|
|
6200
|
+
* },
|
|
6201
|
+
* });
|
|
6202
|
+
* ```
|
|
6203
|
+
*/
|
|
6204
|
+
async setMetafieldPlatforms(definitionId, data) {
|
|
6205
|
+
return this.adminRequest(
|
|
6206
|
+
"PUT",
|
|
6207
|
+
`/api/v1/metafield-definitions/${definitionId}/platforms`,
|
|
6208
|
+
data
|
|
6209
|
+
);
|
|
6210
|
+
}
|
|
6211
|
+
// -------------- Vibe-coded site publishing (Admin Mode) --------------
|
|
6212
|
+
// Publish/unpublish category/tag/brand/metafield definitions to specific
|
|
6213
|
+
// vibe-coded sites. Mirrors the per-channel control already available for
|
|
6214
|
+
// Products/Coupons. When no publishes exist for an entity, it remains
|
|
6215
|
+
// visible to all vibe-coded sites of the store (legacy default).
|
|
6216
|
+
/**
|
|
6217
|
+
* Publish a metafield definition to a vibe-coded site (admin mode).
|
|
6218
|
+
* @example
|
|
6219
|
+
* ```typescript
|
|
6220
|
+
* await client.publishMetafieldDefinitionToVibeCodedSite('def_123', 'conn_456');
|
|
6221
|
+
* ```
|
|
6222
|
+
*/
|
|
6223
|
+
async publishMetafieldDefinitionToVibeCodedSite(definitionId, vibeCodedConnectionId) {
|
|
6224
|
+
return this.adminRequest(
|
|
6225
|
+
"POST",
|
|
6226
|
+
`/api/v1/metafield-definitions/${definitionId}/publish-vibe-coded`,
|
|
6227
|
+
{ vibeCodedConnectionId }
|
|
6228
|
+
);
|
|
6229
|
+
}
|
|
6230
|
+
/** Unpublish a metafield definition from a vibe-coded site (admin mode). */
|
|
6231
|
+
async unpublishMetafieldDefinitionFromVibeCodedSite(definitionId, vibeCodedConnectionId) {
|
|
6232
|
+
return this.adminRequest(
|
|
6233
|
+
"POST",
|
|
6234
|
+
`/api/v1/metafield-definitions/${definitionId}/unpublish-vibe-coded`,
|
|
6235
|
+
{ vibeCodedConnectionId }
|
|
6236
|
+
);
|
|
6237
|
+
}
|
|
6238
|
+
/** Publish a category to a vibe-coded site (admin mode). */
|
|
6239
|
+
async publishCategoryToVibeCodedSite(categoryId, vibeCodedConnectionId) {
|
|
6240
|
+
return this.adminRequest(
|
|
6241
|
+
"POST",
|
|
6242
|
+
`/api/v1/categories/${categoryId}/publish-vibe-coded`,
|
|
6243
|
+
{ vibeCodedConnectionId }
|
|
6244
|
+
);
|
|
6245
|
+
}
|
|
6246
|
+
/** Unpublish a category from a vibe-coded site (admin mode). */
|
|
6247
|
+
async unpublishCategoryFromVibeCodedSite(categoryId, vibeCodedConnectionId) {
|
|
6248
|
+
return this.adminRequest(
|
|
6249
|
+
"POST",
|
|
6250
|
+
`/api/v1/categories/${categoryId}/unpublish-vibe-coded`,
|
|
6251
|
+
{ vibeCodedConnectionId }
|
|
6252
|
+
);
|
|
6253
|
+
}
|
|
6254
|
+
/** Publish a tag to a vibe-coded site (admin mode). */
|
|
6255
|
+
async publishTagToVibeCodedSite(tagId, vibeCodedConnectionId) {
|
|
6256
|
+
return this.adminRequest(
|
|
6257
|
+
"POST",
|
|
6258
|
+
`/api/v1/tags/${tagId}/publish-vibe-coded`,
|
|
6259
|
+
{ vibeCodedConnectionId }
|
|
6260
|
+
);
|
|
6261
|
+
}
|
|
6262
|
+
/** Unpublish a tag from a vibe-coded site (admin mode). */
|
|
6263
|
+
async unpublishTagFromVibeCodedSite(tagId, vibeCodedConnectionId) {
|
|
6264
|
+
return this.adminRequest(
|
|
6265
|
+
"POST",
|
|
6266
|
+
`/api/v1/tags/${tagId}/unpublish-vibe-coded`,
|
|
6267
|
+
{ vibeCodedConnectionId }
|
|
6268
|
+
);
|
|
6269
|
+
}
|
|
6270
|
+
/** Publish a brand to a vibe-coded site (admin mode). */
|
|
6271
|
+
async publishBrandToVibeCodedSite(brandId, vibeCodedConnectionId) {
|
|
6272
|
+
return this.adminRequest(
|
|
6273
|
+
"POST",
|
|
6274
|
+
`/api/v1/brands/${brandId}/publish-vibe-coded`,
|
|
6275
|
+
{ vibeCodedConnectionId }
|
|
6276
|
+
);
|
|
6277
|
+
}
|
|
6278
|
+
/** Unpublish a brand from a vibe-coded site (admin mode). */
|
|
6279
|
+
async unpublishBrandFromVibeCodedSite(brandId, vibeCodedConnectionId) {
|
|
6280
|
+
return this.adminRequest(
|
|
6281
|
+
"POST",
|
|
6282
|
+
`/api/v1/brands/${brandId}/unpublish-vibe-coded`,
|
|
6283
|
+
{ vibeCodedConnectionId }
|
|
6284
|
+
);
|
|
6285
|
+
}
|
|
5743
6286
|
/**
|
|
5744
6287
|
* Replace the list of products a (customer-input) metafield definition is
|
|
5745
6288
|
* attached to. Diff-scoped: only rows for this definition are touched, so
|
|
@@ -6309,6 +6852,55 @@ function createWebhookHandler(handlers) {
|
|
|
6309
6852
|
};
|
|
6310
6853
|
}
|
|
6311
6854
|
|
|
6855
|
+
// src/payment-url.ts
|
|
6856
|
+
var ALLOWED_PAYMENT_HOSTS = [
|
|
6857
|
+
// Stripe
|
|
6858
|
+
"checkout.stripe.com",
|
|
6859
|
+
"js.stripe.com",
|
|
6860
|
+
"hooks.stripe.com",
|
|
6861
|
+
// PayPal
|
|
6862
|
+
"www.paypal.com",
|
|
6863
|
+
"www.sandbox.paypal.com",
|
|
6864
|
+
// Cardcom
|
|
6865
|
+
"secure.cardcom.solutions",
|
|
6866
|
+
// Meshulam
|
|
6867
|
+
"meshulam.co.il",
|
|
6868
|
+
// Grow (Linktech)
|
|
6869
|
+
"grow.link",
|
|
6870
|
+
"grow.security",
|
|
6871
|
+
// CreditGuard
|
|
6872
|
+
"creditguard.co.il",
|
|
6873
|
+
// Brainerce-hosted payment embeds (backend payment-embed proxy at
|
|
6874
|
+
// `/api/payment/embed/...` that fronts provider apps' embed shells —
|
|
6875
|
+
// e.g. cardcom-payments OpenFields wrapper). The match also covers
|
|
6876
|
+
// subdomains like `api.brainerce.com`, `staging.brainerce.com`.
|
|
6877
|
+
"brainerce.com"
|
|
6878
|
+
];
|
|
6879
|
+
function isAllowedPaymentUrl(url, options) {
|
|
6880
|
+
if (!url || typeof url !== "string") return false;
|
|
6881
|
+
let parsed;
|
|
6882
|
+
try {
|
|
6883
|
+
parsed = new URL(url);
|
|
6884
|
+
} catch {
|
|
6885
|
+
return false;
|
|
6886
|
+
}
|
|
6887
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
6888
|
+
if (options?.allowLocalhost && parsed.protocol === "http:" && (hostname === "localhost" || hostname === "127.0.0.1")) {
|
|
6889
|
+
return true;
|
|
6890
|
+
}
|
|
6891
|
+
if (parsed.protocol !== "https:") return false;
|
|
6892
|
+
const allowed = options?.extraHosts ? [...ALLOWED_PAYMENT_HOSTS, ...options.extraHosts] : ALLOWED_PAYMENT_HOSTS;
|
|
6893
|
+
return allowed.some((host) => hostname === host || hostname.endsWith("." + host));
|
|
6894
|
+
}
|
|
6895
|
+
function safePaymentRedirect(url, options) {
|
|
6896
|
+
if (!isAllowedPaymentUrl(url, options)) {
|
|
6897
|
+
throw new Error("Payment redirect URL is not in the allowlist");
|
|
6898
|
+
}
|
|
6899
|
+
if (typeof window !== "undefined") {
|
|
6900
|
+
window.location.href = url;
|
|
6901
|
+
}
|
|
6902
|
+
}
|
|
6903
|
+
|
|
6312
6904
|
// src/types.ts
|
|
6313
6905
|
function isHtmlDescription(product) {
|
|
6314
6906
|
if (product?.descriptionFormat === "html") return true;
|
|
@@ -6549,9 +7141,11 @@ function isCouponApplicableToProduct(coupon, productId) {
|
|
|
6549
7141
|
getStockStatus,
|
|
6550
7142
|
getVariantOptions,
|
|
6551
7143
|
getVariantPrice,
|
|
7144
|
+
isAllowedPaymentUrl,
|
|
6552
7145
|
isCouponApplicableToProduct,
|
|
6553
7146
|
isHtmlDescription,
|
|
6554
7147
|
isWebhookEventType,
|
|
6555
7148
|
parseWebhookEvent,
|
|
7149
|
+
safePaymentRedirect,
|
|
6556
7150
|
verifyWebhook
|
|
6557
7151
|
});
|