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
39 changes: 16 additions & 23 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,40 +103,33 @@ This ensures `integrations/makeswift` remains a faithful mirror of `canary` whil

#### Stage 2: Sync and Release `integrations/makeswift`

2. Follow steps 1-6 under "[Keeping `integrations/makeswift` in sync with `canary`](#keeping-integrationsmakeswift-in-sync-with-canary)"
2. Follow steps 1-6 under "[Keeping `integrations/makeswift` in sync with `canary`](#keeping-integrationsmakeswift-in-sync-with-canary)", with one addition: **include a changeset for `@bigcommerce/catalyst-makeswift` in the sync merge commit** rather than opening a separate PR for it afterwards.

3. **IMPORTANT**: After step 6, you'll need to open another PR into `integrations/makeswift`
- Ensure a local `integrations/makeswift` branch exists and is up to date (`git checkout -B integrations/makeswift origin/integrations/makeswift`)
- Run `git fetch origin` and create a new branch from `integrations/makeswift` (`git checkout -B bump-version origin/integrations/makeswift`)
- From this new `bump-version` branch, run `pnpm changeset`
- Select `@bigcommerce/catalyst-makeswift`
- For choosing between a `patch/minor/major` bump, you should copy the bump from Stage 1. (e.g., if `@bigcommerce/catalyst-core` went from `1.1.0` to `1.2.0`, choose `minor`)
- Example changeset:
- Match the bump type from Stage 1 (e.g., if `@bigcommerce/catalyst-core` went from `1.4.2` to `1.5.0`, use `minor`)
- Create a changeset file in `.changeset/` (e.g., `.changeset/sync-canary-1-5-0.md`):

```
---
"@bigcommerce/catalyst-makeswift": patch
"@bigcommerce/catalyst-makeswift": minor
---

Pulls in changes from the `@bigcommerce/catalyst-core@1.4.1` patch.
Pulls in changes from the `@bigcommerce/catalyst-core@1.5.0` release. For more information, see the [changelog entry](https://github.com/bigcommerce/catalyst/blob/<canary-sha>/core/CHANGELOG.md#150).
```

- Commit the generated changeset file and open a PR to merge this branch into `integrations/makeswift`
- Once merged, you can proceed to the next step
- Replace `<canary-sha>` with the merge commit SHA of the Version Packages PR on `canary` so the link remains stable
- Amend this changeset into the merge commit alongside any other sync changes (changeset cleanup, `core/package.json` and `core/CHANGELOG.md` fixes, etc.)

4. Merge the **Version Packages (`integrations/makeswift`)** PR: Changesets will open another PR (similar to Stage 1) bumping `@bigcommerce/catalyst-makeswift`. Merge it following the same process. This cuts a new release of the Makeswift variant.
3. Merge the **Version Packages (`integrations/makeswift`)** PR: After the sync lands, Changesets will open a PR (similar to Stage 1) bumping `@bigcommerce/catalyst-makeswift`. Merge it following the same process. This cuts a new release of the Makeswift variant.

5. **Tags and Releases:** Confirm tags exist for both `@bigcommerce/catalyst-core` and `@bigcommerce/catalyst-makeswift`. If needed, update `latest` tags in GitHub manually.
4. **Tags and Releases:** Confirm tags exist for both `@bigcommerce/catalyst-core` and `@bigcommerce/catalyst-makeswift`. Update `latest` tags to point to the new releases:

- Push manually:
```
git checkout canary
# Make sure you have the latest code
git fetch origin
git pull
git tag @bigcommerce/catalyst-core@latest -f
git push origin @bigcommerce/catalyst-core@latest -f
```
```bash
git fetch origin --tags
git tag @bigcommerce/catalyst-core@latest @bigcommerce/catalyst-core@<version> -f
git tag @bigcommerce/catalyst-makeswift@latest @bigcommerce/catalyst-makeswift@<version> -f
git push origin @bigcommerce/catalyst-core@latest -f
git push origin @bigcommerce/catalyst-makeswift@latest -f
```

