• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

blockcoders / kuma-wallet / ebbd3c69-fda1-4bf1-80ea-77bef87d3d87

pending completion
ebbd3c69-fda1-4bf1-80ea-77bef87d3d87

Pull #8

circleci

Ruben
fix tests
Pull Request #8: Milestone 2

876 of 1103 branches covered (79.42%)

Branch coverage included in aggregate %.

3452 of 3452 new or added lines in 44 files covered. (100.0%)

6647 of 7185 relevant lines covered (92.51%)

6.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.38
/src/pages/settings/Contacts.tsx
1
import { useState, useEffect, useMemo } from "react";
2✔
2
import { ICON_SIZE } from "@src/constants/icons";
2✔
3
import { FiChevronLeft } from "react-icons/fi";
2✔
4
import { useNavigate } from "react-router-dom";
2✔
5
import { PageWrapper } from "@src/components/common/PageWrapper";
2✔
6
import Contact from "@src/storage/entities/registry/Contact";
2✔
7
import { useTranslation } from "react-i18next";
2✔
8
import { useToast } from "@src/hooks";
2✔
9
import Extension from "@src/Extension";
2✔
10
import { InputErrorMessage, Loading } from "@src/components/common";
2✔
11
import { BsTrash } from "react-icons/bs";
2✔
12
import { useForm } from "react-hook-form";
2✔
13
import { object, string } from "yup";
2✔
14
import { yupResolver } from "@hookform/resolvers/yup";
2✔
15
import { decodeAddress, encodeAddress, isAddress } from "@polkadot/util-crypto";
2✔
16
import { isHex } from "@polkadot/util";
2✔
17

2✔
18
interface AccountForm {
2✔
19
  name: string;
2✔
20
  address: string;
2✔
21
}
2✔
22

