create-brainerce-store 1.61.0 → 1.63.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 (196) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +71 -71
  3. package/dist/index.js +10 -2
  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/blog/[slug]/page.tsx.ejs +262 -262
  15. package/templates/nextjs/base/src/app/blog/page.tsx.ejs +276 -276
  16. package/templates/nextjs/base/src/app/cart/page.tsx +7 -7
  17. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +106 -106
  18. package/templates/nextjs/base/src/app/checkout/page.tsx +12 -0
  19. package/templates/nextjs/base/src/app/globals.css +36 -36
  20. package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +26 -26
  21. package/templates/nextjs/base/src/app/layout.tsx.ejs +10 -0
  22. package/templates/nextjs/base/src/app/llms.txt/route.ts +44 -44
  23. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +295 -278
  24. package/templates/nextjs/base/src/app/order-status/page.tsx +121 -121
  25. package/templates/nextjs/base/src/app/page.tsx +64 -64
  26. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +175 -175
  27. package/templates/nextjs/base/src/app/products/page.tsx +19 -19
  28. package/templates/nextjs/base/src/components/account/order-history.tsx +385 -385
  29. package/templates/nextjs/base/src/components/brainerce-bot.tsx +70 -70
  30. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +612 -564
  31. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +292 -292
  32. package/templates/nextjs/base/src/components/checkout/date-picker.tsx +309 -309
  33. package/templates/nextjs/base/src/components/checkout/datetime-picker.tsx +144 -144
  34. package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +242 -242
  35. package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +198 -198
  36. package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +125 -125
  37. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +74 -74
  38. package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +59 -59
  39. package/templates/nextjs/base/src/components/seo/breadcrumbs.tsx +37 -37
  40. package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +61 -61
  41. package/templates/nextjs/base/src/components/seo/organization-json-ld.tsx +97 -97
  42. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +179 -179
  43. package/templates/nextjs/base/src/components/tracking-bootstrap.tsx +62 -0
  44. package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -53
  45. package/templates/nextjs/base/src/components/ui/badge.tsx +32 -32
  46. package/templates/nextjs/base/src/components/ui/button.tsx +49 -49
  47. package/templates/nextjs/base/src/components/ui/card.tsx +55 -55
  48. package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -28
  49. package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -107
  50. package/templates/nextjs/base/src/components/ui/input.tsx +22 -22
  51. package/templates/nextjs/base/src/components/ui/label.tsx +21 -21
  52. package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -38
  53. package/templates/nextjs/base/src/components/ui/select.tsx +153 -153
  54. package/templates/nextjs/base/src/components/ui/separator.tsx +26 -26
  55. package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -125
  56. package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -7
  57. package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -55
  58. package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -21
  59. package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -32
  60. package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +58 -58
  61. package/templates/nextjs/base/src/core/hooks/use-home-data.ts +51 -51
  62. package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +295 -295
  63. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +20 -2
  64. package/templates/nextjs/base/src/core/lib/image-hosts.ts +7 -7
  65. package/templates/nextjs/base/src/core/lib/product-options.ts +121 -121
  66. package/templates/nextjs/base/src/core/lib/resolve-currency.ts +20 -20
  67. package/templates/nextjs/base/src/core/lib/safe-redirect.ts +38 -38
  68. package/templates/nextjs/base/src/core/lib/seo.ts +46 -46
  69. package/templates/nextjs/base/src/core/lib/store-info.ts +9 -0
  70. package/templates/nextjs/base/src/core/lib/tracking.ts +171 -0
  71. package/templates/nextjs/base/src/core/lib/use-currency.ts +19 -19
  72. package/templates/nextjs/base/src/core/lib/utils.ts +21 -21
  73. package/templates/nextjs/base/src/middleware.ts.ejs +54 -2
  74. package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +117 -117
  75. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +146 -146
  76. package/templates/nextjs/base/src/ui/cart/cart-nudges.tsx +28 -28
  77. package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +110 -110
  78. package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +126 -126
  79. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +140 -140
  80. package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +134 -134
  81. package/templates/nextjs/base/src/ui/cart/free-shipping-bar.tsx +61 -61
  82. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +95 -95
  83. package/templates/nextjs/base/src/ui/home/discount-banner-strip.tsx +20 -20
  84. package/templates/nextjs/base/src/ui/home/featured-products-section.tsx +24 -24
  85. package/templates/nextjs/base/src/ui/home/hero-section.tsx +27 -27
  86. package/templates/nextjs/base/src/ui/home/home-client.tsx +29 -29
  87. package/templates/nextjs/base/src/ui/layout/announcement-bar.tsx.ejs +116 -116
  88. package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -90
  89. package/templates/nextjs/base/src/ui/layout/header-account.tsx +40 -40
  90. package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -80
  91. package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +144 -144
  92. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +152 -152
  93. package/templates/nextjs/base/src/ui/product/customization-fields.tsx +457 -457
  94. package/templates/nextjs/base/src/ui/product/discount-badge.tsx +18 -18
  95. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +191 -191
  96. package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -206
  97. package/templates/nextjs/base/src/ui/product/price-display.tsx +62 -62
  98. package/templates/nextjs/base/src/ui/product/product-card.tsx +7 -2
  99. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +455 -455
  100. package/templates/nextjs/base/src/ui/product/product-listing.tsx +446 -446
  101. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +102 -102
  102. package/templates/nextjs/base/src/ui/product/review-form.tsx +294 -294
  103. package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +108 -108
  104. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +61 -61
  105. package/templates/nextjs/base/src/ui/product/variant-selector.tsx +291 -291
  106. package/templates/nextjs/base/src/ui/shared/cdn-image.tsx +26 -26
  107. package/templates/nextjs/base/tailwind.config.ts +69 -69
  108. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +10 -0
  109. package/templates/nextjs/designs/atelier/globals.css +402 -402
  110. package/templates/nextjs/designs/atelier/messages-patch/en.json +100 -100
  111. package/templates/nextjs/designs/atelier/messages-patch/he.json +100 -100
  112. package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +110 -110
  113. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -163
  114. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -140
  115. package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -34
  116. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -107
  117. package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +123 -123
  118. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -147
  119. package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -142
  120. package/templates/nextjs/designs/atelier/ui/cart/free-shipping-bar.tsx +77 -77
  121. package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +89 -89
  122. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -34
  123. package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +151 -151
  124. package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -27
  125. package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +75 -75
  126. package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -36
  127. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +174 -174
  128. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -54
  129. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -78
  130. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -46
  131. package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -118
  132. package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -96
  133. package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -36
  134. package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -36
  135. package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -137
  136. package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -14
  137. package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -34
  138. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -141
  139. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -138
  140. package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -466
  141. package/templates/nextjs/designs/atelier/ui/product/discount-badge.tsx +17 -17
  142. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +196 -196
  143. package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -193
  144. package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -69
  145. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +13 -11
  146. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +477 -477
  147. package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +42 -42
  148. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -260
  149. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -95
  150. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +267 -267
  151. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -148
  152. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -66
  153. package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -206
  154. package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -29
  155. package/templates/nextjs/designs/atelier/ui/shared/fly-to-cart.ts +86 -86
  156. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +276 -276
  157. package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +63 -63
  158. package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -71
  159. package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -125
  160. package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +89 -89
  161. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +123 -123
  162. package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +27 -27
  163. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +99 -99
  164. package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +105 -105
  165. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +106 -106
  166. package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +119 -119
  167. package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +54 -54
  168. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +81 -81
  169. package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +19 -19
  170. package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +23 -23
  171. package/templates/nextjs/ui-canvas/home/hero-section.tsx +21 -21
  172. package/templates/nextjs/ui-canvas/home/home-client.tsx +26 -26
  173. package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +101 -101
  174. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +71 -71
  175. package/templates/nextjs/ui-canvas/layout/header-account.tsx +31 -31
  176. package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +76 -76
  177. package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +36 -36
  178. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +82 -82
  179. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +119 -119
  180. package/templates/nextjs/ui-canvas/product/customization-fields.tsx +445 -445
  181. package/templates/nextjs/ui-canvas/product/discount-badge.tsx +19 -19
  182. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +168 -168
  183. package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +165 -165
  184. package/templates/nextjs/ui-canvas/product/price-display.tsx +52 -52
  185. package/templates/nextjs/ui-canvas/product/product-card.tsx +7 -2
  186. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +352 -352
  187. package/templates/nextjs/ui-canvas/product/product-grid.tsx +27 -27
  188. package/templates/nextjs/ui-canvas/product/product-listing.tsx +224 -224
  189. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +99 -99
  190. package/templates/nextjs/ui-canvas/product/review-form.tsx +266 -266
  191. package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +96 -96
  192. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +45 -45
  193. package/templates/nextjs/ui-canvas/product/variant-selector.tsx +212 -212
  194. package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +31 -31
  195. package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +27 -27
  196. 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
+ }