diff --git a/package.json b/package.json
index 87c846eb2af..6bbea3feb11 100644
--- a/package.json
+++ b/package.json
@@ -45,10 +45,10 @@
"test:ci": "jest --ci --maxWorkers=4 --silent",
"test:dev": "jest --detectOpenHandles --coverage=false",
"test:tz": "yarn test:tz-eu-rome && yarn test:tz-us-ny && yarn test:tz-us-yt && yarn test:tz-au-syd",
- "test:tz-eu-rome": "TZ='Europe/Rome' jest --config='./jest.config.no.timezone.js' --detectOpenHandles --coverage=false -t 'Check fiscal code date IT|removeTimezoneFromDate should remove the timezone from a date' './ts/utils/__tests__/fiscal-code.test.ts' './ts/utils/__tests__/dates.test.ts'",
- "test:tz-us-ny": "TZ='America/New_York' jest --config='./jest.config.no.timezone.js' --detectOpenHandles --coverage=false -t 'Check fiscal code date NY|removeTimezoneFromDate should remove the timezone from a date' './ts/utils/__tests__/fiscal-code.test.ts' './ts/utils/__tests__/dates.test.ts'",
- "test:tz-us-yt": "TZ='America/Whitehorse' jest --config='./jest.config.no.timezone.js' --detectOpenHandles --coverage=false -t 'Check fiscal code date CA|removeTimezoneFromDate should remove the timezone from a date' './ts/utils/__tests__/fiscal-code.test.ts' './ts/utils/__tests__/dates.test.ts'",
- "test:tz-au-syd": "TZ='Australia/Sydney' jest --config='./jest.config.no.timezone.js' --detectOpenHandles --coverage=false -t 'Check fiscal code date AU|removeTimezoneFromDate should remove the timezone from a date' './ts/utils/__tests__/fiscal-code.test.ts' './ts/utils/__tests__/dates.test.ts'",
+ "test:tz-eu-rome": "TZ='Europe/Rome' jest --config='./jest.config.no.timezone.js' --detectOpenHandles --coverage=false -t 'Check fiscal code date IT|SimpleDateClaim should handle valid, invalid, edge cases, and formatting correctly' './ts/utils/__tests__/fiscal-code.test.ts' './ts/features/itwallet/common/utils/__tests__/itwClaimsUtils.test.ts'",
+ "test:tz-us-ny": "TZ='America/New_York' jest --config='./jest.config.no.timezone.js' --detectOpenHandles --coverage=false -t 'Check fiscal code date NY|SimpleDateClaim should handle valid, invalid, edge cases, and formatting correctly' './ts/utils/__tests__/fiscal-code.test.ts' './ts/features/itwallet/common/utils/__tests__/itwClaimsUtils.test.ts'",
+ "test:tz-us-yt": "TZ='America/Whitehorse' jest --config='./jest.config.no.timezone.js' --detectOpenHandles --coverage=false -t 'Check fiscal code date CA|SimpleDateClaim should handle valid, invalid, edge cases, and formatting correctly' './ts/utils/__tests__/fiscal-code.test.ts' './ts/features/itwallet/common/utils/__tests__/itwClaimsUtils.test.ts'",
+ "test:tz-au-syd": "TZ='Australia/Sydney' jest --config='./jest.config.no.timezone.js' --detectOpenHandles --coverage=false -t 'Check fiscal code date AU|SimpleDateClaim should handle valid, invalid, edge cases, and formatting correctly' './ts/utils/__tests__/fiscal-code.test.ts' './ts/features/itwallet/common/utils/__tests__/itwClaimsUtils.test.ts'",
"prettify": "prettier --write \"ts/**/*.(ts|tsx)\"",
"prettier:check": "prettier --check \"ts/**/*.(ts|tsx)\"",
"packager:clear": "rm -rf $TMPDIR/react-native-packager-cache-* && rm -rf $TMPDIR/metro-bundler-cache-*",
diff --git a/ts/features/itwallet/common/components/ItwCredentialClaim.tsx b/ts/features/itwallet/common/components/ItwCredentialClaim.tsx
index 19d0ab30927..f3a7613dcca 100644
--- a/ts/features/itwallet/common/components/ItwCredentialClaim.tsx
+++ b/ts/features/itwallet/common/components/ItwCredentialClaim.tsx
@@ -1,5 +1,4 @@
import { Divider, ListItemInfo } from "@pagopa/io-app-design-system";
-import { DateFromString } from "@pagopa/ts-commons/lib/dates";
import * as E from "fp-ts/Either";
import * as O from "fp-ts/Option";
import { pipe } from "fp-ts/lib/function";
@@ -7,7 +6,6 @@ import React, { useMemo } from "react";
import { Image } from "react-native";
import I18n from "../../../../i18n";
import { useIOBottomSheetAutoresizableModal } from "../../../../utils/hooks/bottomSheet";
-import { localeDateFormat } from "../../../../utils/locale";
import { useItwInfoBottomSheet } from "../hooks/useItwInfoBottomSheet";
import {
BoolClaim,
@@ -17,15 +15,17 @@ import {
DrivingPrivilegesClaim,
EmptyStringClaim,
EvidenceClaim,
+ extractFiscalCode,
FiscalCodeClaim,
+ getSafeText,
ImageClaim,
+ isExpirationDateClaim,
PdfClaim,
PlaceOfBirthClaim,
PlaceOfBirthClaimType,
- StringClaim,
- extractFiscalCode,
- isExpirationDateClaim,
- getSafeText
+ SimpleDate,
+ SimpleDateClaim,
+ StringClaim
} from "../utils/itwClaimsUtils";
import { ItwCredentialStatus } from "../utils/itwTypesUtils";
@@ -108,15 +108,12 @@ const DateClaimItem = ({
status
}: {
label: string;
- claim: Date;
+ claim: SimpleDate;
status?: ItwCredentialStatus;
}) => {
// Remove the timezone offset to display the date in its original format
- const value = localeDateFormat(
- claim,
- I18n.t("global.dateFormats.shortFormat")
- );
+ const value = claim.toString("DD/MM/YYYY");
const endElement: ListItemInfo["endElement"] = useMemo(() => {
const ns = "features.itWallet.presentation.credentialDetails.status";
@@ -270,14 +267,8 @@ const DrivingPrivilegesClaimItem = ({
claim: DrivingPrivilegeClaimType;
detailsButtonVisible?: boolean;
}) => {
- const localExpiryDate = localeDateFormat(
- claim.expiry_date,
- I18n.t("global.dateFormats.shortFormat")
- );
- const localIssueDate = localeDateFormat(
- claim.issue_date,
- I18n.t("global.dateFormats.shortFormat")
- );
+ const localExpiryDate = claim.expiry_date.toString("DD/MM/YYYY");
+ const localIssueDate = claim.issue_date.toString("DD/MM/YYYY");
const privilegeBottomSheet = useIOBottomSheetAutoresizableModal({
title: I18n.t(
"features.itWallet.verifiableCredentials.claims.mdl.category",
@@ -371,7 +362,7 @@ export const ItwCredentialClaim = ({
const decoded = hidden ? HIDDEN_CLAIM : _decoded;
if (PlaceOfBirthClaim.is(decoded)) {
return ;
- } else if (DateFromString.is(decoded)) {
+ } else if (SimpleDateClaim.is(decoded)) {
return (
;
@@ -61,7 +61,7 @@ const CardClaim = ({
position,
dimensions,
testID,
- dateFormat = "%d/%m/%Y",
+ dateFormat = "DD/MM/YY",
...labelProps
}: WithTestID) => {
const claimContent = React.useMemo(
@@ -70,8 +70,8 @@ const CardClaim = ({
claim?.value,
ClaimValue.decode,
E.fold(constNull, decoded => {
- if (DateFromString.is(decoded)) {
- const formattedDate = localeDateFormat(decoded, dateFormat);
+ if (SimpleDateClaim.is(decoded)) {
+ const formattedDate = decoded.toString(dateFormat);
return {formattedDate};
} else if (EvidenceClaim.is(decoded)) {
return (
diff --git a/ts/features/itwallet/common/components/ItwSkeumorphicCard/CardData.tsx b/ts/features/itwallet/common/components/ItwSkeumorphicCard/CardData.tsx
index 39fb5b078fc..88fce822d10 100644
--- a/ts/features/itwallet/common/components/ItwSkeumorphicCard/CardData.tsx
+++ b/ts/features/itwallet/common/components/ItwSkeumorphicCard/CardData.tsx
@@ -1,12 +1,10 @@
/* eslint-disable dot-notation */
/* eslint-disable @typescript-eslint/dot-notation */
-import { parse } from "date-fns";
import { pipe } from "fp-ts/lib/function";
import * as O from "fp-ts/lib/Option";
-import { Fragment, default as React } from "react";
+import { default as React, Fragment } from "react";
import { StyleSheet, View } from "react-native";
import { QrCodeImage } from "../../../../../components/QrCodeImage";
-import { localeDateFormat } from "../../../../../utils/locale";
import {
DrivingPrivilegesClaim,
StringClaim
@@ -50,7 +48,7 @@ const MdlFrontData = ({ claims }: DataComponentProps) => {
{
claim={claims["issue_date"]}
position={{ left: `${cols[0]}%`, top: `${rows[3]}%` }}
fontWeight={"Bold"}
+ dateFormat={"DD/MM/YYYY"}
/>
{
claim={claims["expiry_date"]}
position={{ left: `${cols[0]}%`, top: `${rows[4]}%` }}
fontWeight={"Bold"}
+ dateFormat={"DD/MM/YYYY"}
/>
{
}}
>
- {localeDateFormat(parse(issue_date), "%d/%m/%y")}
+ {issue_date.toString("DD/MM/YY")}
{
}}
>
- {localeDateFormat(parse(expiry_date), "%d/%m/%y")}
+ {expiry_date.toString("DD/MM/YY")}
{restrictions_conditions && (
diff --git a/ts/features/itwallet/common/components/ItwSkeumorphicCard/__tests__/__snapshots__/CardData.test.tsx.snap b/ts/features/itwallet/common/components/ItwSkeumorphicCard/__tests__/__snapshots__/CardData.test.tsx.snap
index 5dd40cf4d79..bd715f5075b 100644
--- a/ts/features/itwallet/common/components/ItwSkeumorphicCard/__tests__/__snapshots__/CardData.test.tsx.snap
+++ b/ts/features/itwallet/common/components/ItwSkeumorphicCard/__tests__/__snapshots__/CardData.test.tsx.snap
@@ -242,7 +242,7 @@ exports[`CardData should match snapshot for DC front data 1`] = `
}
weight="Semibold"
>
- 15/03/1990
+ 15/03/90
- 23/03/2032
+ 23/03/32
diff --git a/ts/features/itwallet/common/utils/__tests__/itwClaimsUtils.test.ts b/ts/features/itwallet/common/utils/__tests__/itwClaimsUtils.test.ts
index f9c94d536cb..a3174e5e44f 100644
--- a/ts/features/itwallet/common/utils/__tests__/itwClaimsUtils.test.ts
+++ b/ts/features/itwallet/common/utils/__tests__/itwClaimsUtils.test.ts
@@ -7,7 +7,8 @@ import {
getCredentialExpireDays,
getCredentialStatus,
getFiscalCodeFromCredential,
- ImageClaim
+ ImageClaim,
+ SimpleDateClaim
} from "../itwClaimsUtils";
import { StoredCredential } from "../itwTypesUtils";
import { ItwStoredCredentialsMocks } from "../itwMocksUtils";
@@ -468,3 +469,55 @@ describe("getCredentialStatus", () => {
});
});
});
+
+describe("SimpleDateClaim", () => {
+ it("should handle valid, invalid, edge cases, and formatting correctly", () => {
+ // Valid date decoding
+ const validInput = "2024-11-19";
+ const validResult = SimpleDateClaim.decode(validInput);
+ expect(E.isRight(validResult)).toBe(true);
+ if (E.isRight(validResult)) {
+ const decodedDate = validResult.right;
+
+ // Validate date parts
+ expect(decodedDate.getFullYear()).toBe(2024);
+ expect(decodedDate.getMonth()).toBe(10); // 0-indexed month
+ expect(decodedDate.getDate()).toBe(19);
+
+ // Validate default and custom formats
+ expect(decodedDate.toString()).toBe("19/11/2024");
+ expect(decodedDate.toString("DD/MM/YY")).toBe("19/11/24");
+
+ // Validate Date object conversions
+ expect(decodedDate.toDate()).toEqual(new Date(2024, 10, 19));
+ expect(decodedDate.toDateWithoutTimezone().toISOString()).toBe(
+ "2024-11-19T00:00:00.000Z"
+ );
+ }
+
+ // Invalid date decoding
+ const invalidInput = "invalid-date";
+ const invalidResult = SimpleDateClaim.decode(invalidInput);
+ expect(E.isLeft(invalidResult)).toBe(true);
+
+ // Valid leap year date
+ const leapYearInput = "2024-02-29";
+ const leapYearResult = SimpleDateClaim.decode(leapYearInput);
+ expect(E.isRight(leapYearResult)).toBe(true);
+ if (E.isRight(leapYearResult)) {
+ const leapYearDate = leapYearResult.right;
+ expect(leapYearDate.getFullYear()).toBe(2024);
+ expect(leapYearDate.getMonth()).toBe(1); // 0-indexed month
+ expect(leapYearDate.getDate()).toBe(29);
+ }
+
+ // Valid date with padded spaces
+ const paddedInput = " 2024-11-19 ";
+ const paddedResult = SimpleDateClaim.decode(paddedInput.trim());
+ expect(E.isRight(paddedResult)).toBe(true);
+ if (E.isRight(paddedResult)) {
+ const paddedDate = paddedResult.right;
+ expect(paddedDate.toString()).toBe("19/11/2024");
+ }
+ });
+});
diff --git a/ts/features/itwallet/common/utils/itwClaimsUtils.ts b/ts/features/itwallet/common/utils/itwClaimsUtils.ts
index 4b336d98533..655b1bd03a0 100644
--- a/ts/features/itwallet/common/utils/itwClaimsUtils.ts
+++ b/ts/features/itwallet/common/utils/itwClaimsUtils.ts
@@ -2,7 +2,6 @@
* Utility functions for working with credential claims.
*/
-import { patternDateFromString } from "@pagopa/ts-commons/lib/dates";
import { NonEmptyString, PatternString } from "@pagopa/ts-commons/lib/strings";
import { differenceInCalendarDays, isValid } from "date-fns";
import { pipe } from "fp-ts/lib/function";
@@ -12,12 +11,11 @@ import * as E from "fp-ts/lib/Either";
import { truncate } from "lodash";
import { Locales } from "../../../../../locales/locales";
import I18n from "../../../../i18n";
-import { removeTimezoneFromDate } from "../../../../utils/dates";
import { JsonFromString } from "./ItwCodecUtils";
import {
+ ItwCredentialStatus,
ParsedCredential,
- StoredCredential,
- ItwCredentialStatus
+ StoredCredential
} from "./itwTypesUtils";
/**
@@ -106,6 +104,81 @@ export const parseClaims = (
*
*/
+export const SimpleDateFormat = {
+ DDMMYYYY: "DD/MM/YYYY",
+ DDMMYY: "DD/MM/YY"
+} as const;
+
+export type SimpleDateFormat =
+ (typeof SimpleDateFormat)[keyof typeof SimpleDateFormat];
+
+/**
+ * A simpler Date class with day, month and year properties
+ * It simplifies dates handling by removing Date overhead
+ * @property year - the year
+ * @property month - the month (0-11)
+ * @property day - the day (1-31)
+ * @function toDate - returns a Date object
+ * @function toString - returns a string in the format "DD/MM/YYYY"
+ */
+export class SimpleDate {
+ private year: number;
+ private month: number;
+ private day: number;
+
+ constructor(year: number, month: number, day: number) {
+ this.year = year;
+ this.month = month;
+ this.day = day;
+ }
+
+ /**
+ * Returns a string in the format specified by the format parameter
+ */
+ toString(format: SimpleDateFormat = "DD/MM/YYYY"): string {
+ const dayString = this.day.toString().padStart(2, "0");
+ const monthString = (this.month + 1).toString().padStart(2, "0");
+ const yearString = this.year.toString();
+ return format
+ .replace("DD", dayString)
+ .replace("MM", monthString)
+ .replace("YYYY", yearString)
+ .replace("YY", yearString.slice(-2));
+ }
+
+ /**
+ * Returns a Date object
+ */
+ toDate(): Date {
+ return new Date(this.year, this.month, this.day);
+ }
+
+ toDateWithoutTimezone(): Date {
+ return new Date(Date.UTC(this.year, this.month, this.day));
+ }
+
+ /**
+ * Returns the year
+ */
+ getFullYear(): number {
+ return this.year;
+ }
+
+ /**
+ * Returns the month (0-11)
+ */
+ getMonth(): number {
+ return this.month;
+ }
+
+ /**
+ * Returns the day (1-31)
+ */
+ getDate(): number {
+ return this.day;
+ }
+}
+
/**
* Enum for the claims locales.
* This is used to get the correct locale for the claims.
@@ -173,27 +246,31 @@ const FISCAL_CODE_WITH_PREFIX =
* The date format is checked against the regex dateFormatRegex, which is currenlty mocked.
* This is needed because a generic date decoder would accept invalid dates like numbers,
* thus decoding properly and returning a wrong claim item to be displayed.
- * It also removes the timezone from the date given that the date must be displayed regardless of the timezone of the device.
+ * The returned date is a SimpleDate object, which is a simpler date class with day, month and year properties.
*/
-export const DateWithoutTimezoneClaim = new t.Type(
- "DateWithoutTimezone",
- (input: unknown): input is Date => input instanceof Date,
+export const SimpleDateClaim = new t.Type(
+ "SimpleDateClaim",
+ (input: unknown): input is SimpleDate => input instanceof SimpleDate,
(input, context) =>
pipe(
- patternDateFromString(DATE_FORMAT_REGEX, "DateClaim").validate(
- input,
- context
- ),
+ PatternString(DATE_FORMAT_REGEX).validate(input, context),
E.fold(
() => t.failure(input, context, "Date is not in the correct format"),
str => {
- const date = new Date(str);
- return t.success(removeTimezoneFromDate(date));
+ const date = new SimpleDate(
+ +str.slice(0, 4),
+ +str.slice(5, 7) - 1,
+ +str.slice(8, 10)
+ );
+ return t.success(date);
}
)
),
- (date: Date) =>
- `${date.getUTCFullYear()}-${date.getUTCMonth()}-${date.getUTCDate()}`
+ (date: SimpleDate) =>
+ `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
+ 2,
+ "0"
+ )}-${String(date.getDate()).padStart(2, "0")}`
);
/**
@@ -227,8 +304,8 @@ export type PlaceOfBirthClaimType = t.TypeOf;
*/
const DrivingPrivilegeClaim = t.type({
driving_privilege: t.string,
- issue_date: DateWithoutTimezoneClaim,
- expiry_date: DateWithoutTimezoneClaim,
+ issue_date: SimpleDateClaim,
+ expiry_date: SimpleDateClaim,
restrictions_conditions: t.union([t.string, t.null])
});
@@ -295,8 +372,8 @@ export const ClaimValue = t.union([
DrivingPrivilegesClaim,
// Parse an object representing the claim evidence
EvidenceClaim,
- // Otherwise parse a date
- DateWithoutTimezoneClaim,
+ // Otherwise parse a date as string
+ SimpleDateClaim,
// Otherwise parse an image
ImageClaim,
// Otherwise parse a PDF
diff --git a/ts/features/itwallet/issuance/components/ItwRequiredClaimsList.tsx b/ts/features/itwallet/issuance/components/ItwRequiredClaimsList.tsx
index 0c60e3b2c89..93c38ceb118 100644
--- a/ts/features/itwallet/issuance/components/ItwRequiredClaimsList.tsx
+++ b/ts/features/itwallet/issuance/components/ItwRequiredClaimsList.tsx
@@ -12,11 +12,10 @@ import React from "react";
import { StyleSheet, View } from "react-native";
import * as O from "fp-ts/Option";
import I18n from "../../../../i18n";
-import { localeDateFormat } from "../../../../utils/locale";
import {
+ BoolClaim,
ClaimDisplayFormat,
ClaimValue,
- DateWithoutTimezoneClaim,
DrivingPrivilegesClaim,
EmptyStringClaim,
EvidenceClaim,
@@ -24,8 +23,8 @@ import {
FiscalCodeClaim,
getSafeText,
ImageClaim,
- BoolClaim,
PlaceOfBirthClaim,
+ SimpleDateClaim,
StringClaim
} from "../../common/utils/itwClaimsUtils";
import { isStringNullyOrEmpty } from "../../../../utils/strings";
@@ -94,11 +93,8 @@ export const getClaimDisplayValue = (
decoded => {
if (PlaceOfBirthClaim.is(decoded)) {
return `${decoded.locality} (${decoded.country})`;
- } else if (DateWithoutTimezoneClaim.is(decoded)) {
- return localeDateFormat(
- decoded,
- I18n.t("global.dateFormats.shortFormat")
- );
+ } else if (SimpleDateClaim.is(decoded)) {
+ return decoded.toString();
} else if (EvidenceClaim.is(decoded)) {
return decoded[0].record.source.organization_name;
} else if (ImageClaim.is(decoded)) {
diff --git a/ts/utils/__tests__/dates.test.ts b/ts/utils/__tests__/dates.test.ts
index ede77c2a4e7..14d78bb135a 100644
--- a/ts/utils/__tests__/dates.test.ts
+++ b/ts/utils/__tests__/dates.test.ts
@@ -5,8 +5,7 @@ import {
getDateFromExpiryDate,
getExpireStatus,
isExpired,
- isExpiredDate,
- removeTimezoneFromDate
+ isExpiredDate
} from "../dates";
describe("getExpireStatus", () => {
@@ -114,18 +113,3 @@ describe("getDateFromExpiryDate", () => {
expect(isExpiredDate(date!)).toEqual(true);
});
});
-
-describe("removeTimezoneFromDate", () => {
- it("should remove the timezone from a date", () => {
- const date = new Date("2023-02-01");
- const dateWithoutTimezone = removeTimezoneFromDate(date);
- expect(dateWithoutTimezone.getDate()).toBe(1);
- expect(dateWithoutTimezone.getMonth()).toBe(1); // Month is zero based
- expect(dateWithoutTimezone.getFullYear()).toBe(2023);
- });
-
- it("should throw if the date is invalid", () => {
- const date = new Date("invalid-date");
- expect(() => removeTimezoneFromDate(date)).toThrow(Error);
- });
-});
diff --git a/ts/utils/dates.ts b/ts/utils/dates.ts
index be2227cec63..949ba10b71d 100644
--- a/ts/utils/dates.ts
+++ b/ts/utils/dates.ts
@@ -307,16 +307,3 @@ export const getDateFromExpiryDate = (expiryDate: string): Date | undefined => {
return undefined;
}
};
-
-/**
- * Remove timezone from a date.
- * @param date - the date to remove timezone from
- * @returns a new date with the timezone removed
- */
-export const removeTimezoneFromDate = (date: Date) => {
- if (isNaN(date.getTime())) {
- throw new Error("Invalid date");
- }
- const userTimezoneOffset = date.getTimezoneOffset() * 60000;
- return new Date(date.getTime() + userTimezoneOffset);
-};