Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/live-33919-desktop-address-label.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ledger-live-desktop": minor
---

Render the Desktop Contacts address label step
5 changes: 5 additions & 0 deletions .changeset/tidy-address-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@features/flow-contacts": patch
---

Organize Add Address flow files by responsibility
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ jest.mock("~/renderer/store", () => ({
resetStore: jest.fn(),
}));

jest.mock("@ledgerhq/live-common/bridge/index", () => ({
...jest.requireActual<typeof import("@ledgerhq/live-common/bridge/index")>(
"@ledgerhq/live-common/bridge/index",
),
getAccountBridgeByFamily: jest.fn().mockResolvedValue({
validateAddress: jest.fn().mockResolvedValue(true),
}),
}));

const contextMenuValue = {
close: mockClose,
view: CONTEXT_MENU_VIEW.myWallet,
Expand Down Expand Up @@ -543,6 +552,68 @@ describe("Contacts integration", () => {
expect(dialog.querySelector('[data-slot="dialog-body"]')).toHaveClass("!mb-0");
});

it("should render the address and its prefilled name together before review", async () => {
const { store, user } = render(
<MemoryRouter initialEntries={["/contacts"]}>
<Routes>
<Route path="/contacts" element={<ContactsScreen />} />
</Routes>
</MemoryRouter>,
{
skipRouter: true,
initialState: contactsPageInitialState(),
},
);

await user.click(screen.getByTestId("contacts-me-row"));
await user.click(screen.getByTestId("contacts-detail-add-address"));

const dialog = screen.getByRole("dialog");
act(() => {
store
.getState()
.modularDialog.dialogParams?.onAssetSelected?.(getCryptoCurrencyById("ethereum"));
});

const addressInput = await screen.findByTestId("contacts-add-address-input");
const addressNameInput = screen.getByTestId("contacts-add-address-name-input");
expect(screen.getByRole("dialog")).toBe(dialog);
expect(addressNameInput).toHaveValue("Ethereum");
expect(addressNameInput).toHaveAttribute("maxlength", "32");
expect(screen.getByTestId("contacts-add-address-confirm")).toBeDisabled();

fireEvent.change(addressInput, {
target: { value: "0x1ad23b2cf8d2e0591ea417eb82f7cd9746c53034" },
});

const confirmationButton = screen.getByTestId("contacts-add-address-confirm");
await waitFor(() => expect(confirmationButton).toBeEnabled());

await user.clear(addressNameInput);
await user.type(addressNameInput, "Ethereum πŸ’Ž");

expect(screen.getByText("Special characters are not allowed.")).toBeVisible();
expect(confirmationButton).toBeDisabled();

await user.clear(addressNameInput);
await user.type(addressNameInput, "Exchange");
await user.click(confirmationButton);

expect(screen.getByRole("dialog")).toBe(dialog);
expect(screen.getByTestId("contacts-add-address-review")).toBeVisible();

await user.click(
screen.getByRole("button", { name: "components.dialogHeader.goBackAriaLabel" }),
);
expect(screen.getByTestId("contacts-add-address-input")).toBeVisible();
expect(screen.getByTestId("contacts-add-address-name-input")).toHaveValue("Exchange");

await user.click(screen.getByTestId("contacts-add-address-confirm"));
await user.click(screen.getByTestId("contacts-add-address-review-continue"));

expect(screen.getByTestId("contacts-add-address-success")).toBeVisible();
});

it("should expose the Add Address session started for Me", async () => {
const { user } = render(
<MemoryRouter initialEntries={["/contacts"]}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,31 @@ import { DialogFlow, type DialogFlowScreenRegistry } from "LLD/components/Dialog
import { ModularDialogFlow } from "LLD/features/ModularDialog/ModularDialogFlow";
import {
ContactsAddAddressEntry,
ContactsAddAddressCompletion,
ContactsAddAddressName,
type AddAddressFlowState,
type ContactsAddAddressEntryLabels,
type ContactsAddAddressNameLabels,
} from "@features/flow-contacts";
import type { ContactsAddAddressFlowDialogProps } from "./types";
import type { ContactsAddAddressFlowDialogProps, ContactsAddAddressReviewLabels } from "./types";

type ContactsAddAddressDialogStep = "currency" | "address";
type ContactsAddAddressDialogStep = "currency" | "address" | "name" | "review" | "success";

function resolveCurrentStep(state: AddAddressFlowState): ContactsAddAddressDialogStep {
switch (state.status) {
case "enteringAddress":
return "address";
case "namingAddress":
return "name";
case "reviewingAddress":
return "review";
case "success":
return "success";
case "selectingCurrency":
case "closed":
return "currency";
}
}

function isEnteringAddress(
state: AddAddressFlowState,
Expand All @@ -19,21 +38,75 @@ function isEnteringAddress(
function createAddressContent(
state: Extract<AddAddressFlowState, { status: "enteringAddress" }>,
labels: ContactsAddAddressEntryLabels,
nameLabels: ContactsAddAddressNameLabels,
onAddressChange: ContactsAddAddressFlowDialogProps["onAddressChange"],
onAddressLabelChange: ContactsAddAddressFlowDialogProps["onAddressLabelChange"],
onContinueFromAddressDetails: ContactsAddAddressFlowDialogProps["onContinueFromAddressDetails"],
): React.JSX.Element {
return (
<ContactsAddAddressEntry
addressEntry={state.addressEntry}
addressLabel={state.addressLabel}
labels={labels}
nameLabels={nameLabels}
onAddressChange={onAddressChange}
onAddressLabelChange={onAddressLabelChange}
onConfirm={onContinueFromAddressDetails}
/>
);
}

function isNamingAddress(
state: AddAddressFlowState,
): state is Extract<AddAddressFlowState, { status: "namingAddress" }> {
return state.status === "namingAddress";
}

function createNameContent(
state: Extract<AddAddressFlowState, { status: "namingAddress" }>,
labels: ContactsAddAddressNameLabels,
onAddressLabelChange: ContactsAddAddressFlowDialogProps["onAddressLabelChange"],
onContinueFromName: ContactsAddAddressFlowDialogProps["onContinueFromName"],
): React.JSX.Element {
return (
<ContactsAddAddressName
addressEntry={state.addressEntry}
addressLabel={state.addressLabel}
labels={labels}
onAddressLabelChange={onAddressLabelChange}
onContinue={onContinueFromName}
/>
);
}

function createCompletionContent(
state: Extract<AddAddressFlowState, { status: "reviewingAddress" | "success" }>,
labels: ContactsAddAddressReviewLabels,
onContinueFromReview: ContactsAddAddressFlowDialogProps["onContinueFromReview"],
onClose: ContactsAddAddressFlowDialogProps["onClose"],
): React.JSX.Element {
const isReviewingAddress = state.status === "reviewingAddress";

return (
<ContactsAddAddressCompletion
buttonLabel={isReviewingAddress ? labels.continue : labels.close}
onContinue={isReviewingAddress ? onContinueFromReview : onClose}
testID={isReviewingAddress ? "contacts-add-address-review" : "contacts-add-address-success"}
title={isReviewingAddress ? labels.title : labels.successTitle}
/>
);
}

export function ContactsAddAddressFlowDialog({
state,
labels,
entryLabels,
nameLabels,
reviewLabels,
onAddressChange,
onContinueFromAddressDetails,
onAddressLabelChange,
onContinueFromName,
onContinueFromReview,
onBack,
onClose,
}: ContactsAddAddressFlowDialogProps): React.JSX.Element | null {
Expand All @@ -45,7 +118,10 @@ export function ContactsAddAddressFlowDialog({
<ModularDialogFlow onClose={onClose}>
{modularDialog => {
const isAddressEntry = isEnteringAddress(state);
const currentStep: ContactsAddAddressDialogStep = isAddressEntry ? "address" : "currency";
const isAddressNaming = isNamingAddress(state);
const isReviewingAddress = state.status === "reviewingAddress";
const isSuccess = state.status === "success";
const currentStep = resolveCurrentStep(state);
const screens: DialogFlowScreenRegistry<ContactsAddAddressDialogStep> = {
currency: {
content: modularDialog.content,
Expand All @@ -60,13 +136,49 @@ export function ContactsAddAddressFlowDialog({
},
address: {
content: isAddressEntry
? createAddressContent(state, labels, onAddressChange)
? createAddressContent(
state,
entryLabels,
nameLabels,
onAddressChange,
onAddressLabelChange,
onContinueFromAddressDetails,
)
: modularDialog.content,
options: {
dialogHeaderProps: { density: "expanded", title: entryLabels.title },
hasBackButton: true,
},
},
name: {
content: isAddressNaming
? createNameContent(state, nameLabels, onAddressLabelChange, onContinueFromName)
: modularDialog.content,
options: {
dialogHeaderProps: { density: "expanded", title: labels.title },
dialogHeaderProps: { density: "expanded", title: entryLabels.title },
hasBackButton: true,
},
},
review: {
content:
isReviewingAddress || isSuccess
? createCompletionContent(state, reviewLabels, onContinueFromReview, onClose)
: modularDialog.content,
options: {
dialogHeaderProps: { density: "expanded", title: entryLabels.title },
hasBackButton: true,
},
},
success: {
content:
isReviewingAddress || isSuccess
? createCompletionContent(state, reviewLabels, onContinueFromReview, onClose)
: modularDialog.content,
options: {
dialogHeaderProps: { density: "expanded", title: entryLabels.title },
hasBackButton: false,
},
},
};

return (
Expand All @@ -77,7 +189,11 @@ export function ContactsAddAddressFlowDialog({
dialogContentProps: { className: "w-400 bg-canvas-sheet pb-0" },
}}
isOpen
onBack={isAddressEntry ? onBack : modularDialog.onBack}
onBack={
isAddressEntry || isAddressNaming || isReviewingAddress
? onBack
: modularDialog.onBack
}
onClose={onClose}
screens={screens}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { ContactsAddAddressFlowDialog } from "./ContactsAddAddressFlowDialog";
export type { ContactsAddAddressFlowDialogProps } from "./types";
export type { ContactsAddAddressFlowDialogProps, ContactsAddAddressReviewLabels } from "./types";
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,26 @@ import type {
AddAddressFlowState,
AddAddressInputSource,
ContactsAddAddressEntryLabels,
ContactsAddAddressNameLabels,
} from "@features/flow-contacts";

export type ContactsAddAddressReviewLabels = Readonly<{
title: string;
continue: string;
successTitle: string;
close: string;
}>;

export type ContactsAddAddressFlowDialogProps = Readonly<{
state: AddAddressFlowState;
labels: ContactsAddAddressEntryLabels;
entryLabels: ContactsAddAddressEntryLabels;
nameLabels: ContactsAddAddressNameLabels;
reviewLabels: ContactsAddAddressReviewLabels;
onAddressChange: (address: string, inputMethod: AddAddressInputSource) => void;
onContinueFromAddressDetails: () => void;
onAddressLabelChange: (value: string) => void;
onContinueFromName: () => void;
onContinueFromReview: () => void;
onBack: () => void;
onClose: () => void;
}>;
Loading
Loading