Skip to content

Commit

Permalink
feat(ui): Validation: Connections - Identifier - User Name (#824)
Browse files Browse the repository at this point in the history
* feat(ui): implement validation for username, connection, identifier

* feat(ui): fix UT

* feat(ui): remove ios unsupported css property and update background blur for rotate modal

* fix(ui): update UI for profile page

* feat(ui): update edit identifier

---------

Co-authored-by: Vu Van Duc <vuvanduc@Vus-MacBook-Pro.local>
  • Loading branch information
Sotatek-DukeVu and Vu Van Duc authored Nov 12, 2024
1 parent ae946c4 commit 50453e1
Show file tree
Hide file tree
Showing 20 changed files with 555 additions and 80 deletions.
11 changes: 6 additions & 5 deletions src/locales/en/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,6 @@
"label": "Display name",
"theme": "Edit theme",
"confirm": "Confirm changes",
"error": "Must be less than 32 characters long",
"color": "Choose card colour"
}
},
Expand Down Expand Up @@ -1757,12 +1756,14 @@
},
"button": {
"confirm": "Confirm"
},
"toast": {
"usernameCreated": "Welcome, {{username}}!",
"usernameError": "Unable to set name. Please try again."
}
},
"nameerror": {
"onlyspace": "Must contain at least 1 (non-space) character",
"maxlength": "You have reached the 32 character limit",
"hasspecialchar": "Only use letters (a-z), numbers (0-9), underscores (_) and hyphens (-)",
"duplicatename": "Must not be a name already in use"
},
"biometry": {
"reason": "Please authenticate",
"canceltitle": "Cancel",
Expand Down
2 changes: 1 addition & 1 deletion src/ui/components/CreateIdentifier/CreateIdentifier.scss
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ ion-modal.create-identifier-modal {
}

.error-message-container {
height: 1rem;
min-height: 1rem;

p {
font-size: 0.875rem;
Expand Down
14 changes: 11 additions & 3 deletions src/ui/components/CreateIdentifier/CreateIdentifier.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { setupIonicReact } from "@ionic/react";
import { mockIonicReact } from "@ionic/react-test-utils";
import { ionFireEvent, mockIonicReact } from "@ionic/react-test-utils";
import { fireEvent, render, waitFor } from "@testing-library/react";
import { act } from "react";
import { Provider } from "react-redux";
Expand Down Expand Up @@ -34,7 +34,14 @@ jest.mock("../../../core/agent/agent", () => ({
agent: {
connections: {
getMultisigLinkedContacts: (args: any) =>
mockGetMultisigConnection(args),
mockGetMultisigConnection(args)
},
identifiers: {
getIdentifiersCache: jest.fn(),
createIdentifier: jest.fn((args: unknown) => ({
identifier: "mock-id",
isPending: true,
}))
},
},
},
Expand Down Expand Up @@ -146,8 +153,9 @@ describe("Create Identifier modal", () => {
);
const displayNameInput = getByTestId("display-name-input");
act(() => {
fireEvent.change(displayNameInput, { target: { value: "Test" } });
ionFireEvent.ionInput(displayNameInput, "Test");
});

act(() => {
fireEvent.click(getByTestId("primary-button-create-identifier-modal"));
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { OperationType, ToastMsgType } from "../../../globals/types";
import { CustomInputProps } from "../../CustomInput/CustomInput.types";
import { IdentifierColor } from "./IdentifierColorSelector";
import { IdentifierStage0 } from "./IdentifierStage0";
import { IdentifierService } from "../../../../core/agent/services";

setupIonicReact();
mockIonicReact();
Expand Down Expand Up @@ -145,6 +146,14 @@ describe("Identifier Stage 0", () => {
const setBlur = jest.fn();
const resetModal = jest.fn();

beforeEach(() => {
createIdentifierMock.mockImplementation(() => ({
identifier: "mock-id",
isPending: true,
}));

})

test("IdentifierStage0 renders default content", async () => {
const { getByTestId, getByText } = render(
<Provider store={storeMocked}>
Expand Down Expand Up @@ -183,7 +192,7 @@ describe("Identifier Stage 0", () => {
);
const displayNameInput = getByTestId("display-name-input");
act(() => {
fireEvent.change(displayNameInput, { target: { value: "Test" } });
ionFireEvent.ionInput(displayNameInput, "Test");
});
act(() => {
fireEvent.click(getByTestId("primary-button-create-identifier-modal"));
Expand Down Expand Up @@ -240,6 +249,11 @@ describe("Identifier Stage 0", () => {
expect(setState).toBeCalledTimes(2);
});

const displayNameInput = getByTestId("display-name-input");
act(() => {
ionFireEvent.ionInput(displayNameInput, "Test");
});

act(() => {
fireEvent.click(getByTestId("primary-button-create-identifier-modal"));
});
Expand All @@ -253,7 +267,7 @@ describe("Identifier Stage 0", () => {
connections: [connectionsFix[3]],
})
);
expect(setState).toBeCalledTimes(3);
expect(setState).toBeCalledTimes(4);
expect(dispatchMock).toBeCalledWith(
setCurrentOperation(OperationType.IDLE)
);
Expand All @@ -280,6 +294,11 @@ describe("Identifier Stage 0", () => {
expect(setState).toBeCalledTimes(2);
});

const displayNameInput = getByTestId("display-name-input");
act(() => {
ionFireEvent.ionInput(displayNameInput, "Test");
});

act(() => {
fireEvent.click(getByTestId("primary-button-create-identifier-modal"));
});
Expand All @@ -290,12 +309,16 @@ describe("Identifier Stage 0", () => {
theme: 10,
groupMetadata: undefined,
});
expect(setState).toBeCalledTimes(2);
expect(setState).toBeCalledTimes(3);
});
});

test("Display error when display name invalid", async () => {
const { getByTestId, getByText } = render(
createIdentifierMock.mockImplementation(() => {
throw new Error(IdentifierService.IDENTIFIER_NAME_TAKEN)
})

const { getByTestId, getByText, queryByTestId } = render(
<Provider store={storeMocked}>
<IdentifierStage0
state={stage0State}
Expand All @@ -312,24 +335,61 @@ describe("Identifier Stage 0", () => {
);

act(() => {
ionFireEvent.ionInput(
getByTestId("display-name-input"),
"Lorem ipsum dolor sit amet consectetur, adipisicing elit. Ipsa praesentium a sed impedit ex consectetur dolorem molestiae laudantium enim neque, quos fugit itaque, vitae autem nihil adipisci pariatur eum? Repellendus, dicta minima. hmmm"
);
ionFireEvent.ionInput(getByTestId("display-name-input"), "");
});

await waitFor(() => {
expect(getByTestId("error-message")).toBeVisible();
expect(
getByText(EN_TRANSLATIONS.nameerror.onlyspace)
).toBeVisible();
});

act(() => {
fireEvent.click(getByText(EN_TRANSLATIONS.createidentifier.back));
ionFireEvent.ionInput(getByTestId("display-name-input"), " ");
});

await waitFor(() => {
expect(dispatchMock).toBeCalledWith(
setCurrentOperation(OperationType.IDLE)
);
expect(
getByText(EN_TRANSLATIONS.nameerror.onlyspace)
).toBeVisible();
});

act(() => {
ionFireEvent.ionInput(getByTestId("display-name-input"), "Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke Duke");
});

await waitFor(() => {
expect(
getByText(EN_TRANSLATIONS.nameerror.maxlength)
).toBeVisible();
});

act(() => {
ionFireEvent.ionInput(getByTestId("display-name-input"), "Duke@@");
});

await waitFor(() => {
expect(
getByText(EN_TRANSLATIONS.nameerror.hasspecialchar)
).toBeVisible();
});

act(() => {
ionFireEvent.ionInput(getByTestId("display-name-input"), "Duke");
});

await waitFor(() => {
expect(queryByTestId("error-message")).toBe(null);
});

act(() => {
fireEvent.click(getByTestId("primary-button-create-identifier-modal"));
});

await waitFor(() => {
expect(
getByText(EN_TRANSLATIONS.nameerror.duplicatename)
).toBeVisible();
});
});

Expand Down
52 changes: 36 additions & 16 deletions src/ui/components/CreateIdentifier/components/IdentifierStage0.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import { createThemeValue } from "../../../utils/theme";
import { IADTypeInfoModal } from "./AIDTypeInfoModal";
import { showError } from "../../../utils/error";
import { combineClassNames } from "../../../utils/style";
import { nameChecker } from "../../../utils/nameChecker";
import { IdentifierService } from "../../../../core/agent/services";

const IdentifierStage0 = ({
state,
Expand All @@ -57,8 +59,10 @@ const IdentifierStage0 = ({
state.displayNameValue
);
const [selectedTheme, setSelectedTheme] = useState(state.selectedTheme);
const displayNameValueIsValid =
displayNameValue.length > 0 && displayNameValue.length <= 32;

const [duplicateName, setDuplicateName] = useState(false);
const [inputChange, setInputChange] = useState(false);
const localValidateMessage = inputChange ? nameChecker.getError(displayNameValue) : undefined;

useEffect(() => {
if (Capacitor.isNativePlatform()) {
Expand Down Expand Up @@ -143,15 +147,7 @@ const IdentifierStage0 = ({
multiSigGroup && dispatch(setCurrentOperation(OperationType.IDLE));
}
}
} catch (e) {
showError("Unable to create identifier", e, dispatch);
}
};

const handleContinue = async () => {
setBlur && setBlur(true);
setTimeout(async () => {
await handleCreateIdentifier();
if (state.selectedAidType !== 0 || multiSigGroup) {
setBlur && setBlur(false);
} else {
Expand All @@ -166,6 +162,22 @@ const IdentifierStage0 = ({
: ToastMsgType.IDENTIFIER_CREATED
)
);
} catch (e) {
if((e as Error).message.includes(IdentifierService.IDENTIFIER_NAME_TAKEN)) {
setDuplicateName(true);
return;
}

showError("Unable to create identifier", e, dispatch);
} finally {
setBlur && setBlur(false);
}
};

const handleContinue = async () => {
setBlur && setBlur(true);
setTimeout(async () => {
await handleCreateIdentifier();
}, CREATE_IDENTIFIER_BLUR_TIMEOUT);
};

Expand All @@ -178,6 +190,15 @@ const IdentifierStage0 = ({
setOpenAIDInfo(true);
};

const handleChangeName = (value: string) => {
setDisplayNameValue(value);
setInputChange(true);
setDuplicateName(false);
}

const hasError = localValidateMessage || duplicateName;
const errorMessage = localValidateMessage || `${i18n.t("nameerror.duplicatename")}`;

return (
<>
<ScrollablePageLayout
Expand Down Expand Up @@ -206,7 +227,7 @@ const IdentifierStage0 = ({
>
<div
className={`identifier-name${
state.displayNameValue.length !== 0 && !displayNameValueIsValid
hasError
? " identifier-name-error"
: ""
}`}
Expand All @@ -218,14 +239,13 @@ const IdentifierStage0 = ({
"createidentifier.displayname.placeholder"
)}`}
hiddenInput={false}
onChangeInput={setDisplayNameValue}
onChangeInput={handleChangeName}
value={displayNameValue}
/>
<div className="error-message-container">
{displayNameValue.length !== 0 && !displayNameValueIsValid && (
{hasError && (
<ErrorMessage
message={`${i18n.t("createidentifier.error.maxlength")}`}
timeout={true}
message={errorMessage}
/>
)}
</div>
Expand Down Expand Up @@ -334,7 +354,7 @@ const IdentifierStage0 = ({
: "createidentifier.add.confirmbutton"
)}`}
primaryButtonAction={handleContinue}
primaryButtonDisabled={!displayNameValueIsValid}
primaryButtonDisabled={displayNameValue.length === 0 || !!localValidateMessage}
/>
<IADTypeInfoModal
isOpen={openAIDInfo}
Expand Down
3 changes: 3 additions & 0 deletions src/ui/components/EditIdentifier/EditIdentifier.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
.indentifier-input {
margin-bottom: 2.375rem;
&.has-error {
ion-item.custom-input .input-line {
border-color: var(--ion-color-danger);
}
margin-bottom: 0;
}

Expand Down
Loading

0 comments on commit 50453e1

Please sign in to comment.