create-brainerce-store 1.62.0 → 1.64.0

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.
Files changed (203) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +71 -71
  3. package/dist/index.js +26 -3
  4. package/messages/en.json +502 -502
  5. package/messages/he.json +502 -502
  6. package/package.json +1 -1
  7. package/templates/nextjs/base/.claude/commands/design.md +60 -60
  8. package/templates/nextjs/base/.env.local.ejs +24 -24
  9. package/templates/nextjs/base/.eslintrc.json +57 -57
  10. package/templates/nextjs/base/.mcp.json +8 -8
  11. package/templates/nextjs/base/AI-GUIDE.md +174 -174
  12. package/templates/nextjs/base/TRANSLATIONS.md +200 -200
  13. package/templates/nextjs/base/package.json.ejs +52 -52
  14. package/templates/nextjs/base/src/app/agents.md/route.ts +87 -0
  15. package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +308 -262
  16. package/templates/nextjs/base/src/app/blog/page.tsx.ejs +276 -276
  17. package/templates/nextjs/base/src/app/blog/rss.xml/route.ts +70 -0
  18. package/templates/nextjs/base/src/app/cart/page.tsx +7 -7
  19. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +29 -3
  20. package/templates/nextjs/base/src/app/checkout/page.tsx +12 -0
  21. package/templates/nextjs/base/src/app/contact/layout.tsx.ejs +14 -0
  22. package/templates/nextjs/base/src/app/globals.css +36 -36
  23. package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +26 -26
  24. package/templates/nextjs/base/src/app/layout.tsx.ejs +30 -0
  25. package/templates/nextjs/base/src/app/llms.txt/route.ts +28 -5
  26. package/templates/nextjs/base/src/app/not-found.tsx.ejs +35 -0
  27. package/templates/nextjs/base/src/app/opengraph-image.tsx +109 -0
  28. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +295 -278
  29. package/templates/nextjs/base/src/app/order-status/page.tsx +121 -121
  30. package/templates/nextjs/base/src/app/page.tsx +64 -64
  31. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +14 -2
  32. package/templates/nextjs/base/src/app/products/page.tsx +19 -19
  33. package/templates/nextjs/base/src/app/robots.ts +73 -14
  34. package/templates/nextjs/base/src/app/sitemap.ts +89 -66
  35. package/templates/nextjs/base/src/components/account/order-history.tsx +385 -385
  36. package/templates/nextjs/base/src/components/brainerce-bot.tsx +70 -70
  37. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +612 -612
  38. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +292 -292
  39. package/templates/nextjs/base/src/components/checkout/date-picker.tsx +309 -309
  40. package/templates/nextjs/base/src/components/checkout/datetime-picker.tsx +144 -144
  41. package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +242 -242
  42. package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +198 -198
  43. package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +125 -125
  44. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +74 -74
  45. package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +59 -59
  46. package/templates/nextjs/base/src/components/seo/breadcrumbs.tsx +37 -37
  47. package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +61 -61
  48. package/templates/nextjs/base/src/components/seo/organization-json-ld.tsx +97 -97
  49. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +23 -131
  50. package/templates/nextjs/base/src/components/tracking-bootstrap.tsx +62 -0
  51. package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -53
  52. package/templates/nextjs/base/src/components/ui/badge.tsx +32 -32
  53. package/templates/nextjs/base/src/components/ui/button.tsx +49 -49
  54. package/templates/nextjs/base/src/components/ui/card.tsx +55 -55
  55. package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -28
  56. package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -107
  57. package/templates/nextjs/base/src/components/ui/input.tsx +22 -22
  58. package/templates/nextjs/base/src/components/ui/label.tsx +21 -21
  59. package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -38
  60. package/templates/nextjs/base/src/components/ui/select.tsx +153 -153
  61. package/templates/nextjs/base/src/components/ui/separator.tsx +26 -26
  62. package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -125
  63. package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -7
  64. package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -55
  65. package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -21
  66. package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -32
  67. package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +58 -58
  68. package/templates/nextjs/base/src/core/hooks/use-home-data.ts +51 -51
  69. package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +295 -295
  70. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +20 -2
  71. package/templates/nextjs/base/src/core/lib/image-hosts.ts +7 -7
  72. package/templates/nextjs/base/src/core/lib/product-options.ts +121 -121
  73. package/templates/nextjs/base/src/core/lib/resolve-currency.ts +20 -20
  74. package/templates/nextjs/base/src/core/lib/safe-redirect.ts +38 -38
  75. package/templates/nextjs/base/src/core/lib/seo.ts +46 -46
  76. package/templates/nextjs/base/src/core/lib/store-info.ts +17 -0
  77. package/templates/nextjs/base/src/core/lib/tracking.ts +171 -0
  78. package/templates/nextjs/base/src/core/lib/use-currency.ts +19 -19
  79. package/templates/nextjs/base/src/core/lib/utils.ts +21 -21
  80. package/templates/nextjs/base/src/middleware.ts.ejs +54 -2
  81. package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +117 -117
  82. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +146 -146
  83. package/templates/nextjs/base/src/ui/cart/cart-nudges.tsx +28 -28
  84. package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +110 -110
  85. package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +126 -126
  86. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +140 -140
  87. package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +134 -134
  88. package/templates/nextjs/base/src/ui/cart/free-shipping-bar.tsx +61 -61
  89. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +95 -95
  90. package/templates/nextjs/base/src/ui/home/discount-banner-strip.tsx +22 -20
  91. package/templates/nextjs/base/src/ui/home/featured-products-section.tsx +24 -24
  92. package/templates/nextjs/base/src/ui/home/hero-section.tsx +27 -27
  93. package/templates/nextjs/base/src/ui/home/home-client.tsx +29 -29
  94. package/templates/nextjs/base/src/ui/layout/announcement-bar.tsx.ejs +116 -116
  95. package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -90
  96. package/templates/nextjs/base/src/ui/layout/header-account.tsx +40 -40
  97. package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -80
  98. package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +144 -144
  99. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +152 -152
  100. package/templates/nextjs/base/src/ui/product/customization-fields.tsx +457 -457
  101. package/templates/nextjs/base/src/ui/product/discount-badge.tsx +18 -18
  102. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +191 -191
  103. package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -206
  104. package/templates/nextjs/base/src/ui/product/price-display.tsx +62 -62
  105. package/templates/nextjs/base/src/ui/product/product-card.tsx +7 -2
  106. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +455 -455
  107. package/templates/nextjs/base/src/ui/product/product-listing.tsx +446 -446
  108. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +102 -102
  109. package/templates/nextjs/base/src/ui/product/review-form.tsx +294 -294
  110. package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +108 -108
  111. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +61 -61
  112. package/templates/nextjs/base/src/ui/product/variant-selector.tsx +291 -291
  113. package/templates/nextjs/base/src/ui/shared/cdn-image.tsx +26 -26
  114. package/templates/nextjs/base/tailwind.config.ts +69 -69
  115. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +30 -0
  116. package/templates/nextjs/designs/atelier/globals.css +402 -402
  117. package/templates/nextjs/designs/atelier/messages-patch/en.json +100 -100
  118. package/templates/nextjs/designs/atelier/messages-patch/he.json +100 -100
  119. package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +110 -110
  120. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -163
  121. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -140
  122. package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -34
  123. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -107
  124. package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +123 -123
  125. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -147
  126. package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -142
  127. package/templates/nextjs/designs/atelier/ui/cart/free-shipping-bar.tsx +77 -77
  128. package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +89 -89
  129. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -34
  130. package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +151 -151
  131. package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -27
  132. package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +75 -75
  133. package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -36
  134. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +174 -174
  135. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -54
  136. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -78
  137. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -46
  138. package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -118
  139. package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -96
  140. package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -36
  141. package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -36
  142. package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -137
  143. package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -14
  144. package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -34
  145. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -141
  146. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -138
  147. package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -466
  148. package/templates/nextjs/designs/atelier/ui/product/discount-badge.tsx +17 -17
  149. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +196 -196
  150. package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -193
  151. package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -69
  152. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +13 -11
  153. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +477 -477
  154. package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +42 -42
  155. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -260
  156. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -95
  157. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +267 -267
  158. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -148
  159. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -66
  160. package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -206
  161. package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -29
  162. package/templates/nextjs/designs/atelier/ui/shared/fly-to-cart.ts +86 -86
  163. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +276 -276
  164. package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +63 -63
  165. package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -71
  166. package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -125
  167. package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +89 -89
  168. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +123 -123
  169. package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +29 -27
  170. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +99 -99
  171. package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +105 -105
  172. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +106 -106
  173. package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +119 -119
  174. package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +54 -54
  175. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +81 -81
  176. package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +21 -19
  177. package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +23 -23
  178. package/templates/nextjs/ui-canvas/home/hero-section.tsx +21 -21
  179. package/templates/nextjs/ui-canvas/home/home-client.tsx +26 -26
  180. package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +101 -101
  181. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +71 -71
  182. package/templates/nextjs/ui-canvas/layout/header-account.tsx +31 -31
  183. package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +76 -76
  184. package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +36 -36
  185. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +82 -82
  186. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +119 -119
  187. package/templates/nextjs/ui-canvas/product/customization-fields.tsx +445 -445
  188. package/templates/nextjs/ui-canvas/product/discount-badge.tsx +19 -19
  189. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +168 -168
  190. package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +165 -165
  191. package/templates/nextjs/ui-canvas/product/price-display.tsx +52 -52
  192. package/templates/nextjs/ui-canvas/product/product-card.tsx +7 -2
  193. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +352 -352
  194. package/templates/nextjs/ui-canvas/product/product-grid.tsx +27 -27
  195. package/templates/nextjs/ui-canvas/product/product-listing.tsx +224 -224
  196. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +99 -99
  197. package/templates/nextjs/ui-canvas/product/review-form.tsx +266 -266
  198. package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +96 -96
  199. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +45 -45
  200. package/templates/nextjs/ui-canvas/product/variant-selector.tsx +212 -212
  201. package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +31 -31
  202. package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +27 -27
  203. package/templates/nextjs/base/tsconfig.tsbuildinfo +0 -1