### Additional Notes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@ import { graphql, VariablesOf } from '~/client/graphql';
import { FieldNameToFieldId } from '~/data-transformers/form-field-transformer/utils';
import { redirect } from '~/i18n/routing';
import { getCartId } from '~/lib/cart';
import { assertRecaptchaTokenPresent, getRecaptchaFromForm } from '~/lib/recaptcha';

import { ADDRESS_FIELDS_NAME_PREFIX, CUSTOMER_FIELDS_NAME_PREFIX } from './prefixes';

const RegisterCustomerMutation = graphql(`
mutation RegisterCustomerMutation($input: RegisterCustomerInput!) {
mutation RegisterCustomerMutation(
$input: RegisterCustomerInput!
$reCaptchaV2: ReCaptchaV2Input
) {
customer {
registerCustomer(input: $input) {
registerCustomer(input: $input, reCaptchaV2: $reCaptchaV2) {
customer {
firstName
lastName
Expand Down Expand Up @@ -356,12 +360,23 @@ export async function registerCustomer<F extends Field>(
};
}

const { siteKey, token } = await getRecaptchaFromForm(formData);
const recaptchaValidation = assertRecaptchaTokenPresent(siteKey, token, t('recaptchaRequired'));

if (!recaptchaValidation.success) {
return {
lastResult: submission.reply({ formErrors: recaptchaValidation.formErrors }),
};
}

try {
const input = parseRegisterCustomerInput(submission.value, fields);
const response = await client.fetch({
document: RegisterCustomerMutation,
variables: {
input,
reCaptchaV2:
recaptchaValidation.token != null ? { token: recaptchaValidation.token } : undefined,
},
fetchOptions: { cache: 'no-store' },
});
Expand Down
4 changes: 4 additions & 0 deletions core/app/[locale]/(default)/(auth)/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
REGISTER_CUSTOMER_FORM_LAYOUT,
transformFieldsToLayout,
} from '~/data-transformers/form-field-transformer/utils';
import { getRecaptchaSiteKey } from '~/lib/recaptcha';
import { exists } from '~/lib/utils';

import { ADDRESS_FIELDS_NAME_PREFIX, CUSTOMER_FIELDS_NAME_PREFIX } from './_actions/prefixes';
Expand Down Expand Up @@ -63,6 +64,8 @@ export default async function Register({ params }: Props) {
const { addressFields, customerFields, countries, passwordComplexitySettings } =
registerCustomerData;

const recaptchaSiteKey = await getRecaptchaSiteKey();

const fields = transformFieldsToLayout(
[
...addressFields.map((field) => {
Expand Down Expand Up @@ -154,6 +157,7 @@ export default async function Register({ params }: Props) {
}}
fields={fields}
passwordComplexity={passwordComplexitySettings}
recaptchaSiteKey={recaptchaSiteKey}
submitLabel={t('cta')}
title={t('heading')}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ import { schema } from '@/vibes/soul/sections/reviews/schema';
import { getSessionCustomerAccessToken } from '~/auth';
import { client } from '~/client';
import { graphql } from '~/client/graphql';
import { assertRecaptchaTokenPresent, getRecaptchaFromForm } from '~/lib/recaptcha';

const AddProductReviewMutation = graphql(`
mutation AddProductReviewMutation($input: AddProductReviewInput!) {
mutation AddProductReviewMutation(
$input: AddProductReviewInput!
$reCaptchaV2: ReCaptchaV2Input
) {
catalog {
addProductReview(input: $input) {
addProductReview(input: $input, reCaptchaV2: $reCaptchaV2) {
__typename
errors {
__typename
Expand All @@ -38,6 +42,16 @@ export async function submitReview(
return { ...prevState, lastResult: submission.reply() };
}

const { siteKey, token } = await getRecaptchaFromForm(payload);
const recaptchaValidation = assertRecaptchaTokenPresent(siteKey, token, t('recaptchaRequired'));

if (!recaptchaValidation.success) {
return {
...prevState,
lastResult: submission.reply({ formErrors: recaptchaValidation.formErrors }),
};
}

const { productEntityId, ...input } = submission.value;

try {
Expand All @@ -52,6 +66,8 @@ export async function submitReview(
},
productEntityId,
},
reCaptchaV2:
recaptchaValidation.token != null ? { token: recaptchaValidation.token } : undefined,
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,15 @@ interface Props {
pageInfo?: { hasNextPage: boolean; endCursor: string | null };
}>;
streamableProduct: Streamable<Awaited<ReturnType<typeof getStreamableProduct>>>;
recaptchaSiteKey?: string;
}

export const Reviews = async ({
productId,
searchParams,
streamableProduct,
streamableImages,
recaptchaSiteKey,
}: Props) => {
const t = await getTranslations('Product.Reviews');

Expand Down Expand Up @@ -189,6 +191,7 @@ export const Reviews = async ({
paginationInfo={streamablePaginationInfo}
previousLabel={t('previous')}
productId={productId}
recaptchaSiteKey={recaptchaSiteKey}
reviews={streamableReviews}
reviewsLabel={t('title')}
streamableImages={streamableImages}
Expand Down
8 changes: 7 additions & 1 deletion core/app/[locale]/(default)/product/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { productOptionsTransformer } from '~/data-transformers/product-options-t
import { getPreferredCurrencyCode } from '~/lib/currency';
import { getMakeswiftPageMetadata } from '~/lib/makeswift';
import { ProductDetail } from '~/lib/makeswift/components/product-detail';
import { getRecaptchaSiteKey } from '~/lib/recaptcha';
import { getMetadataAlternates } from '~/lib/seo/canonical';

import { addToCart } from './_actions/add-to-cart';
Expand Down Expand Up @@ -80,7 +81,10 @@ export default async function Product({ params, searchParams }: Props) {

const productId = Number(slug);

const { product: baseProduct, settings } = await getProduct(productId, customerAccessToken);
const [{ product: baseProduct, settings }, recaptchaSiteKey] = await Promise.all([
getProduct(productId, customerAccessToken),
getRecaptchaSiteKey(),
]);

const reviewsEnabled = Boolean(settings?.reviews.enabled && !settings.display.showProductRating);
const showRating = Boolean(settings?.reviews.enabled && settings.display.showProductRating);
Expand Down Expand Up @@ -586,6 +590,7 @@ export default async function Product({ params, searchParams }: Props) {
}}
productId={baseProduct.entityId}
quantityLabel={t('ProductDetails.quantity')}
recaptchaSiteKey={recaptchaSiteKey}
reviewFormAction={submitReview}
thumbnailLabel={t('ProductDetails.thumbnail')}
user={streamableUser}
Expand All @@ -607,6 +612,7 @@ export default async function Product({ params, searchParams }: Props) {
<div id="reviews">
<Reviews
productId={productId}
recaptchaSiteKey={recaptchaSiteKey}
searchParams={searchParams}
streamableImages={streamableImages}
streamableProduct={streamableProduct}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Field, schema } from '@/vibes/soul/form/dynamic-form/schema';
import { client } from '~/client';
import { graphql, VariablesOf } from '~/client/graphql';
import { redirect } from '~/i18n/routing';
import { assertRecaptchaTokenPresent, getRecaptchaFromForm } from '~/lib/recaptcha';

const inputSchema = z.object({
data: z.object({
Expand All @@ -26,8 +27,8 @@ const inputSchema = z.object({
});

const SubmitContactUsMutation = graphql(`
mutation SubmitContactUsMutation($input: SubmitContactUsInput!) {
submitContactUs(input: $input) {
mutation SubmitContactUsMutation($input: SubmitContactUsInput!, $reCaptchaV2: ReCaptchaV2Input) {
submitContactUs(input: $input, reCaptchaV2: $reCaptchaV2) {
__typename
errors {
__typename
Expand Down Expand Up @@ -74,12 +75,23 @@ export async function submitContactForm<F extends Field>(
};
}

const { siteKey, token } = await getRecaptchaFromForm(formData);
const recaptchaValidation = assertRecaptchaTokenPresent(siteKey, token, t('recaptchaRequired'));

if (!recaptchaValidation.success) {
return {
lastResult: submission.reply({ formErrors: recaptchaValidation.formErrors }),
};
}

try {
const input = parseContactFormInput(submission.value);
const response = await client.fetch({
document: SubmitContactUsMutation,
variables: {
input,
reCaptchaV2:
recaptchaValidation.token != null ? { token: recaptchaValidation.token } : undefined,
},
fetchOptions: { cache: 'no-store' },
});
Expand Down
4 changes: 4 additions & 0 deletions core/app/[locale]/(default)/webpages/[id]/contact/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
truncateBreadcrumbs,
} from '~/data-transformers/breadcrumbs-transformer';
import { getMakeswiftPageMetadata } from '~/lib/makeswift';
import { getRecaptchaSiteKey } from '~/lib/recaptcha';
import { getMetadataAlternates } from '~/lib/seo/canonical';

import { WebPage, WebPageContent } from '../_components/web-page';
Expand Down Expand Up @@ -198,6 +199,8 @@ export default async function ContactPage({ params, searchParams }: Props) {
);
}

const recaptchaSiteKey = await getRecaptchaSiteKey();

return (
<WebPageContent
breadcrumbs={Streamable.from(() => getWebPageBreadcrumbs(id))}
Expand All @@ -207,6 +210,7 @@ export default async function ContactPage({ params, searchParams }: Props) {
<DynamicForm
action={submitContactForm}
fields={await getContactFields(id)}
recaptchaSiteKey={recaptchaSiteKey}
submitLabel={t('cta')}
/>
</div>
Expand Down
79 changes: 79 additions & 0 deletions core/lib/recaptcha.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import 'server-only';

import { cache } from 'react';

import { client } from '~/client';
import { graphql } from '~/client/graphql';
import { revalidate } from '~/client/revalidate-target';

import { RECAPTCHA_TOKEN_FORM_KEY, type ReCaptchaSettings } from './recaptcha/constants';

export { RECAPTCHA_TOKEN_FORM_KEY } from './recaptcha/constants';
export type { ReCaptchaSettings } from './recaptcha/constants';

export const ReCaptchaSettingsQuery = graphql(`
query ReCaptchaSettingsQuery {
site {
settings {
reCaptcha {
isEnabledOnStorefront
siteKey
}
}
}
}
`);

export const getReCaptchaSettings = cache(async (): Promise<ReCaptchaSettings | null> => {
const { data } = await client.fetch({
document: ReCaptchaSettingsQuery,
fetchOptions: { next: { revalidate } },
});

const reCaptcha = data.site.settings?.reCaptcha;

if (!reCaptcha?.siteKey) {
return null;
}

return {
isEnabledOnStorefront: reCaptcha.isEnabledOnStorefront,
siteKey: reCaptcha.siteKey,
};
});

export const getRecaptchaSiteKey = cache(async (): Promise<string | undefined> => {
const settings = await getReCaptchaSettings();

return settings?.isEnabledOnStorefront === true && settings.siteKey
? settings.siteKey
: undefined;
});

export async function getRecaptchaFromForm(
formData: FormData,
): Promise<{ siteKey: string | undefined; token: string }> {
const siteKey = await getRecaptchaSiteKey();
const raw = formData.get(RECAPTCHA_TOKEN_FORM_KEY);
const token = typeof raw === 'string' ? raw : '';

return { siteKey, token };
}

export function assertRecaptchaTokenPresent(
siteKey: string | undefined,
token: string,
recaptchaRequiredMessage: string,
): { success: true; token: string | undefined } | { success: false; formErrors: [string] } {
if (!siteKey) {
return { success: true, token: undefined };
}

const tokenValue = token.trim();

if (!tokenValue) {
return { success: false, formErrors: [recaptchaRequiredMessage] };
}

return { success: true, token: tokenValue };
}
6 changes: 6 additions & 0 deletions core/lib/recaptcha/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface ReCaptchaSettings {
isEnabledOnStorefront: boolean;
siteKey: string;
}

export const RECAPTCHA_TOKEN_FORM_KEY = 'g-recaptcha-response';
Loading
Loading