@nexus-cross/dapp-ui
v1.3.0
Published
Shared DApp UI components for the CROSS ecosystem — AppLauncher, WalletInfo, WalletPortfolio, WalletConnectModal
Readme
@nexus-cross/dapp-ui
CROSS 생태계 DApp 공통 UI 컴포넌트 패키지.
네 가지 독립적인 기능을 제공한다:
- AppLauncher: 글로벌 서비스 메뉴
- WalletInfo: 지갑 잔액·가격·등락률 조회 UI
- WalletConnectModal: 지갑 연결 모달/드로어 UI
- WalletPortfolio: 리워드·DEX 포지션 포트폴리오 UI
이전 패키지 이름:
cross-app-launcher(@nexus-cross/[email protected]+부터 rename).
Install
npm install @nexus-cross/dapp-uiPeer Dependencies
소비자 프로젝트에 아래 패키지가 이미 설치되어 있어야 한다.
react>= 18react-dom>= 18@tanstack/react-query>= 5@radix-ui/react-popover>= 1vaul>= 1
Import
모든 기능은 루트 경로에서 import한다.
import {
AppLauncher,
AppLauncherTrigger,
AppLauncherContent,
WalletInfo,
WalletConnectModal,
WalletPortfolio,
} from "@nexus-cross/dapp-ui";AppLauncher
S3에 호스팅된 글로벌 메뉴 메타데이터를 fetch하여 일관된 UI로 렌더링한다. Desktop에서는 Popover, Mobile에서는 오른쪽 Drawer로 자동 전환된다.
Quick Start
import {
AppLauncher,
AppLauncherTrigger,
AppLauncherContent,
} from "@nexus-cross/dapp-ui";
function MyNav() {
return (
<AppLauncher env="stage">
<AppLauncherTrigger />
<AppLauncherContent />
</AppLauncher>
);
}모든 prop은 optional이며, 별도의 CSS import 없이 스타일이 자동 적용된다.
<AppLauncher>
Root 컴포넌트. Context Provider + Popover/Drawer Root 역할.
| Prop | Type | Default | Description |
| ------------------ | ---------------------------------- | -------------------------- | ---------------------------------------- |
| env | 'dev' \| 'stage' \| 'production' | 'production' | 환경에 따라 메뉴 아이템의 URL이 결정된다 |
| theme | 'dark' \| 'light' | 'dark' | 다크/라이트 테마 |
| mobileBreakpoint | number | 768 | 이 px 이하에서 Drawer로 전환 |
| domain | string | window.location.hostname | 내부/외부 링크 판별 기준 도메인 |
<AppLauncherTrigger>
Trigger 버튼. children 없이 사용하면 기본 dots grid 아이콘 버튼이 렌더링된다.
| Prop | Type | Default | Description |
| --------- | --------- | ------- | ------------------------------------------------------ |
| asChild | boolean | true | true이면 자식 엘리먼트를 trigger로 사용 (Radix 패턴) |
<AppLauncherContent>
메뉴 콘텐츠. 2열 그리드 레이아웃. 패키지가 디자인을 통제한다.
| Prop | Type | Default | Description |
| ------------ | ------------------------------ | ------- | ------------------------------- |
| align | 'start' \| 'center' \| 'end' | 'end' | 팝오버 정렬 위치 (Desktop only) |
| sideOffset | number | 12 | trigger와의 간격 px (Desktop) |
| className | string | - | 추가 CSS 클래스 |
Link Handling
- 같은 도메인 →
window.location.href로 이동 (풀 URL 사용) - 다른 도메인 →
window.open으로 새 창 열기 - 현재 페이지 → 클릭 무시 (Active State)
Hooks
import { useGlobalMenu } from "@nexus-cross/dapp-ui";
const { data, isLoading } = useGlobalMenu();WalletInfo
지갑 주소를 받아 CROSS·BSC 두 체인의 토큰 잔액, 가격, 24시간 등락률을 조회하고 표시하는 Compound Component. Desktop에서는 Popover, Mobile에서는 Drawer로 자동 전환된다.
Dot notation 패턴(WalletInfo.Trigger, WalletInfo.Content 등)으로 사용한다.
Quick Start
import { WalletInfo } from "@nexus-cross/dapp-ui";
function MyWallet() {
return (
<WalletInfo
env="stage"
theme="dark"
walletAddress="0xe0c8d120...c279"
onCopyAddress={(address, success) => {
if (success) toast.success("주소가 복사되었습니다");
}}
>
<WalletInfo.Trigger />
<WalletInfo.Content />
{/* 선택: dapp별 네비게이션 메뉴 */}
<WalletInfo.Nav position="top">
<nav>
<a href="/about">About</a>
<a href="/faq">FAQ</a>
</nav>
</WalletInfo.Nav>
{/* 선택: dapp별 푸터 (Disconnect 등) */}
<WalletInfo.Footer>
<button onClick={() => disconnect()}>Disconnect</button>
</WalletInfo.Footer>
</WalletInfo>
);
}<WalletInfo> (Root)
Context Provider + Popover/Drawer Root.
| Prop | Type | Default | Description |
| ------------------ | --------------------------------------------- | -------------- | --------------------------------------------------- |
| env | 'dev' \| 'stage' \| 'production' | 'production' | API URL 및 chainId 결정 |
| theme | 'dark' \| 'light' | 'dark' | 다크/라이트 테마 |
| walletAddress | string | (필수) | 토큰 잔액을 조회할 지갑 주소 |
| mobileBreakpoint | number | 768 | 이 px 이하에서 Drawer로 전환 |
| drawerDirection | 'right' \| 'bottom' \| 'left' | 'right' | 모바일 Drawer 방향 |
| modal | boolean | false | true이면 배경 overlay 표시 (Drawer, Popover 공통) |
| showBalance | boolean | false | 토큰 잔고 리스트 표시 여부 |
| showForgeToken | boolean | false | Forge(본딩커브) 토큰 보유 리스트 추가 표시 여부 |
| showGameToken | boolean | true | game 카테고리 토큰 표시 여부 (false이면 숨김) |
| showQR | boolean | true | QR 코드 버튼/뷰 활성화 여부 |
| onCopyAddress | (address: string, success: boolean) => void | - | 주소 복사 콜백 (토스트 표시용) |
| onBuy | () => void | - | 기본 액션 row의 Buy 클릭 핸들러 |
| onDisconnect | () => void | - | 지정 시 하단에 기본 Disconnect(Log Out) 버튼 렌더 |
| disconnectLabel | string | 'Disconnect' | 기본 Disconnect 버튼 라벨 |
| open | boolean | - | 외부에서 열림 상태를 제어 (controlled) |
| onOpenChange | (open: boolean) => void | - | 열림 상태 변경 콜백 |
| style | WalletInfoStyle | - | CSS 커스텀 변수 오버라이드 (자동완성 지원) |
<WalletInfo.Trigger>
Trigger 버튼. children 없이 사용하면 지갑 아이콘 + 축약 주소가 렌더링된다.
| Prop | Type | Default | Description |
| ----------- | --------- | ------- | ----------------------------------------- |
| asChild | boolean | true | true이면 자식 엘리먼트를 trigger로 사용 |
| className | string | - | 추가 CSS 클래스 (배경색 커스터마이징 등) |
// 기본 트리거 (지갑 아이콘 + 축약 주소)
<WalletInfo.Trigger />
// 배경색 커스텀 (Tailwind 예시)
<WalletInfo.Trigger className="bg-emerald-900!" />
// 완전 커스텀 트리거
<WalletInfo.Trigger>
<button>My Wallet</button>
</WalletInfo.Trigger><WalletInfo.Content>
지갑 정보 콘텐츠. 헤더, 주소 바, 토큰 리스트(가격·등락률·보유 수량·USD 환산가)를 렌더링한다.
| Prop | Type | Default | Description |
| ------------ | ------------------------------ | ------- | ------------------------------- |
| align | 'start' \| 'center' \| 'end' | 'end' | 팝오버 정렬 위치 (Desktop only) |
| sideOffset | number | 12 | trigger와의 간격 px (Desktop) |
| className | string | - | 추가 CSS 클래스 (배경색 등) |
<WalletInfo.Nav>
dapp별 네비게이션 메뉴 슬롯. 스크롤 영역 외부에 고정 배치된다.
| Prop | Type | Default | Description |
| ---------- | ------------------- | ------- | ------------------------ |
| position | 'top' \| 'bottom' | 'top' | 토큰 리스트 위/아래 배치 |
| children | ReactNode | (필수) | 네비게이션 콘텐츠 |
<WalletInfo.Footer>
하단 고정 영역 슬롯. My Profile·Settings 같은 임의의 커스텀 버튼을 배치한다. wallet·QR 뷰에서만 노출되고 portfolio·send 뷰에서는 숨겨진다. 전용 버튼 배열 prop은 없고 children으로 직접 구성한다.
Log Out 처리는 레이아웃 목표에 따라 택일한다: (A) 단독 Disconnect만 필요하면 onDisconnect(본문 하단·우측 정렬 기본 버튼), (B) Profile/Settings/Log Out을 한 덩어리로 묶으려면 onDisconnect를 생략하고 세 버튼을 모두 Footer에 넣는다. 둘은 위치·정렬이 달라 함께 쓰면 Log Out만 떨어져 보이므로 동시에 쓰지 않는다.
| Prop | Type | Default | Description |
| ---------- | ----------- | ------- | ----------- |
| children | ReactNode | (필수) | 푸터 콘텐츠 |
토큰 노출 정책
| 규칙 | 설명 |
| --------------- | ----------------------------------------------------------------------- |
| 필터링 | 보유 수량 > 0인 토큰만 표시 |
| 정렬 | USD 환산가 내림차순 |
| 고정 노출 | CROSSD(CROSS), CROSS(CROSS), CROSS(BSC)는 잔고 0이어도 항상 최상단 배치 |
| Forge 토큰 | showForgeToken={true} 시 Forge 토큰이 기존 리스트 아래에 추가 표시. balance API와 중복되는 토큰은 자동 제거 (balance 우선) |
| 카테고리 필터 | showGameToken={false} 시 game 카테고리 토큰 숨김 (pinned 토큰은 영향 없음) |
숫자 포맷팅
| 대상 | >= 1 | < 1 | 처리 |
| ------------ | ------------ | ------------ | ------------------------ |
| 가격 | 소수점 2자리 | 소수점 4자리 | 버림(floor) |
| 보유 수량 | 소수점 2자리 | 소수점 4자리 | 버림(floor) |
| USD 환산가 | 소수점 2자리 | 소수점 4자리 | 버림(floor) |
| 등락률(양수) | - | - | 소수점 2자리 버림(floor) |
| 등락률(음수) | - | - | 소수점 2자리 올림(ceil) |
| 등락률(0) | - | - | 부호 없이 0% 표시 |
Hooks
import {
useTokenBalance,
useTokenStats,
USER_BALANCE_QUERY_KEY,
TOKEN_STATS_QUERY_KEY,
} from "@nexus-cross/dapp-ui";
// 토큰 잔액 (CROSS + BSC 두 체인 병렬 fetch)
const { tokens, isLoading } = useTokenBalance("stage", "0xe0c8...");
// 토큰 가격/등락률
const { statsMap } = useTokenStats("stage");
// 트랜잭션 후 잔액 갱신
queryClient.invalidateQueries({ queryKey: [USER_BALANCE_QUERY_KEY] });API
토큰 잔액과 가격은 CROSS Wallet API에서, Forge 토큰은 Bonding Curve API에서 자동으로 fetch된다.
Wallet API
| 엔드포인트 | 설명 |
| -------------------------- | ---------------------------- |
| /v1/public/token/balance | 토큰 보유 수량 (체인별 호출) |
| /v1/public/token/stats | 토큰 가격·등락률 (전체 조회) |
| /v1/public/token/info | 토큰 카테고리 조회 |
| Environment | API Base URL | CROSS Chain ID | BSC Chain ID |
| --------------- | --------------------------------------------- | -------------- | ------------ |
| dev / stage | https://stg-wallet-server.crosstoken.io/api | 612044 | 97 |
| production | https://wallet-server.crosstoken.io/api | 612055 | 56 |
Forge API (showForgeToken={true} 시 사용)
| 엔드포인트 | 설명 |
| -------------------------------- | ----------------------------- |
| /v1/users/{address}/holdings | Forge 토큰 보유 리스트 |
| /v1/tokens/{address} | Forge 토큰 상세 (이미지 등) |
| Environment | API Base URL |
| --------------- | -------------------------------------------------- |
| dev / stage | https://stg-bonding-curve-api.crosstoken.io/api |
| production | https://bonding-curve-api.crosstoken.io/api |
WalletConnectModal
DApp별로 다른 지갑 연결 팝업 UI를 통합하는 Compound Component. Desktop에서는 Dialog, Mobile에서는 Drawer(Bottom Sheet)로 자동 전환된다. 지갑 연결 로직(Wagmi 등)은 외부에서 콜백으로 주입한다.
Dot notation 패턴(WalletConnectModal.Trigger, WalletConnectModal.Content)으로 사용한다.
Quick Start
import { WalletConnectModal } from "@nexus-cross/dapp-ui";
function ConnectButton() {
return (
<WalletConnectModal
wallets={{
cross_wallet: () => connect({ connectorType: "cross_wallet" }),
cross_extension: () => connect({ connectorType: "cross_extension" }),
metamask: () => connect({ connectorType: "metamask" }),
binance: () => connect({ connectorType: "binance" }),
}}
socialProviders={{
google: () => signInWithProvider("google"),
apple: () => signInWithProvider("apple"),
}}
>
<WalletConnectModal.Trigger />
<WalletConnectModal.Content />
</WalletConnectModal>
);
}<WalletConnectModal> (Root)
Context Provider + Dialog/Drawer Root.
| Prop | Type | Default | Description |
| ------------------ | ---------------------------------------- | ---------- | ------------------------------------------ |
| wallets | WalletHandlers | (필수) | 지갑별 연결 콜백 (key가 표시할 지갑 결정) |
| socialProviders | SocialHandlers | - | Google/Apple 소셜 로그인 콜백 |
| termsUrl | string | - | Footer Terms 링크 URL |
| privacyUrl | string | - | Footer Privacy 링크 URL |
| theme | 'dark' \| 'light' | 'dark' | 다크/라이트 테마 |
| mobileBreakpoint | number | 768 | 이 px 이하에서 Drawer로 전환 |
| drawerDirection | 'bottom' \| 'left' \| 'right' \| 'top' | 'bottom' | 모바일 Drawer 방향 |
| dialogWidth | string | '480px' | Dialog 너비 |
| drawerMaxWidth | string | - | Drawer 최대 너비 |
| drawerMinWidth | string | - | Drawer 최소 너비 |
| style | WalletConnectModalStyle | - | CSS 커스텀 변수 오버라이드 (자동완성 지원) |
| open | boolean | - | Controlled mode: 외부에서 열림 상태 제어 |
| onOpenChange | (open: boolean) => void | - | 열림 상태 변경 콜백 |
WalletHandlers 타입
type WalletHandlers = Partial<Record<WalletId, () => void | Promise<void>>>;
// WalletId = 'cross_embedded' | 'cross_wallet' | 'cross_extension' | 'metamask' | 'binance' | 'verse8' | 'tron'객체에 포함된 key의 지갑만 UI에 표시된다. key는 타입 추론이 된다.
SocialHandlers는 Google/Apple 소셜 로그인 행에만 사용된다.
type SocialHandlers = Partial<Record<'google' | 'apple', () => void | Promise<void>>>;<WalletConnectModal.Trigger>
Trigger 버튼. children 없이 사용하면 기본 "Connect Wallet" 버튼이 렌더링된다.
| Prop | Type | Default | Description |
| --------- | --------- | ------- | ----------------------------------------- |
| asChild | boolean | auto | true이면 자식 엘리먼트를 trigger로 사용 |
// 기본 트리거
<WalletConnectModal.Trigger />
// 커스텀 트리거
<WalletConnectModal.Trigger>
<button className="my-btn">지갑 연결</button>
</WalletConnectModal.Trigger><WalletConnectModal.Content>
지갑 선택 콘텐츠. Featured 지갑, 일반 지갑 리스트, 도움말, 이용약관을 렌더링한다.
| Prop | Type | Default | Description |
| ----------- | -------- | ------- | --------------- |
| className | string | - | 추가 CSS 클래스 |
스타일 커스터마이징
style prop으로 CSS 커스텀 변수를 오버라이드할 수 있다. WalletConnectModalStyle 타입이 자동완성을 지원한다.
<WalletConnectModal
wallets={walletHandlers}
style={{
'--wcm-dialog-width': '500px',
'--wcm-drawer-max-width': '500px',
'--wcm-drawer-min-width': '325px',
'--wcm-surface-bg': 'var(--color-localGray-0)',
'--wcm-primary': '#7346f3',
}}
>사용 가능한 CSS 변수:
| 변수 | 설명 |
| -------------------------- | ------------------- |
| --wcm-primary | 주요 색상 |
| --wcm-secondary | 보조 색상 (배지 등) |
| --wcm-surface-bg | 모달 배경색 |
| --wcm-surface-default | 아이템 배경색 |
| --wcm-surface-subtle | Installed 배지 배경 |
| --wcm-border-default | 모달 테두리 |
| --wcm-border-subtle | 드로어 핸들 색상 |
| --wcm-texticon-primary | 제목 텍스트 색상 |
| --wcm-texticon-secondary | 본문 텍스트 색상 |
| --wcm-texticon-tertiary | 보조 텍스트 색상 |
| --wcm-dialog-width | Dialog 너비 |
| --wcm-drawer-max-width | Drawer 최대 너비 |
| --wcm-drawer-min-width | Drawer 최소 너비 |
EIP-6963 지갑 감지
패키지 내부에서 EIP-6963 프로토콜(eip6963:announceProvider)과 CROSS 커스텀 이벤트(eip6963:crossAnnounceProvider)를 통해 설치된 지갑을 자동 감지한다.
- 감지된 지갑: "Installed" 배지 표시, 클릭 시 연결 콜백 실행
- 미설치 지갑(
installUrl설정 시): "Install" 버튼 표시, 클릭 시 Chrome Web Store로 이동
// 감지 훅을 직접 사용할 수도 있다
import { useWalletDetect } from "@nexus-cross/dapp-ui";
const { wallets, isDetected, isLoading } = useWalletDetect();
if (isDetected("io.metamask")) {
// MetaMask 설치됨
}WALLET_REGISTRY
내장된 지갑 메타데이터. 아이콘, 이름, 설명, rdns 등이 포함되어 있다.
| WalletId | 이름 | 비고 |
| ----------------- | ------------------ | ----------------------------- |
| cross_embedded | CROSSx with Social | Featured, Google/Apple 로그인 |
| cross_wallet | CROSSx | 앱 승인 방식 |
| cross_extension | CROSSx Extension | 브라우저 확장, 데스크톱 전용 |
| metamask | MetaMask | 브라우저 확장 |
| binance | Binance | 모바일 앱 / WalletConnect |
| verse8 | Verse8 Wallet | For Forge 배지 |
| tron | Tron Wallet | For SHOP 배지 |
@nexus-cross/connect-kit-react를 쓰는 경우에는 직접 WalletConnectModal을
조립하지 않아도 된다. <CrossConnectKitProvider>가 이 컴포넌트를 기반으로 한
unified connect modal을 자동 렌더링하며, 지갑 필터링은
walletAllowlist / extraWallets props로 처리한다.
Styling
두 기능 모두 Vanilla CSS + CSS Custom Properties를 사용하며 별도 CSS import가 불필요하다.
| Feature | 클래스 접두사 | Content Font | Trigger Font |
| ------------------ | ------------- | ------------ | ------------------- |
| AppLauncher | al- | Inter | Inter |
| WalletInfo | wi- | Sora | inherit (dapp 상속) |
| WalletConnectModal | wcm- | inherit | inherit (dapp 상속) |
배경색 등을 Tailwind로 커스텀할 때는 !important가 필요할 수 있다:
<WalletInfo.Trigger className="bg-emerald-900!" />
<WalletInfo.Content className="bg-slate-900!" />Development
npm install
npm run build # 빌드
npm run dev # watch 모드
npm run typecheck # 타입 체크Publishing
npm 배포는 GitHub Actions의 Publish to npm 워크플로우(workflow_dispatch)를 통해 수동으로 실행한다. 로컬에서 npm publish를 직접 실행하지 않는다.
브랜치 운영 룰
| 브랜치 | 배포 타입 |
| ---------------------------- | --------- |
| master | prod |
| develop, feature/*, 그 외 | beta |
prod 배포는 반드시 master 브랜치에서만 실행한다. 그 외 브랜치(develop, feature/* 등)에서는 beta로만 배포한다.
배포 절차
- GitHub 저장소 → Actions 탭 → 좌측 Publish to npm 워크플로우 선택
- 우측 상단 Run workflow 클릭
- 아래 입력 항목을 채우고 Run workflow 실행
| 항목 | 설명 |
| ------------------- | ----------------------------------------------------------------------------- |
| Use workflow from | 배포할 대상 브랜치 (prod은 master, beta는 develop/feature/* 등) |
| 배포 타입 | beta 또는 prod 중 선택 (브랜치 운영 룰에 맞게 선택) |
| 배포할 버전 | SemVer 형식의 base 버전 (예: 1.0.0, v 접두사 없이) |
배포 타입별 동작
| 타입 | 입력 컨벤션 | 최종 버전 | npm dist-tag | Git 태그 | GitHub Release |
| ------ | -------------------------------------------- | ----------------------- | ------------ | ------------------- | -------------- |
| prod | 현재 npm에 배포된 최신 정식 버전 + 1 (수동) | 입력한 버전 그대로 | latest | v{version} | 정식 릴리스 |
| beta | 0.0.0 고정 입력 | 0.0.0-beta.N 자동 | beta | v0.0.0-beta.N | Pre-release |
beta의N은 GitHub Releases와 npm registry에 존재하는 최신 beta 번호 중 더 큰 값 + 1로 자동 계산되므로, base 버전은 항상0.0.0으로 고정 입력해도 충돌하지 않는다.prod는 npm 정책상 동일 버전 재배포가 불가능하므로, npm @nexus-cross/dapp-ui 또는package.json의 현재 버전을 확인 후patch/minor/major단위로 1 증가시켜 입력한다.- 실행 흐름: SonarQube 스캔 → 버전 계산 → 빌드 & npm publish → GitHub Release 생성 → Slack 알림.
설치 예시
# 정식 버전 (prod로 배포된 최신)
npm install @nexus-cross/dapp-ui
# 최신 beta
npm install @nexus-cross/dapp-ui@beta
# 특정 beta 버전 고정
npm install @nexus-cross/[email protected]주의 사항
prod배포는 반드시master브랜치에서,beta배포는develop/feature/*등 그 외 브랜치에서 실행한다.- 버전은 반드시 SemVer(
x.y.z) 형식이어야 하며v접두사를 붙이지 않는다. beta배포는 항상0.0.0을 입력하면 된다. (실제 publish 버전은0.0.0-beta.N으로 자동 증가)prod배포는 동일 버전 재배포가 불가능(npm 정책)하므로, 현재 정식 버전에서 1을 올려 입력한다.- 정식 배포 전
beta로 먼저 검증한 뒤prod로 올리는 흐름을 권장한다. - 배포 전 대상 브랜치의 변경 사항과 현재
package.json의 버전이 충돌하지 않는지 확인한다.
