Skip to content

Commit b525ef5

Browse files
committed
feat(core): add toggle for newsletter in account settings
1 parent abcb16a commit b525ef5

File tree

7 files changed

+246
-2
lines changed

7 files changed

+246
-2
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
'use server';
2+
3+
import { BigCommerceGQLError } from '@bigcommerce/catalyst-client';
4+
import { SubmissionResult } from '@conform-to/react';
5+
import { parseWithZod } from '@conform-to/zod';
6+
import { getTranslations } from 'next-intl/server';
7+
import { z } from 'zod';
8+
9+
import { client } from '~/client';
10+
import { graphql } from '~/client/graphql';
11+
import { serverToast } from '~/lib/server-toast';
12+
13+
const updateNewsletterSubscriptionSchema = z.object({
14+
intent: z.enum(['subscribe', 'unsubscribe']),
15+
});
16+
17+
const SubscribeToNewsletterMutation = graphql(`
18+
mutation SubscribeToNewsletterMutation($input: SubscribeToNewsletterInput!) {
19+
newsletter {
20+
subscribe(input: $input) {
21+
success
22+
errors {
23+
__typename
24+
... on CreateSubscriberEmailInvalidError {
25+
message
26+
}
27+
... on CreateSubscriberAlreadySubscribedError {
28+
message
29+
}
30+
}
31+
}
32+
}
33+
}
34+
`);
35+
36+
const UnsubscribeFromNewsletterMutation = graphql(`
37+
mutation UnsubscribeFromNewsletterMutation($input: UnsubscribeFromNewsletterInput!) {
38+
newsletter {
39+
unsubscribe(input: $input) {
40+
success
41+
errors {
42+
__typename
43+
... on DeleteSubscriberEmailInvalidError {
44+
message
45+
}
46+
... on DeleteSubscriberNotSubscribedError {
47+
message
48+
}
49+
}
50+
}
51+
}
52+
}
53+
`);
54+
55+
export const updateNewsletterSubscription = async (
56+
{
57+
customerInfo,
58+
}: {
59+
customerInfo: {
60+
entityId: number;
61+
email: string;
62+
firstName: string;
63+
lastName: string;
64+
company: string;
65+
};
66+
},
67+
_prevState: { lastResult: SubmissionResult | null },
68+
formData: FormData,
69+
) => {
70+
const t = await getTranslations('Account.Settings.NewsletterSubscription');
71+
72+
console.log('form submit!', formData);
73+
74+
const submission = parseWithZod(formData, { schema: updateNewsletterSubscriptionSchema });
75+
76+
// TODO: fix error
77+
console.log('submission', submission.status);
78+
79+
if (submission.status !== 'success') {
80+
return { lastResult: submission.reply() };
81+
}
82+
83+
try {
84+
const response = await client.fetch({
85+
document:
86+
submission.value.intent === 'subscribe'
87+
? SubscribeToNewsletterMutation
88+
: UnsubscribeFromNewsletterMutation,
89+
variables: {
90+
input: {
91+
email: customerInfo.email,
92+
firstName: customerInfo.firstName,
93+
lastName: customerInfo.lastName,
94+
},
95+
},
96+
});
97+
98+
if (response.data.success) {
99+
await serverToast.success(t('success'));
100+
} else {
101+
await serverToast.error(t('somethingWentWrong'));
102+
}
103+
104+
return { lastResult: submission.reply() };
105+
} catch (error) {
106+
// eslint-disable-next-line no-console
107+
console.error(error);
108+
109+
if (error instanceof BigCommerceGQLError) {
110+
// TODO: handle custom errors?
111+
await serverToast.error(t('somethingWentWrong'));
112+
}
113+
114+
if (error instanceof Error) {
115+
await serverToast.error(error.message);
116+
}
117+
118+
return { lastResult: submission.reply({ formErrors: [t('somethingWentWrong')] }) };
119+
}
120+
};

