diff --git a/.changeset/hot-plums-give.md b/.changeset/hot-plums-give.md index 6e42c80a42..e55241f7c8 100644 --- a/.changeset/hot-plums-give.md +++ b/.changeset/hot-plums-give.md @@ -1,5 +1,5 @@ --- -"@bigcommerce/catalyst-core": patch +"@bigcommerce/catalyst-core": minor --- Make newsletter signup component on homepage render conditionally based on BigCommerce settings. diff --git a/.changeset/tall-walls-tan.md b/.changeset/tall-walls-tan.md index 882e34e0d5..8236fdf4df 100644 --- a/.changeset/tall-walls-tan.md +++ b/.changeset/tall-walls-tan.md @@ -1,5 +1,5 @@ --- -"@bigcommerce/catalyst-core": patch +"@bigcommerce/catalyst-core": minor --- Implement functional newsletter subscription feature with BigCommerce GraphQL API integration. @@ -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." diff --git a/.changeset/violet-adults-do.md b/.changeset/violet-adults-do.md new file mode 100644 index 0000000000..64f7b562ea --- /dev/null +++ b/.changeset/violet-adults-do.md @@ -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 && ( +
+

+ {newsletterSubscriptionTitle} +

+ +
+)} +``` + +### 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 + +``` + +### 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). diff --git a/core/app/[locale]/(default)/account/settings/_actions/update-newsletter-subscription.ts b/core/app/[locale]/(default)/account/settings/_actions/update-newsletter-subscription.ts new file mode 100644 index 0000000000..b984b53601 --- /dev/null +++ b/core/app/[locale]/(default)/account/settings/_actions/update-newsletter-subscription.ts @@ -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)] }), + }; + } +}; diff --git a/core/app/[locale]/(default)/account/settings/page-data.tsx b/core/app/[locale]/(default)/account/settings/page-data.tsx index 43dfa5d323..f0db7b0f6a 100644 --- a/core/app/[locale]/(default)/account/settings/page-data.tsx +++ b/core/app/[locale]/(default)/account/settings/page-data.tsx @@ -6,9 +6,9 @@ import { graphql, VariablesOf } from '~/client/graphql'; import { TAGS } from '~/client/tags'; import { FormFieldsFragment } from '~/data-transformers/form-field-transformer/fragment'; -const CustomerSettingsQuery = graphql( +const AccountSettingsQuery = graphql( ` - query CustomerSettingsQuery( + query AccountSettingsQuery( $customerFilters: FormFieldFiltersInput $customerSortBy: FormFieldSortInput $addressFilters: FormFieldFiltersInput @@ -20,6 +20,7 @@ const CustomerSettingsQuery = graphql( firstName lastName company + isSubscribedToNewsletter } site { settings { @@ -31,6 +32,9 @@ const CustomerSettingsQuery = graphql( ...FormFieldsFragment } } + newsletter { + showNewsletterSignup + } } } } @@ -38,7 +42,7 @@ const CustomerSettingsQuery = graphql( [FormFieldsFragment], ); -type Variables = VariablesOf; +type Variables = VariablesOf; interface Props { address?: { @@ -52,11 +56,11 @@ interface Props { }; } -export const getCustomerSettingsQuery = cache(async ({ address, customer }: Props = {}) => { +export const getAccountSettingsQuery = cache(async ({ address, customer }: Props = {}) => { const customerAccessToken = await getSessionCustomerAccessToken(); const response = await client.fetch({ - document: CustomerSettingsQuery, + document: AccountSettingsQuery, variables: { addressFilters: address?.filters, addressSortBy: address?.sortBy, @@ -70,6 +74,7 @@ export const getCustomerSettingsQuery = cache(async ({ address, customer }: Prop const addressFields = response.data.site.settings?.formFields.shippingAddress; const customerFields = response.data.site.settings?.formFields.customer; const customerInfo = response.data.customer; + const newsletterSettings = response.data.site.settings?.newsletter; if (!addressFields || !customerFields || !customerInfo) { return null; @@ -79,5 +84,6 @@ export const getCustomerSettingsQuery = cache(async ({ address, customer }: Prop addressFields, customerFields, customerInfo, + newsletterSettings, }; }); diff --git a/core/app/[locale]/(default)/account/settings/page.tsx b/core/app/[locale]/(default)/account/settings/page.tsx index b37db8e530..6d074e9431 100644 --- a/core/app/[locale]/(default)/account/settings/page.tsx +++ b/core/app/[locale]/(default)/account/settings/page.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/jsx-no-bind */ import { Metadata } from 'next'; import { notFound } from 'next/navigation'; import { getTranslations, setRequestLocale } from 'next-intl/server'; @@ -6,7 +7,8 @@ import { AccountSettingsSection } from '@/vibes/soul/sections/account-settings'; import { changePassword } from './_actions/change-password'; import { updateCustomer } from './_actions/update-customer'; -import { getCustomerSettingsQuery } from './page-data'; +import { updateNewsletterSubscription } from './_actions/update-newsletter-subscription'; +import { getAccountSettingsQuery } from './page-data'; interface Props { params: Promise<{ locale: string }>; @@ -29,24 +31,40 @@ export default async function Settings({ params }: Props) { const t = await getTranslations('Account.Settings'); - const customerSettings = await getCustomerSettingsQuery(); + const accountSettings = await getAccountSettingsQuery(); - if (!customerSettings) { + if (!accountSettings) { notFound(); } + const newsletterSubscriptionEnabled = accountSettings.newsletterSettings?.showNewsletterSignup; + const isAccountSubscribed = accountSettings.customerInfo.isSubscribedToNewsletter; + + const updateNewsletterSubscriptionActionWithCustomerInfo = updateNewsletterSubscription.bind( + null, + { + customerInfo: accountSettings.customerInfo, + }, + ); + return ( ); } diff --git a/core/components/subscribe/_actions/subscribe.ts b/core/components/subscribe/_actions/subscribe.ts index 25dbe6ef3c..62a82ec774 100644 --- a/core/components/subscribe/_actions/subscribe.ts +++ b/core/components/subscribe/_actions/subscribe.ts @@ -57,13 +57,13 @@ export const subscribe = async ( const errors = response.data.newsletter.subscribe.errors; - const subcriberAlreadyExists = errors.some( + // If subscriber already exists, treat it as success for privacy reasons + // We don't want to reveal that the email is already subscribed + const subscriberAlreadyExists = errors.some( ({ __typename }) => __typename === 'CreateSubscriberAlreadyExistsError', ); - // If there are no errors or the subscriber already exists, we want to reset the form and show the success message - // This is for privacy reasons, we don't want to show the error message to the user if they are already subscribed - if (!errors.length || subcriberAlreadyExists) { + if (subscriberAlreadyExists) { return { lastResult: submission.reply(), successMessage: t('subscribedToNewsletter'), @@ -87,7 +87,11 @@ export const subscribe = async ( }; } - return { lastResult: submission.reply({ formErrors: [t('Errors.somethingWentWrong')] }) }; + // If there are no errors, we want to show the success message to the user + return { + lastResult: submission.reply(), + successMessage: t('subscribedToNewsletter'), + }; } catch (error) { // eslint-disable-next-line no-console console.error(error); @@ -104,6 +108,6 @@ export const subscribe = async ( return { lastResult: submission.reply({ formErrors: [error.message] }) }; } - return { lastResult: submission.reply({ formErrors: [t('Errors.somethingWentWrong')] }) }; + return { lastResult: submission.reply({ formErrors: [String(error)] }) }; } }; diff --git a/core/messages/en.json b/core/messages/en.json index ffe0a75353..88172788f1 100644 --- a/core/messages/en.json +++ b/core/messages/en.json @@ -198,7 +198,13 @@ "currentPassword": "Current password", "newPassword": "New password", "confirmPassword": "Confirm password", - "cta": "Update" + "cta": "Update", + "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." + } } }, "Wishlist": { @@ -504,7 +510,7 @@ "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." diff --git a/core/tests/fixtures/subscribe/index.ts b/core/tests/fixtures/subscribe/index.ts index 4bb2f7af0c..84ba6518b1 100644 --- a/core/tests/fixtures/subscribe/index.ts +++ b/core/tests/fixtures/subscribe/index.ts @@ -7,6 +7,20 @@ export class SubscribeFixture extends Fixture { this.subscribedEmails.push(email); } + async subscribe(email: string, firstName: string, lastName: string): Promise { + this.skipIfReadonly(); + + await this.api.subscribe.subscribe(email, firstName, lastName); + + this.trackSubscription(email); + } + + async unsubscribe(email: string): Promise { + this.skipIfReadonly(); + + await this.api.subscribe.unsubscribe(email); + } + async cleanup(): Promise { this.skipIfReadonly(); diff --git a/core/tests/fixtures/utils/api/subscribe/http.ts b/core/tests/fixtures/utils/api/subscribe/http.ts index e02729828b..b6c6876a43 100644 --- a/core/tests/fixtures/utils/api/subscribe/http.ts +++ b/core/tests/fixtures/utils/api/subscribe/http.ts @@ -3,6 +3,13 @@ import { httpClient } from '../client'; import { SubscribeApi } from '.'; export const subscribeHttpClient: SubscribeApi = { + subscribe: async (email: string, firstName: string, lastName: string) => { + await httpClient.post('/v3/customers/subscribers', { + email, + first_name: firstName, + last_name: lastName, + }); + }, unsubscribe: async (email: string) => { await httpClient.delete(`/v3/customers/subscribers?email=${encodeURIComponent(email)}`); }, diff --git a/core/tests/fixtures/utils/api/subscribe/index.ts b/core/tests/fixtures/utils/api/subscribe/index.ts index 68bd544b07..cfe108c234 100644 --- a/core/tests/fixtures/utils/api/subscribe/index.ts +++ b/core/tests/fixtures/utils/api/subscribe/index.ts @@ -1,4 +1,5 @@ export interface SubscribeApi { + subscribe(email: string, firstName: string, lastName: string): Promise; unsubscribe(email: string): Promise; } diff --git a/core/tests/ui/e2e/account/account-settings.spec.ts b/core/tests/ui/e2e/account/account-settings.spec.ts index 1545115344..ce8dabf08e 100644 --- a/core/tests/ui/e2e/account/account-settings.spec.ts +++ b/core/tests/ui/e2e/account/account-settings.spec.ts @@ -2,6 +2,7 @@ import { faker } from '@faker-js/faker'; import { expect, test } from '~/tests/fixtures'; import { getTranslations } from '~/tests/lib/i18n'; +import { TAGS } from '~/tests/tags'; test('Updating account information works as expected', async ({ page, customer }) => { const t = await getTranslations('Account.Settings'); @@ -18,7 +19,7 @@ test('Updating account information works as expected', async ({ page, customer } // TODO: Account settings form fields need to be translated await expect(page.getByLabel('First Name')).toHaveValue(testCustomer.firstName); await expect(page.getByLabel('Last Name')).toHaveValue(testCustomer.lastName); - await expect(page.getByLabel('Email')).toHaveValue(testCustomer.email); + await expect(page.getByLabel('Email').first()).toHaveValue(testCustomer.email); await page.getByLabel('First Name').fill(updatedFirstName); await page.getByLabel('Last Name').fill(updatedLastName); @@ -61,7 +62,7 @@ test('Changing password works as expected', async ({ page, customer }) => { await page.getByLabel(t('confirmPassword')).fill(newPassword); await page .getByRole('button', { name: t('cta') }) - .last() + .nth(1) // The second button is the update password button .click(); await page.waitForLoadState('networkidle'); @@ -77,3 +78,93 @@ test('Changing password works as expected', async ({ page, customer }) => { await expect(page).toHaveURL('/account/orders/'); }); + +test( + 'Subscribing to newsletter works as expected', + { tag: [TAGS.writesData] }, + async ({ page, customer, subscribe }) => { + const t = await getTranslations('Account.Settings'); + const tNewsletter = await getTranslations('Account.Settings.NewsletterSubscription'); + const testCustomer = await customer.createNewCustomer(); + + // Ensure customer is unsubscribed initially + await subscribe.unsubscribe(testCustomer.email); + + await customer.loginAs(testCustomer); + + await page.goto('/account/settings'); + await expect(page.getByRole('heading', { name: t('title') })).toBeVisible(); + + // Find the newsletter subscription switch + const newsletterSwitch = page.getByLabel(t('NewsletterSubscription.label')); + + await expect(newsletterSwitch).toBeVisible(); + + // Verify switch is unchecked (customer is not subscribed) + const switchElement = page.getByRole('switch', { name: t('NewsletterSubscription.label') }); + + await expect(switchElement).not.toBeChecked(); + + // Click the switch to subscribe + await switchElement.click(); + + // Click the submit button (should be the last button on the page) + await page + .getByRole('button', { name: t('cta') }) + .last() + .click(); + + await page.waitForLoadState('networkidle'); + + // Verify success message appears + await expect(page.getByText(tNewsletter('marketingPreferencesUpdated'))).toBeVisible(); + + // Track subscription for cleanup + subscribe.trackSubscription(testCustomer.email); + }, +); + +test( + 'Unsubscribing from newsletter works as expected', + { tag: [TAGS.writesData] }, + async ({ page, customer, subscribe }) => { + const t = await getTranslations('Account.Settings'); + const tNewsletter = await getTranslations('Account.Settings.NewsletterSubscription'); + const testCustomer = await customer.createNewCustomer(); + + // Ensure customer is subscribed initially + await subscribe.subscribe(testCustomer.email, testCustomer.firstName, testCustomer.lastName); + + await customer.loginAs(testCustomer); + + await page.goto('/account/settings'); + await expect(page.getByRole('heading', { name: t('title') })).toBeVisible(); + + // Find the newsletter subscription switch + const newsletterSwitch = page.getByLabel(t('NewsletterSubscription.label')); + + await expect(newsletterSwitch).toBeVisible(); + + // Verify switch is checked (customer is subscribed) + const switchElement = page.getByRole('switch', { name: t('NewsletterSubscription.label') }); + + await expect(switchElement).toBeChecked(); + + // Click the switch to unsubscribe + await switchElement.click(); + + // Click the submit button (should be the last button on the page) + await page + .getByRole('button', { name: t('cta') }) + .last() + .click(); + + await page.waitForLoadState('networkidle'); + + // Verify success message appears + await expect(page.getByText(tNewsletter('marketingPreferencesUpdated'))).toBeVisible(); + + // Track subscription for cleanup + subscribe.trackSubscription(testCustomer.email); + }, +); diff --git a/core/tests/ui/e2e/subscribe.spec.ts b/core/tests/ui/e2e/subscribe.spec.ts index b0c28d3402..273971b736 100644 --- a/core/tests/ui/e2e/subscribe.spec.ts +++ b/core/tests/ui/e2e/subscribe.spec.ts @@ -26,6 +26,7 @@ test( await expect(page.getByText(t('subscribedToNewsletter'))).toBeVisible(); + // Track that we attempted to subscribe this email subscribe.trackSubscription(email); }, ); diff --git a/core/vibes/soul/sections/account-settings/index.tsx b/core/vibes/soul/sections/account-settings/index.tsx index 583894341d..8c6377d621 100644 --- a/core/vibes/soul/sections/account-settings/index.tsx +++ b/core/vibes/soul/sections/account-settings/index.tsx @@ -1,4 +1,8 @@ import { ChangePasswordAction, ChangePasswordForm } from './change-password-form'; +import { + NewsletterSubscriptionForm, + UpdateNewsletterSubscriptionAction, +} from './newsletter-subscription-form'; import { Account, UpdateAccountAction, UpdateAccountForm } from './update-account-form'; export interface AccountSettingsSectionProps { @@ -12,6 +16,12 @@ export interface AccountSettingsSectionProps { confirmPasswordLabel?: string; currentPasswordLabel?: string; newPasswordLabel?: string; + newsletterSubscriptionEnabled?: boolean; + isAccountSubscribed?: boolean; + newsletterSubscriptionTitle?: string; + newsletterSubscriptionLabel?: string; + newsletterSubscriptionCtaLabel?: string; + updateNewsletterSubscriptionAction?: UpdateNewsletterSubscriptionAction; } // eslint-disable-next-line valid-jsdoc @@ -39,6 +49,12 @@ export function AccountSettingsSection({ confirmPasswordLabel, currentPasswordLabel, newPasswordLabel, + newsletterSubscriptionEnabled = false, + isAccountSubscribed = false, + newsletterSubscriptionTitle = 'Marketing preferences', + newsletterSubscriptionLabel = 'Opt-in to receive emails about new products and promotions.', + newsletterSubscriptionCtaLabel = 'Save preferences', + updateNewsletterSubscriptionAction, }: AccountSettingsSectionProps) { return (
@@ -56,7 +72,7 @@ export function AccountSettingsSection({ submitLabel={updateAccountSubmitLabel} /> -
+

{changePasswordTitle}

@@ -68,6 +84,19 @@ export function AccountSettingsSection({ submitLabel={changePasswordSubmitLabel} />
+ {newsletterSubscriptionEnabled && updateNewsletterSubscriptionAction && ( +
+

+ {newsletterSubscriptionTitle} +

+ +
+ )}
diff --git a/core/vibes/soul/sections/account-settings/newsletter-subscription-form.tsx b/core/vibes/soul/sections/account-settings/newsletter-subscription-form.tsx new file mode 100644 index 0000000000..efc24eb954 --- /dev/null +++ b/core/vibes/soul/sections/account-settings/newsletter-subscription-form.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { SubmissionResult } from '@conform-to/react'; +import { useTranslations } from 'next-intl'; +import { useActionState, useEffect, useState } from 'react'; + +import { Switch } from '@/vibes/soul/form/switch'; +import { Button } from '@/vibes/soul/primitives/button'; +import { toast } from '@/vibes/soul/primitives/toaster'; + +type Action = (state: Awaited, payload: P) => S | Promise; + +interface State { + lastResult: SubmissionResult | null; + successMessage?: string; +} + +export type UpdateNewsletterSubscriptionAction = Action; + +export interface NewsletterSubscriptionFormProps { + action: UpdateNewsletterSubscriptionAction; + isAccountSubscribed: boolean; + label?: string; + ctaLabel?: string; +} + +export function NewsletterSubscriptionForm({ + action, + isAccountSubscribed, + label = 'Opt-in to receive emails about new products and promotions.', + ctaLabel = 'Update', +}: NewsletterSubscriptionFormProps) { + const t = useTranslations('Account.Settings.NewsletterSubscription'); + + const [checked, setChecked] = useState(isAccountSubscribed); + const [state, formAction, isPending] = useActionState(action, { + lastResult: null, + }); + + const onCheckedChange = (value: boolean) => { + setChecked(value); + }; + + useEffect(() => { + if (state.lastResult?.status === 'success' && state.successMessage != null) { + toast.success(state.successMessage); + } + + if (state.lastResult?.error) { + // eslint-disable-next-line no-console + console.log(state.lastResult.error); + toast.error(t('somethingWentWrong')); + } + }, [state, t]); + + return ( +
+ + + + + ); +}