SCOPE UI 구현
이 문서는 사용 가이드의 규격을 React에 적용하고 SDK 제공 화면에 연결하는 방법을 설명합니다.
1. 공통 UI 구현
브랜드 자산에서 검정 또는 흰색 SCOPE SVG를 내려받아 애플리케이션의 정적 파일 경로에 복사합니다. 버튼 안에는 전체 SCOPE Connect 로고가 아니라 SCOPE 로고만 사용합니다.
type ScopeButtonAction = "connect" | "pay";
type ScopeButtonTone = "dark" | "light";
type ScopeBadgeColor = "black" | "white";
const BADGE_SRC = {
black: "/brand/scope-black.svg",
white: "/brand/scope-white.svg",
} as const;
const BUTTON_COPY = {
connect: {
label: "Connect with",
accessibleName: "Connect wallet with SCOPE",
},
pay: {
label: "Pay with",
accessibleName: "Pay with SCOPE",
},
} as const;
export function ScopeBadge({
color = "black",
}: {
color?: ScopeBadgeColor;
}) {
return (
<img
className="scopeBadge"
src={BADGE_SRC[color]}
alt="SCOPE"
width={81}
height={24}
/>
);
}
export function ScopeActionButton({
action,
tone = "dark",
loading = false,
disabled = false,
onClick,
}: {
action: ScopeButtonAction;
tone?: ScopeButtonTone;
loading?: boolean;
disabled?: boolean;
onClick: () => void;
}) {
const copy = BUTTON_COPY[action];
const logoSrc =
tone === "dark" ? BADGE_SRC.white : BADGE_SRC.black;
return (
<button
type="button"
className={`scopeButton scopeButton--${action} scopeButton--${tone}`}
aria-label={copy.accessibleName}
aria-busy={loading}
data-loading={loading}
disabled={disabled || loading}
onClick={onClick}
>
<span>{copy.label}</span>
<img
className="scopeButton__logo"
src={logoSrc}
alt=""
width={81}
height={24}
/>
</button>
);
}
.scopeBadge {
display: block;
width: auto;
height: 24px;
}
.scopeButton {
display: inline-flex;
height: 48px;
align-items: center;
justify-content: center;
gap: 10px;
border: 1px solid transparent;
border-radius: 8px;
padding: 0 20px;
cursor: pointer;
font: 600 14px/1 system-ui, sans-serif;
white-space: nowrap;
}
.scopeButton--connect { min-width: 200px; }
.scopeButton--pay { min-width: 176px; }
.scopeButton__logo {
width: auto;
height: 18px;
}
.scopeButton--dark {
border-color: #111;
color: #fff;
background: #111;
}
.scopeButton--dark:hover:not(:disabled) { background: #2a2a2a; }
.scopeButton--light {
border-color: #d8dce2;
color: #111;
background: #fff;
}
.scopeButton--light:hover:not(:disabled) { background: #f5f6f8; }
.scopeButton:focus-visible {
outline: 3px solid rgba(23, 105, 255, 0.35);
outline-offset: 2px;
}
.scopeButton:disabled {
cursor: not-allowed;
opacity: 0.48;
}
.scopeButton[data-loading="true"]::after {
width: 14px;
height: 14px;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
content: "";
animation: scopeButtonSpin 800ms linear infinite;
}
@keyframes scopeButtonSpin { to { transform: rotate(360deg); } }
@media (max-width: 480px) {
.scopeButton { width: 100%; }
}
@media (prefers-reduced-motion: reduce) {
.scopeButton[data-loading="true"]::after { animation: none; }
}
ScopeBadge는 파트너가 구현한 선택 영역 안에서 SCOPE를 식별하는 배지형 로고입니다. 배지 자체에 클릭 동작을 넣지 않고, 배지를 포함하는 컨트롤에 선택 동작과 접근성 이름을 지정합니다. ScopeActionButton은 지갑 연결 또는 결제를 직접 실행하는 버튼형 로고입니다.
<label aria-label="Pay with SCOPE">
<input type="radio" name="paymentMethod" value="scope" />
<ScopeBadge />
</label>
<ScopeActionButton action="connect" onClick={openWalletPicker} />
<ScopeActionButton action="pay" onClick={startPayment} />
2. 지갑 선택창 열기
Connect with SCOPE를 누르면 애플리케이션의 다이얼로그 안에 <ScopeConnect />를 렌더링합니다. 아래 Modal은 dApp에서 사용하는 다이얼로그 컴포넌트를 뜻합니다.
"use client";
import { useState } from "react";
import { ScopeConnect } from "@scope-connect/appkit-react";
import { ScopeActionButton } from "./ScopeActionButton";
import { Modal } from "./Modal";
export function WalletConnectEntry() {
const [open, setOpen] = useState(false);
return (
<>
<ScopeActionButton action="connect" onClick={() => setOpen(true)} />
<Modal
open={open}
title="Connect wallet"
onClose={() => setOpen(false)}
>
<ScopeConnect />
</Modal>
</>
);
}
3. 직접 연결 후 결제 요청창 열기
Pay with SCOPE는 지갑 연결 여부와 관계없이 결제 정보가 확정되면 활성화할 수 있습니다. 클릭하면 기존 연결을 재사용하고, 연결이 없으면 connect()로 사용 가능한 기본 지갑에 바로 연결한 뒤 <PaymentRequest />를 엽니다.
앱이 특정 기본 지갑을 정했다면 connect() 대신 selectWallet(walletId, {chainId})를 사용합니다. 직접 연결할 지갑을 찾지 못한 WALLET_NOT_FOUND 상황에서만 <ScopeConnect payment={PAYMENT} />를 폴백으로 열어 사용자가 지갑을 고르게 합니다.
"use client";
import { useState } from "react";
import {
AppError,
AppResultCode,
type WalletConnection,
} from "@scope-connect/appkit";
import {
PaymentRequest,
ScopeConnect,
truncateAddress,
useScopeAppKit,
useScopeAppKitReady,
type ScopePaymentConfig,
} from "@scope-connect/appkit-react";
import { ScopeActionButton } from "./ScopeActionButton";
import { Modal } from "./Modal";
const PAYMENT: ScopePaymentConfig = {
merchantName: "Example Store",
merchantUrl: "checkout.example.com",
recipient: "0x0000000000000000000000000000000000000001",
amount: "7.00",
amountFiat: "$7.00 USD",
};
type PaymentView = "closed" | "picker" | "payment";
export function PaymentEntry() {
const appkit = useScopeAppKit();
const ready = useScopeAppKitReady();
const [connection, setConnection] = useState<WalletConnection | null>(null);
const [view, setView] = useState<PaymentView>("closed");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function startPayment() {
setLoading(true);
setError(null);
try {
const nextConnection = appkit.connection ?? (await appkit.connect());
setConnection(nextConnection);
setView("payment");
} catch (cause) {
if (
cause instanceof AppError &&
cause.code === AppResultCode.WALLET_NOT_FOUND
) {
setView("picker");
return;
}
setError(cause instanceof Error ? cause.message : "Wallet connection failed");
} finally {
setLoading(false);
}
}
const payerAddress = connection?.accounts[0] ?? null;
return (
<>
<ScopeActionButton
action="pay"
loading={loading}
disabled={!ready}
onClick={() => void startPayment()}
/>
{error ? <p role="alert">{error}</p> : null}
<Modal
open={view !== "closed"}
title={view === "picker" ? "Choose wallet" : "Payment request"}
onClose={() => setView("closed")}
>
{view === "picker" ? <ScopeConnect payment={PAYMENT} /> : null}
{view === "payment" && payerAddress ? (
<PaymentRequest
config={PAYMENT}
payerAddress={payerAddress}
payerDisplay={truncateAddress(payerAddress)}
onClose={() => setView("closed")}
/>
) : null}
</Modal>
</>
);
}
이 예제에서 결제 버튼은 SDK 초기화 중에만 비활성화됩니다. 미연결 상태는 비활성화 사유가 아니며, 직접 연결이 진행되는 동안 loading 상태로 중복 클릭만 막습니다.
<ScopeConnect payment={PAYMENT} />는 지갑 선택 폴백 안에서도 원래 결제 정보를 유지합니다. 사용자가 지갑을 연결하면 연결 완료 카드의 결제 버튼으로 같은 <PaymentRequest /> 화면을 열 수 있습니다.