create-brainerce-store 1.14.3 → 1.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +47 -4
- package/messages/en.json +34 -3
- package/messages/he.json +34 -3
- package/package.json +1 -1
- package/templates/nextjs/base/src/app/account/layout.tsx +9 -0
- package/templates/nextjs/base/src/app/account/page.tsx +122 -112
- package/templates/nextjs/base/src/app/cart/layout.tsx +9 -0
- package/templates/nextjs/base/src/app/checkout/layout.tsx +9 -0
- package/templates/nextjs/base/src/app/checkout/page.tsx +101 -3
- package/templates/nextjs/base/src/app/layout.tsx.ejs +29 -1
- package/templates/nextjs/base/src/app/login/layout.tsx +9 -0
- package/templates/nextjs/base/src/app/order-confirmation/layout.tsx +9 -0
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +5 -1
- package/templates/nextjs/base/src/app/products/layout.tsx +18 -0
- package/templates/nextjs/base/src/app/products/page.tsx +1 -0
- package/templates/nextjs/base/src/app/register/layout.tsx +9 -0
- package/templates/nextjs/base/src/app/register/page.tsx +1 -0
- package/templates/nextjs/base/src/app/verify-email/page.tsx +1 -1
- package/templates/nextjs/base/src/components/account/address-book.tsx +432 -0
- package/templates/nextjs/base/src/components/auth/register-form.tsx +232 -184
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +359 -305
- package/templates/nextjs/base/src/components/products/product-card.tsx +26 -4
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +40 -7
- package/templates/nextjs/base/src/lib/auth.ts +1 -0
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "create-brainerce-store",
|
|
34
|
-
version: "1.14.
|
|
34
|
+
version: "1.14.4",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
36
|
bin: {
|
|
37
37
|
"create-brainerce-store": "dist/index.js"
|
|
@@ -412,12 +412,22 @@ async function fetchStoreInfo(connectionId, baseUrl = "https://api.brainerce.com
|
|
|
412
412
|
// src/utils/logger.ts
|
|
413
413
|
var import_chalk = __toESM(require("chalk"));
|
|
414
414
|
var logger = {
|
|
415
|
-
banner() {
|
|
415
|
+
banner(version) {
|
|
416
416
|
console.log();
|
|
417
|
-
console.log(
|
|
417
|
+
console.log(
|
|
418
|
+
import_chalk.default.bold.cyan(" create-brainerce-store") + (version ? import_chalk.default.dim(` v${version}`) : "")
|
|
419
|
+
);
|
|
418
420
|
console.log(import_chalk.default.dim(" Scaffold a production-ready e-commerce storefront"));
|
|
419
421
|
console.log();
|
|
420
422
|
},
|
|
423
|
+
updateAvailable(latest, current) {
|
|
424
|
+
console.log();
|
|
425
|
+
console.log(
|
|
426
|
+
import_chalk.default.yellow(` Update available: `) + import_chalk.default.dim(current) + import_chalk.default.yellow(" \u2192 ") + import_chalk.default.green.bold(latest)
|
|
427
|
+
);
|
|
428
|
+
console.log(import_chalk.default.dim(" Run: ") + import_chalk.default.white("npm install -g create-brainerce-store"));
|
|
429
|
+
console.log();
|
|
430
|
+
},
|
|
421
431
|
info(message) {
|
|
422
432
|
console.log(import_chalk.default.cyan(message));
|
|
423
433
|
},
|
|
@@ -446,10 +456,41 @@ function createSpinner(text) {
|
|
|
446
456
|
|
|
447
457
|
// src/index.ts
|
|
448
458
|
var pkg = require_package();
|
|
459
|
+
async function checkForUpdate(name, current) {
|
|
460
|
+
try {
|
|
461
|
+
const https2 = require("https");
|
|
462
|
+
return await new Promise((resolve) => {
|
|
463
|
+
const req = https2.get(
|
|
464
|
+
`https://registry.npmjs.org/${name}/latest`,
|
|
465
|
+
{ timeout: 3e3 },
|
|
466
|
+
(res) => {
|
|
467
|
+
let data = "";
|
|
468
|
+
res.on("data", (chunk) => data += chunk);
|
|
469
|
+
res.on("end", () => {
|
|
470
|
+
try {
|
|
471
|
+
const latest = JSON.parse(data).version;
|
|
472
|
+
resolve(latest && latest !== current ? latest : null);
|
|
473
|
+
} catch {
|
|
474
|
+
resolve(null);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
req.on("error", () => resolve(null));
|
|
480
|
+
req.on("timeout", () => {
|
|
481
|
+
req.destroy();
|
|
482
|
+
resolve(null);
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
} catch {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
449
489
|
var program = new import_commander.Command();
|
|
450
490
|
program.name("create-brainerce-store").description("Scaffold a production-ready e-commerce storefront connected to Brainerce").version(pkg.version).argument("[project-name]", "Name for the project directory").option("--connection-id <id>", "Brainerce vibe-coded connection ID (vc_*)").option("--language <lang>", "Store language (en, he)").option("--framework <framework>", "Framework to use", "nextjs").option("--theme <theme>", "Theme to apply", "minimal").option("--pkg-manager <manager>", "Package manager (npm, pnpm, yarn, bun)").option("--no-git", "Skip git initialization").option("--no-install", "Skip dependency installation").action(async (projectNameArg, options) => {
|
|
451
491
|
try {
|
|
452
|
-
logger.banner();
|
|
492
|
+
logger.banner(pkg.version);
|
|
493
|
+
const updateCheck = checkForUpdate(pkg.name, pkg.version);
|
|
453
494
|
let projectName = projectNameArg;
|
|
454
495
|
let connectionId = options.connectionId;
|
|
455
496
|
let language = options.language;
|
|
@@ -547,6 +588,8 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
|
|
|
547
588
|
logger.step(`${pkgManager}${pkgManager === "npm" ? " run" : ""} dev`);
|
|
548
589
|
logger.info(`
|
|
549
590
|
Your store will be running at http://localhost:3000`);
|
|
591
|
+
const latestVersion = await updateCheck;
|
|
592
|
+
if (latestVersion) logger.updateAvailable(latestVersion, pkg.version);
|
|
550
593
|
} catch (err) {
|
|
551
594
|
if (err instanceof Error && err.message === "PROMPT_CANCELLED") {
|
|
552
595
|
logger.info("\nSetup cancelled.");
|
package/messages/en.json
CHANGED
|
@@ -174,7 +174,11 @@
|
|
|
174
174
|
"addressRequired": "Address is required",
|
|
175
175
|
"cityRequired": "City is required",
|
|
176
176
|
"postalCodeRequired": "Postal code is required",
|
|
177
|
-
"countryRequired": "Country is required"
|
|
177
|
+
"countryRequired": "Country is required",
|
|
178
|
+
"privacyAcceptPrefix": "I have read and agree to the",
|
|
179
|
+
"privacyPolicyLink": "Privacy Policy",
|
|
180
|
+
"privacyRequired": "You must accept the privacy policy to continue",
|
|
181
|
+
"acceptsMarketing": "Send me news, promotions, and updates by email"
|
|
178
182
|
},
|
|
179
183
|
"auth": {
|
|
180
184
|
"loginPageTitle": "Sign In",
|
|
@@ -240,7 +244,11 @@
|
|
|
240
244
|
"passwordResetSuccess": "Password reset successfully! Redirecting to login...",
|
|
241
245
|
"passwordsMustMatch": "Passwords must match",
|
|
242
246
|
"invalidResetLink": "Invalid reset link",
|
|
243
|
-
"invalidResetLinkDesc": "This password reset link is invalid or has expired. Please request a new one."
|
|
247
|
+
"invalidResetLinkDesc": "This password reset link is invalid or has expired. Please request a new one.",
|
|
248
|
+
"privacyAcceptPrefix": "I have read and agree to the",
|
|
249
|
+
"privacyPolicyLink": "Privacy Policy",
|
|
250
|
+
"privacyRequired": "You must accept the privacy policy to continue",
|
|
251
|
+
"acceptsMarketing": "Send me news, promotions, and updates by email"
|
|
244
252
|
},
|
|
245
253
|
"account": {
|
|
246
254
|
"pageTitle": "My Account",
|
|
@@ -273,7 +281,30 @@
|
|
|
273
281
|
"downloadsRemaining": "{used} of {limit} downloads used",
|
|
274
282
|
"unlimitedDownloads": "Unlimited downloads",
|
|
275
283
|
"expiresAt": "Expires {date}",
|
|
276
|
-
"noExpiry": "No expiry"
|
|
284
|
+
"noExpiry": "No expiry",
|
|
285
|
+
"addressBook": "Address Book",
|
|
286
|
+
"addAddress": "Add Address",
|
|
287
|
+
"editAddress": "Edit Address",
|
|
288
|
+
"deleteAddress": "Delete",
|
|
289
|
+
"setDefault": "Set as default",
|
|
290
|
+
"defaultAddress": "Default",
|
|
291
|
+
"noAddresses": "No saved addresses yet.",
|
|
292
|
+
"addressSaved": "Address saved",
|
|
293
|
+
"addressDeleted": "Address deleted",
|
|
294
|
+
"addressLabel": "Label (e.g. Home, Work)",
|
|
295
|
+
"line1": "Street Address",
|
|
296
|
+
"line2": "Apt, Suite, etc.",
|
|
297
|
+
"city": "City",
|
|
298
|
+
"region": "State / Region",
|
|
299
|
+
"postalCode": "Postal Code",
|
|
300
|
+
"country": "Country",
|
|
301
|
+
"isDefault": "Set as my default address"
|
|
302
|
+
},
|
|
303
|
+
"checkoutAddress": {
|
|
304
|
+
"saveToProfile": "Save this address to my profile?",
|
|
305
|
+
"saveYes": "Save",
|
|
306
|
+
"saveNo": "No thanks",
|
|
307
|
+
"addressSaved": "Address saved to your profile"
|
|
277
308
|
},
|
|
278
309
|
"orderConfirmation": {
|
|
279
310
|
"pageTitle": "Order Confirmation",
|
package/messages/he.json
CHANGED
|
@@ -174,7 +174,11 @@
|
|
|
174
174
|
"addressRequired": "כתובת היא שדה חובה",
|
|
175
175
|
"cityRequired": "עיר היא שדה חובה",
|
|
176
176
|
"postalCodeRequired": "מיקוד הוא שדה חובה",
|
|
177
|
-
"countryRequired": "מדינה היא שדה חובה"
|
|
177
|
+
"countryRequired": "מדינה היא שדה חובה",
|
|
178
|
+
"privacyAcceptPrefix": "קראתי ואני מסכים/ה ל",
|
|
179
|
+
"privacyPolicyLink": "מדיניות הפרטיות",
|
|
180
|
+
"privacyRequired": "יש לאשר את מדיניות הפרטיות כדי להמשיך",
|
|
181
|
+
"acceptsMarketing": "שלחו לי חדשות, מבצעים ועדכונים במייל"
|
|
178
182
|
},
|
|
179
183
|
"auth": {
|
|
180
184
|
"loginPageTitle": "התחברות",
|
|
@@ -240,7 +244,11 @@
|
|
|
240
244
|
"passwordResetSuccess": "...הסיסמא אופסה בהצלחה! מעביר להתחברות",
|
|
241
245
|
"passwordsMustMatch": "הסיסמאות חייבות להיות זהות",
|
|
242
246
|
"invalidResetLink": "קישור איפוס לא תקין",
|
|
243
|
-
"invalidResetLinkDesc": "קישור האיפוס הזה אינו תקין או שפג תוקפו. בקשו קישור חדש."
|
|
247
|
+
"invalidResetLinkDesc": "קישור האיפוס הזה אינו תקין או שפג תוקפו. בקשו קישור חדש.",
|
|
248
|
+
"privacyAcceptPrefix": "קראתי ואני מסכים/ה ל",
|
|
249
|
+
"privacyPolicyLink": "מדיניות הפרטיות",
|
|
250
|
+
"privacyRequired": "יש לאשר את מדיניות הפרטיות כדי להמשיך",
|
|
251
|
+
"acceptsMarketing": "שלחו לי חדשות, מבצעים ועדכונים במייל"
|
|
244
252
|
},
|
|
245
253
|
"account": {
|
|
246
254
|
"pageTitle": "החשבון שלי",
|
|
@@ -273,7 +281,30 @@
|
|
|
273
281
|
"downloadsRemaining": "{used} מתוך {limit} הורדות נוצלו",
|
|
274
282
|
"unlimitedDownloads": "הורדות ללא הגבלה",
|
|
275
283
|
"expiresAt": "פג תוקף {date}",
|
|
276
|
-
"noExpiry": "ללא תפוגה"
|
|
284
|
+
"noExpiry": "ללא תפוגה",
|
|
285
|
+
"addressBook": "ספר כתובות",
|
|
286
|
+
"addAddress": "הוסף כתובת",
|
|
287
|
+
"editAddress": "עריכת כתובת",
|
|
288
|
+
"deleteAddress": "מחיקה",
|
|
289
|
+
"setDefault": "הגדר כברירת מחדל",
|
|
290
|
+
"defaultAddress": "ברירת מחדל",
|
|
291
|
+
"noAddresses": "אין כתובות שמורות עדיין.",
|
|
292
|
+
"addressSaved": "הכתובת נשמרה",
|
|
293
|
+
"addressDeleted": "הכתובת נמחקה",
|
|
294
|
+
"addressLabel": "תווית (למשל בית, עבודה)",
|
|
295
|
+
"line1": "כתובת",
|
|
296
|
+
"line2": "דירה, קומה וכו'",
|
|
297
|
+
"city": "עיר",
|
|
298
|
+
"region": "מדינה / אזור",
|
|
299
|
+
"postalCode": "מיקוד",
|
|
300
|
+
"country": "מדינה",
|
|
301
|
+
"isDefault": "הגדר ככתובת ברירת המחדל שלי"
|
|
302
|
+
},
|
|
303
|
+
"checkoutAddress": {
|
|
304
|
+
"saveToProfile": "לשמור כתובת זו בפרופיל שלי?",
|
|
305
|
+
"saveYes": "שמור",
|
|
306
|
+
"saveNo": "לא תודה",
|
|
307
|
+
"addressSaved": "הכתובת נשמרה בפרופיל שלך"
|
|
277
308
|
},
|
|
278
309
|
"orderConfirmation": {
|
|
279
310
|
"pageTitle": "אישור הזמנה",
|
package/package.json
CHANGED
|
@@ -1,112 +1,122 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
|
-
import { useRouter } from 'next/navigation';
|
|
5
|
-
import type { CustomerProfile, Order } from 'brainerce';
|
|
6
|
-
import { getClient } from '@/lib/brainerce';
|
|
7
|
-
import { useAuth } from '@/providers/store-provider';
|
|
8
|
-
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
9
|
-
import { ProfileSection } from '@/components/account/profile-section';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const [
|
|
20
|
-
const [
|
|
21
|
-
const [
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
client.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
<
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
{
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import type { CustomerProfile, Order } from 'brainerce';
|
|
6
|
+
import { getClient } from '@/lib/brainerce';
|
|
7
|
+
import { useAuth } from '@/providers/store-provider';
|
|
8
|
+
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
9
|
+
import { ProfileSection } from '@/components/account/profile-section';
|
|
10
|
+
import { AddressBook } from '@/components/account/address-book';
|
|
11
|
+
import { OrderHistory } from '@/components/account/order-history';
|
|
12
|
+
import { useTranslations } from '@/lib/translations';
|
|
13
|
+
|
|
14
|
+
export default function AccountPage() {
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
const { isLoggedIn, authLoading, logout } = useAuth();
|
|
17
|
+
const t = useTranslations('account');
|
|
18
|
+
const tc = useTranslations('common');
|
|
19
|
+
const [profile, setProfile] = useState<CustomerProfile | null>(null);
|
|
20
|
+
const [orders, setOrders] = useState<Order[]>([]);
|
|
21
|
+
const [loading, setLoading] = useState(true);
|
|
22
|
+
const [error, setError] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (authLoading) return;
|
|
26
|
+
|
|
27
|
+
if (!isLoggedIn) {
|
|
28
|
+
router.push('/login');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function loadAccountData() {
|
|
33
|
+
try {
|
|
34
|
+
const client = getClient();
|
|
35
|
+
const [profileResult, ordersResult] = await Promise.allSettled([
|
|
36
|
+
client.getMyProfile(),
|
|
37
|
+
client.getMyOrders({ limit: 20 }),
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
if (profileResult.status === 'fulfilled') {
|
|
41
|
+
setProfile(profileResult.value);
|
|
42
|
+
} else {
|
|
43
|
+
setError('Failed to load profile.');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (ordersResult.status === 'fulfilled') {
|
|
47
|
+
setOrders(ordersResult.value.data);
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const message = err instanceof Error ? err.message : 'Failed to load account data.';
|
|
51
|
+
setError(message);
|
|
52
|
+
} finally {
|
|
53
|
+
setLoading(false);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
loadAccountData();
|
|
58
|
+
}, [isLoggedIn, authLoading, router]);
|
|
59
|
+
|
|
60
|
+
if (authLoading || !isLoggedIn) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (loading) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex min-h-[60vh] items-center justify-center">
|
|
67
|
+
<LoadingSpinner size="lg" />
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (error && !profile) {
|
|
73
|
+
return (
|
|
74
|
+
<div className="mx-auto max-w-3xl px-4 py-16 text-center sm:px-6 lg:px-8">
|
|
75
|
+
<h1 className="text-foreground text-2xl font-bold">{tc('error')}</h1>
|
|
76
|
+
<p className="text-muted-foreground mt-2">{error}</p>
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
onClick={() => window.location.reload()}
|
|
80
|
+
className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
|
|
81
|
+
>
|
|
82
|
+
{tc('tryAgain')}
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
|
|
90
|
+
<div className="mb-6 flex items-center justify-between">
|
|
91
|
+
<h1 className="text-foreground text-2xl font-bold">{t('myAccount')}</h1>
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
onClick={logout}
|
|
95
|
+
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
|
96
|
+
>
|
|
97
|
+
{t('signOut')}
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* Profile Section */}
|
|
102
|
+
{profile && (
|
|
103
|
+
<ProfileSection profile={profile} onProfileUpdate={setProfile} className="mb-6" />
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{/* Address Book */}
|
|
107
|
+
{profile && (
|
|
108
|
+
<AddressBook
|
|
109
|
+
addresses={profile.addresses}
|
|
110
|
+
onUpdate={(updated) => setProfile((p) => (p ? { ...p, addresses: updated } : p))}
|
|
111
|
+
className="mb-8"
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{/* Order History */}
|
|
116
|
+
<div>
|
|
117
|
+
<h2 className="text-foreground mb-4 text-lg font-semibold">{t('orderHistory')}</h2>
|
|
118
|
+
<OrderHistory orders={orders} />
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
} from 'brainerce';
|
|
14
14
|
import { formatPrice } from 'brainerce';
|
|
15
15
|
import { getClient } from '@/lib/brainerce';
|
|
16
|
-
import { useStoreInfo, useCart } from '@/providers/store-provider';
|
|
16
|
+
import { useStoreInfo, useCart, useAuth } from '@/providers/store-provider';
|
|
17
17
|
import { CheckoutForm } from '@/components/checkout/checkout-form';
|
|
18
18
|
import { ShippingStep } from '@/components/checkout/shipping-step';
|
|
19
19
|
import { PaymentStep } from '@/components/checkout/payment-step';
|
|
@@ -32,9 +32,11 @@ function CheckoutContent() {
|
|
|
32
32
|
const searchParams = useSearchParams();
|
|
33
33
|
const { storeInfo } = useStoreInfo();
|
|
34
34
|
const { cart, refreshCart } = useCart();
|
|
35
|
+
const { isLoggedIn } = useAuth();
|
|
35
36
|
const currency = storeInfo?.currency || 'USD';
|
|
36
37
|
const t = useTranslations('checkout');
|
|
37
38
|
const tc = useTranslations('common');
|
|
39
|
+
const tAddr = useTranslations('checkoutAddress');
|
|
38
40
|
|
|
39
41
|
const [step, setStep] = useState<CheckoutStep>('address');
|
|
40
42
|
const [checkout, setCheckout] = useState<Checkout | null>(null);
|
|
@@ -47,11 +49,28 @@ function CheckoutContent() {
|
|
|
47
49
|
const [pickupLocations, setPickupLocations] = useState<PickupLocation[]>([]);
|
|
48
50
|
const [deliveryType, setDeliveryType] = useState<'shipping' | 'pickup'>('shipping');
|
|
49
51
|
const [isAllDigital, setIsAllDigital] = useState(false);
|
|
52
|
+
const [prefillAddress, setPrefillAddress] = useState<SetShippingAddressDto | null>(null);
|
|
53
|
+
const [lastSubmittedAddress, setLastSubmittedAddress] = useState<SetShippingAddressDto | null>(
|
|
54
|
+
null
|
|
55
|
+
);
|
|
56
|
+
const [showSavePrompt, setShowSavePrompt] = useState(false);
|
|
57
|
+
const [savingAddress, setSavingAddress] = useState(false);
|
|
50
58
|
|
|
51
59
|
// Check for returning from canceled payment
|
|
52
60
|
const canceled = searchParams.get('canceled') === 'true';
|
|
53
61
|
const existingCheckoutId = searchParams.get('checkout_id');
|
|
54
62
|
|
|
63
|
+
// Pre-fill address from customer profile when logged in
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!isLoggedIn) return;
|
|
66
|
+
getClient()
|
|
67
|
+
.getCheckoutPrefillData()
|
|
68
|
+
.then((data) => {
|
|
69
|
+
if (data.shippingAddress) setPrefillAddress(data.shippingAddress);
|
|
70
|
+
})
|
|
71
|
+
.catch(() => {});
|
|
72
|
+
}, [isLoggedIn]);
|
|
73
|
+
|
|
55
74
|
// Initialize or resume checkout
|
|
56
75
|
const initCheckout = useCallback(async () => {
|
|
57
76
|
try {
|
|
@@ -134,7 +153,10 @@ function CheckoutContent() {
|
|
|
134
153
|
}, [cartLoaded, initCheckout]);
|
|
135
154
|
|
|
136
155
|
// Handle shipping address submission
|
|
137
|
-
async function handleAddressSubmit(
|
|
156
|
+
async function handleAddressSubmit(
|
|
157
|
+
address: SetShippingAddressDto,
|
|
158
|
+
consent: { acceptsMarketing: boolean }
|
|
159
|
+
) {
|
|
138
160
|
if (!checkout) return;
|
|
139
161
|
|
|
140
162
|
try {
|
|
@@ -145,6 +167,22 @@ function CheckoutContent() {
|
|
|
145
167
|
const response = await client.setShippingAddress(checkout.id, address);
|
|
146
168
|
setCheckout(response.checkout);
|
|
147
169
|
setShippingRates(response.rates);
|
|
170
|
+
|
|
171
|
+
// Update marketing preference for logged-in users
|
|
172
|
+
if (isLoggedIn) {
|
|
173
|
+
try {
|
|
174
|
+
await client.updateMyProfile({ acceptsMarketing: consent.acceptsMarketing });
|
|
175
|
+
} catch {
|
|
176
|
+
// non-critical
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Offer to save address to profile if logged in and no prefill (new address)
|
|
181
|
+
if (isLoggedIn && !prefillAddress) {
|
|
182
|
+
setLastSubmittedAddress(address);
|
|
183
|
+
setShowSavePrompt(true);
|
|
184
|
+
}
|
|
185
|
+
|
|
148
186
|
setStep('shipping');
|
|
149
187
|
} catch (err) {
|
|
150
188
|
const message = err instanceof Error ? err.message : t('failedToSaveAddress');
|
|
@@ -154,6 +192,30 @@ function CheckoutContent() {
|
|
|
154
192
|
}
|
|
155
193
|
}
|
|
156
194
|
|
|
195
|
+
async function handleSaveAddressToProfile() {
|
|
196
|
+
if (!lastSubmittedAddress) return;
|
|
197
|
+
setSavingAddress(true);
|
|
198
|
+
try {
|
|
199
|
+
await getClient().addMyAddress({
|
|
200
|
+
firstName: lastSubmittedAddress.firstName,
|
|
201
|
+
lastName: lastSubmittedAddress.lastName,
|
|
202
|
+
line1: lastSubmittedAddress.line1,
|
|
203
|
+
line2: lastSubmittedAddress.line2,
|
|
204
|
+
city: lastSubmittedAddress.city,
|
|
205
|
+
region: lastSubmittedAddress.region,
|
|
206
|
+
postalCode: lastSubmittedAddress.postalCode,
|
|
207
|
+
country: lastSubmittedAddress.country,
|
|
208
|
+
phone: lastSubmittedAddress.phone,
|
|
209
|
+
isDefault: true,
|
|
210
|
+
});
|
|
211
|
+
} catch {
|
|
212
|
+
// ignore
|
|
213
|
+
} finally {
|
|
214
|
+
setSavingAddress(false);
|
|
215
|
+
setShowSavePrompt(false);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
157
219
|
// Handle shipping method selection
|
|
158
220
|
async function handleShippingSelect(rateId: string) {
|
|
159
221
|
if (!checkout) return;
|
|
@@ -405,6 +467,29 @@ function CheckoutContent() {
|
|
|
405
467
|
</button>
|
|
406
468
|
)}
|
|
407
469
|
</div>
|
|
470
|
+
{/* Save-to-profile prompt */}
|
|
471
|
+
{showSavePrompt && (
|
|
472
|
+
<div className="border-border bg-muted/50 mb-4 flex items-center justify-between gap-3 rounded-lg border px-4 py-3 text-sm">
|
|
473
|
+
<span className="text-foreground">{tAddr('saveToProfile')}</span>
|
|
474
|
+
<div className="flex gap-2">
|
|
475
|
+
<button
|
|
476
|
+
type="button"
|
|
477
|
+
onClick={handleSaveAddressToProfile}
|
|
478
|
+
disabled={savingAddress}
|
|
479
|
+
className="bg-primary text-primary-foreground rounded px-3 py-1 text-xs font-medium transition-opacity hover:opacity-90 disabled:opacity-50"
|
|
480
|
+
>
|
|
481
|
+
{savingAddress ? '...' : tAddr('saveYes')}
|
|
482
|
+
</button>
|
|
483
|
+
<button
|
|
484
|
+
type="button"
|
|
485
|
+
onClick={() => setShowSavePrompt(false)}
|
|
486
|
+
className="text-muted-foreground hover:text-foreground rounded px-3 py-1 text-xs transition-colors"
|
|
487
|
+
>
|
|
488
|
+
{tAddr('saveNo')}
|
|
489
|
+
</button>
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
)}
|
|
408
493
|
<CheckoutForm
|
|
409
494
|
onSubmit={handleAddressSubmit}
|
|
410
495
|
loading={loading}
|
|
@@ -423,7 +508,20 @@ function CheckoutContent() {
|
|
|
423
508
|
country: checkout.shippingAddress.country,
|
|
424
509
|
phone: checkout.shippingAddress.phone || '',
|
|
425
510
|
}
|
|
426
|
-
:
|
|
511
|
+
: prefillAddress
|
|
512
|
+
? {
|
|
513
|
+
email: prefillAddress.email,
|
|
514
|
+
firstName: prefillAddress.firstName,
|
|
515
|
+
lastName: prefillAddress.lastName,
|
|
516
|
+
line1: prefillAddress.line1,
|
|
517
|
+
line2: prefillAddress.line2 || '',
|
|
518
|
+
city: prefillAddress.city,
|
|
519
|
+
region: prefillAddress.region || '',
|
|
520
|
+
postalCode: prefillAddress.postalCode,
|
|
521
|
+
country: prefillAddress.country,
|
|
522
|
+
phone: prefillAddress.phone || '',
|
|
523
|
+
}
|
|
524
|
+
: undefined
|
|
427
525
|
}
|
|
428
526
|
/>
|
|
429
527
|
</div>
|