@@ -1,19 +1,19 @@
1
- 'use client';
2
-
3
- import { useStoreInfo } from '@/core/providers/store-provider';
4
-
5
- /**
6
- * Single source of truth for "which currency code do I format prices with?"
7
- * in client components. Collapses the historical three-line fallback chain
8
- * (`storeInfo?.currency || process.env.NEXT_PUBLIC_STORE_CURRENCY || 'USD'`)
9
- * into one call so the chain lives in exactly one place — and so the day
10
- * we drop the env var or the USD last-resort, we only touch this file.
11
- *
12
- * @param override pass when the caller already has a specific currency
13
- * (e.g. an `Order` is denominated in its own currency,
14
- * not the live store currency). Wins over `storeInfo`.
15
- */
16
- export function useCurrency(override?: string | null): string {
17
- const { storeInfo } = useStoreInfo();
18
- return override || storeInfo?.currency || process.env.NEXT_PUBLIC_STORE_CURRENCY || 'USD';
19
- }
1
+ 'use client';
2
+
3
+ import { useStoreInfo } from '@/core/providers/store-provider';
4
+
5
+ /**
6
+ * Single source of truth for "which currency code do I format prices with?"
7
+ * in client components. Collapses the historical three-line fallback chain
8
+ * (`storeInfo?.currency || process.env.NEXT_PUBLIC_STORE_CURRENCY || 'USD'`)
9
+ * into one call so the chain lives in exactly one place — and so the day
10
+ * we drop the env var or the USD last-resort, we only touch this file.
11
+ *
12
+ * @param override pass when the caller already has a specific currency
13
+ * (e.g. an `Order` is denominated in its own currency,
14
+ * not the live store currency). Wins over `storeInfo`.
15
+ */
16
+ export function useCurrency(override?: string | null): string {
17
+ const { storeInfo } = useStoreInfo();
18
+ return override || storeInfo?.currency || process.env.NEXT_PUBLIC_STORE_CURRENCY || 'USD';
19
+ }
@@ -1,21 +1,21 @@
1
- import { clsx, type ClassValue } from 'clsx';
2
- import { twMerge } from 'tailwind-merge';
3
-
4
- export function cn(...inputs: ClassValue[]) {
5
- return twMerge(clsx(inputs));
6
- }
7
-
8
- /**
9
- * Normalize a Next.js dynamic-route param into its decoded human-readable
10
- * form. App Router usually decodes params automatically, but some runtime
11
- * + middleware combinations leak percent-encoded values for non-ASCII
12
- * slugs (e.g. Hebrew `%D7%A7…`), which then surface in canonical URLs and
13
- * SEO tags. Idempotent on already-decoded input.
14
- */
15
- export function decodeSlug(slug: string): string {
16
- try {
17
- return decodeURIComponent(slug);
18
- } catch {
19
- return slug;
20
- }
21
- }
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
7
+
8
+ /**
9
+ * Normalize a Next.js dynamic-route param into its decoded human-readable
10
+ * form. App Router usually decodes params automatically, but some runtime
11
+ * + middleware combinations leak percent-encoded values for non-ASCII
12
+ * slugs (e.g. Hebrew `%D7%A7…`), which then surface in canonical URLs and
13
+ * SEO tags. Idempotent on already-decoded input.
14
+ */
15
+ export function decodeSlug(slug: string): string {
16
+ try {
17
+ return decodeURIComponent(slug);
18
+ } catch {
19
+ return slug;
20
+ }
21
+ }
@@ -30,6 +30,32 @@ function brainerceApiOrigin(): string {
30
30
  }
