Skip to content
Open
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/plenty-peaches-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---

Replace a few moment.js invocations with native browser APIs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
// @ts-strict-ignore
import moment from "moment";

import { type ExtendedGiftCard, type GiftCardBase } from "./types";

function isGiftCardExpired<T extends GiftCardBase>(giftCard: T): boolean {
if (!giftCard?.expiryDate) {
return false;
}

return moment(giftCard?.expiryDate).isBefore(moment());
return new Date(giftCard.expiryDate) < new Date();
}
Comment on lines +8 to 9
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new Date(giftCard.expiryDate) changes parsing semantics for date-only strings like YYYY-MM-DD: JS treats them as UTC midnight, while moment("YYYY-MM-DD") treated them as local midnight. This can shift the effective expiry time by timezone and change isExpired around boundary cases. Consider normalizing to the same timezone semantics as before (e.g., parse date-only values as local midnight) before comparing.

Copilot uses AI. Check for mistakes.

export function getExtendedGiftCard<T extends GiftCardBase>(giftCard?: T): ExtendedGiftCard<T> {
export function getExtendedGiftCard<T extends GiftCardBase>(
giftCard?: T,
Comment on lines +11 to +12
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing getExtendedGiftCard to accept an optional input and return undefined forces call sites that map over known arrays of gift cards to add extra filtering. Consider using TypeScript overloads (non-optional input -> non-optional output; optional input -> optional output) so .map(getExtendedGiftCard) can remain type-safe without a separate .filter(...) pass.

Suggested change
export function getExtendedGiftCard<T extends GiftCardBase>(
giftCard?: T,
export function getExtendedGiftCard<T extends GiftCardBase>(
giftCard: T,
): ExtendedGiftCard<T>;
export function getExtendedGiftCard<T extends GiftCardBase>(
giftCard?: T,
): ExtendedGiftCard<T> | undefined;
export function getExtendedGiftCard<T extends GiftCardBase>(
giftCard?: T,

Copilot uses AI. Check for mistakes.
): ExtendedGiftCard<T> | undefined {
// todo do not accept optional value, check for existence higher
if (!giftCard) {
return undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,10 @@ export const GiftCardsListProvider = ({ children, params }: GiftCardsListProvide
variables: newQueryVariables,
handleError: handleGiftCardListError,
});
const giftCards = mapEdgesToItems(data?.giftCards)?.map(getExtendedGiftCard) ?? [];
const giftCards =
mapEdgesToItems(data?.giftCards)
?.map(getExtendedGiftCard)
.filter((g): g is NonNullable<typeof g> => g !== undefined) ?? [];
Comment on lines +138 to +141
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mapEdgesToItems returns an array of nodes (not undefined entries), so the filter(g !== undefined) here is only needed because getExtendedGiftCard now returns undefined in its type. If you add overloads / a non-optional variant of getExtendedGiftCard, you can remove this extra filter pass and simplify the pipeline.

Suggested change
const giftCards =
mapEdgesToItems(data?.giftCards)
?.map(getExtendedGiftCard)
.filter((g): g is NonNullable<typeof g> => g !== undefined) ?? [];
const mapToExtendedGiftCard = (
giftCard: Parameters<typeof getExtendedGiftCard>[0],
): ExtendedGiftCard => getExtendedGiftCard(giftCard) as ExtendedGiftCard;
const giftCards =
mapEdgesToItems(data?.giftCards)?.map(mapToExtendedGiftCard) ?? [];

Copilot uses AI. Check for mistakes.
const providerValues: GiftCardsListConsumerProps = {
onSort: handleSort,
sort: getSortParams(params),
Expand Down
3 changes: 1 addition & 2 deletions src/products/utils/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
} from "@dashboard/graphql";
import { type FormChange, type UseFormResult } from "@dashboard/hooks/useForm";
import { diff } from "fast-array-diff";
import moment from "moment";

export function createChannelsPriceChangeHandler(
channelListings: ChannelData[],
Expand Down Expand Up @@ -113,7 +112,7 @@ export const createPreorderEndDateChangeHandler =
event => {
form.change(event);

if (moment(event.target.value).isSameOrBefore(Date.now())) {
if (new Date(event.target.value) <= new Date()) {
form.setError("preorderEndDateTime", preorderPastDateErrorMessage);
} else {
form.clearErrors("preorderEndDateTime");
Expand Down
57 changes: 57 additions & 0 deletions src/taxes/components/TaxAppLabel.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Meta, StoryObj } from "@storybook/react-vite";

import { TaxAppLabel } from "./TaxAppLabel";

const meta: Meta<typeof TaxAppLabel> = {
title: "Taxes/TaxAppLabel",
component: TaxAppLabel,
};

export default meta;

type Story = StoryObj<typeof TaxAppLabel>;

export const WithNameAndDate: Story = {
args: {
name: "Avalara",
logoUrl: undefined,
created: "2024-01-15T10:30:00Z",
id: "app-1",
},
};

export const WithLogo: Story = {
args: {
name: "TaxJar",
logoUrl: "https://placeholdit.com/128x128/dddddd/999999?text=app",
created: "2024-06-20T14:00:00Z",
id: "app-2",
},
};

export const WithoutDate: Story = {
args: {
name: "Tax App",
logoUrl: undefined,
created: null,
id: "app-3",
},
};

export const WithoutName: Story = {
args: {
name: null,
logoUrl: undefined,
created: "2024-03-10T08:00:00Z",
id: "app-4",
},
};

export const MinimalProps: Story = {
args: {
name: null,
logoUrl: undefined,
created: null,
id: "app-5",
},
};
56 changes: 56 additions & 0 deletions src/taxes/components/TaxAppLabel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { render, screen } from "@testing-library/react";

import { TaxAppLabel } from "./TaxAppLabel";

jest.mock("@dashboard/extensions/components/AppAvatar/AppAvatar", () => ({
AppAvatar: () => <div data-testid="app-avatar" />,
}));

jest.mock("@dashboard/hooks/useLocale", () => () => ({
locale: "en",
setLocale: () => undefined,
}));

describe("TaxAppLabel", () => {
it("renders locale-formatted date from ISO string", () => {
// Arrange & Act
render(
<TaxAppLabel name="Tax App" logoUrl={undefined} created="2024-06-15T10:30:00Z" id="1" />,
);

// Assert
const expected = new Intl.DateTimeFormat("en", {
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(new Date("2024-06-15T10:30:00Z"));

expect(screen.getByText(`(${expected})`)).toBeInTheDocument();
});

it("does not render date when created is null", () => {
// Arrange & Act
const { container } = render(
<TaxAppLabel name="Tax App" logoUrl={undefined} created={null} id="1" />,
);

// Assert
expect(container.textContent).not.toContain("(");
});

it("renders app name section when name is provided", () => {
// Arrange & Act
render(<TaxAppLabel name="Tax App" logoUrl={undefined} created={null} id="1" />);

// Assert
expect(screen.getByText(/Use app/)).toBeInTheDocument();
});

it("does not render name section when name is null", () => {
// Arrange & Act
render(<TaxAppLabel name={null} logoUrl={undefined} created="2024-01-01T00:00:00Z" id="1" />);

// Assert
expect(screen.queryByText(/Use app/)).not.toBeInTheDocument();
});
});
15 changes: 13 additions & 2 deletions src/taxes/components/TaxAppLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AppAvatar } from "@dashboard/extensions/components/AppAvatar/AppAvatar";
import useLocale from "@dashboard/hooks/useLocale";
import { Box, Text } from "@saleor/macaw-ui-next";
import moment from "moment";
import { FormattedMessage } from "react-intl";

interface TaxAppLabelProps {
Expand All @@ -10,7 +10,18 @@ interface TaxAppLabelProps {
id: string;
}

const formatDate = (locale: string, created: string): string => {
const dateTimeString = new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(new Date(created));
Comment on lines +14 to +18
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new Date(created) passed into Intl.DateTimeFormat(...).format(...) will throw a RangeError: Invalid time value if created is not a valid date string, which would crash the label render. Consider validating the parsed Date (e.g., isNaN(date.getTime())) and either skipping rendering or falling back to a safe placeholder when invalid.

Suggested change
const dateTimeString = new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(new Date(created));
const date = new Date(created);
if (isNaN(date.getTime())) {
return "";
}
const dateTimeString = new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(date);

Copilot uses AI. Check for mistakes.

return `(${dateTimeString})`;
};

export const TaxAppLabel = ({ name, logoUrl, created }: TaxAppLabelProps) => {
const { locale } = useLocale();
const logo = logoUrl ? { source: logoUrl } : undefined;

return (
Expand All @@ -33,7 +44,7 @@ export const TaxAppLabel = ({ name, logoUrl, created }: TaxAppLabelProps) => {
)}
{created && (
<Text size={2} color="default2">
({moment(created).format("YYYY-MM-DD")})
{formatDate(locale, created)}
</Text>
)}
</Box>
Expand Down
10 changes: 5 additions & 5 deletions src/welcomePage/WelcomePageOnboarding/hooks/useNewUserCheck.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useUser } from "@dashboard/auth/useUser";
import moment from "moment";

export const useNewUserCheck = () => {
const { user } = useUser();
Expand All @@ -19,18 +18,19 @@ export const useNewUserCheck = () => {
};
}

const userJoinedDate = moment(user.dateJoined);
const thresholdDate = moment(thresholdDateString);
const userJoinedDate = new Date(user.dateJoined);
// Reset time, so timezone will not flip the day
const thresholdDate = new Date(`${thresholdDateString}T00:00:00`);
Comment on lines +22 to +23
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thresholdDateString is treated as a plain date and you append T00:00:00 unconditionally. If the env var is ever set to an ISO datetime (contains T/timezone), this will produce an invalid date and make all users evaluate as not new. Consider parsing the env value as-is when it already includes a time component, and only appending T00:00:00 for date-only values.

Suggested change
// Reset time, so timezone will not flip the day
const thresholdDate = new Date(`${thresholdDateString}T00:00:00`);
// Reset time, so timezone will not flip the day for date-only values.
// If the env var already includes a time component (e.g. full ISO datetime),
// parse it as-is to avoid creating an invalid date string.
const thresholdDate = thresholdDateString.includes("T")
? new Date(thresholdDateString)
: new Date(`${thresholdDateString}T00:00:00`);

Copilot uses AI. Check for mistakes.

if (!userJoinedDate.isValid() || !thresholdDate.isValid()) {
if (isNaN(userJoinedDate.getTime()) || isNaN(thresholdDate.getTime())) {
return {
isNewUser: false,
isUserLoading: false,
};
}

return {
isNewUser: userJoinedDate.isAfter(thresholdDate),
isNewUser: userJoinedDate > thresholdDate,
isUserLoading: false,
};
};
Loading