Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic preview #108

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
24 changes: 24 additions & 0 deletions developer-preview/IMPROVEMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Developer Preview Improvements

## Interactive Device Simulation

- Added interactive simulation capabilities for Ledger device interactions
- Developers can now accurately test message signing flows for:
- Ledger Stax
- Ledger Flex

### Key Benefits

- Provides realistic testing environment without physical devices
- Helps developers understand user experience across different Ledger models
- Streamlines development and testing workflows

### Implementation Details

- Integrated device simulation interface
- Accurate representation of device displays and interactions
- Support for message signing workflows

### Demo Video
https://github.com/user-attachments/assets/e1db3f5c-5fa6-43e6-a8e5-26307fbd7a19

6 changes: 3 additions & 3 deletions developer-preview/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ Run this web app locally to preview how Clear Signed messages will appear on dev

### Prerequisite

Node.js: https://nodejs.org/en
Node.js: <https://nodejs.org/en>

We recommend using a Node version manager like **nvm** https://github.com/nvm-sh/nvm
We recommend using a Node version manager like **nvm** <https://github.com/nvm-sh/nvm>

### Install dependencies

Expand All @@ -24,7 +24,7 @@ npm i
npm run dev
```

Open tool at http://localhost:3000
Open tool at <http://localhost:3000>

☝️ _or another port if `3000` is already in use_

Expand Down
Binary file added developer-preview/demo.mp4
Binary file not shown.
213 changes: 213 additions & 0 deletions developer-preview/src/app/DeviceInteractive.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { useContext, type ReactNode } from "react";
import Image from "next/image";
import { DeviceContext } from "~/app/DeviceContext";
import { Flex } from "~/app/Flex";
import { Stax } from "~/app/Stax";
import { StaxInteractive } from "~/app/StaxInteractive";
import flexInfo from "~/app/screens/assets/flex-info.svg";
import staxInfo from "~/app/screens/assets/stax-info.svg";
import flexSignButton from "~/app/screens/assets/flex-sign-button.svg";
import staxSignButton from "~/app/screens/assets/stax-sign-button.svg";
import { cn } from "~/lib/utils";
import { getIconFor } from "~/app/screens/getIconFor";
import { FlexInteractive } from "./FlexInteractive";

export const Device = {
ActionText: ({ children }: { children: string }) => {
const isStax = useContext(DeviceContext) === "stax";

return (
<div
className={cn(
"font-semibold",
isStax ? "text-[12px] leading-[16px]" : "text-[14px] leading-[18px]",
)}
>
{children}
</div>
);
},
ContentText: ({ children }: { children: ReactNode }) => {
const isStax = useContext(DeviceContext) === "stax";

return (
<div
className={cn(
"break-words",
isStax ? "text-[12px] leading-[16px]" : "text-[14px] leading-[18px]",
)}
>
{children}
</div>
);
},
Frame: ({ children }: { children: ReactNode }) => {
const isStax = useContext(DeviceContext) === "stax";
const Component = isStax ? Stax : Flex;

return (
<Component.Bezel>
<div className="flex w-full flex-col justify-between antialiased">
{children}
</div>
</Component.Bezel>
);
},
HeadingText: ({ children }: { children: ReactNode }) => {
const isStax = useContext(DeviceContext) === "stax";

return (
<div
className={cn(
"font-medium leading-[20px]",
isStax ? "text-[16px]" : "text-[18px]",
)}
>
{children}
</div>
);
},
InfoBlock: ({
owner,
onInfoClick,
}: {
owner: string;
onInfoClick?: () => void;
}) => {
const isStax = useContext(DeviceContext) === "stax";

return (
<div
className={cn(
"flex items-center",
isStax ? "gap-3 p-3" : "gap-4 px-4 py-3",
)}
>
<div>
<Device.ContentText>
{`You're interacting with a smart contract from ${owner}.`}
</Device.ContentText>
</div>
<div>
<div
className="flex h-[32px] w-[32px] cursor-pointer items-center justify-center rounded-full border border-light-grey"
onClick={() => onInfoClick?.()}
>
{isStax ? (
<Image src={staxInfo} alt="More info" width={16} height={16} />
) : (
<Image src={flexInfo} alt="More info" width={20} height={20} />
)}
</div>
</div>
</div>
);
},
Icon: ({ bgUrl }: { bgUrl: string }) => (
<div
className={cn("h-[32px] w-[32px] self-center bg-contain bg-no-repeat")}
style={{ backgroundImage: `url(${bgUrl})` }}
/>
),
OperationSummary: ({
chainId,
children,
type,
}: {
chainId: number;
children: string;
type: string;
}) => {
const selectedDevice = useContext(DeviceContext);
const chainIcon = getIconFor(selectedDevice, chainId) ?? "/assets/eth.svg";
const bgUrl = type === "message" ? "/assets/scroll.svg" : chainIcon;

const isStax = selectedDevice === "stax";

return (
<div
className={cn(
"align-center flex grow flex-col justify-center gap-3 border-b border-light-grey",
isStax ? "p-3" : "p-4",
)}
>
<Device.Icon bgUrl={bgUrl} />
<Device.HeadingText>
<div className="text-center">{children}</div>
</Device.HeadingText>
</div>
);
},
Pagination: ({
current,
total,
onNext,
onPrevious,
}: {
current: number;
total: number;
onNext?: () => void;
onPrevious?: () => void;
}) => {
const isStax = useContext(DeviceContext) === "stax";

return isStax ? (
<StaxInteractive.Pagination
current={current}
total={total}
onNext={onNext}
onPrevious={onPrevious}
/>
) : (
<FlexInteractive.Pagination
current={current}
total={total}
onNext={onNext}
onPrevious={onPrevious}
/>
);
},
Section: ({ children }: { children: ReactNode }) => {
const isStax = useContext(DeviceContext) === "stax";

return (
<div
className={cn(
"flex flex-col border-b border-light-grey py-[14px] last:border-0",
isStax ? "gap-[8px] px-3" : "gap-[6px] px-4",
)}
>
{children}
</div>
);
},
SignButton: () => {
const isStax = useContext(DeviceContext) === "stax";

const Button = () =>
isStax ? (
<Image src={staxSignButton} alt="Sign" width={40} height={40} />
) : (
<Image src={flexSignButton} alt="Sign" width={44} height={44} />
);

return (
<div
className={cn(
"flex items-center justify-between",
isStax ? "px-3 py-[10px]" : "p-4",
)}
>
<Device.HeadingText>Hold to sign</Device.HeadingText>
<div
className={cn(
"flex items-center justify-center rounded-full border border-light-grey",
isStax ? "h-[40px] w-[40px]" : "h-[44px] w-[44px]",
)}
>
<Button />
</div>
</div>
);
},
};
47 changes: 47 additions & 0 deletions developer-preview/src/app/DevicesDemoInteractive.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { DeviceContext } from "~/app/DeviceContext";
import { ScreensInteractive } from "~/app/ScreensInteractive";
import { type Deploymnent, type PreviewData } from "~/types/PreviewData";

