Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/hot-plums-give.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
"@bigcommerce/catalyst-core": patch
"@bigcommerce/catalyst-core": minor
---

Make newsletter signup component on homepage render conditionally based on BigCommerce settings.
Expand Down
4 changes: 2 additions & 2 deletions .changeset/tall-walls-tan.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
"@bigcommerce/catalyst-core": patch
"@bigcommerce/catalyst-core": minor
---

Implement functional newsletter subscription feature with BigCommerce GraphQL API integration.
Expand Down Expand Up @@ -73,7 +73,7 @@ Add the following translation keys to your locale files (e.g., `messages/en.json
"title": "Sign up for our newsletter",
"placeholder": "Enter your email",
"description": "Stay up to date with the latest news and offers from our store.",
"subscribedToNewsletter": "You have been subscribed to our newsletter.",
"subscribedToNewsletter": "You have been subscribed to our newsletter!",
"Errors": {
"invalidEmail": "Please enter a valid email address.",
"somethingWentWrong": "Something went wrong. Please try again later."
Expand Down
181 changes: 181 additions & 0 deletions .changeset/violet-adults-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
---
"@bigcommerce/catalyst-core": minor
---

Add newsletter subscription toggle to account settings page, allowing customers to manage their marketing preferences directly from their account.

## What Changed

- Added `NewsletterSubscriptionForm` component with a toggle switch for subscribing/unsubscribing to newsletters
- Created `updateNewsletterSubscription` server action that handles both subscribe and unsubscribe operations via BigCommerce GraphQL API
- Updated `AccountSettingsSection` to conditionally display the newsletter subscription form when enabled
- Enhanced `CustomerSettingsQuery` to fetch `isSubscribedToNewsletter` status and `showNewsletterSignup` store setting
- Updated account settings page to pass newsletter subscription props and bind customer info to the action
- Added translation keys for newsletter subscription UI in `Account.Settings.NewsletterSubscription` namespace
- Added E2E tests for subscribing and unsubscribing functionality

## Migration Guide

To add the newsletter subscription toggle to your account settings page:

### Step 1: Copy the server action

Copy the new server action file to your account settings directory:

```bash
cp core/app/[locale]/(default)/account/settings/_actions/update-newsletter-subscription.ts \
your-app/app/[locale]/(default)/account/settings/_actions/update-newsletter-subscription.ts
```

### Step 2: Update the GraphQL query

Update `core/app/[locale]/(default)/account/settings/page-data.tsx` to include newsletter subscription fields:

```tsx
// Renamed CustomerSettingsQuery to AccountSettingsQuery
const AccountSettingsQuery = graphql(`
query AccountSettingsQuery(...) {
customer {
...
isSubscribedToNewsletter # Add this field
}
site {
settings {
...
newsletter { # Add this section
showNewsletterSignup
}
}
}
}
`);
```

Also update the return statement to include `newsletterSettings`:

```tsx
const newsletterSettings = response.data.site.settings?.newsletter;

return {
...
newsletterSettings, // Add this
};
```

### Step 3: Copy the NewsletterSubscriptionForm component

Copy the new form component:

```bash
cp core/vibes/soul/sections/account-settings/newsletter-subscription-form.tsx \
your-app/vibes/soul/sections/account-settings/newsletter-subscription-form.tsx
```

### Step 4: Update AccountSettingsSection

Update `core/vibes/soul/sections/account-settings/index.tsx`:

1. Import the new component:
```tsx
import {
NewsletterSubscriptionForm,
UpdateNewsletterSubscriptionAction,
} from './newsletter-subscription-form';
```

2. Add props to the interface:
```tsx
export interface AccountSettingsSectionProps {
...
newsletterSubscriptionEnabled?: boolean;
isAccountSubscribed?: boolean;
newsletterSubscriptionTitle?: string;
newsletterSubscriptionLabel?: string;
newsletterSubscriptionCtaLabel?: string;
updateNewsletterSubscriptionAction?: UpdateNewsletterSubscriptionAction;
}
```

3. Add the form section in the component (after the change password form):
```tsx
{newsletterSubscriptionEnabled && updateNewsletterSubscriptionAction && (
<div className="border-t border-[var(--account-settings-section-border,hsl(var(--contrast-100)))] pt-12">
<h1 className="mb-10 font-[family-name:var(--account-settings-section-font-family,var(--font-family-heading))] text-2xl font-medium leading-none text-[var(--account-settings-section-text,var(--foreground))] @xl:text-2xl">
{newsletterSubscriptionTitle}
</h1>
<NewsletterSubscriptionForm
action={updateNewsletterSubscriptionAction}
ctaLabel={newsletterSubscriptionCtaLabel}
isAccountSubscribed={isAccountSubscribed}
label={newsletterSubscriptionLabel}
/>
</div>
)}
```

### Step 5: Update the account settings page

Update `core/app/[locale]/(default)/account/settings/page.tsx`:

1. Import the action:
```tsx
import { updateNewsletterSubscription } from './_actions/update-newsletter-subscription';
```

2. Extract newsletter settings from the query:
```tsx
const newsletterSubscriptionEnabled = accountSettings.storeSettings?.showNewsletterSignup;
const isAccountSubscribed = accountSettings.customerInfo.isSubscribedToNewsletter;
```

3. Bind customer info to the action:
```tsx
const updateNewsletterSubscriptionActionWithCustomerInfo = updateNewsletterSubscription.bind(
null,
{
customerInfo: accountSettings.customerInfo,
},
);
```

4. Pass props to `AccountSettingsSection`:
```tsx
<AccountSettingsSection
...
isAccountSubscribed={isAccountSubscribed}
newsletterSubscriptionCtaLabel={t('cta')}
newsletterSubscriptionEnabled={newsletterSubscriptionEnabled}
newsletterSubscriptionLabel={t('NewsletterSubscription.label')}
newsletterSubscriptionTitle={t('NewsletterSubscription.title')}
updateNewsletterSubscriptionAction={updateNewsletterSubscriptionActionWithCustomerInfo}
/>
```

### Step 6: Add translation keys

Add the following keys to your locale files (e.g., `messages/en.json`):

```json
{
"Account": {
"Settings": {
...
"NewsletterSubscription": {
"title": "Marketing preferences",
"label": "Opt-in to receive emails about new products and promotions.",
"marketingPreferencesUpdated": "Marketing preferences have been updated successfully!",
"somethingWentWrong": "Something went wrong. Please try again later."
}
}
}
}
```

### Step 7: Verify the feature

1. Ensure your BigCommerce store has newsletter signup enabled in store settings
2. Navigate to `/account/settings` as a logged-in customer
3. Verify the newsletter subscription toggle appears below the change password form
4. Test subscribing and unsubscribing functionality

The newsletter subscription form will only display if `newsletterSubscriptionEnabled` is `true` (controlled by the `showNewsletterSignup` store setting).
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
'use server';

import { BigCommerceGQLError } from '@bigcommerce/catalyst-client';
import { SubmissionResult } from '@conform-to/react';
import { parseWithZod } from '@conform-to/zod';
import { unstable_expireTag } from 'next/cache';
import { getTranslations } from 'next-intl/server';
import { z } from 'zod';

import { client } from '~/client';
import { graphql } from '~/client/graphql';
import { TAGS } from '~/client/tags';

const updateNewsletterSubscriptionSchema = z.object({
intent: z.enum(['subscribe', 'unsubscribe']),
});

const SubscribeToNewsletterMutation = graphql(`
mutation SubscribeToNewsletterMutation($input: CreateSubscriberInput!) {
newsletter {
subscribe(input: $input) {
errors {
__typename
... on CreateSubscriberAlreadyExistsError {
message
}
... on CreateSubscriberEmailInvalidError {
message
}
... on CreateSubscriberUnexpectedError {
message
}
... on CreateSubscriberLastNameInvalidError {
message
}
... on CreateSubscriberFirstNameInvalidError {
message
}
}
}
}
}
`);

const UnsubscribeFromNewsletterMutation = graphql(`
mutation UnsubscribeFromNewsletterMutation($input: RemoveSubscriberInput!) {
newsletter {
unsubscribe(input: $input) {
errors {
__typename
... on RemoveSubscriberEmailInvalidError {
message
}
... on RemoveSubscriberUnexpectedError {
message
}
}
}
}
}
`);

export const updateNewsletterSubscription = async (
{
customerInfo,
}: {
customerInfo: {
email: string;
firstName: string;
lastName: string;
};
},
_prevState: { lastResult: SubmissionResult | null },
formData: FormData,
) => {
const t = await getTranslations('Account.Settings.NewsletterSubscription');

const submission = parseWithZod(formData, { schema: updateNewsletterSubscriptionSchema });

if (submission.status !== 'success') {
return { lastResult: submission.reply() };
}

try {
let errors;

if (submission.value.intent === 'subscribe') {
const response = await client.fetch({
document: SubscribeToNewsletterMutation,
variables: {
input: {
email: customerInfo.email,
firstName: customerInfo.firstName,
lastName: customerInfo.lastName,
},
},
});

errors = response.data.newsletter.subscribe.errors;
} else {
const response = await client.fetch({
document: UnsubscribeFromNewsletterMutation,
variables: {
input: {
email: customerInfo.email,
},
},
});

errors = response.data.newsletter.unsubscribe.errors;
}

if (errors.length > 0) {
// Not handling returned errors from API since we will display a generic error message to the user
// Still returning the errors to the client for debugging purposes
return {
lastResult: submission.reply({
formErrors: errors.map(({ message }) => message),
}),
};
}

unstable_expireTag(TAGS.customer);

return {
lastResult: submission.reply(),
successMessage: t('marketingPreferencesUpdated'),
};
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);

if (error instanceof BigCommerceGQLError) {
return {
lastResult: submission.reply({
formErrors: error.errors.map(({ message }) => message),
}),
};
}

if (error instanceof Error) {
return {
lastResult: submission.reply({ formErrors: [error.message] }),
};
}

return {
lastResult: submission.reply({ formErrors: [String(error)] }),
};
}
};
Loading