2✔
23
export const Contacts = () => {
2✔
24
  const { t } = useTranslation("contacts");
22✔
25
  const { t: tCommon } = useTranslation("common");
22✔
26
  const navigate = useNavigate();
22✔
27

22✔
28
  const schema = object({
22✔
29
    name: string().required(t("required") as string),
22✔
30
    address: string()
22✔
31
      .typeError(t("required") as string)
22✔
32
      .test(
22✔
33
        "valid address",
22✔
34
        tCommon("invalid_address") as string,
22✔
35
        (address) => {
22✔
36
          try {
2✔
37
            if (!address) return false;
2!
38

2✔
39
            if (isHex(address)) {
2✔
40
              return isAddress(address);
2✔
41
            } else {
2!
42
              encodeAddress(decodeAddress(address));
×
43
            }
×
44

×
45
            return true;
×
46
          } catch (error) {
×
47
            return false;
×
48
          }
×
49
        }
2✔
50
      )
22✔
51
      .required(t("required") as string),
22✔
52
  }).required();
22✔
53

22✔
54
  const {
22✔
55
    register,
22✔
56
    handleSubmit,
22✔
57
    reset,
22✔
58
    formState: { errors },
22✔
59
  } = useForm<AccountForm>({
22✔
60
    defaultValues: {
22✔
61
      name: "",
22✔
62
      address: "",
22✔
63
    },
22✔
64
    resolver: yupResolver(schema),
22✔
65
  });
22✔
66

22✔
67
  const [isLoading, setIsLoading] = useState(true);
22✔
68
  const [isCreateContact, setIsCreateContact] = useState(false);
22✔
69
  const [contacts, setContacts] = useState([] as Contact[]);
22✔
70
  const [search, setSearch] = useState("" as string);
22✔
71
  const { showErrorToast } = useToast();
22✔
72

22✔
73
  useEffect(() => {
22✔
74
    setIsLoading(true);
6✔
75
    getContacts();
6✔
76
  }, []);
22✔
77

22✔
78
  const getContacts = async () => {
22✔
79
    try {
8✔
80
      const contacts = await Extension.getContacts();
8✔
81
      setContacts(contacts);
8✔
82
    } catch (error) {
8!
83
      setContacts([]);
×
84
      showErrorToast(tCommon(error as string));
×
85
    } finally {
8✔
86
      setIsLoading(false);
8✔
87
    }
8✔
88
  };
8✔
89

22✔
90
  const saveContact = handleSubmit(async (form: AccountForm) => {
22✔
91
    try {
2✔
92
      const { name, address } = form;
2✔
93

2✔
94
      const contact = new Contact(name, address);
2✔
95
      await Extension.saveContact(contact);
2✔
96
      setSearch("");
2✔
97
      getContacts();
2✔
98
    } catch (error) {
2!
99
      showErrorToast(tCommon(error as string));
×
100
    } finally {
2✔
101
      setIsCreateContact(false);
2✔
102
      reset();
2✔
103
    }
2✔
104
  });
22✔
105

22✔
106
  const deleteContact = async (address: string) => {
22✔
107
    try {
×
108
      await Extension.removeContact(address);
×
109
      getContacts();
×
110
    } catch (error) {
×
111
      showErrorToast(tCommon(error as string));
×
112
    }
×
113
  };
×
114

22✔
115
  const toggleCreateContact = () => {
22✔
116
    setIsCreateContact(!isCreateContact);
2✔
117
  };
2✔
118

22✔
119
  const groupedContacts = useMemo(() => {
22✔
120
    const groupedContacts = contacts
12✔
121
      .filter(
12✔
122
        (contact) =>
12✔
123
          contact.name.toLowerCase().includes(search.toLowerCase()) ||
4!
124
          contact.address.toLowerCase().includes(search.toLowerCase())
×
125
      )
12✔
126
      .reduce((acc: any, contact) => {
12✔
127
        const firstLetter = contact.name.charAt(0).toUpperCase();
4✔
128
        if (!acc[firstLetter]) {
4✔
129
          acc[firstLetter] = [];
4✔
130
        }
4✔
131
        acc[firstLetter].push(contact);
4✔
132
        return acc;
4✔
133
      }, {});
12✔
134
    return Object.entries(groupedContacts).sort(([letterA], [letterB]) =>
12✔
135
      letterA.localeCompare(letterB)
2✔
136
    );
12✔
137
  }, [contacts, search]);
22✔
138

22✔
139
  if (isLoading) {
22✔
140
    return <Loading />;
6✔
141
  }
6✔
142
  return (
16✔
143
    <PageWrapper>
16✔
144
      <div className="flex items-center gap-3 mb-10">
16✔
145
        <FiChevronLeft
16✔
146
          className="cursor-pointer"
16✔
147
          size={ICON_SIZE}
16✔
148
          onClick={() => navigate(-1)}
16✔
149
        />
16✔
150
        <p className="font-medium text-2xl">{t("title")}</p>
16✔
151
        {!isCreateContact && (
16✔
152
          <div className="flex-1 flex justify-end">
12✔
153
            <button
12✔
154
              data-testid="new-contact"
12✔
155
              type="button"
12✔
156
              className="inline-flex justify-between items-center rounded-lg border border-transparent hover:bg-gray-400 hover:bg-opacity-30 px-4 py-2 text-sm"
12✔
157
              onClick={() => toggleCreateContact()}
12✔
158
            >
12✔
159
              <span>{t("new_contact")} </span>
12✔
160
            </button>
12✔
161
          </div>
12✔
162
        )}
22✔
163
      </div>
22✔
164
      {isCreateContact ? (
22✔
165
        <>
4✔
166
          <div className="mb-5">
4✔
167
            <label htmlFor="name" className="block text-sm font-medium mb-1">
4✔
168
              {t("name")}
4✔
169
            </label>
4✔
170
            <input
4✔
171
              data-testid="name"
4✔
172
              id="name"
4✔
173
              placeholder="Insert contact name"
4✔
174
              max={32}
4✔
175
              min={1}
4✔
176
              className=" border text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white"
4✔
177
              {...register("name")}
4✔
178
            />
4✔
179
            <InputErrorMessage message={errors.name?.message} />
4!
180
          </div>
4✔
181

4✔
182
          <div className="mb-5">
4✔
183
            <label htmlFor="name" className="block text-sm font-medium mb-1">
4✔
184
              {t("address")}
4✔
185
            </label>
4✔
186
            <input
4✔
187
              data-testid="address"
4✔
188
              id="address"
4✔
189
              placeholder="Insert address"
4✔
190
              className="border text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white"
4✔
191
              {...register("address")}
4✔
192
            />
4✔
193
            <InputErrorMessage message={errors.address?.message} />
4!
194
          </div>
4✔
195

4✔
196
          <div className="flex justify-end">
4✔
197
            <button
4✔
198
              type="button"
4✔
199
              className="mt-5 inline-flex justify-center border border-custom-gray-bg text-white rounded-lg py-2 px-4 hover:bg-gray-400 hover:bg-opacity-30 transition duration-500 ease select-none focus:outline-none focus:shadow-outline text-sm"
4✔
200
              onClick={() => toggleCreateContact()}
4✔
201
            >
4✔
202
              {tCommon("cancel")}
4✔
203
            </button>
4✔
204
            <button
4✔
205
              data-testid="save"
4✔
206
              type="button"
4✔
207
              className="mt-5 ml-4 inline-flex justify-center border border-custom-green-bg text-white rounded-lg py-2 px-4 transition duration-500 ease select-none bg-custom-green-bg focus:outline-none focus:shadow-outline text-sm"
4✔
208
              onClick={saveContact}
4✔
209
            >
4✔
210
              {tCommon("save")}
4✔
211
            </button>
4✔
212
          </div>
4✔
213
        </>
4✔
214
      ) : (
12✔
215
        <>
12✔
216
          <input
12✔
217
            id="search"
12✔
218
            placeholder={t("search") || "Search"}
12!
219
            className=" border text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white"
12✔
220
            onChange={(e) => {
12✔
221
              setSearch(e.target.value);
×
222
            }}
×
223
          />
12✔
224

12✔
225
          <div className="flex flex-col gap-1 mt-5">
12✔
226
            {contacts.length === 0 && (
12✔
227
              <div className="flex justify-center items-center mt-5">
10✔
228
                <p className="text-lg font-medium">
10✔
229
                  {tCommon("no_contacts_found")}
10✔
230
                </p>
10✔
231
              </div>
10✔
232
            )}
12✔
233
            {groupedContacts.map(([letter, contacts]) => (
12✔
234
              <section key={letter}>
4✔
235
                <h3 className="text-lg font-medium my-2 text-custom-green-bg">
4✔
236
                  {letter}
4✔
237
                </h3>
4✔
238
                {(contacts as Contact[]).map((contact, index) => (
4✔
239
                  <div
4✔
240
                    data-testid="contact"
4✔
241
                    key={index}
4✔
242
                    className="flex justify-between items-center hover:bg-custom-green-bg hover:bg-opacity-40 rounded-xl px-3 py-3 cursor-pointer"
4✔
243
                  >
4✔
244
                    <div className="overflow-hidden text-ellipsis w-[75%] break-all">
4✔
245
                      <p className="text-lg font-medium">{contact?.name}</p>
4✔
246
                      <p>{contact?.address}</p>
4✔
247
                    </div>
4✔
248
                    <div className="w-[20%] flex justify-end">
4✔
249
                      <BsTrash
4✔
250
                        className="text-lg hover:text-custom-red-bg"
4✔
251
                        onClick={() => deleteContact(contact.address)}
4✔
252
                      />
4✔
253
                    </div>
4✔
254
                  </div>
4✔
255
                ))}
4✔
256
              </section>
4✔
257
            ))}
12✔
258
          </div>
12✔
259
        </>
12✔
260
      )}
22✔
261
    </PageWrapper>
22✔
262
  );
22✔
263
};
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc