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
5 changes: 5 additions & 0 deletions .changeset/many-beds-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-makeswift": minor
---

This release includes all changes included in the `canary` 1.4.0 release (see the 1.4.0 changelog for more details: https://github.com/bigcommerce/catalyst/blob/44c682ef988030d7500275f3e4e4503a3a1af63c/core/CHANGELOG.md#140)
7 changes: 6 additions & 1 deletion .github/workflows/changesets-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ on:

concurrency: ${{ github.workflow }}-${{ github.ref }}

permissions:
id-token: write
contents: write
packages: write
pull-requests: write

jobs:
changesets-release:
name: Changesets Release
Expand Down Expand Up @@ -42,4 +48,3 @@ jobs:
commit: "Version Packages (`${{ github.ref_name }}`)"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
22
24
5 changes: 4 additions & 1 deletion core/app/[locale]/(default)/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface Props {
params: Promise<{ locale: string }>;
searchParams: Promise<{
redirectTo?: string;
error?: string;
}>;
}

Expand All @@ -29,7 +30,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {

export default async function Login({ params, searchParams }: Props) {
const { locale } = await params;
const { redirectTo = '/account/orders' } = await searchParams;
const { redirectTo = '/account/orders', error } = await searchParams;

setRequestLocale(locale);

Expand All @@ -38,13 +39,15 @@ export default async function Login({ params, searchParams }: Props) {
const vanityUrl = buildConfig.get('urls').vanityUrl;
const redirectUrl = new URL(redirectTo, vanityUrl);
const redirectTarget = redirectUrl.pathname + redirectUrl.search;
const tokenErrorMessage = error === 'InvalidToken' ? t('invalidToken') : undefined;

return (
<>
<ForceRefresh />
<SignInSection
action={login.bind(null, { redirectTo: redirectTarget })}
emailLabel={t('email')}
error={tokenErrorMessage}
forgotPasswordHref="/login/forgot-password"
forgotPasswordLabel={t('forgotPassword')}
passwordLabel={t('password')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export async function GET(_: Request, { params }: { params: Promise<{ token: str
// and redirect to redirectTo
await signIn('jwt', { jwt: token, cartId, redirectTo });
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);

rethrow(error);

redirect(`/login?error=InvalidToken`);
Expand Down
15 changes: 14 additions & 1 deletion core/app/[locale]/(default)/(auth)/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { getTranslations, setRequestLocale } from 'next-intl/server';

import { Field } from '@/vibes/soul/form/dynamic-form/schema';
import { DynamicFormSection } from '@/vibes/soul/sections/dynamic-form-section';
import {
formFieldTransformer,
Expand Down Expand Up @@ -32,6 +33,17 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
};
}

// There is currently a GraphQL gap where the "Exclusive Offers" field isn't accounted for
// during customer registration, so the field should not be shown on the Catalyst storefront until it is hooked up.
function removeExlusiveOffersField(field: Field | Field[]): boolean {
if (Array.isArray(field)) {
// Exclusive offers field will always have ID '25', since it is made upon store creation and is also read-only.
return !field.some((f) => f.id === '25');
}

return field.id !== '25';
}

export default async function Register({ params }: Props) {
const { locale } = await params;

Expand Down Expand Up @@ -90,7 +102,8 @@ export default async function Register({ params }: Props) {

return injectCountryCodeOptions(field, countries ?? []);
})
.filter(exists);
.filter(exists)
.filter(removeExlusiveOffersField);

return (
<DynamicFormSection
Expand Down
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)] }),
};
}
};
16 changes: 11 additions & 5 deletions core/app/[locale]/(default)/account/settings/page-data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,6 +20,7 @@ const CustomerSettingsQuery = graphql(
firstName
lastName
company
isSubscribedToNewsletter
}
site {
settings {
Expand All @@ -31,14 +32,17 @@ const CustomerSettingsQuery = graphql(
...FormFieldsFragment
}
}
newsletter {
showNewsletterSignup
}
}
}
}
`,
[FormFieldsFragment],
);

type Variables = VariablesOf<typeof CustomerSettingsQuery>;
type Variables = VariablesOf<typeof AccountSettingsQuery>;

interface Props {
address?: {
Expand All @@ -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,
Expand All @@ -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;
Expand All @@ -79,5 +84,6 @@ export const getCustomerSettingsQuery = cache(async ({ address, customer }: Prop
addressFields,
customerFields,
customerInfo,
newsletterSettings,
};
});
26 changes: 22 additions & 4 deletions core/app/[locale]/(default)/account/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 }>;
Expand All @@ -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 (
<AccountSettingsSection
account={customerSettings.customerInfo}
account={accountSettings.customerInfo}
changePasswordAction={changePassword}
changePasswordSubmitLabel={t('cta')}
changePasswordTitle={t('changePassword')}
confirmPasswordLabel={t('confirmPassword')}
currentPasswordLabel={t('currentPassword')}
isAccountSubscribed={isAccountSubscribed}
newPasswordLabel={t('newPassword')}
newsletterSubscriptionCtaLabel={t('cta')}
newsletterSubscriptionEnabled={newsletterSubscriptionEnabled}
newsletterSubscriptionLabel={t('NewsletterSubscription.label')}
newsletterSubscriptionTitle={t('NewsletterSubscription.title')}
title={t('title')}
updateAccountAction={updateCustomer}
updateAccountSubmitLabel={t('cta')}
updateNewsletterSubscriptionAction={updateNewsletterSubscriptionActionWithCustomerInfo}
/>
);
}
Loading