31
31
  }
32
32
 
33
+ // Beacon/XHR endpoints for the merchant's marketing tags (GA4, GTM, Meta,
34
+ // TikTok). Only `connect-src` needs them: `script-src` runs with
35
+ // 'strict-dynamic', under which host allowlists are IGNORED and trust
36
+ // propagates from the nonce'd bundle to the tag scripts it injects — so
37
+ // adding hosts there would be dead config. Without these entries the tags
38
+ // load fine and then every hit is blocked, which fails silently: the
39
+ // dashboard shows a connected pixel and the ad platform records nothing.
40
+ //
41
+ // Fixed list, not read from store config: middleware cannot see which apps a
42
+ // merchant connected without a fetch on every request, and these are the same
43
+ // well-known vendor origins for every store.
44
+ const MARKETING_TAG_ORIGINS = [
45
+ // Google (gtag.js, GTM, GA4 collect, Ads conversion pings)
46
+ 'https://www.googletagmanager.com',
47
+ 'https://www.google-analytics.com',
48
+ 'https://*.google-analytics.com',
49
+ 'https://*.analytics.google.com',
50
+ 'https://stats.g.doubleclick.net',
51
+ 'https://www.google.com',
52
+ // Meta pixel
53
+ 'https://connect.facebook.net',
54
+ 'https://www.facebook.com',
55
+ // TikTok pixel
56
+ 'https://analytics.tiktok.com',
57
+ ].join(' ');
58
+
33
59
  function buildCsp(nonce: string): string {
34
60
  const isDev = process.env.NODE_ENV === 'development';
35
61
  // Next dev uses webpack's eval-source-map devtool, which requires 'unsafe-eval'
@@ -44,7 +70,7 @@ function buildCsp(nonce: string): string {
44
70
  "img-src 'self' data: blob: https:",
45
71
  "font-src 'self' data:",
46
72
  "frame-src 'self' https://meshulam.co.il https://*.meshulam.co.il https://grow.link https://*.grow.link https://grow.security https://*.grow.security https://creditguard.co.il https://*.creditguard.co.il https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://secure.cardcom.solutions https://checkout.stripe.com https://www.paypal.com https://www.sandbox.paypal.com https://www.youtube.com https://www.youtube-nocookie.com https://player.vimeo.com https://*.brainerce.com",
47
- `connect-src 'self' ${brainerceApiOrigin()} https://api.brainerce.com https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://pay.google.com https://*.stripe.com https://*.creditguard.co.il`,
73
+ `connect-src 'self' ${brainerceApiOrigin()} https://api.brainerce.com https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://pay.google.com https://*.stripe.com https://*.creditguard.co.il ${MARKETING_TAG_ORIGINS}`,
48
74
  "worker-src 'self' blob:",
49
75
  // 'self' (not 'none') so iframe-based payment providers (e.g. Cardcom)
50
76
  // can redirect the iframe back to /payment-complete on the storefront
@@ -158,6 +184,32 @@ function brainerceApiOrigin(): string {
158
184
  }
159
185
  }
160
186
 
187
+ // Beacon/XHR endpoints for the merchant's marketing tags (GA4, GTM, Meta,
188
+ // TikTok). Only `connect-src` needs them: `script-src` runs with
189
+ // 'strict-dynamic', under which host allowlists are IGNORED and trust
190
+ // propagates from the nonce'd bundle to the tag scripts it injects — so
191
+ // adding hosts there would be dead config. Without these entries the tags
192
+ // load fine and then every hit is blocked, which fails silently: the
193
+ // dashboard shows a connected pixel and the ad platform records nothing.
194
+ //
195
+ // Fixed list, not read from store config: middleware cannot see which apps a
196
+ // merchant connected without a fetch on every request, and these are the same
197
+ // well-known vendor origins for every store.
198
+ const MARKETING_TAG_ORIGINS = [
199
+ // Google (gtag.js, GTM, GA4 collect, Ads conversion pings)
200
+ 'https://www.googletagmanager.com',
201
+ 'https://www.google-analytics.com',
202
+ 'https://*.google-analytics.com',
203
+ 'https://*.analytics.google.com',
204
+ 'https://stats.g.doubleclick.net',
205
+ 'https://www.google.com',
206
+ // Meta pixel
207
+ 'https://connect.facebook.net',
208
+ 'https://www.facebook.com',
209
+ // TikTok pixel
210
+ 'https://analytics.tiktok.com',
211
+ ].join(' ');
212
+
161
213
  function buildCsp(nonce: string): string {
162
214
  const isDev = process.env.NODE_ENV === 'development';
163
215
  // Next dev uses webpack's eval-source-map devtool, which requires 'unsafe-eval'
@@ -172,7 +224,7 @@ function buildCsp(nonce: string): string {
172
224
  "img-src 'self' data: blob: https:",
173
225
  "font-src 'self' data:",
174
226
  "frame-src 'self' https://meshulam.co.il https://*.meshulam.co.il https://grow.link https://*.grow.link https://grow.security https://*.grow.security https://creditguard.co.il https://*.creditguard.co.il https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://secure.cardcom.solutions https://checkout.stripe.com https://www.paypal.com https://www.sandbox.paypal.com https://www.youtube.com https://www.youtube-nocookie.com https://player.vimeo.com https://*.brainerce.com",
175
- `connect-src 'self' ${brainerceApiOrigin()} https://api.brainerce.com https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://pay.google.com https://*.stripe.com https://*.creditguard.co.il`,
227
+ `connect-src 'self' ${brainerceApiOrigin()} https://api.brainerce.com https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://pay.google.com https://*.stripe.com https://*.creditguard.co.il ${MARKETING_TAG_ORIGINS}`,
176
228
  "worker-src 'self' blob:",
177
229
  // 'self' (not 'none') so iframe-based payment providers (e.g. Cardcom)
178
230
  // can redirect the iframe back to /payment-complete on the storefront
@@ -1,117 +1,117 @@
1
- 'use client';
2
-
3
- import { useState } from 'react';
4
- import { CdnImage as Image } from '@/ui/shared/cdn-image';
5
- import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
6
- import { formatPrice } from 'brainerce';
7
- import { useCurrency } from '@/core/lib/use-currency';
8
- import { useTranslations } from '@/core/lib/translations';
9
- import { Badge } from '@/components/ui/badge';
10
- import { Button } from '@/components/ui/button';
11
- import { Card } from '@/components/ui/card';
12
- import { Separator } from '@/components/ui/separator';
13
- import { cn } from '@/core/lib/utils';
14
-
15
- interface CartBundleOfferCardProps {
16
- offer: CartBundleOfferType;
17
- cartId: string;
18
- onAdd: () => void;
19
- className?: string;
20
- }
21
-
22
- export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBundleOfferCardProps) {
23
- const t = useTranslations('cart');
24
- const currency = useCurrency();
25
- const [adding, setAdding] = useState(false);
26
-
27
- const offered = offer.offeredProducts;
28
-
29
- const totalOriginal = parseFloat(offer.totalOriginalPrice);
30
- const totalDiscounted = parseFloat(offer.totalDiscountedPrice);
31
- const discountLabel =
32
- offer.discountType === 'PERCENTAGE'
33
- ? `${offer.discountValue}%`
34
- : (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
35
-
36
- async function handleAdd() {
37
- if (adding) return;
38
- try {
39
- setAdding(true);
40
- const { getClient } = await import('@/core/lib/brainerce');
41
- const client = getClient();
42
- // No variant selections in the default template — products with
43
- // variants are best handled with a dedicated picker per offered item.
44
- await client.addBundleToCart(cartId, offer.id);
45
- onAdd();
46
- } catch (err) {
47
- console.error('Failed to add bundle:', err);
48
- } finally {
49
- setAdding(false);
50
- }
51
- }
52
-
53
- return (
54
- <Card className={cn('p-4 shadow-none', className)}>
55
- <div className="mb-3">
56
- <p className="text-foreground text-sm font-medium">{offer.name}</p>
57
- {offer.description && (
58
- <p className="text-muted-foreground mt-0.5 text-xs">{offer.description}</p>
59
- )}
60
- </div>
61
-
62
- <ul className="space-y-2">
63
- {offered.map((p) => {
64
- const firstImage = p.images?.[0];
65
- const imageUrl = firstImage?.url ?? null;
66
- return (
67
- <li key={p.id} className="flex items-center gap-3">
68
- <div className="bg-muted relative h-12 w-12 flex-shrink-0 overflow-hidden rounded">
69
- {imageUrl ? (
70
- <Image src={imageUrl} alt={p.name} fill sizes="48px" className="object-cover" />
71
- ) : null}
72
- </div>
73
- <div className="min-w-0 flex-1">
74
- <p className="text-foreground truncate text-sm">{p.name}</p>
75
- <div className="mt-0.5 flex items-center gap-2">
76
- <span className="text-muted-foreground text-xs line-through">
77
- {formatPrice(parseFloat(p.originalPrice), { currency }) as string}
78
- </span>
79
- <span className="text-foreground text-xs font-semibold">
80
- {formatPrice(parseFloat(p.discountedPrice), { currency }) as string}
81
- </span>
82
- </div>
83
- </div>
84
- </li>
85
- );
86
- })}
87
- </ul>
88
-
89
- <Separator className="mt-3" />
90
- <div className="flex items-center justify-between pt-3">
91
- <div className="flex items-center gap-2">
92
- <span className="text-muted-foreground text-sm line-through">
93
- {formatPrice(totalOriginal, { currency }) as string}
94
- </span>
95
- <span className="text-foreground text-sm font-semibold">
96
- {formatPrice(totalDiscounted, { currency }) as string}
97
- </span>
98
- <Badge
99
- variant="outline"
100
- className="bg-destructive/10 text-destructive rounded border-transparent px-1.5 py-0.5 font-medium"
101
- >
102
- -{discountLabel}
103
- </Badge>
104
- </div>
105
- <Button
106
- type="button"
107
- size="sm"
108
- onClick={handleAdd}
109
- disabled={adding}
110
- className="h-auto flex-shrink-0 rounded px-4 py-2 text-xs"
111
- >
112
- {adding ? t('addingBundle') : t('addBundleItem')}
113
- </Button>
114
- </div>
115
- </Card>
116
- );
117
- }
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { CdnImage as Image } from '@/ui/shared/cdn-image';
5
+ import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
6
+ import { formatPrice } from 'brainerce';
7
+ import { useCurrency } from '@/core/lib/use-currency';
8
+ import { useTranslations } from '@/core/lib/translations';
9
+ import { Badge } from '@/components/ui/badge';
10
+ import { Button } from '@/components/ui/button';
11
+ import { Card } from '@/components/ui/card';
12
+ import { Separator } from '@/components/ui/separator';
13
+ import { cn } from '@/core/lib/utils';
14
+
15
+ interface CartBundleOfferCardProps {
16
+ offer: CartBundleOfferType;
17
+ cartId: string;
18
+ onAdd: () => void;
19
+ className?: string;
20
+ }
21
+
22
+ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBundleOfferCardProps) {
23
+ const t = useTranslations('cart');
24
+ const currency = useCurrency();
25
+ const [adding, setAdding] = useState(false);
26
+
27
+ const offered = offer.offeredProducts;
28
+
29
+ const totalOriginal = parseFloat(offer.totalOriginalPrice);
30
+ const totalDiscounted = parseFloat(offer.totalDiscountedPrice);
31
+ const discountLabel =
32
+ offer.discountType === 'PERCENTAGE'
33
+ ? `${offer.discountValue}%`
34
+ : (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
35
+
36
+ async function handleAdd() {
37
+ if (adding) return;
38
+ try {
39
+ setAdding(true);
40
+ const { getClient } = await import('@/core/lib/brainerce');
41
+ const client = getClient();
42
+ // No variant selections in the default template — products with
43
+ // variants are best handled with a dedicated picker per offered item.
44
+ await client.addBundleToCart(cartId, offer.id);
45
+ onAdd();
46
+ } catch (err) {
47
+ console.error('Failed to add bundle:', err);
48
+ } finally {
49
+ setAdding(false);
50
+ }
51
+ }
52
+
53
+ return (
54
+ <Card className={cn('p-4 shadow-none', className)}>
55
+ <div className="mb-3">
56
+ <p className="text-foreground text-sm font-medium">{offer.name}</p>
57
+ {offer.description && (
58
+ <p className="text-muted-foreground mt-0.5 text-xs">{offer.description}</p>
59
+ )}
60
+ </div>
61
+
62
+ <ul className="space-y-2">
63
+ {offered.map((p) => {
64
+ const firstImage = p.images?.[0];
65
+ const imageUrl = firstImage?.url ?? null;
66
+ return (
67
+ <li key={p.id} className="flex items-center gap-3">
68
+ <div className="bg-muted relative h-12 w-12 flex-shrink-0 overflow-hidden rounded">
69
+ {imageUrl ? (
70
+ <Image src={imageUrl} alt={p.name} fill sizes="48px" className="object-cover" />
71
+ ) : null}
72
+ </div>
73
+ <div className="min-w-0 flex-1">
74
+ <p className="text-foreground truncate text-sm">{p.name}</p>
75
+ <div className="mt-0.5 flex items-center gap-2">
76
+ <span className="text-muted-foreground text-xs line-through">
77
+ {formatPrice(parseFloat(p.originalPrice), { currency }) as string}
78
+ </span>
79
+ <span className="text-foreground text-xs font-semibold">
80
+ {formatPrice(parseFloat(p.discountedPrice), { currency }) as string}
81
+ </span>
82
+ </div>
83
+ </div>
84
+ </li>
85
+ );
86
+ })}
87
+ </ul>
88
+
89
+ <Separator className="mt-3" />
90
+ <div className="flex items-center justify-between pt-3">
91
+ <div className="flex items-center gap-2">
92
+ <span className="text-muted-foreground text-sm line-through">
93
+ {formatPrice(totalOriginal, { currency }) as string}
94
+ </span>
95
+ <span className="text-foreground text-sm font-semibold">
96
+ {formatPrice(totalDiscounted, { currency }) as string}
97
+ </span>
98
+ <Badge
99
+ variant="outline"
100
+ className="bg-destructive/10 text-destructive rounded border-transparent px-1.5 py-0.5 font-medium"
101
+ >
102
+ -{discountLabel}
103
+ </Badge>
104
+ </div>
105
+ <Button
106
+ type="button"
107
+ size="sm"
108
+ onClick={handleAdd}
109
+ disabled={adding}
110
+ className="h-auto flex-shrink-0 rounded px-4 py-2 text-xs"
111
+ >
112
+ {adding ? t('addingBundle') : t('addBundleItem')}
113
+ </Button>
114
+ </div>
115
+ </Card>
116
+ );
117
+ }