Skip to content

Commit af40fbf

Browse files
committed
feat(core): allow customer subscription to Newsletter
1 parent abcb16a commit af40fbf

5 files changed

Lines changed: 189 additions & 4 deletions

File tree

core/components/subscribe/_actions/subscribe.ts

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,32 @@
11
'use server';
22

3+
import { BigCommerceGQLError } from '@bigcommerce/catalyst-client';
34
import { SubmissionResult } from '@conform-to/react';
45
import { parseWithZod } from '@conform-to/zod';
56
import { getTranslations } from 'next-intl/server';
67

78
import { schema } from '@/vibes/soul/primitives/inline-email-form/schema';
9+
import { client } from '~/client';
10+
import { graphql } from '~/client/graphql';
11+
12+
const SubscribeToNewsletterMutation = graphql(`
13+
mutation SubscribeToNewsletterMutation($input: SubscribeToNewsletterInput!) {
14+
newsletter {
15+
subscribe(input: $input) {
16+
success
17+
errors {
18+
__typename
19+
... on CreateSubscriberEmailInvalidError {
20+
message
21+
}
22+
... on CreateSubscriberAlreadySubscribedError {
23+
message
24+
}
25+
}
26+
}
27+
}
28+
}
29+
`);
830

931
export const subscribe = async (
1032
_lastResult: { lastResult: SubmissionResult | null },
@@ -18,8 +40,59 @@ export const subscribe = async (
1840
return { lastResult: submission.reply() };
1941
}
2042

21-
// Simulate a network request
22-
await new Promise((resolve) => setTimeout(resolve, 1000));
43+
try {
44+
const response = await client.fetch({
45+
document: SubscribeToNewsletterMutation,
46+
variables: {
47+
input: {
48+
email: submission.value.email,
49+
},
50+
},
51+
fetchOptions: {
52+
cache: 'no-store',
53+
},
54+
});
55+
56+
if (response.data.success) {
57+
return { lastResult: submission.reply(), successMessage: t('success') };
58+
}
59+
60+
if (response.errors?.length > 0) {
61+
return {
62+
lastResult: submission.reply({
63+
formErrors: response.errors.map(({ __typename }) => {
64+
switch (__typename) {
65+
case 'CreateSubscriberAlreadySubscribedError':
66+
return t('Errors.subcriberAlreadyExists');
67+
68+
case 'CreateSubscriberEmailInvalidError':
69+
return t('Errors.invalidEmail');
2370

24-
return { lastResult: submission.reply(), successMessage: t('success') };
71+
default:
72+
return t('Errors.somethingWentWrong');
73+
}
74+
}),
75+
}),
76+
};
77+
}
78+
79+
return { lastResult: submission.reply({ formErrors: [t('Errors.somethingWentWrong')] }) };
80+
} catch (error) {
81+
// eslint-disable-next-line no-console
82+
console.error(error);
83+
84+
if (error instanceof BigCommerceGQLError) {
85+
return {
86+
lastResult: submission.reply({
87+
formErrors: error.errors.map(({ message }) => message),
88+
}),
89+
};
90+
}
91+
92+
if (error instanceof Error) {
93+
return { lastResult: submission.reply({ formErrors: [error.message] }) };
94+
}
95+
96+
return { lastResult: submission.reply({ formErrors: [t('Errors.somethingWentWrong')] }) };
97+
}
2598
};

