Skip to content
Merged
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
52 changes: 51 additions & 1 deletion extension/e2e-tests/loadAccount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@ test("Renames wallets", async ({ page, extensionId, context }) => {
// needing to select a row first.
await page.getByTestId("wallets-header-edit-name").click();
await page.getByTestId("rename-wallet-input").fill("New Wallet");
await page.getByText("Save").click();
await page.getByText("Set name").click();

// The new name now renders in two places at once (the active-account
// header, plus that account's own row among several in this seed's
Expand All @@ -697,6 +697,56 @@ test("Renames wallets", async ({ page, extensionId, context }) => {
).toHaveCount(1);
});

test("Trims the wallet name and rejects blank ones", async ({
page,
extensionId,
context,
}) => {
await loginToTestAccount({ page, extensionId, context });
await page.getByTestId("account-view-account-name").click();
await expect(page.getByText("Wallets")).toBeVisible();
await page.getByTestId("wallets-header-edit-name").click();

const input = page.getByTestId("rename-wallet-input");
const setName = page.getByText("Set name");

// A blank or whitespace-only name would leave the wallet unlabelled, so
// the submit button stays disabled rather than saving an empty string.
await input.fill("");
await expect(setName).toBeDisabled();
await input.fill(" ");
await expect(setName).toBeDisabled();

// Surrounding whitespace is trimmed off before saving.
await input.fill(" Padded Wallet ");
await expect(setName).toBeEnabled();
await setName.click();

await expect(page.getByTestId("wallets-header")).toContainText(
"Padded Wallet",
);
await expect(
page.getByTestId("wallet-row-select").filter({ hasText: "Padded Wallet" }),
).toHaveCount(1);
});

test("Closes the rename modal when the name is unchanged", async ({
page,
extensionId,
context,
}) => {
await loginToTestAccount({ page, extensionId, context });
await page.getByTestId("account-view-account-name").click();
await expect(page.getByText("Wallets")).toBeVisible();
await page.getByTestId("wallets-header-edit-name").click();

// Submitting without editing is a no-op save, but it must still dismiss the
// modal — otherwise the button reads as broken.
await expect(page.getByTestId("rename-wallet-input")).toBeVisible();
await page.getByText("Set name").click();
await expect(page.getByTestId("rename-wallet-input")).not.toBeVisible();
});

test("Copies the active wallet address", async ({
page,
extensionId,
Expand Down
153 changes: 87 additions & 66 deletions extension/src/popup/components/account/RenameWallet/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import { Button, Card, Input } from "@stellar/design-system";
import { Button, Icon, Input } from "@stellar/design-system";
import { Field, FieldProps, Form, Formik } from "formik";
import { object as YupObject, string as YupString } from "yup";

import { AppDispatch } from "popup/App";
import { Account } from "@shared/api/types";
import { View } from "popup/basics/layout/View";
import { updateAccountName } from "popup/ducks/accountServices";
import { truncatedPublicKey } from "helpers/stellar";
import { IdenticonImg } from "popup/components/identicons/IdenticonImg";
import { METRIC_NAMES } from "popup/constants/metricsNames";
import { emitMetric } from "helpers/metrics";

Expand All @@ -19,6 +18,10 @@ interface FormValue {
accountName: string;
}

// Matches the mobile app's ACCOUNT_NAME_MIN_LENGTH / ACCOUNT_NAME_MAX_LENGTH.
const ACCOUNT_NAME_MIN_LENGTH = 1;
const ACCOUNT_NAME_MAX_LENGTH = 24;

interface RenameWalletProps {
allAccounts: Account[];
publicKey: string;
Expand All @@ -38,83 +41,101 @@ export const RenameWallet = ({
(account) => account.publicKey === publicKey,
)!;
const accountName = account.name;
const shortPublicKey = truncatedPublicKey(publicKey);
const initialValues: FormValue = {
accountName,
};
const handleSubmit = async (values: FormValue) => {
const { accountName: newAccountName } = values;
if (accountName !== newAccountName) {
const newAccountName = values.accountName.trim();
// Submitting an unchanged name is a no-op save, not a broken button, so
// always close. Only touch storage when the name actually differs.
if (newAccountName !== accountName) {
await dispatch(
updateAccountName({ accountName: newAccountName, publicKey }),
);
emitMetric(METRIC_NAMES.accountRenamed, { source: "wallets" });
onSubmit();
onClose();
}
onClose();
};

return (
<View.Content hasNoTopPadding>
<div className="RenameWallet">
<Card>
<p>{t("Rename Wallet")}</p>
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
validationSchema={YupObject().shape({
accountName: YupString().max(
24,
t("max of 24 characters allowed"),
),
})}
<div className="RenameWallet">
<div className="RenameWallet__header">
<div className="RenameWallet__header__actions">
<button
className="RenameWallet__close"
onClick={onClose}
data-testid="rename-wallet-close"
aria-label={t("Close")}
>
{({ errors }) => (
<>
<Form className="RenameWallet__form">
<Field name="accountName">
{({ field }: FieldProps) => (
<Input
data-testid="rename-wallet-input"
autoFocus
fieldSize="md"
autoComplete="off"
id="accountName"
placeholder={accountName}
maxLength={24}
{...field}
error={errors.accountName}
/>
)}
</Field>
<div className="RenameWallet__short-address">
{t("Address")}: {shortPublicKey}
</div>
<div className="RenameWallet__actions">
<Button
type="button"
size="md"
isRounded
variant="tertiary"
onClick={onClose}
>
{t("Cancel")}
</Button>
<Button
type="submit"
size="md"
isRounded
variant="secondary"
>
{t("Save")}
</Button>
</div>
</Form>
</>
)}
</Formik>
</Card>
<Icon.X />
</button>
</div>

<div className="RenameWallet__identicon">
<IdenticonImg publicKey={publicKey} />
</div>
</div>
</View.Content>

<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
validationSchema={YupObject().shape({
accountName: YupString()
.trim()
.max(ACCOUNT_NAME_MAX_LENGTH, t("max of 24 characters allowed")),
})}
>
{({ errors, values }) => {
// Trim before measuring so a whitespace-only name counts as empty.
const trimmedName = values.accountName.trim();
const isNameValid =
trimmedName.length >= ACCOUNT_NAME_MIN_LENGTH &&
trimmedName.length <= ACCOUNT_NAME_MAX_LENGTH;

return (
<Form className="RenameWallet__form">
<Field name="accountName">
{({ field }: FieldProps) => (
<Input
data-testid="rename-wallet-input"
autoFocus
fieldSize="md"
autoComplete="off"
id="accountName"
placeholder={accountName}
maxLength={ACCOUNT_NAME_MAX_LENGTH}
{...field}
error={errors.accountName}
/>
)}
</Field>
<div className="RenameWallet__actions">
<Button
isFullWidth
type="button"
size="lg"
isRounded
variant="tertiary"
onClick={onClose}
>
{t("Cancel")}
</Button>
<Button
isFullWidth
type="submit"
size="lg"
isRounded
variant="secondary"
disabled={!isNameValid}
>
{t("Set name")}
</Button>
</div>
</Form>
);
}}
</Formik>
</div>
);
};
66 changes: 57 additions & 9 deletions extension/src/popup/components/account/RenameWallet/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,69 @@
.RenameWallet {
z-index: var(--z-index--banner);
position: relative;
display: flex;
flex-direction: column;
gap: #{pxToRem(24px)};
width: #{pxToRem(312px)};
margin: 0 auto;
padding: #{pxToRem(24px)};
background-color: var(--sds-clr-gray-01);
border-radius: #{pxToRem(32px)};

&__short-address {
font-size: 14px;
color: var(--sds-clr-gray-10);
margin: #{pxToRem(12px)} 0 #{pxToRem(18px)} 0;
&__header {
display: flex;
flex-direction: column;
align-items: center;

&__actions {
display: flex;
justify-content: flex-end;
width: 100%;
}
}

&__actions {
&__close {
display: flex;
align-items: center;
justify-content: center;
width: #{pxToRem(34px)};
height: #{pxToRem(34px)};
padding: 0;
border: 0;
border-radius: 50%;
background-color: var(--sds-clr-gray-03);
color: var(--sds-clr-gray-11);
cursor: pointer;

button {
flex: 1;
svg {
width: #{pxToRem(14px)};
height: #{pxToRem(14px)};
}
button:not(:first-child) {
margin-left: #{pxToRem(10px)};
}

&__identicon {
display: flex;
align-items: center;
justify-content: center;
width: #{pxToRem(48px)};
height: #{pxToRem(48px)};
border-radius: 50%;
background-color: var(--sds-clr-gray-03);

.IdenticonImg {
width: #{pxToRem(20.8px)};
height: #{pxToRem(20.8px)};
}
}

&__form {
display: flex;
flex-direction: column;
gap: #{pxToRem(24px)};
}

&__actions {
display: flex;
gap: #{pxToRem(8px)};
}
}
3 changes: 2 additions & 1 deletion extension/src/popup/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,6 @@
"Refresh": "Refresh",
"Refresh metadata": "Refresh metadata",
"Reject": "Reject",
"Request rejected on your device.": "Request rejected on your device.",
"Remember, Freighter will now display accounts related to the new backup phrase that was just created.": "Remember, Freighter will now display accounts related to the new backup phrase that was just created.",
"Remove": "Remove",
"Remove asset": "Remove asset",
Expand All @@ -536,6 +535,7 @@
"Rename wallet": "Rename wallet",
"Rename Wallet": "Rename Wallet",
"Report issue on Github": "Report issue on Github",
"Request rejected on your device.": "Request rejected on your device.",
"Reserved Balance*": "Reserved Balance*",
"Resource Fee": "Resource Fee",
"Retry": "Retry",
Expand Down Expand Up @@ -579,6 +579,7 @@
"Set default": "Set default",
"Set Flags": "Set Flags",
"Set Max": "Set Max",
"Set name": "Set name",
"Set recommended": "Set recommended",
"Settings": "Settings",
"Share feedback": "Share feedback",
Expand Down
3 changes: 2 additions & 1 deletion extension/src/popup/locales/pt/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,6 @@
"Refresh": "Atualizar",
"Refresh metadata": "Atualizar metadados",
"Reject": "Rejeitar",
"Request rejected on your device.": "Solicitação rejeitada no seu dispositivo.",
"Remember, Freighter will now display accounts related to the new backup phrase that was just created.": "Lembre-se, o Freighter agora exibirá contas relacionadas à nova frase de backup que acabou de ser criada.",
"Remove": "Remover",
"Remove asset": "Remover ativo",
Expand All @@ -536,6 +535,7 @@
"Rename wallet": "Renomear carteira",
"Rename Wallet": "Renomear Carteira",
"Report issue on Github": "Reportar issue no Github",
"Request rejected on your device.": "Solicitação rejeitada no seu dispositivo.",
"Reserved Balance*": "Saldo Reservado*",
"Resource Fee": "Taxa de Recurso",
"Retry": "Tentar novamente",
Expand Down Expand Up @@ -579,6 +579,7 @@
"Set default": "Definir padrão",
"Set Flags": "Definir Flags",
"Set Max": "Definir Maximo",
"Set name": "Definir nome",
"Set recommended": "Definir recomendado",
"Settings": "Configurações",
"Share feedback": "Compartilhar feedback",
Expand Down
5 changes: 4 additions & 1 deletion extension/src/popup/views/Wallets/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

.RenameWalletWrapper {
position: absolute;
z-index: var(--z-index--banner);
top: 50%;
left: 0;
width: 100%;
top: 30%;
transform: translateY(-50%);
}

.AddWalletWrapper {
Expand Down
Loading