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 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.3",
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(import_chalk.default.bold.cyan(" create-brainerce-store"));
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,6 +1,6 @@
1
1
  {
2
2
  "name": "create-brainerce-store",
3
- "version": "1.14.3",
3
+ "version": "1.14.4",
4
4
  "description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
5
5
  "bin": {
6
6
  "create-brainerce-store": "dist/index.js"
@@ -0,0 +1,9 @@
1
+ import type { Metadata } from 'next';
2
+
3
+ export const metadata: Metadata = {
4
+ robots: { index: false, follow: false },
5
+ };
6
+
7
+ export default function Layout({ children }: { children: React.ReactNode }) {
8
+ return <>{children}</>;
9
+ }
@@ -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 { OrderHistory } from '@/components/account/order-history';
11
- import { useTranslations } from '@/lib/translations';
12
-
13
- export default function AccountPage() {
14
- const router = useRouter();
15
- const { isLoggedIn, authLoading, logout } = useAuth();
16
- const t = useTranslations('account');
17
- const tc = useTranslations('common');
18
- const [profile, setProfile] = useState<CustomerProfile | null>(null);
19
- const [orders, setOrders] = useState<Order[]>([]);
20
- const [loading, setLoading] = useState(true);
21
- const [error, setError] = useState<string | null>(null);
22
-
23
- useEffect(() => {
24
- if (authLoading) return;
25
-
26
- if (!isLoggedIn) {
27
- router.push('/login');
28
- return;
29
- }
30
-
31
- async function loadAccountData() {
32
- try {
33
- const client = getClient();
34
- const [profileResult, ordersResult] = await Promise.allSettled([
35
- client.getMyProfile(),
36
- client.getMyOrders({ limit: 20 }),
37
- ]);
38
-
39
- if (profileResult.status === 'fulfilled') {
40
- setProfile(profileResult.value);
41
- } else {
42
- setError('Failed to load profile.');
43
- }
44
-
45
- if (ordersResult.status === 'fulfilled') {
46
- setOrders(ordersResult.value.data);
47
- }
48
- } catch (err) {
49
- const message = err instanceof Error ? err.message : 'Failed to load account data.';
50
- setError(message);
51
- } finally {
52
- setLoading(false);
53
- }
54
- }
55
-
56
- loadAccountData();
57
- }, [isLoggedIn, authLoading, router]);
58
-
59
- if (authLoading || !isLoggedIn) {
60
- return null;
61
- }
62
-
63
- if (loading) {
64
- return (
65
- <div className="flex min-h-[60vh] items-center justify-center">
66
- <LoadingSpinner size="lg" />
67
- </div>
68
- );
69
- }
70
-
71
- if (error && !profile) {
72
- return (
73
- <div className="mx-auto max-w-3xl px-4 py-16 text-center sm:px-6 lg:px-8">
74
- <h1 className="text-foreground text-2xl font-bold">{tc('error')}</h1>
75
- <p className="text-muted-foreground mt-2">{error}</p>
76
- <button
77
- type="button"
78
- onClick={() => window.location.reload()}
79
- className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
80
- >
81
- {tc('tryAgain')}
82
- </button>
83
- </div>
84
- );
85
- }
86
-
87
- return (
88
- <div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
89
- <div className="mb-6 flex items-center justify-between">
90
- <h1 className="text-foreground text-2xl font-bold">{t('myAccount')}</h1>
91
- <button
92
- type="button"
93
- onClick={logout}
94
- className="text-muted-foreground hover:text-foreground text-sm transition-colors"
95
- >
96
- {t('signOut')}
97
- </button>
98
- </div>
99
-
100
- {/* Profile Section */}
101
- {profile && (
102
- <ProfileSection profile={profile} onProfileUpdate={setProfile} className="mb-8" />
103
- )}
104
-
105
- {/* Order History */}
106
- <div>
107
- <h2 className="text-foreground mb-4 text-lg font-semibold">{t('orderHistory')}</h2>
108
- <OrderHistory orders={orders} />
109
- </div>
110
- </div>
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
+ }
@@ -0,0 +1,9 @@
1
+ import type { Metadata } from 'next';
2
+
3
+ export const metadata: Metadata = {
4
+ robots: { index: false, follow: false },
5
+ };
6
+
7
+ export default function Layout({ children }: { children: React.ReactNode }) {
8
+ return <>{children}</>;
9
+ }
@@ -0,0 +1,9 @@
1
+ import type { Metadata } from 'next';
2
+
3
+ export const metadata: Metadata = {
4
+ robots: { index: false, follow: false },
5
+ };
6
+
7
+ export default function Layout({ children }: { children: React.ReactNode }) {
8
+ return <>{children}</>;
9
+ }
@@ -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(address: SetShippingAddressDto) {
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
- : undefined
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>