interface Props {
data: PreviewData;
selectedDevice: "flex" | "stax";
selectedOperation: string;
}

export const DevicesDemoInteractive = ({
data,
selectedDevice,
selectedOperation,
}: Props) => {
const { contract, metadata, operations, type } = data;

const chosenOperation =
operations.find(
({ id, intent }) =>
selectedOperation === id || selectedOperation === intent,
) ?? operations[0];

if (!chosenOperation || contract.deployments.length < 1) return null;

const { address: contractAddress, chainId } = contract
.deployments[0] as Deploymnent;

return (
<>
<DeviceContext.Provider value={selectedDevice}>
<div className="overflow-x-scroll p-16">
<div className="flex w-fit space-x-10 pe-16 font-inter text-sm">
<ScreensInteractive
chainId={chainId}
contractAddress={contractAddress}
chosenOperation={chosenOperation}
info={metadata.info}
owner={metadata.owner}
operationType={type}
/>
</div>
</div>
</DeviceContext.Provider>
</>
);
};
61 changes: 61 additions & 0 deletions developer-preview/src/app/FlexInteractive.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Image from "next/image";
import { type ReactNode } from "react";
import { Device } from "~/app/Device";
import flexChevronLeft from "~/app/screens/assets/flex-chevron-left.svg";
import flexChevronRight from "../app/screens/assets/flex-chevron-right.svg";
import { cn } from "~/lib/utils";

export const FlexInteractive = {
Bezel: ({ children }: { children: ReactNode }) => (
<div className="h-[416.5px] w-[301.5px] bg-[url(/assets/DeviceBezel-Flex.png)] bg-contain p-[29.5px]">
<div className="flex h-[300px] w-[240px] overflow-hidden rounded-[8px]">
{children}
</div>
</div>
),
Pagination: ({
current,
total,
onPrevious,
onNext,
}: {
current: number;
total: number;
onPrevious?: () => void;
onNext?: () => void;
}) => {
const first = current === 1;
const last = current === total;

return (
<div className="flex border-t border-light-grey">
<div className="border-r border-light-grey px-[26.5px] py-[14px]">
<Device.ActionText>Reject</Device.ActionText>
</div>
<div className="flex w-full items-center justify-center gap-4 px-4 text-dark-grey">
<Image
src={flexChevronLeft as string}
alt="left"
className={cn("inline-block h-[15px] cursor-pointer", {
"opacity-15": first,
"cursor-default": first,
})}
onClick={() => !first && onPrevious?.()}
/>
<Device.ContentText>
{current} of {total}
</Device.ContentText>
<Image
src={flexChevronRight as string}
alt="right"
className={cn("inline-block h-[15px] cursor-pointer", {
"opacity-15": last,
"cursor-default": last,
})}
onClick={() => !last && onNext?.()}
/>
</div>
</div>
);
},
};
4 changes: 2 additions & 2 deletions developer-preview/src/app/PreviewTool.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import { type Dispatch, type SetStateAction, useEffect, useState } from "react";
import { DevicesDemo } from "~/app/DevicesDemo";
import { ContractInfo } from "~/app/ContractInfo";
import { UI } from "~/ui/UI";
import { SelectMetadataFile } from "~/app/SelectMetadataFile";
Expand All @@ -12,6 +11,7 @@ import { type ERC7730Schema } from "~/types/ERC7730Schema";
import { SelectDevice } from "~/app/SelectDevice";
import { SelectOperation } from "~/app/SelectOperation";
import { SelectValues } from "~/app/SelectValues";
import { DevicesDemoInteractive } from "./DevicesDemoInteractive";

interface Props {
jsonInRegistry: string[];
Expand Down Expand Up @@ -119,7 +119,7 @@ export default function PreviewTool({ jsonInRegistry }: Props) {
setSelectedDevice={setSelectedDevice}
/>
</UI.Container>
<DevicesDemo
<DevicesDemoInteractive
data={previewData}
selectedDevice={selectedDevice}
selectedOperation={selectedOperation}
Expand Down
Loading