core/app/[locale]/(default)/account/settings/page-data.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ const CustomerSettingsQuery = graphql(
3131
...FormFieldsFragment
3232
}
3333
}
34+
newsletter {
35+
showNewsletterSignup
36+
}
3437
}
3538
}
3639
}
@@ -70,6 +73,7 @@ export const getCustomerSettingsQuery = cache(async ({ address, customer }: Prop
7073
const addressFields = response.data.site.settings?.formFields.shippingAddress;
7174
const customerFields = response.data.site.settings?.formFields.customer;
7275
const customerInfo = response.data.customer;
76+
const storeSettings = response.data.site.settings?.newsletter;
7377

7478
if (!addressFields || !customerFields || !customerInfo) {
7579
return null;
@@ -79,5 +83,6 @@ export const getCustomerSettingsQuery = cache(async ({ address, customer }: Prop
7983
addressFields,
8084
customerFields,
8185
customerInfo,
86+
storeSettings,
8287
};
8388
});

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable react/jsx-no-bind */
12
import { Metadata } from 'next';
23
import { notFound } from 'next/navigation';
34
import { getTranslations, setRequestLocale } from 'next-intl/server';
@@ -6,6 +7,7 @@ import { AccountSettingsSection } from '@/vibes/soul/sections/account-settings';
67

78
import { changePassword } from './_actions/change-password';
89
import { updateCustomer } from './_actions/update-customer';
10+
import { updateNewsletterSubscription } from './_actions/update-newsletter-subscription';
911
import { getCustomerSettingsQuery } from './page-data';
1012

1113
interface Props {
@@ -35,6 +37,15 @@ export default async function Settings({ params }: Props) {
3537
notFound();
3638
}
3739

40+
const newsletterSubscriptionEnabled = customerSettings.storeSettings?.showNewsletterSignup;
41+
42+
const updateNewsletterSubscriptionActionWithCustomerInfo = updateNewsletterSubscription.bind(
43+
null,
44+
{
45+
customerInfo: customerSettings.customerInfo,
46+
},
47+
);
48+
3849
return (
3950
<AccountSettingsSection
4051
account={customerSettings.customerInfo}
@@ -44,9 +55,14 @@ export default async function Settings({ params }: Props) {
4455
confirmPasswordLabel={t('confirmPassword')}
4556
currentPasswordLabel={t('currentPassword')}
4657
newPasswordLabel={t('newPassword')}
58+
newsletterSubscriptionCtaLabel={t('cta')}
59+
newsletterSubscriptionEnabled={newsletterSubscriptionEnabled}
60+
newsletterSubscriptionLabel={t('NewsletterSubscription.label')}
61+
newsletterSubscriptionTitle={t('NewsletterSubscription.title')}
4762
title={t('title')}
4863
updateAccountAction={updateCustomer}
4964
updateAccountSubmitLabel={t('cta')}
65+
updateNewsletterSubscriptionAction={updateNewsletterSubscriptionActionWithCustomerInfo}
5066
/>
5167
);
5268
}

core/messages/en.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,13 @@
198198
"currentPassword": "Current password",
199199
"newPassword": "New password",
200200
"confirmPassword": "Confirm password",
201-
"cta": "Update"
201+
"cta": "Update",
202+
"NewsletterSubscription": {
203+
"title": "Marketing preferences",
204+
"label": "Opt-in to receive emails about new products and promotions.",
205+
"success": "Marketing preferences have been updated successfully!",
206+
"somethingWentWrong": "Something went wrong. Please try again later."
207+
}
202208
}
203209
},
204210
"Wishlist": {

core/vibes/soul/form/switch/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface Props {
1616
onCheckedChange?: (checked: boolean) => void | Promise<void>;
1717
disabled?: boolean;
1818
loading?: boolean;
19+
value?: string;
1920
}
2021

2122
export const Switch = ({
@@ -28,6 +29,7 @@ export const Switch = ({
2829
loading,
2930
checked,
3031
onCheckedChange,
32+
value,
3133
}: Props) => {
3234
const id = useId();
3335
const hasLabel = label != null && label !== '';
@@ -56,6 +58,7 @@ export const Switch = ({
5658
id={id}
5759
name={name}
5860
onCheckedChange={onCheckedChange}
61+
value={value}
5962
>
6063
<SwitchPrimitive.Thumb
6164
className={clsx(

core/vibes/soul/sections/account-settings/index.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { ChangePasswordAction, ChangePasswordForm } from './change-password-form';
2+
import {
3+
NewsletterSubscriptionForm,
4+
UpdateNewsletterSubscriptionAction,
5+
} from './newsletter-subscription-form';
26
import { Account, UpdateAccountAction, UpdateAccountForm } from './update-account-form';
37

48
export interface AccountSettingsSectionProps {
@@ -12,6 +16,11 @@ export interface AccountSettingsSectionProps {
1216
confirmPasswordLabel?: string;
1317
currentPasswordLabel?: string;
1418
newPasswordLabel?: string;
19+
newsletterSubscriptionEnabled?: boolean;
20+
newsletterSubscriptionTitle?: string;
21+
newsletterSubscriptionLabel?: string;
22+
newsletterSubscriptionCtaLabel?: string;
23+
updateNewsletterSubscriptionAction?: UpdateNewsletterSubscriptionAction;
1524
}
1625

1726
// eslint-disable-next-line valid-jsdoc
@@ -39,6 +48,11 @@ export function AccountSettingsSection({
3948
confirmPasswordLabel,
4049
currentPasswordLabel,
4150
newPasswordLabel,
51+
newsletterSubscriptionEnabled = false,
52+
newsletterSubscriptionTitle = 'Marketing preferences',
53+
newsletterSubscriptionLabel = 'Opt-in to receive emails about new products and promotions.',
54+
newsletterSubscriptionCtaLabel = 'Save preferences',
55+
updateNewsletterSubscriptionAction,
4256
}: AccountSettingsSectionProps) {
4357
return (
4458
<section className="w-full @container">
@@ -56,7 +70,7 @@ export function AccountSettingsSection({
5670
submitLabel={updateAccountSubmitLabel}
5771
/>
5872
</div>
59-
<div className="border-t border-[var(--account-settings-section-border,hsl(var(--contrast-100)))] pt-12">
73+
<div className="border-t border-[var(--account-settings-section-border,hsl(var(--contrast-100)))] py-12">
6074
<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">
6175
{changePasswordTitle}
6276
</h1>
@@ -68,6 +82,19 @@ export function AccountSettingsSection({
6882
submitLabel={changePasswordSubmitLabel}
6983
/>
7084
</div>
85+
{newsletterSubscriptionEnabled && updateNewsletterSubscriptionAction && (
86+
<div className="border-t border-[var(--account-settings-section-border,hsl(var(--contrast-100)))] pt-12">
87+
<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">
88+
{newsletterSubscriptionTitle}
89+
</h1>
90+
<NewsletterSubscriptionForm
91+
action={updateNewsletterSubscriptionAction}
92+
ctaLabel={newsletterSubscriptionCtaLabel}
93+
isCustomerSubscribed={false}
94+
label={newsletterSubscriptionLabel}
95+
/>
96+
</div>
97+
)}
7198
</div>
7299
</div>
73100
</section>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use client';
2+
3+
import { SubmissionResult } from '@conform-to/react';
4+
import { ReactNode, useActionState, useState } from 'react';
5+
import { useFormStatus } from 'react-dom';
6+
7+
import { Switch } from '@/vibes/soul/form/switch';
8+
import { Button } from '@/vibes/soul/primitives/button';
9+
10+
type Action<S, P> = (state: Awaited<S>, payload: P) => S | Promise<S>;
11+
12+
interface State {
13+
lastResult: SubmissionResult | null;
14+
}
15+
16+
export type UpdateNewsletterSubscriptionAction = Action<State, FormData>;
17+
18+
export interface NewsletterSubscriptionFormProps {
19+
action: UpdateNewsletterSubscriptionAction;
20+
isCustomerSubscribed: boolean;
21+
label?: string;
22+
ctaLabel?: string;
23+
}
24+
25+
export function NewsletterSubscriptionForm({
26+
action,
27+
isCustomerSubscribed,
28+
label = 'Opt-in to receive emails about new products and promotions.',
29+
ctaLabel = 'Save preferences',
30+
}: NewsletterSubscriptionFormProps) {
31+
const [checked, setChecked] = useState(isCustomerSubscribed);
32+
const [, formAction] = useActionState(action, { lastResult: null });
33+
34+
const onCheckedChange = (value: boolean) => {
35+
setChecked(value);
36+
};
37+
38+
return (
39+
<form action={formAction} className="space-y-5">
40+
<input name="intent" type="hidden" value={checked ? 'unsubscribe' : 'subscribe'} />
41+
<Switch
42+
checked={checked}
43+
label={label}
44+
name="intent"
45+
onCheckedChange={onCheckedChange}
46+
value={checked ? 'unsubscribe' : 'subscribe'}
47+
/>
48+
<SubmitButton disabled={isCustomerSubscribed === checked}>{ctaLabel}</SubmitButton>
49+
</form>
50+
);
51+
}
52+
53+
function SubmitButton({ children, disabled = false }: { children: ReactNode; disabled?: boolean }) {
54+
const { pending } = useFormStatus();
55+
56+
return (
57+
<Button
58+
disabled={disabled || pending}
59+
loading={pending}
60+
size="small"
61+
type="submit"
62+
variant="secondary"
63+
>
64+
{children}
65+
</Button>
66+
);
67+
}

0 commit comments

Comments
 (0)