create-brainerce-store 1.79.0 → 1.80.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.
- package/dist/index.js +17 -2
- package/messages/en.json +5 -3
- package/messages/he.json +5 -3
- package/package.json +10 -1
- package/templates/nextjs/base/src/components/account/profile-section.tsx +7 -1
- package/templates/nextjs/base/src/components/auth/register-form.tsx +18 -1
- package/templates/nextjs/base/src/components/tracking-bootstrap.tsx +1 -1
- package/templates/nextjs/base/src/ui/layout/newsletter-signup.tsx +59 -12
- package/templates/nextjs/ui-canvas/layout/newsletter-signup.tsx +50 -8
package/dist/index.js
CHANGED
|
@@ -31,8 +31,17 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "create-brainerce-store",
|
|
34
|
-
version: "1.
|
|
34
|
+
version: "1.80.0",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
|
+
homepage: "https://brainerce.com",
|
|
37
|
+
repository: {
|
|
38
|
+
type: "git",
|
|
39
|
+
url: "https://github.com/brainerce/brainerce.git",
|
|
40
|
+
directory: "packages/create-brainerce-store"
|
|
41
|
+
},
|
|
42
|
+
bugs: {
|
|
43
|
+
url: "https://github.com/brainerce/brainerce/issues"
|
|
44
|
+
},
|
|
36
45
|
bin: {
|
|
37
46
|
"create-brainerce-store": "dist/index.js"
|
|
38
47
|
},
|
|
@@ -315,7 +324,13 @@ var BRAINERCE_RUNTIME_DEPS = Object.freeze({
|
|
|
315
324
|
// off an SDK-typed `Product`, so on an older SDK a scaffolded store fails
|
|
316
325
|
// type-check at creation. Verified against the published 2.4.0 tarball: zero
|
|
317
326
|
// files mention either field.
|
|
318
|
-
|
|
327
|
+
//
|
|
328
|
+
// 2.7.0 is the first SDK with `marketing.getBenefit()` and
|
|
329
|
+
// `PublicNewsletterBenefitOffer`, and the scaffolded newsletter signup reads
|
|
330
|
+
// both to show the merchant's welcome offer beside the field. On an older
|
|
331
|
+
// SDK a scaffolded store fails type-check at creation, exactly like the KIT
|
|
332
|
+
// floor above — so this floor is load-bearing, not cosmetic.
|
|
333
|
+
brainerce: "^2.7.0",
|
|
319
334
|
"isomorphic-dompurify": "^3.8.0"
|
|
320
335
|
});
|
|
321
336
|
|
package/messages/en.json
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"birthday": "Birthday",
|
|
43
43
|
"birthdayMonth": "Month",
|
|
44
44
|
"birthdayDay": "Day",
|
|
45
|
-
"birthdayGiftNote": "Add your birthday and a gift arrives by email on the day.",
|
|
45
|
+
"birthdayGiftNote": "Add your birthday and a gift arrives by email on the day, as long as you are signed up for our emails.",
|
|
46
46
|
"birthdayIncomplete": "Choose both a month and a day, or leave both empty.",
|
|
47
47
|
"birthdayRequired": "Choose your birthday month and day to continue.",
|
|
48
48
|
"birthdayPlaceholder": "Choose your birthday",
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"addSelectedToCart": "Add Selected to Cart",
|
|
123
123
|
"totalPrice": "Total: {price}",
|
|
124
124
|
"addingAll": "Adding...",
|
|
125
|
-
|
|
125
|
+
"whatsInTheBox": "What's in the box",
|
|
126
126
|
"by": "By"
|
|
127
127
|
},
|
|
128
128
|
"reviews": {
|
|
@@ -569,7 +569,9 @@
|
|
|
569
569
|
"submitting": "Sending...",
|
|
570
570
|
"checkEmailTitle": "Almost there — check your email",
|
|
571
571
|
"checkEmailBody": "We've sent you a link to confirm. Look in your spam folder too; you won't get another copy for 24 hours.",
|
|
572
|
-
"
|
|
572
|
+
"offerPercent": "{value}% off your first order",
|
|
573
|
+
"offerAmount": "{value} off your first order",
|
|
574
|
+
"offerByEmail": "Your personal code is in that email too. It is one per address.",
|
|
573
575
|
"genericError": "Something went wrong. Please try again."
|
|
574
576
|
},
|
|
575
577
|
"loyalty": {
|
package/messages/he.json
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"birthday": "יום הולדת",
|
|
43
43
|
"birthdayMonth": "חודש",
|
|
44
44
|
"birthdayDay": "יום",
|
|
45
|
-
"birthdayGiftNote": "הוסיפו את יום ההולדת ותקבלו מתנה במייל ביום
|
|
45
|
+
"birthdayGiftNote": "הוסיפו את יום ההולדת ותקבלו מתנה במייל ביום עצמו, בתנאי שאתם רשומים לדיוור שלנו.",
|
|
46
46
|
"birthdayIncomplete": "בחרו גם חודש וגם יום, או השאירו את שניהם ריקים.",
|
|
47
47
|
"birthdayRequired": "בחרו את חודש ויום ההולדת כדי להמשיך.",
|
|
48
48
|
"birthdayPlaceholder": "בחרו את יום ההולדת",
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"addSelectedToCart": "הוסף הנבחרים לעגלה",
|
|
123
123
|
"totalPrice": "סה\"כ: {price}",
|
|
124
124
|
"addingAll": "מוסיף...",
|
|
125
|
-
|
|
125
|
+
"whatsInTheBox": "מה יש בקופסה",
|
|
126
126
|
"by": "מאת"
|
|
127
127
|
},
|
|
128
128
|
"reviews": {
|
|
@@ -569,7 +569,9 @@
|
|
|
569
569
|
"submitting": "שולח...",
|
|
570
570
|
"checkEmailTitle": "כמעט סיימנו — בדקו את המייל",
|
|
571
571
|
"checkEmailBody": "שלחנו לכם קישור לאישור. בדקו גם בתיקיית הספאם; עותק נוסף לא יישלח ב-24 השעות הקרובות.",
|
|
572
|
-
"
|
|
572
|
+
"offerPercent": "{value}% הנחה על ההזמנה הראשונה",
|
|
573
|
+
"offerAmount": "{value} הנחה על ההזמנה הראשונה",
|
|
574
|
+
"offerByEmail": "הקוד האישי שלכם נמצא באותו מייל. הטבה אחת לכל כתובת.",
|
|
573
575
|
"genericError": "משהו השתבש. נסו שוב."
|
|
574
576
|
},
|
|
575
577
|
"loyalty": {
|
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-brainerce-store",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.80.0",
|
|
4
4
|
"description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
5
|
+
"homepage": "https://brainerce.com",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/brainerce/brainerce.git",
|
|
9
|
+
"directory": "packages/create-brainerce-store"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/brainerce/brainerce/issues"
|
|
13
|
+
},
|
|
5
14
|
"bin": {
|
|
6
15
|
"create-brainerce-store": "dist/index.js"
|
|
7
16
|
},
|
|
@@ -7,6 +7,7 @@ import { useTranslations } from '@/core/lib/translations';
|
|
|
7
7
|
import { cn } from '@/core/lib/utils';
|
|
8
8
|
import { birthMonthKey, toBirthdayNumber } from '@/core/lib/birthday';
|
|
9
9
|
import { BirthdayPicker } from '@/components/shared/birthday-picker';
|
|
10
|
+
import { useStoreCapabilities } from '@/core/providers/store-provider';
|
|
10
11
|
|
|
11
12
|
interface ProfileSectionProps {
|
|
12
13
|
profile: CustomerProfile;
|
|
@@ -17,6 +18,9 @@ interface ProfileSectionProps {
|
|
|
17
18
|
export function ProfileSection({ profile, onProfileUpdate, className }: ProfileSectionProps) {
|
|
18
19
|
const t = useTranslations('account');
|
|
19
20
|
const tc = useTranslations('common');
|
|
21
|
+
const { capabilities } = useStoreCapabilities();
|
|
22
|
+
/** Explicit `true` only — see the note on the same gate in `register-form.tsx`. */
|
|
23
|
+
const birthdayGiftOn = capabilities?.features.hasBirthdayRewards === true;
|
|
20
24
|
const [editing, setEditing] = useState(false);
|
|
21
25
|
const [saving, setSaving] = useState(false);
|
|
22
26
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
@@ -182,7 +186,9 @@ export function ProfileSection({ profile, onProfileUpdate, className }: ProfileS
|
|
|
182
186
|
onChange={selectBirthday}
|
|
183
187
|
triggerClassName="rounded-md"
|
|
184
188
|
/>
|
|
185
|
-
|
|
189
|
+
{birthdayGiftOn && (
|
|
190
|
+
<p className="text-muted-foreground mt-1 text-xs">{tc('birthdayGiftNote')}</p>
|
|
191
|
+
)}
|
|
186
192
|
</div>
|
|
187
193
|
<p className="text-muted-foreground truncate text-sm">{profile.email}</p>
|
|
188
194
|
<div className="flex items-center gap-2">
|
|
@@ -9,6 +9,7 @@ import { useStoreInfo } from '@/core/providers/store-provider';
|
|
|
9
9
|
import { toBirthdayNumber } from '@/core/lib/birthday';
|
|
10
10
|
import { BirthdayPicker } from '@/components/shared/birthday-picker';
|
|
11
11
|
import { readReferralCookie } from '@/core/lib/referral';
|
|
12
|
+
import { useStoreCapabilities } from '@/core/providers/store-provider';
|
|
12
13
|
|
|
13
14
|
interface RegisterData {
|
|
14
15
|
firstName: string;
|
|
@@ -66,6 +67,20 @@ export function RegisterForm({ onSubmit, error, className }: RegisterFormProps)
|
|
|
66
67
|
const [privacyError, setPrivacyError] = useState(false);
|
|
67
68
|
const [passwordError, setPasswordError] = useState<string | null>(null);
|
|
68
69
|
const [acceptsMarketing, setAcceptsMarketing] = useState(false);
|
|
70
|
+
const { capabilities } = useStoreCapabilities();
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether to promise a birthday gift beside the picker.
|
|
74
|
+
*
|
|
75
|
+
* The gate is inverted relative to `ReferralGreeting`, and deliberately so.
|
|
76
|
+
* There, an undecided capability must not suppress a real referral, so only
|
|
77
|
+
* an explicit `false` hides anything. Here the text makes a PROMISE, and a
|
|
78
|
+
* promise may only be printed on an explicit `true`: a store with birthday
|
|
79
|
+
* rewards switched off — which is the default — would otherwise tell every
|
|
80
|
+
* customer a gift is coming and send nothing, and `requireBirthday` can make
|
|
81
|
+
* handing over the date compulsory on top of that.
|
|
82
|
+
*/
|
|
83
|
+
const birthdayGiftOn = capabilities?.features.hasBirthdayRewards === true;
|
|
69
84
|
const [birthMonth, setBirthMonth] = useState('');
|
|
70
85
|
const [birthDay, setBirthDay] = useState('');
|
|
71
86
|
const [birthdayError, setBirthdayError] = useState<string | null>(null);
|
|
@@ -293,7 +308,9 @@ export function RegisterForm({ onSubmit, error, className }: RegisterFormProps)
|
|
|
293
308
|
required={birthdayRequired}
|
|
294
309
|
invalid={birthdayError !== null}
|
|
295
310
|
/>
|
|
296
|
-
|
|
311
|
+
{birthdayGiftOn && (
|
|
312
|
+
<p className="text-muted-foreground mt-1.5 text-xs">{tc('birthdayGiftNote')}</p>
|
|
313
|
+
)}
|
|
297
314
|
{birthdayError && <p className="text-destructive mt-1 text-xs">{birthdayError}</p>}
|
|
298
315
|
</div>
|
|
299
316
|
|
|
@@ -12,7 +12,7 @@ import { useStoreInfo } from '@/core/providers/store-provider';
|
|
|
12
12
|
*
|
|
13
13
|
* There is nothing to configure here and no environment variable to set. The
|
|
14
14
|
* tag ids arrive inside `storeInfo.tracking`, which the backend resolves from
|
|
15
|
-
* the merchant's connected marketplace apps: connecting the Google
|
|
15
|
+
* the merchant's connected marketplace apps: connecting the Google
|
|
16
16
|
* app runs GA4 discovery and the measurement id shows up on its own, likewise
|
|
17
17
|
* the Meta and TikTok pixels. Connect an app in the dashboard and this file
|
|
18
18
|
* starts loading its tag within a minute — no redeploy, no code change.
|
|
@@ -21,17 +21,26 @@
|
|
|
21
21
|
* commonest reason a signup never becomes a subscriber, and the resend cooldown
|
|
22
22
|
* means no second copy arrives for 24 hours.
|
|
23
23
|
*
|
|
24
|
-
* **
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
24
|
+
* **The welcome offer.** `marketing.getBenefit()` returns what the merchant is
|
|
25
|
+
* offering people who confirm, or `null` when they offer nothing. Show its
|
|
26
|
+
* terms beside the field so the shopper knows what they are signing up for.
|
|
27
|
+
*
|
|
28
|
+
* ⛔ DO NOT RENDER A COUPON CODE HERE, now or after the success state. No coupon
|
|
29
|
+
* exists at either moment. It is minted when the recipient clicks the link in
|
|
30
|
+
* their own mailbox and is emailed to them there — which is exactly what stops a
|
|
31
|
+
* forwarded link handing the discount to someone who never asked. The
|
|
32
|
+
* confirmation page is served by the API and already shows the code, its expiry
|
|
33
|
+
* and its terms, so there is nothing here to poll for either.
|
|
34
|
+
*
|
|
35
|
+
* ⛔ There is no eligibility check to call, deliberately: a per-address answer
|
|
36
|
+
* would let anyone test who already subscribed. The offer is one per address per
|
|
37
|
+
* store, forever. Say so in the terms rather than trying to detect it.
|
|
30
38
|
*
|
|
31
39
|
* Rate limited to 3 requests / 60s per IP.
|
|
32
40
|
*/
|
|
33
41
|
|
|
34
|
-
import { useMemo, useState } from 'react';
|
|
42
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
43
|
+
import type { PublicNewsletterBenefitOffer } from 'brainerce';
|
|
35
44
|
import { getClient } from '@/core/lib/brainerce';
|
|
36
45
|
import { useTranslations } from '@/core/lib/translations';
|
|
37
46
|
|
|
@@ -42,6 +51,10 @@ export function NewsletterSignup() {
|
|
|
42
51
|
const [loading, setLoading] = useState(false);
|
|
43
52
|
const [done, setDone] = useState(false);
|
|
44
53
|
const [error, setError] = useState<string | null>(null);
|
|
54
|
+
// null covers both "no offer configured" and "the read failed". Neither is
|
|
55
|
+
// worth an error state on a footer signup: the form still works, it just
|
|
56
|
+
// promises nothing, and promising nothing is always safe.
|
|
57
|
+
const [offer, setOffer] = useState<PublicNewsletterBenefitOffer | null>(null);
|
|
45
58
|
|
|
46
59
|
// Same source the contact form uses — <html lang> is set from the active
|
|
47
60
|
// locale, and passing it is what decides the language of the confirmation
|
|
@@ -51,6 +64,31 @@ export function NewsletterSignup() {
|
|
|
51
64
|
return undefined;
|
|
52
65
|
}, []);
|
|
53
66
|
|
|
67
|
+
// Read once per mount. The offer belongs to the store, not to the visitor,
|
|
68
|
+
// so re-reading it per keystroke or per submit is wasted work.
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
let cancelled = false;
|
|
71
|
+
getClient()
|
|
72
|
+
.marketing.getBenefit(locale)
|
|
73
|
+
.then((result) => {
|
|
74
|
+
if (!cancelled) setOffer(result);
|
|
75
|
+
})
|
|
76
|
+
.catch(() => {
|
|
77
|
+
// Silent on purpose. A failed offer read must not stop someone
|
|
78
|
+
// subscribing, and there is nothing the shopper could do about it.
|
|
79
|
+
});
|
|
80
|
+
return () => {
|
|
81
|
+
cancelled = true;
|
|
82
|
+
};
|
|
83
|
+
}, [locale]);
|
|
84
|
+
|
|
85
|
+
const offerHeadline = offer
|
|
86
|
+
? offer.headline?.trim() ||
|
|
87
|
+
(offer.discountType === 'PERCENTAGE'
|
|
88
|
+
? t('offerPercent', { value: String(offer.discountValue) })
|
|
89
|
+
: t('offerAmount', { value: String(offer.discountValue) }))
|
|
90
|
+
: null;
|
|
91
|
+
|
|
54
92
|
async function handleSubmit(e: React.FormEvent) {
|
|
55
93
|
e.preventDefault();
|
|
56
94
|
if (loading || !email.trim()) return;
|
|
@@ -80,11 +118,10 @@ export function NewsletterSignup() {
|
|
|
80
118
|
<div className="space-y-1">
|
|
81
119
|
<p className="text-sm font-medium">{t('checkEmailTitle')}</p>
|
|
82
120
|
<p className="text-sm opacity-70">{t('checkEmailBody')}</p>
|
|
83
|
-
{/*
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
*/}
|
|
121
|
+
{/* Says where the code comes from, without ever showing one. The coupon
|
|
122
|
+
is created by the confirmation click and arrives in the same inbox,
|
|
123
|
+
so "check your email" is the whole instruction. */}
|
|
124
|
+
{offer ? <p className="text-sm opacity-70">{t('offerByEmail')}</p> : null}
|
|
88
125
|
</div>
|
|
89
126
|
);
|
|
90
127
|
}
|
|
@@ -96,6 +133,16 @@ export function NewsletterSignup() {
|
|
|
96
133
|
</label>
|
|
97
134
|
<p className="text-sm opacity-70">{t('subtitle')}</p>
|
|
98
135
|
|
|
136
|
+
{/* The merchant's own words when they wrote any, a plain statement of the
|
|
137
|
+
discount when they did not. Both are the promise the shopper is
|
|
138
|
+
agreeing to, so they belong above the input, not under the button. */}
|
|
139
|
+
{offerHeadline ? (
|
|
140
|
+
<div className="space-y-0.5">
|
|
141
|
+
<p className="text-sm font-medium">{offerHeadline}</p>
|
|
142
|
+
{offer?.terms ? <p className="text-xs opacity-70">{offer.terms}</p> : null}
|
|
143
|
+
</div>
|
|
144
|
+
) : null}
|
|
145
|
+
|
|
99
146
|
<div className="flex flex-wrap gap-2">
|
|
100
147
|
<input
|
|
101
148
|
id="newsletter-email"
|
|
@@ -17,9 +17,18 @@
|
|
|
17
17
|
*
|
|
18
18
|
* ⛔ KEEP THE HONEYPOT. It is the bot filter; deleting the input silently opens
|
|
19
19
|
* the form up.
|
|
20
|
+
*
|
|
21
|
+
* **The welcome offer.** `marketing.getBenefit()` returns what the merchant
|
|
22
|
+
* offers people who confirm, or `null` when they offer nothing. Show its terms
|
|
23
|
+
* beside the field. ⛔ NEVER RENDER A COUPON CODE — none exists here or in the
|
|
24
|
+
* success state; it is minted by the confirmation click and emailed to the
|
|
25
|
+
* recipient, which is what stops a forwarded link handing the discount to
|
|
26
|
+
* someone who never asked. There is no eligibility check to call either: a
|
|
27
|
+
* per-address answer would let anyone test who already subscribed.
|
|
20
28
|
*/
|
|
21
29
|
|
|
22
|
-
import { useMemo, useState } from 'react';
|
|
30
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
31
|
+
import type { PublicNewsletterBenefitOffer } from 'brainerce';
|
|
23
32
|
import { getClient } from '@/core/lib/brainerce';
|
|
24
33
|
import { useTranslations } from '@/core/lib/translations';
|
|
25
34
|
|
|
@@ -30,6 +39,9 @@ export function NewsletterSignup() {
|
|
|
30
39
|
const [loading, setLoading] = useState(false);
|
|
31
40
|
const [done, setDone] = useState(false);
|
|
32
41
|
const [error, setError] = useState<string | null>(null);
|
|
42
|
+
// null covers "no offer" and "the read failed" alike. Neither deserves an
|
|
43
|
+
// error state: the form still works and simply promises nothing.
|
|
44
|
+
const [offer, setOffer] = useState<PublicNewsletterBenefitOffer | null>(null);
|
|
33
45
|
|
|
34
46
|
// <html lang> carries the active locale, and passing it decides the language
|
|
35
47
|
// of the confirmation email. Omit it and a Hebrew shopper gets English.
|
|
@@ -38,6 +50,29 @@ export function NewsletterSignup() {
|
|
|
38
50
|
return undefined;
|
|
39
51
|
}, []);
|
|
40
52
|
|
|
53
|
+
// Once per mount. The offer belongs to the store, not the visitor.
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
let cancelled = false;
|
|
56
|
+
getClient()
|
|
57
|
+
.marketing.getBenefit(locale)
|
|
58
|
+
.then((result) => {
|
|
59
|
+
if (!cancelled) setOffer(result);
|
|
60
|
+
})
|
|
61
|
+
.catch(() => {
|
|
62
|
+
// Silent: a failed offer read must not stop anyone subscribing.
|
|
63
|
+
});
|
|
64
|
+
return () => {
|
|
65
|
+
cancelled = true;
|
|
66
|
+
};
|
|
67
|
+
}, [locale]);
|
|
68
|
+
|
|
69
|
+
const offerHeadline = offer
|
|
70
|
+
? offer.headline?.trim() ||
|
|
71
|
+
(offer.discountType === 'PERCENTAGE'
|
|
72
|
+
? t('offerPercent', { value: String(offer.discountValue) })
|
|
73
|
+
: t('offerAmount', { value: String(offer.discountValue) }))
|
|
74
|
+
: null;
|
|
75
|
+
|
|
41
76
|
async function handleSubmit(e: React.FormEvent) {
|
|
42
77
|
e.preventDefault();
|
|
43
78
|
if (loading || !email.trim()) return;
|
|
@@ -68,13 +103,9 @@ export function NewsletterSignup() {
|
|
|
68
103
|
{/* DESIGN ME — newsletter success state. Do NOT reword to "you're subscribed": the address is not on the list until the recipient clicks the emailed link. Naming the spam folder is deliberate — a filtered confirmation is the commonest reason a signup never converts, and no second copy is sent for 24 hours. */}
|
|
69
104
|
<p>{t('checkEmailTitle')}</p>
|
|
70
105
|
<p>{t('checkEmailBody')}</p>
|
|
71
|
-
{/*
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
<p>{t('discountCode', { code: 'WELCOME10' })}</p>
|
|
75
|
-
The code comes from a dashboard coupon with the `customer_first_order`
|
|
76
|
-
condition; subscribing mints nothing on its own.
|
|
77
|
-
*/}
|
|
106
|
+
{/* Says where the code comes from without ever showing one: the coupon
|
|
107
|
+
is created by the confirmation click and lands in the same inbox. */}
|
|
108
|
+
{offer ? <p>{t('offerByEmail')}</p> : null}
|
|
78
109
|
</div>
|
|
79
110
|
);
|
|
80
111
|
}
|
|
@@ -85,6 +116,17 @@ export function NewsletterSignup() {
|
|
|
85
116
|
<label htmlFor="newsletter-email">{t('title')}</label>
|
|
86
117
|
<p>{t('subtitle')}</p>
|
|
87
118
|
|
|
119
|
+
{/* DESIGN ME — the welcome offer, when the merchant configured one. Keep
|
|
120
|
+
both lines: the headline is the promise and the terms are its
|
|
121
|
+
conditions, and a shopper who reads one without the other is being
|
|
122
|
+
mis-sold. Never add a coupon code here. */}
|
|
123
|
+
{offerHeadline ? (
|
|
124
|
+
<div>
|
|
125
|
+
<p>{offerHeadline}</p>
|
|
126
|
+
{offer?.terms ? <p>{offer.terms}</p> : null}
|
|
127
|
+
</div>
|
|
128
|
+
) : null}
|
|
129
|
+
|
|
88
130
|
<input
|
|
89
131
|
id="newsletter-email"
|
|
90
132
|
type="email"
|