core/messages/en.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,12 @@
504504
"title": "Sign up for our newsletter",
505505
"placeholder": "Enter your email",
506506
"description": "Stay up to date with the latest news and offers from our store.",
507-
"success": "Thank you for your interest! Newsletter feature is coming soon!"
507+
"success": "You have been subscribed to our newsletter.",
508+
"Errors": {
509+
"subcriberAlreadyExists": "You are already subscribed to our newsletter.",
510+
"invalidEmail": "Please enter a valid email address.",
511+
"somethingWentWrong": "Something went wrong. Please try again later."
512+
}
508513
},
509514
"ConsentManager": {
510515
"Common": {

core/tests/fixtures/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { extendedPage, toHaveURL } from './page';
1616
import { PromotionFixture } from './promotion';
1717
import { RedirectsFixture } from './redirects';
1818
import { SettingsFixture } from './settings';
19+
import { SubscribeFixture } from './subscribe';
1920
import { WebPageFixture } from './webpage';
2021

2122
interface Fixtures {
@@ -27,6 +28,7 @@ interface Fixtures {
2728
promotion: PromotionFixture;
2829
redirects: RedirectsFixture;
2930
settings: SettingsFixture;
31+
subscribe: SubscribeFixture;
3032
webPage: WebPageFixture;
3133
/**
3234
* 'reuseCustomerSession' sets the the configuration for the customer fixture and determines whether to reuse the customer session.
@@ -131,6 +133,16 @@ export const test = baseTest.extend<Fixtures>({
131133
},
132134
{ scope: 'test' },
133135
],
136+
subscribe: [
137+
async ({ page }, use, currentTest) => {
138+
const subscribeFixture = new SubscribeFixture(page, currentTest);
139+
140+
await use(subscribeFixture);
141+
142+
await subscribeFixture.cleanup();
143+
},
144+
{ scope: 'test' },
145+
],
134146
webPage: [
135147
async ({ page }, use, currentTest) => {
136148
const webPageFixture = new WebPageFixture(page, currentTest);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Fixture } from '~/tests/fixtures/fixture';
2+
3+
export class SubscribeFixture extends Fixture {
4+
subscribedEmails: string[] = [];
5+
6+
trackSubscription(email: string): void {
7+
this.subscribedEmails.push(email);
8+
}
9+
10+
async cleanup(): Promise<void> {
11+
// Note: BigCommerce GraphQL API does not currently provide an unsubscribe mutation.
12+
// Newsletter subscriptions cannot be cleaned up programmatically.
13+
// This method is kept for consistency with other fixtures and future extensibility.
14+
this.subscribedEmails = [];
15+
}
16+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { faker } from '@faker-js/faker';
2+
3+
import { expect, test } from '~/tests/fixtures';
4+
import { getTranslations } from '~/tests/lib/i18n';
5+
import { TAGS } from '~/tests/tags';
6+
7+
// Shared email for tests that need to use the same email
8+
const sharedEmail = faker.internet.email();
9+
10+
test(
11+
'Successfully subscribes a user to the newsletter',
12+
{ tag: [TAGS.writesData] },
13+
async ({ page, subscribe }) => {
14+
const t = await getTranslations('Components.Subscribe');
15+
16+
await page.goto('/');
17+
await page.waitForLoadState('networkidle');
18+
19+
const email = sharedEmail;
20+
21+
const emailInput = page.getByPlaceholder(t('placeholder'));
22+
23+
await emailInput.fill(email);
24+
25+
const submitButton = page.locator('input[type="email"]').locator('..').getByRole('button');
26+
27+
await submitButton.click();
28+
await page.waitForLoadState('networkidle');
29+
30+
await expect(page.getByText(t('success'))).toBeVisible();
31+
32+
subscribe.trackSubscription(email);
33+
},
34+
);
35+
36+
test('Shows error when email is invalid', async ({ page }) => {
37+
const t = await getTranslations('Components.Subscribe');
38+
39+
await page.goto('/');
40+
await page.waitForLoadState('networkidle');
41+
42+
const invalidEmail = 'not-an-email';
43+
44+
const emailInput = page.getByPlaceholder(t('placeholder'));
45+
46+
await emailInput.fill(invalidEmail);
47+
48+
const submitButton = page.locator('input[type="email"]').locator('..').getByRole('button');
49+
50+
await submitButton.click();
51+
await page.waitForLoadState('networkidle');
52+
53+
await expect(page.getByText(t('Errors.invalidEmail'))).toBeVisible();
54+
});
55+
56+
test('Shows error when user tries to subscribe again with the same email', async ({
57+
page,
58+
subscribe,
59+
}) => {
60+
const t = await getTranslations('Components.Subscribe');
61+
62+
await page.goto('/');
63+
await page.waitForLoadState('networkidle');
64+
65+
const email = sharedEmail;
66+
67+
const emailInput = page.getByPlaceholder(t('placeholder'));
68+
const submitButton = page.locator('input[type="email"]').locator('..').getByRole('button');
69+
70+
// Try to subscribe again with the same email (already subscribed in first test)
71+
await emailInput.fill(email);
72+
await submitButton.click();
73+
await page.waitForLoadState('networkidle');
74+
75+
await expect(page.getByText(t('Errors.subcriberAlreadyExists'))).toBeVisible();
76+
77+
// Track that we attempted to subscribe this email (already tracked in first test)
78+
subscribe.trackSubscription(email);
79+
});

0 commit comments

Comments
 (0)