npm package: @nexus-cross/crossx-sdk-react
Description: CROSSx React SDK - React Hooks and Components for Embedded Wallet
Latest version: 2.0.0

# @nexus-cross/crossx-sdk-react

CROSSx Embedded Wallet을 React 앱에 통합하기 위한 Provider + Hooks 패키지.

> **wagmi를 사용하는 경우** `@nexus-cross/crossx-sdk-wagmi`를 대신 사용하세요.
> 이 패키지는 wagmi 없이 순수 React만으로 CROSSx 지갑을 사용할 때 적합합니다.

---

## 패키지 구조

```
@nexus-cross/crossx-sdk-core     ← 핵심 SDK (인증, 서명, 트랜잭션)
@nexus-cross/crossx-sdk-react    ← React Hooks & Provider (이 패키지)
@nexus-cross/crossx-sdk-wagmi    ← wagmi Connector (wagmi 사용 시)
```

---

## 설치

```bash
# pnpm (monorepo 내부)
pnpm add @nexus-cross/crossx-sdk-react

# npm
npm install @nexus-cross/crossx-sdk-react
```

`peerDependencies`로 `react ^18.0.0`이 필요합니다.

---

## 환경 변수

`.env` 파일에 아래 값을 설정합니다. SDK 내부에서 자동으로 읽습니다.

```bash
# OAuth 서비스 URL (로그인용) — 필수
VITE_OAUTH_SERVICE_URL=https://dev-cross-wallet-oauth.crosstoken.io

# JWT 검증 API URL — 필수
VITE_AUTH_API_URL=https://dev-cross-auth.crosstoken.io

# Embedded Wallet Gateway URL (지갑 기능용) — 필수
VITE_WALLET_GATEWAY_URL=https://dev-embedded-wallet-gateway.crosstoken.io/api/v1
```

> URL은 `SDKConfig`에 노출하지 않습니다. 환경 변수로만 전환 가능합니다. (외부 개발사에는 위 설정을 알리지 않을 예정)

---

## Quick Start

### 1. Provider 설정

앱 최상위에 `CROSSxProvider`를 배치합니다. `config`에는 `SDKConfig` 객체를 전달합니다.

```tsx
// main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { CROSSxProvider } from '@nexus-cross/crossx-sdk-react';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <CROSSxProvider config={{}}>
      <App />
    </CROSSxProvider>
  </React.StrictMode>
);
```

Provider가 마운트되면 내부적으로 `createCROSSxSDK(config)` → `sdk.init()`을 자동 호출합니다.
기존 세션이 있으면 자동으로 복원됩니다.

### 2. 로그인 / 로그아웃 — `useAuth`

```tsx
import { useAuth } from '@nexus-cross/crossx-sdk-react';

function LoginButton() {
  const { signIn, signOut, isAuthenticated, isLoading, error } = useAuth();

  if (isLoading) return <p>처리 중...</p>;

  return isAuthenticated ? (
    <button onClick={signOut}>로그아웃</button>
  ) : (
    <div>
      <button onClick={signIn}>로그인</button>
      {error && <p style={{ color: 'red' }}>{error}</p>}
    </div>
  );
}
```

### 3. 서명 / 전송 — `useWallet`

```tsx
import { useWallet } from '@nexus-cross/crossx-sdk-react';
import { ChainId } from '@nexus-cross/crossx-sdk-react';

function WalletActions() {
  const { address, signMessage, sendTransaction, isLoading, error } = useWallet();

  const handleSign = async () => {
    // chainId는 CAIP-2 형식 문자열 (예: 'eip155:612055')
    const result = await signMessage(ChainId.CROSS_MAINNET, 'Hello CROSSx!');
    console.log('signature:', result.signature);
  };

  const handleSend = async () => {
    const result = await sendTransaction(ChainId.CROSS_MAINNET, {
      from: address!,
      to: '0x920A31f0E48739C3FbB790D992b0690f7F5C42ea',
      value: '0x2386f26fc10000', // 0.01 CROSS (hex wei)
      gasLimit: '0x5208',
      maxFeePerGas: '0x77359400',
      maxPriorityFeePerGas: '0x3b9aca00',
    });
    console.log('txHash:', result.txHash);
  };

  return (
    <div>
      <p>주소: {address ?? '—'}</p>
      <button onClick={handleSign} disabled={isLoading}>메시지 서명</button>
      <button onClick={handleSend} disabled={isLoading}>전송</button>
      {error && <p style={{ color: 'red' }}>{error}</p>}
    </div>
  );
}
```

### 4. SDK 직접 접근 — `useCROSSx`

hooks로 커버되지 않는 기능(잔액 조회, nonce 조회, RPC 요청 등)은
`useCROSSx`로 SDK 인스턴스에 직접 접근합니다.

```tsx
import { useCROSSx } from '@nexus-cross/crossx-sdk-react';

function AdvancedPanel() {
  const { sdk, isInitialized, isAuthenticated, walletAddress } = useCROSSx();

  const handleGetBalance = async () => {
    if (!sdk) return;
    const { formatted } = await sdk.getBalance('eip155:612055');
    console.log('잔액:', formatted, 'CROSS');
  };

  const handleSignTypedData = async () => {
    if (!sdk) return;
    const result = await sdk.signTypedData('eip155:612055', {
      types: {
        EIP712Domain: [
          { name: 'name', type: 'string' },
          { name: 'version', type: 'string' },
          { name: 'chainId', type: 'uint256' },
          { name: 'verifyingContract', type: 'address' },
        ],
        Permit: [
          { name: 'owner', type: 'address' },
          { name: 'spender', type: 'address' },
          { name: 'value', type: 'uint256' },
          { name: 'nonce', type: 'uint256' },
          { name: 'deadline', type: 'uint256' },
        ],
      },
      primaryType: 'Permit',
      domain: {
        name: 'MyToken',
        version: '1',
        chainId: 612055,
        verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
      },
      message: {
        owner: walletAddress,
        spender: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
        value: '1000000000000000000',
        nonce: '0',
        deadline: '1234567890',
      },
    });
    console.log('typed data signature:', result.signature);
  };

  const handleRpcCall = async () => {
    if (!sdk) return;
    // eth_call로 ERC-20 balanceOf 직접 호출
    const data = '0x70a08231000000000000000000000000' +
      walletAddress!.slice(2).toLowerCase().padStart(64, '0');
    const raw = await sdk.walletRpc(
      'eth_call',
      [{ to: '0x9f85c7b5d7637e18f946cc8af9c131318c6833d9', data }, 'latest'],
      'eip155:612044'
    );
    console.log('balanceOf result:', raw);
  };

  if (!isInitialized) return <p>초기화 중...</p>;

  return (
    <div>
      <p>인증: {isAuthenticated ? '✅' : '❌'}</p>
      <p>주소: {walletAddress ?? '—'}</p>
      <button onClick={handleGetBalance}>잔액 조회</button>
      <button onClick={handleSignTypedData}>EIP-712 서명</button>
      <button onClick={handleRpcCall}>eth_call (tUSDT)</button>
    </div>
  );
}
```

---

## API Reference

### `<CROSSxProvider>`

| Prop | 타입 | 설명 |
|------|------|------|
| `config` | `SDKConfig` | SDK 설정 객체 |
| `children` | `ReactNode` | 자식 컴포넌트 |

`SDKConfig` 주요 필드:

| 필드 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| `theme` | `'light' \| 'dark'` | `'light'` | 확인 모달 테마 |
| `themeTokens` | `ConfirmationTokenOverride` | — | 모달 색상 커스터마이징 |
| `oauthDisplayMode` | `'popup' \| 'modal'` | `'popup'` | OAuth 로그인 표시 방식 |
| `useMockWallet` | `boolean` | `false` | Mock 지갑 사용 (개발용) |
| `debug` | `boolean` | `true` | 디버그 로그 출력 |

`ConfirmationTokenOverride` 구조:

```ts
themeTokens: {
  light?: ConfirmationThemeTokens;  // 라이트 모드 오버라이드
  dark?:  ConfirmationThemeTokens;  // 다크 모드 오버라이드
}
```

`ConfirmationThemeTokens` 오버라이드 가능 필드:

| 필드 | Semantic 토큰 | 기본값 (light / dark) |
|------|------|------|
| `primary` | Primary | `#019D92` / `#019D92` |
| `secondary` | Secondary | `#E70077` / `#E70077` |
| `onPrimary` | OnPrimary | `#FFFFFF` / `#FFFFFF` |
| `borderDefault` | Border/Default | `rgba(18,18,18,0.05)` / `rgba(255,255,255,0.05)` |
| `borderSubtle` | Border/Subtle | `rgba(18,18,18,0.1)` / `rgba(255,255,255,0.1)` |
| `textPrimary` | TextIcon/Primary | `#121212` / `#FFFFFF` |
| `textSecondary` | TextIcon/Secondary | `rgba(18,18,18,0.7)` / `rgba(255,255,255,0.7)` |
| `textTertiary` | TextIcon/Tertiary | `rgba(18,18,18,0.5)` / `rgba(255,255,255,0.5)` |
| `surfaceDefault` | Surface/default | `rgba(18,18,18,0.05)` / `rgba(255,255,255,0.05)` |
| `bg` | Surface/BG | `#FFFFFF` / `#121212` |

---

### `useAuth()`

인증(로그인/로그아웃) 관련 상태와 액션을 제공합니다.

```ts
const {
  isAuthenticated,  // boolean — 현재 인증 상태
  isLoading,        // boolean — 비동기 작업 진행 중
  error,            // string | null — 마지막 에러 메시지
  signIn,           // () => Promise<AuthResult> — OAuth 로그인
  signOut,          // () => Promise<void> — 로그아웃
} = useAuth();
```

`signIn()` 호출 시 OAuth 팝업/모달이 열립니다.
성공하면 내부적으로 `window.location.reload()`가 호출되어 Provider 상태가 갱신됩니다.

---

### `useWallet()`

지갑 주소, 메시지 서명, 트랜잭션 전송 기능을 제공합니다.

```ts
const {
  address,          // string | null — 지갑 주소 (0x...)
  isLoading,        // boolean — 비동기 작업 진행 중
  error,            // string | null — 마지막 에러 메시지
  signMessage,      // (chainId, message) => Promise<SignMessageResult>
  sendTransaction,  // (chainId, tx) => Promise<TransactionResult>
} = useWallet();
```

**`signMessage(chainId, message)`**

| 파라미터 | 타입 | 예시 |
|----------|------|------|
| `chainId` | `string` | `'eip155:612055'` 또는 `ChainId.CROSS_MAINNET` |
| `message` | `string` | `'Hello CROSSx!'` |

반환: `{ chainId, signature, message, address }`

**`sendTransaction(chainId, tx)`**

| 파라미터 | 타입 | 설명 |
|----------|------|------|
| `chainId` | `string` | CAIP-2 체인 ID |
| `tx` | `EvmTransactionRequest` | 트랜잭션 객체 |

`EvmTransactionRequest` 주요 필드:

| 필드 | 타입 | 필수 | 설명 |
|------|------|:----:|------|
| `from` | `string` | O | 보내는 주소 |
| `to` | `string` | O | 받는 주소 |
| `value` | `string` | | hex wei (예: `'0x2386f26fc10000'`) |
| `data` | `string` | | calldata hex |
| `gasLimit` | `string` | | gas limit hex |
| `maxFeePerGas` | `string` | | EIP-1559 max fee |
| `maxPriorityFeePerGas` | `string` | | EIP-1559 priority fee |
| `nonce` | `number` | | 트랜잭션 순서 |

반환: `{ chainId, txHash, status }`

---

### `useCROSSx()`

SDK 인스턴스와 전역 상태에 직접 접근합니다.

```ts
const {
  sdk,              // CROSSxSDK | null — SDK 인스턴스
  isInitialized,    // boolean — init() 완료 여부
  isAuthenticated,  // boolean — 인증 상태
  walletAddress,    // string | null — 지갑 주소
} = useCROSSx();
```

`sdk` 인스턴스를 통해 호출할 수 있는 주요 메서드:

| 메서드 | 설명 |
|--------|------|
| `sdk.signMessage(chainId, message)` | EIP-191 메시지 서명 |
| `sdk.signTypedData(chainId, typedData)` | EIP-712 Typed Data 서명 |
| `sdk.signTransaction(chainId, tx)` | 트랜잭션 서명 (전송 없음) |
| `sdk.sendTransaction(chainId, tx)` | 트랜잭션 서명 + 전송 |
| `sdk.sendTransactionAndWait(chainId, tx)` | 전송 + Receipt 폴링 |
| `sdk.getBalance(chainId)` | 네이티브 잔액 조회 |
| `sdk.getNonce(chainId)` | 현재 nonce 조회 |
| `sdk.walletRpc(method, params, chainId)` | 범용 JSON-RPC 호출 |
| `sdk.getProvider(chainId)` | EIP-1193 Provider 반환 |
| `sdk.createWallet()` | 수동 지갑 생성 |
| `sdk.setTheme('dark')` | 확인 모달 테마 런타임 전환 (`themeTokens` 오버라이드 유지) |

---

## ChainId 상수

체인 ID 오타 방지를 위한 상수를 제공합니다.

```ts
import { ChainId } from '@nexus-cross/crossx-sdk-react';

ChainId.CROSS_MAINNET  // 'eip155:612055'
ChainId.CROSS_TESTNET  // 'eip155:612044'
```

---

## wagmi와의 차이

| | `@nexus-cross/crossx-sdk-react` | `@nexus-cross/crossx-sdk-wagmi` |
|---|---|---|
| **의존성** | React만 필요 | wagmi + viem + @tanstack/react-query |
| **접근 방식** | CROSSx 전용 hooks | wagmi 표준 hooks (`useAccount`, `useConnect` 등) |
| **EIP-1193** | 필요 시 `sdk.getProvider()` | 자동 제공 (wagmi connector) |
| **적합 대상** | CROSSx 지갑만 사용하는 앱 | 멀티 지갑(MetaMask 등) 지원 앱 |

wagmi를 사용하는 경우 `@nexus-cross/crossx-sdk-wagmi`의 `createCROSSxConnector`를 사용하세요.
wagmi 예제는 `examples/wagmi-app`을 참고하세요.

---

## 확인 모달 (Confirmation)

서명/전송 시 자동으로 확인 모달이 표시됩니다.

- **Message Sign** — "Signature Request" 모달 (Cancel / Confirm)
- **EIP-712 Typed Data** — 구조화된 key-value 표시
- **Transaction Sign/Send** — 수신자, 금액, 수수료 표시

모바일(480px 이하)에서는 자동으로 하단 바텀시트로 전환됩니다.

### 테마 설정

**초기화 시 고정:**
```tsx
<CROSSxProvider config={{ theme: 'dark' }}>
```

**런타임 전환:**
```ts
const { sdk } = useCROSSx();
sdk?.setTheme('dark'); // 다음 모달부터 적용. themeTokens 오버라이드는 유지됨
```

### 색상 커스터마이징

`themeTokens.light` / `themeTokens.dark` 로 각 모드를 독립적으로 오버라이드합니다.
지정하지 않은 항목은 해당 모드의 기본값을 유지합니다.

```tsx
<CROSSxProvider
  config={{
    theme: 'light',
    themeTokens: {
      light: {
        primary: '#FF6B35',  // 버튼·강조색 (기본: #019D92)
        bg:      '#F5F0EB',  // 카드 배경색 (기본: #FFFFFF)
      },
      dark: {
        primary: '#FF6B35',
        bg:      '#1A0A00',  // 카드 배경색 (기본: #121212)
      },
    },
  }}
>
```

특정 모드만 지정하는 것도 가능합니다.

```tsx
// 라이트 모드만 커스터마이징
themeTokens: { light: { primary: '#FF6B35' } }
```

---

## 마이그레이션 (자동)

이전 CROSSx 네이티브 앱 사용자가 로그인하면,
기존 백업이 감지될 경우 PIN 입력 팝업이 자동으로 표시되어
Embedded Wallet로 마이그레이션됩니다.

별도 코드 작성이 필요 없습니다.

---

## 트러블슈팅

### `useCROSSx must be used within CROSSxProvider`
앱 최상위에 `<CROSSxProvider>`가 빠져있습니다.

### OAuth 팝업이 열리지 않음
브라우저의 팝업 차단을 확인하세요.
또는 `config.oauthDisplayMode = 'modal'`로 변경하세요.

### 로그인 후 상태가 업데이트되지 않음
현재 `useAuth().signIn()`은 성공 시 `window.location.reload()`를 호출합니다.
SPA 라우터와 충돌하는 경우 `useCROSSx()`로 직접 상태를 관리하세요.

---

## 라이센스

MIT