create-brainerce-store 1.62.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 +5 -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 -612
  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,278 +1,295 @@
1
- 'use client';
2
-
3
- import { Suspense, useEffect, useState } from 'react';
4
- import { useSearchParams } from 'next/navigation';
5
- import { Link } from '@/core/lib/navigation';
6
- import type { WaitForOrderResult, OrderDownloadLink } from 'brainerce';
7
- import { getClient } from '@/core/lib/brainerce';
8
- import { useCart } from '@/core/providers/store-provider';
9
- import { LoadingSpinner } from '@/ui/shared/loading-spinner';
10
- import { useTranslations } from '@/core/lib/translations';
11
-
12
- function OrderConfirmationContent() {
13
- const searchParams = useSearchParams();
14
- const checkoutId = searchParams.get('checkout_id');
15
-
16
- const { refreshCart } = useCart();
17
- const t = useTranslations('orderConfirmation');
18
- const tc = useTranslations('common');
19
- const [result, setResult] = useState<WaitForOrderResult | null>(null);
20
- const [loading, setLoading] = useState(true);
21
- const [error, setError] = useState<string | null>(null);
22
-
23
- useEffect(() => {
24
- if (!checkoutId) {
25
- setError(t('missingCheckoutInfo'));
26
- setLoading(false);
27
- return;
28
- }
29
-
30
- async function waitForOrder() {
31
- try {
32
- const client = getClient();
33
-
34
- // Clear cart state after successful payment
35
- client.handlePaymentSuccess(checkoutId!);
36
- await refreshCart();
37
-
38
- // For redirect-based payment providers, the customer returns with
39
- // provider params in the URL CardCom: lowprofilecode; PayPal:
40
- // token + PayerID. Send these to the backend for server-side
41
- // verification via the provider's API (e.g. GetLpResult / PayPal
42
- // capture) never trust URL params alone. For PayPal this call is
43
- // what actually captures the approved payment.
44
- const lowProfileCode =
45
- searchParams.get('lowprofilecode') || searchParams.get('LowProfileCode');
46
- const paypalOrderToken =
47
- searchParams.get('token') && searchParams.get('PayerID')
48
- ? searchParams.get('token')
49
- : null;
50
- const redirectReturnToken = lowProfileCode || paypalOrderToken;
51
- if (redirectReturnToken) {
52
- try {
53
- await client.confirmSdkPayment(checkoutId!, {
54
- paymentIntentId: redirectReturnToken,
55
- });
56
- } catch (err) {
57
- console.warn('Redirect payment confirmation failed:', err);
58
- // Don't block — payment-status polling re-verifies server-side
59
- }
60
- }
61
-
62
- const orderResult = await client.waitForOrder(checkoutId!, {
63
- maxWaitMs: 30000,
64
- });
65
- setResult(orderResult);
66
- } catch (err) {
67
- const message = err instanceof Error ? err.message : 'Failed to confirm order';
68
- setError(message);
69
- } finally {
70
- setLoading(false);
71
- }
72
- }
73
-
74
- waitForOrder();
75
- }, [checkoutId, refreshCart]);
76
-
77
- if (loading) {
78
- return (
79
- <div className="flex min-h-[60vh] flex-col items-center justify-center">
80
- <LoadingSpinner size="lg" />
81
- <p className="text-muted-foreground mt-4">{t('confirming')}</p>
82
- <p className="text-muted-foreground mt-1 text-xs">{t('confirmingHint')}</p>
83
- </div>
84
- );
85
- }
86
-
87
- if (error) {
88
- return (
89
- <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
90
- <svg
91
- className="text-destructive mx-auto mb-4 h-16 w-16"
92
- fill="none"
93
- viewBox="0 0 24 24"
94
- stroke="currentColor"
95
- >
96
- <path
97
- strokeLinecap="round"
98
- strokeLinejoin="round"
99
- strokeWidth={1.5}
100
- d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.834-2.694-.834-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"
101
- />
102
- </svg>
103
- <h1 className="text-foreground text-2xl font-bold">{t('errorTitle')}</h1>
104
- <p className="text-muted-foreground mt-2">{error}</p>
105
- <p className="text-muted-foreground mt-1 text-sm">{t('errorChargedHint')}</p>
106
- <Link
107
- href="/"
108
- className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
109
- >
110
- {t('returnHome')}
111
- </Link>
112
- </div>
113
- );
114
- }
115
-
116
- // Order was created successfully
117
- if (result?.success) {
118
- const orderNumber = result.status.orderNumber;
119
-
120
- return (
121
- <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
122
- <svg
123
- className="text-primary mx-auto mb-4 h-16 w-16"
124
- fill="none"
125
- viewBox="0 0 24 24"
126
- stroke="currentColor"
127
- >
128
- <path
129
- strokeLinecap="round"
130
- strokeLinejoin="round"
131
- strokeWidth={1.5}
132
- d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
133
- />
134
- </svg>
135
-
136
- <h1 className="text-foreground text-2xl font-bold">{t('thankYou')}</h1>
137
-
138
- {orderNumber && (
139
- <p className="text-foreground mt-3 text-lg">
140
- {t('orderNumber')} <span className="font-semibold">{orderNumber}</span>
141
- </p>
142
- )}
143
-
144
- <p className="text-muted-foreground mt-2">{t('confirmationEmail')}</p>
145
-
146
- {result.status.orderId && (
147
- <ConfirmationDownloads orderId={result.status.orderId} checkoutId={checkoutId!} />
148
- )}
149
-
150
- <div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
151
- <Link
152
- href="/products"
153
- className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
154
- >
155
- {tc('continueShopping')}
156
- </Link>
157
-
158
- <Link
159
- href="/account"
160
- className="border-border text-foreground hover:bg-muted inline-flex items-center rounded border px-6 py-3 font-medium transition-colors"
161
- >
162
- {t('viewOrders')}
163
- </Link>
164
- </div>
165
- </div>
166
- );
167
- }
168
-
169
- // Order not yet confirmed (polling timed out) - still show success
170
- return (
171
- <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
172
- <svg
173
- className="text-primary mx-auto mb-4 h-16 w-16"
174
- fill="none"
175
- viewBox="0 0 24 24"
176
- stroke="currentColor"
177
- >
178
- <path
179
- strokeLinecap="round"
180
- strokeLinejoin="round"
181
- strokeWidth={1.5}
182
- d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
183
- />
184
- </svg>
185
-
186
- <h1 className="text-foreground text-2xl font-bold">{t('paymentReceived')}</h1>
187
-
188
- <p className="text-muted-foreground mt-2">{t('orderProcessing')}</p>
189
-
190
- <div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
191
- <Link
192
- href="/products"
193
- className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
194
- >
195
- {tc('continueShopping')}
196
- </Link>
197
-
198
- <Link
199
- href="/account"
200
- className="border-border text-foreground hover:bg-muted inline-flex items-center rounded border px-6 py-3 font-medium transition-colors"
201
- >
202
- {t('viewOrders')}
203
- </Link>
204
- </div>
205
- </div>
206
- );
207
- }
208
-
209
- function ConfirmationDownloads({ orderId, checkoutId }: { orderId: string; checkoutId: string }) {
210
- const t = useTranslations('orderConfirmation');
211
- const [downloads, setDownloads] = useState<OrderDownloadLink[] | null>(null);
212
-
213
- useEffect(() => {
214
- let cancelled = false;
215
- async function fetchDownloads() {
216
- const client = getClient();
217
- // Retry a few times the worker may still be writing downloadMeta
218
- for (let attempt = 0; attempt < 3; attempt++) {
219
- try {
220
- const links = await client.getOrderDownloads(orderId, { checkoutId });
221
- if (!cancelled && links.length > 0) {
222
- setDownloads(links);
223
- return;
224
- }
225
- } catch {
226
- // Not all orders have downloads
227
- }
228
- if (attempt < 2 && !cancelled) {
229
- await new Promise((r) => setTimeout(r, 1500));
230
- }
231
- }
232
- }
233
- fetchDownloads();
234
- return () => {
235
- cancelled = true;
236
- };
237
- }, [orderId, checkoutId]);
238
-
239
- if (!downloads || downloads.length === 0) return null;
240
-
241
- return (
242
- <div className="border-border bg-muted/30 mx-auto mt-8 max-w-md rounded-lg border p-6 text-start">
243
- <h3 className="text-foreground mb-3 text-sm font-semibold">{t('yourDownloads')}</h3>
244
- <div className="space-y-2">
245
- {downloads.map((link, idx) => (
246
- <div key={idx} className="flex items-center justify-between gap-3">
247
- <div className="min-w-0 flex-1">
248
- <p className="text-foreground truncate text-sm">{link.fileName}</p>
249
- <p className="text-muted-foreground truncate text-xs">{link.productName}</p>
250
- </div>
251
- <a
252
- href={link.downloadUrl}
253
- target="_blank"
254
- rel="noopener noreferrer"
255
- className="bg-primary text-primary-foreground flex-shrink-0 rounded px-3 py-1.5 text-xs font-medium hover:opacity-90"
256
- >
257
- {t('download')}
258
- </a>
259
- </div>
260
- ))}
261
- </div>
262
- </div>
263
- );
264
- }
265
-
266
- export default function OrderConfirmationPage() {
267
- return (
268
- <Suspense
269
- fallback={
270
- <div className="flex min-h-[60vh] items-center justify-center">
271
- <LoadingSpinner size="lg" />
272
- </div>
273
- }
274
- >
275
- <OrderConfirmationContent />
276
- </Suspense>
277
- );
278
- }
1
+ 'use client';
2
+
3
+ import { Suspense, useEffect, useState } from 'react';
4
+ import { useSearchParams } from 'next/navigation';
5
+ import { Link } from '@/core/lib/navigation';
6
+ import type { WaitForOrderResult, OrderDownloadLink } from 'brainerce';
7
+ import { getClient } from '@/core/lib/brainerce';
8
+ import { useCart } from '@/core/providers/store-provider';
9
+ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
10
+ import { useTranslations } from '@/core/lib/translations';
11
+ import { trackPurchase } from '@/core/lib/tracking';
12
+
13
+ function OrderConfirmationContent() {
14
+ const searchParams = useSearchParams();
15
+ const checkoutId = searchParams.get('checkout_id');
16
+
17
+ const { refreshCart } = useCart();
18
+ const t = useTranslations('orderConfirmation');
19
+ const tc = useTranslations('common');
20
+ const [result, setResult] = useState<WaitForOrderResult | null>(null);
21
+ const [loading, setLoading] = useState(true);
22
+ const [error, setError] = useState<string | null>(null);
23
+
24
+ useEffect(() => {
25
+ if (!checkoutId) {
26
+ setError(t('missingCheckoutInfo'));
27
+ setLoading(false);
28
+ return;
29
+ }
30
+
31
+ async function waitForOrder() {
32
+ try {
33
+ const client = getClient();
34
+
35
+ // Clear cart state after successful payment
36
+ client.handlePaymentSuccess(checkoutId!);
37
+ await refreshCart();
38
+
39
+ // For redirect-based payment providers, the customer returns with
40
+ // provider params in the URL CardCom: lowprofilecode; PayPal:
41
+ // token + PayerID. Send these to the backend for server-side
42
+ // verification via the provider's API (e.g. GetLpResult / PayPal
43
+ // capture) never trust URL params alone. For PayPal this call is
44
+ // what actually captures the approved payment.
45
+ const lowProfileCode =
46
+ searchParams.get('lowprofilecode') || searchParams.get('LowProfileCode');
47
+ const paypalOrderToken =
48
+ searchParams.get('token') && searchParams.get('PayerID')
49
+ ? searchParams.get('token')
50
+ : null;
51
+ const redirectReturnToken = lowProfileCode || paypalOrderToken;
52
+ if (redirectReturnToken) {
53
+ try {
54
+ await client.confirmSdkPayment(checkoutId!, {
55
+ paymentIntentId: redirectReturnToken,
56
+ });
57
+ } catch (err) {
58
+ console.warn('Redirect payment confirmation failed:', err);
59
+ // Don't block — payment-status polling re-verifies server-side
60
+ }
61
+ }
62
+
63
+ const orderResult = await client.waitForOrder(checkoutId!, {
64
+ maxWaitMs: 30000,
65
+ });
66
+ setResult(orderResult);
67
+
68
+ // `purchase` — the one event ad platforms optimize campaigns on.
69
+ // Read the real order rather than the pre-payment cart so value, tax,
70
+ // shipping and coupon match what was actually charged; `trackPurchase`
71
+ // de-duplicates on the order id, so a refresh cannot double-count it.
72
+ if (orderResult.success) {
73
+ try {
74
+ const order = await client.getOrderByCheckout(checkoutId!);
75
+ trackPurchase(order);
76
+ } catch (err) {
77
+ // A conversion we failed to report must never break the page the
78
+ // shopper is looking at — but it should be visible, because a
79
+ // silent gap here looks identical to "no sales" in ad reporting.
80
+ console.warn('Purchase conversion not reported:', err);
81
+ }
82
+ }
83
+ } catch (err) {
84
+ const message = err instanceof Error ? err.message : 'Failed to confirm order';
85
+ setError(message);
86
+ } finally {
87
+ setLoading(false);
88
+ }
89
+ }
90
+
91
+ waitForOrder();
92
+ }, [checkoutId, refreshCart]);
93
+
94
+ if (loading) {
95
+ return (
96
+ <div className="flex min-h-[60vh] flex-col items-center justify-center">
97
+ <LoadingSpinner size="lg" />
98
+ <p className="text-muted-foreground mt-4">{t('confirming')}</p>
99
+ <p className="text-muted-foreground mt-1 text-xs">{t('confirmingHint')}</p>
100
+ </div>
101
+ );
102
+ }
103
+
104
+ if (error) {
105
+ return (
106
+ <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
107
+ <svg
108
+ className="text-destructive mx-auto mb-4 h-16 w-16"
109
+ fill="none"
110
+ viewBox="0 0 24 24"
111
+ stroke="currentColor"
112
+ >
113
+ <path
114
+ strokeLinecap="round"
115
+ strokeLinejoin="round"
116
+ strokeWidth={1.5}
117
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.834-2.694-.834-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"
118
+ />
119
+ </svg>
120
+ <h1 className="text-foreground text-2xl font-bold">{t('errorTitle')}</h1>
121
+ <p className="text-muted-foreground mt-2">{error}</p>
122
+ <p className="text-muted-foreground mt-1 text-sm">{t('errorChargedHint')}</p>
123
+ <Link
124
+ href="/"
125
+ className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
126
+ >
127
+ {t('returnHome')}
128
+ </Link>
129
+ </div>
130
+ );
131
+ }
132
+
133
+ // Order was created successfully
134
+ if (result?.success) {
135
+ const orderNumber = result.status.orderNumber;
136
+
137
+ return (
138
+ <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
139
+ <svg
140
+ className="text-primary mx-auto mb-4 h-16 w-16"
141
+ fill="none"
142
+ viewBox="0 0 24 24"
143
+ stroke="currentColor"
144
+ >
145
+ <path
146
+ strokeLinecap="round"
147
+ strokeLinejoin="round"
148
+ strokeWidth={1.5}
149
+ d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
150
+ />
151
+ </svg>
152
+
153
+ <h1 className="text-foreground text-2xl font-bold">{t('thankYou')}</h1>
154
+
155
+ {orderNumber && (
156
+ <p className="text-foreground mt-3 text-lg">
157
+ {t('orderNumber')} <span className="font-semibold">{orderNumber}</span>
158
+ </p>
159
+ )}
160
+
161
+ <p className="text-muted-foreground mt-2">{t('confirmationEmail')}</p>
162
+
163
+ {result.status.orderId && (
164
+ <ConfirmationDownloads orderId={result.status.orderId} checkoutId={checkoutId!} />
165
+ )}
166
+
167
+ <div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
168
+ <Link
169
+ href="/products"
170
+ className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
171
+ >
172
+ {tc('continueShopping')}
173
+ </Link>
174
+
175
+ <Link
176
+ href="/account"
177
+ className="border-border text-foreground hover:bg-muted inline-flex items-center rounded border px-6 py-3 font-medium transition-colors"
178
+ >
179
+ {t('viewOrders')}
180
+ </Link>
181
+ </div>
182
+ </div>
183
+ );
184
+ }
185
+
186
+ // Order not yet confirmed (polling timed out) - still show success
187
+ return (
188
+ <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
189
+ <svg
190
+ className="text-primary mx-auto mb-4 h-16 w-16"
191
+ fill="none"
192
+ viewBox="0 0 24 24"
193
+ stroke="currentColor"
194
+ >
195
+ <path
196
+ strokeLinecap="round"
197
+ strokeLinejoin="round"
198
+ strokeWidth={1.5}
199
+ d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
200
+ />
201
+ </svg>
202
+
203
+ <h1 className="text-foreground text-2xl font-bold">{t('paymentReceived')}</h1>
204
+
205
+ <p className="text-muted-foreground mt-2">{t('orderProcessing')}</p>
206
+
207
+ <div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
208
+ <Link
209
+ href="/products"
210
+ className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
211
+ >
212
+ {tc('continueShopping')}
213
+ </Link>
214
+
215
+ <Link
216
+ href="/account"
217
+ className="border-border text-foreground hover:bg-muted inline-flex items-center rounded border px-6 py-3 font-medium transition-colors"
218
+ >
219
+ {t('viewOrders')}
220
+ </Link>
221
+ </div>
222
+ </div>
223
+ );
224
+ }
225
+
226
+ function ConfirmationDownloads({ orderId, checkoutId }: { orderId: string; checkoutId: string }) {
227
+ const t = useTranslations('orderConfirmation');
228
+ const [downloads, setDownloads] = useState<OrderDownloadLink[] | null>(null);
229
+
230
+ useEffect(() => {
231
+ let cancelled = false;
232
+ async function fetchDownloads() {
233
+ const client = getClient();
234
+ // Retry a few times — the worker may still be writing downloadMeta
235
+ for (let attempt = 0; attempt < 3; attempt++) {
236
+ try {
237
+ const links = await client.getOrderDownloads(orderId, { checkoutId });
238
+ if (!cancelled && links.length > 0) {
239
+ setDownloads(links);
240
+ return;
241
+ }
242
+ } catch {
243
+ // Not all orders have downloads
244
+ }
245
+ if (attempt < 2 && !cancelled) {
246
+ await new Promise((r) => setTimeout(r, 1500));
247
+ }
248
+ }
249
+ }
250
+ fetchDownloads();
251
+ return () => {
252
+ cancelled = true;
253
+ };
254
+ }, [orderId, checkoutId]);
255
+
256
+ if (!downloads || downloads.length === 0) return null;
257
+
258
+ return (
259
+ <div className="border-border bg-muted/30 mx-auto mt-8 max-w-md rounded-lg border p-6 text-start">
260
+ <h3 className="text-foreground mb-3 text-sm font-semibold">{t('yourDownloads')}</h3>
261
+ <div className="space-y-2">
262
+ {downloads.map((link, idx) => (
263
+ <div key={idx} className="flex items-center justify-between gap-3">
264
+ <div className="min-w-0 flex-1">
265
+ <p className="text-foreground truncate text-sm">{link.fileName}</p>
266
+ <p className="text-muted-foreground truncate text-xs">{link.productName}</p>
267
+ </div>
268
+ <a
269
+ href={link.downloadUrl}
270
+ target="_blank"
271
+ rel="noopener noreferrer"
272
+ className="bg-primary text-primary-foreground flex-shrink-0 rounded px-3 py-1.5 text-xs font-medium hover:opacity-90"
273
+ >
274
+ {t('download')}
275
+ </a>
276
+ </div>
277
+ ))}
278
+ </div>
279
+ </div>
280
+ );
281
+ }
282
+
283
+ export default function OrderConfirmationPage() {
284
+ return (
285
+ <Suspense
286
+ fallback={
287
+ <div className="flex min-h-[60vh] items-center justify-center">
288
+ <LoadingSpinner size="lg" />
289
+ </div>
290
+ }
291
+ >
292
+ <OrderConfirmationContent />
293
+ </Suspense>
294
+ );
295
+ }