diff --git a/packages/lake/__stories__/DatePicker.stories.tsx b/packages/lake/__stories__/DatePicker.stories.tsx
index ffe046845..fede209b8 100644
--- a/packages/lake/__stories__/DatePicker.stories.tsx
+++ b/packages/lake/__stories__/DatePicker.stories.tsx
@@ -10,6 +10,10 @@ import {
isTodayOrFutureDate,
validateDateRangeOrder,
} from "@swan-io/shared-business/src/components/DatePicker";
+import { InlineDatePicker } from "@swan-io/shared-business/src/components/InlineDatePicker";
+import { extractDate } from "@swan-io/shared-business/src/utils/date";
+import { validateBirthdate } from "@swan-io/shared-business/src/utils/validation";
+import { useForm } from "@swan-io/use-form";
import { useRef, useState } from "react";
import { StyleSheet, View } from "react-native";
import { Except } from "type-fest";
@@ -198,3 +202,40 @@ export const ButtonWithRangePopover = () => {
);
};
+
+export const Inline = () => {
+ const initialValue = extractDate("1990-12-19");
+
+ const { Field } = useForm({
+ birthDate: {
+ initialValue: {
+ day: initialValue?.day ?? "",
+ month: initialValue?.month ?? "",
+ year: initialValue?.year ?? "",
+ },
+ validate: validateBirthdate,
+ },
+ });
+ return (
+
+
+
+
+ {({ value, error, onChange, onBlur }) => (
+
+
+
+ )}
+
+
+
+
+ );
+};
diff --git a/packages/shared-business/src/components/InlineDatePicker.tsx b/packages/shared-business/src/components/InlineDatePicker.tsx
new file mode 100644
index 000000000..eaf0f5dc8
--- /dev/null
+++ b/packages/shared-business/src/components/InlineDatePicker.tsx
@@ -0,0 +1,143 @@
+import { Box } from "@swan-io/lake/src/components/Box";
+import { InputError } from "@swan-io/lake/src/components/InputError";
+import { LakeLabel } from "@swan-io/lake/src/components/LakeLabel";
+import { LakeSelect } from "@swan-io/lake/src/components/LakeSelect";
+import { LakeTextInput } from "@swan-io/lake/src/components/LakeTextInput";
+import { Stack } from "@swan-io/lake/src/components/Stack";
+import { colors } from "@swan-io/lake/src/constants/design";
+import { isNotNullish } from "@swan-io/lake/src/utils/nullish";
+import { StyleSheet, View } from "react-native";
+import { ExtractedDate } from "../utils/date";
+import { t } from "../utils/i18n";
+
+const months = [
+ { value: "01", name: t("datePicker.month.january") },
+ { value: "02", name: t("datePicker.month.february") },
+ { value: "03", name: t("datePicker.month.march") },
+ { value: "04", name: t("datePicker.month.april") },
+ { value: "05", name: t("datePicker.month.may") },
+ { value: "06", name: t("datePicker.month.june") },
+ { value: "07", name: t("datePicker.month.july") },
+ { value: "08", name: t("datePicker.month.august") },
+ { value: "09", name: t("datePicker.month.september") },
+ { value: "10", name: t("datePicker.month.october") },
+ { value: "11", name: t("datePicker.month.november") },
+ { value: "12", name: t("datePicker.month.december") },
+];
+
+const styles = StyleSheet.create({
+ day: {
+ maxWidth: 90,
+ flexGrow: 0,
+ },
+ year: {
+ maxWidth: 120,
+ flexGrow: 0,
+ },
+ error: {
+ borderColor: colors.negative[400],
+ },
+});
+
+export type InlineDatePickerProps = {
+ label: string;
+ value: ExtractedDate | undefined;
+ onValueChange: (value: ExtractedDate) => void;
+ error?: string;
+ onBlur?: () => void;
+ order: "day-month-year" | "month-day-year";
+};
+
+export const InlineDatePicker = ({
+ value = { day: "", month: "", year: "" },
+ label,
+ onValueChange,
+ error,
+ onBlur,
+ order,
+}: InlineDatePickerProps) => {
+ return (
+ {
+ const day = (
+
+ {
+ onValueChange({
+ day,
+ month: value.month,
+ year: value.year,
+ });
+ }}
+ pattern="[0-9]"
+ maxLength={2}
+ autoComplete="bday-day"
+ />
+
+ );
+
+ const month = (
+ {
+ onValueChange({
+ day: value.day,
+ month,
+ year: value.year,
+ });
+ }}
+ />
+ );
+
+ const year = (
+
+
+ onValueChange({
+ day: value.day,
+ month: value.month,
+ year,
+ })
+ }
+ pattern="[0-9]"
+ maxLength={4}
+ autoComplete="bday-year"
+ />
+
+ );
+
+ return (
+
+ {order === "day-month-year" ? (
+
+ {day} {month} {year}
+
+ ) : (
+
+ {month} {day} {year}
+
+ )}
+
+
+
+ );
+ }}
+ />
+ );
+};
diff --git a/packages/shared-business/src/locales/en.json b/packages/shared-business/src/locales/en.json
index 0d875dbc2..c34921d29 100644
--- a/packages/shared-business/src/locales/en.json
+++ b/packages/shared-business/src/locales/en.json
@@ -40,6 +40,7 @@
"common.skipToContent": "Skip to content",
"copyButton.copiedTooltip": "Copied to clipboard",
"copyButton.copyTooltip": "Click to copy",
+ "datePicker.day": "Day",
"datePicker.day.friday": "Friday",
"datePicker.day.monday": "Monday",
"datePicker.day.saturday": "Saturday",
@@ -47,6 +48,7 @@
"datePicker.day.thursday": "Thursday",
"datePicker.day.tuesday": "Tuesday",
"datePicker.day.wednesday": "Wednesday",
+ "datePicker.month": "Month",
"datePicker.month.april": "April",
"datePicker.month.august": "August",
"datePicker.month.december": "December",
@@ -61,6 +63,7 @@
"datePicker.month.october": "October",
"datePicker.month.previous": "Previous month",
"datePicker.month.september": "September",
+ "datePicker.year": "Year",
"error.generic": "An error occurred",
"error.iban.invalid": "This IBAN doesn't look right. Trying entering it again.",
"error.network.500": "Internal server error",
@@ -293,5 +296,7 @@
"transactionStatement.title.creditor": "Creditor information",
"transactionStatement.title.debtor": "Debtor information",
"transactionStatement.title.document": "Transaction confirmation",
- "transactionStatement.title.information": "Information"
+ "transactionStatement.title.information": "Information",
+ "validation.invalidBirthDate": "Invalid birthdate",
+ "validation.birthdateCannotBeFuture": "Birthdate cannot be in the future"
}
diff --git a/packages/shared-business/src/utils/date.ts b/packages/shared-business/src/utils/date.ts
index 937d60cc8..b53dad761 100644
--- a/packages/shared-business/src/utils/date.ts
+++ b/packages/shared-business/src/utils/date.ts
@@ -1,11 +1,37 @@
import dayjs from "dayjs";
export const decodeBirthDate = (value: string) => {
- const date = dayjs.utc(value, "YYYY-MM-DD");
+ const date = dayjs.utc(value, "YYYY-MM-DD", true);
return date.isValid() ? date.format("DD/MM/YYYY") : "";
};
export const encodeBirthDate = (value: string) => {
- const date = dayjs.utc(value, "DD/MM/YYYY");
+ const date = dayjs.utc(value, "DD/MM/YYYY", true);
return date.isValid() ? date.format("YYYY-MM-DD") : "";
};
+
+export type ExtractedDate = {
+ day: string;
+ month: string;
+ year: string;
+};
+
+export const extractDate = (value: string): ExtractedDate | undefined => {
+ const date = dayjs.utc(value, "YYYY-MM-DD", true);
+
+ if (date.isValid()) {
+ return {
+ day: date.format("DD"),
+ month: date.format("MM"),
+ year: date.format("YYYY"),
+ };
+ }
+};
+
+export const formatExtractedDate = (date: ExtractedDate): string => {
+ const day = date.day.trim().padStart(2, "0");
+ const month = date.month.trim().padStart(2, "0");
+ const year = date.year.trim().padStart(4, "0");
+
+ return `${year}-${month}-${day}`;
+};
diff --git a/packages/shared-business/src/utils/validation.ts b/packages/shared-business/src/utils/validation.ts
index f8539077b..a25783bfa 100644
--- a/packages/shared-business/src/utils/validation.ts
+++ b/packages/shared-business/src/utils/validation.ts
@@ -1,7 +1,9 @@
import { noop } from "@swan-io/lake/src/utils/function";
import { Validator } from "@swan-io/use-form";
+import dayjs from "dayjs";
import { isValid as isValidIban } from "iban";
import { match } from "ts-pattern";
+import { ExtractedDate, formatExtractedDate } from "./date";
import { t } from "./i18n";
import { AccountCountry } from "./templateTranslations";
@@ -122,3 +124,18 @@ export const validateIban = (iban: string) => {
return t("error.iban.invalid");
}
};
+
+export const validateBirthdate = (value: ExtractedDate) => {
+ const date = dayjs.utc(formatExtractedDate(value), "YYYY-MM-DD", true);
+
+ const isBirthdateOver150years = date.isBefore(dayjs.utc().subtract(150, "years"));
+ const isBirthdateWithin4years = date.isAfter(dayjs.utc().subtract(4, "years"));
+
+ if (!date.isValid() || isBirthdateOver150years || isBirthdateWithin4years) {
+ return t("validation.invalidBirthDate");
+ }
+
+ if (date.isAfter(dayjs.utc().add(1, "day"))) {
+ return t("validation.birthdateCannotBeFuture");
+ }
+};