@taskon/widget-react
v0.0.1-beta.2
Published
TaskOn React Widget Library
Readme
@taskon/widget-react
TaskOn React Widget - Embeddable white-label components for integrating TaskOn quest/task functionality into your application.
Installation
npm install @taskon/widget-react @taskon/core
# or
pnpm add @taskon/widget-react @taskon/core
# or
yarn add @taskon/widget-react @taskon/coreQuick Start
// Providers from core (minimal CSS ~3KB)
import { TaskOnProvider } from "@taskon/widget-react/core";
// Widgets from their sub-paths (isolated CSS)
import { QuestWidget } from "@taskon/widget-react/quest";
const App = () => (
<TaskOnProvider
config={{
apiKey: "your-api-key",
}}
>
<YourApp />
</TaskOnProvider>
);Import Methods
Recommended: Sub-path Imports (Optimal CSS)
// Core: Providers and hooks only (~3KB CSS for Toast)
import { TaskOnProvider, ThemeProvider, useTaskOnAuth } from "@taskon/widget-react/core";
// Widgets: Each loads only its own CSS
import { QuestWidget } from "@taskon/widget-react/quest"; // ~26KB CSS
import { CommunityTaskList } from "@taskon/widget-react/community-task"; // ~51KB CSS
import { LeaderboardWidget } from "@taskon/widget-react/leaderboard"; // ~17KB CSS
import { UserCenterWidget } from "@taskon/widget-react/user-center"; // UserCenter CSS onlyAlternative: Main Entry Import (Loads All CSS)
// Loads ALL widget CSS (~92KB) - not recommended for production
import { TaskOnProvider, QuestWidget, CommunityTaskList } from "@taskon/widget-react";Usage Example
import { TaskOnProvider, useTaskOnAuth } from "@taskon/widget-react/core";
import { QuestWidget } from "@taskon/widget-react/quest";
const App = () => (
<TaskOnProvider
config={{
apiKey: "your-api-key",
}}
>
<YourApp />
</TaskOnProvider>
);
// Your app controls the login UI
const YourApp = () => {
const { userId, login, logout } = useTaskOnAuth();
const { user, getSignature } = useYourAuth(); // Your auth hook
// When user logs in to your app, login to TaskOn
useEffect(() => {
if (user?.email && !userId) {
const { sign, timestamp } = getSignature(); // Get signature from your backend
login({ method: "email", value: user.email, sign, timestamp });
}
}, [user, userId]);
// When user logs out from your app, logout from TaskOn
useEffect(() => {
if (!user && userId) {
logout();
}
}, [user, userId]);
return (
<div>
{userId && <p>TaskOn User ID: {userId}</p>}
{/* Theme configured in TaskOn Dashboard */}
<QuestWidget configId="cfg_abc123" />
</div>
);
};Architecture
┌─ TaskOnProvider ─────────────────────────────────────────┐
│ config: { apiKey } │
│ Purpose: Authentication only │
│ │
│ ┌─ Mode A: Cloud Config ──────────────────────────────┐ │
│ │ <QuestWidget configId="cfg_abc123" /> │ │
│ │ → Theme from TaskOn cloud │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Mode B: Local Theme ───────────────────────────────┐ │
│ │ <ThemeProvider theme={{ mode: 'dark' }}> │ │
│ │ <QuestWidget /> /* no configId */ │ │
│ │ </ThemeProvider> │ │
│ └─────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘Theme Source (mutually exclusive)
| Mode | Theme Source |
| --------------------- | ------------------------------------ |
| configId provided | Cloud config (ThemeProvider ignored) |
| No configId | ThemeProvider or default theme |
Security
TaskOn uses API Key authentication to verify that widget requests come from authorized projects.
Step 1: Get API Key from TaskOn Dashboard
API Key: /KDiqEFNCaGTVTdTpCFrZOsUj5vDi5uGLSFmwyHeboE= (for X-API-Key header)Step 2: Configure TaskOnProvider
<TaskOnProvider
config={{
apiKey: "your-api-key",
}}
>
<YourApp />
</TaskOnProvider>;HTTP Headers
All API requests include:
X-API-Key: your-api-key # Project authorization
Authorization: Bearer xxx # User authorization (after login)Security Best Practices
- Keep API Key secure - Don't expose in public repositories
- Use HTTPS - All communication must be encrypted
TaskOnProvider
The root provider component for authentication. Must wrap your application.
Props
| Prop | Type | Required | Default | Description |
| ------------ | ------------------------ | -------- | ------- | -------------------- |
| config | TaskOnProviderConfig | Yes | - | Configuration object |
| children | ReactNode | Yes | - | Child components |
TaskOnProviderConfig
interface TaskOnProviderConfig {
// Required: API Key for authentication (X-API-Key header)
apiKey: string;
// Locale setting
locale?: "en" | "ko" | "ja" | "ru" | "es"; // default: auto-detect
// Wallet configuration (only needed if using wallet login)
walletConfig?: {
evmAdapter?: WalletAdapter; // Custom EVM wallet adapter
solanaAdapter?: WalletAdapter; // Custom Solana wallet adapter
disableAutoDetect?: boolean; // Disable auto-detection
};
// WalletConnect Project ID (required for WalletConnect support)
// Get your project ID at https://cloud.walletconnect.com
walletConnectProjectId?: string;
// Callback when user needs to login (e.g., clicks login overlay)
onRequestLogin?: () => void;
}Internationalization
Supported Locales
| Locale | Language |
| ------ | ----------------- |
| en | English (default) |
| ko | Korean |
| ja | Japanese |
| ru | Russian |
| es | Spanish |
Configuration
<TaskOnProvider
config={{
apiKey: "your-api-key",
locale: "ko",
}}
>
<App />
</TaskOnProvider>Dynamic Locale Switching
Control locale via your own state:
const App = () => {
const [locale, setLocale] = useState("en");
return (
<TaskOnProvider config={{ apiKey: "your-api-key", locale }}>
<select value={locale} onChange={(e) => setLocale(e.target.value)}>
<option value="en">English</option>
<option value="ko">한국어</option>
<option value="ja">日本語</option>
</select>
<QuestWidget configId="cfg_abc123" />
</TaskOnProvider>
);
};Locale Auto-Detection
When locale is not specified, TaskOn detects from browser language, falling back to en.
ThemeProvider
Optional provider for theme configuration. Supports nesting for different theme zones.
Props
| Prop | Type | Required | Default | Description |
| ------------ | --------------------- | -------- | -------- | -------------------- |
| theme | TaskOnThemeConfig | No | - | Theme configuration |
| children | ReactNode | Yes | - | Child components |
| inherit | boolean | No | true | Inherit parent theme |
TaskOnThemeConfig
interface TaskOnThemeConfig {
// Theme mode
mode?: "light" | "dark" | "auto"; // default: 'light'
// Compact mode
compact?: boolean; // default: false
// Seed tokens - algorithm input, auto-derives other values
seed?: SeedToken;
// Map tokens - override derived values
map?: MapToken;
// Optional: separate config for light/dark mode
light?: {
seed?: SeedToken;
map?: MapToken;
};
dark?: {
seed?: SeedToken;
map?: MapToken;
};
}
interface SeedToken {
colorPrimary?: string; // e.g., '#6366f1'
colorSecondary?: string;
colorSuccess?: string;
colorWarning?: string;
colorError?: string;
borderRadius?: number; // e.g., 8
fontSize?: number; // e.g., 14
fontFamily?: string;
}
interface MapToken {
// Primary colors (derived from seed.colorPrimary)
colorPrimary?: string;
colorPrimaryHover?: string;
colorPrimaryActive?: string;
colorPrimaryBg?: string;
// Background
colorBg?: string;
colorBgElevated?: string;
colorBgSpotlight?: string;
// Text
colorText?: string;
colorTextSecondary?: string;
colorTextTertiary?: string;
colorTextDisabled?: string;
// Border
colorBorder?: string;
colorBorderSecondary?: string;
// Layout
borderRadius?: number;
borderRadiusSm?: number;
borderRadiusLg?: number;
// Typography
fontSize?: number;
fontSizeSm?: number;
fontSizeLg?: number;
// Spacing
spacing?: number; // base spacing unit, e.g., 8
spacingXs?: number; // e.g., 4
spacingSm?: number; // e.g., 8
spacingMd?: number; // e.g., 16
spacingLg?: number; // e.g., 24
spacingXl?: number; // e.g., 32
}Token Priority
light/dark.map > light/dark.seed (derived) > map > seed (derived) > defaultTheme Inheritance
Nested ThemeProviders inherit from parent and can override specific values:
<TaskOnProvider config={{ apiKey: "your-api-key" }}>
<ThemeProvider theme={{ mode: "light", seed: { colorPrimary: "#6366f1" } }}>
<Header /> {/* light + primary #6366f1 */}
<ThemeProvider theme={{ mode: "dark" }}>
<Sidebar /> {/* dark + inherits primary #6366f1 */}
</ThemeProvider>
<ThemeProvider theme={{ seed: { colorPrimary: "#ef4444" } }}>
<DangerZone /> {/* light + primary #ef4444 */}
</ThemeProvider>
</ThemeProvider>
</TaskOnProvider>Light/Dark Separate Config
Configure different themes for light and dark modes:
// Different primary colors for each mode
<ThemeProvider
theme={{
mode: 'auto',
light: { seed: { colorPrimary: '#6366f1' } },
dark: { seed: { colorPrimary: '#818cf8' } },
}}
>
<App />
</ThemeProvider>
// Override specific derived values
<ThemeProvider
theme={{
mode: 'auto',
light: {
seed: { colorPrimary: '#6366f1' },
map: { colorPrimaryHover: '#4f46e5' },
},
dark: {
seed: { colorPrimary: '#818cf8' },
map: { colorBg: '#0a0a0a' },
},
}}
>
<App />
</ThemeProvider>Disable Inheritance
Use inherit={false} for completely independent themes:
<ThemeProvider theme={{ mode: "dark" }}>
<DarkContent />
<ThemeProvider theme={{ mode: "light" }} inherit={false}>
<LightPopup /> {/* Fully independent light theme */}
</ThemeProvider>
</ThemeProvider>Widgets
Cloud Configuration
Widgets support cloud configuration via configId. Configure in TaskOn Dashboard and load at runtime:
// Cloud config includes: theme, feature flags, custom texts
<QuestWidget configId="cfg_abc123" />Widget Props
interface WidgetProps {
// Cloud config ID from TaskOn Dashboard
configId?: string;
// Custom class names for widget parts
classNames?: {
root?: string;
header?: string;
body?: string;
footer?: string;
// ... widget-specific parts
};
// Custom inline styles for widget parts
styles?: {
root?: React.CSSProperties;
header?: React.CSSProperties;
body?: React.CSSProperties;
footer?: React.CSSProperties;
// ... widget-specific parts
};
}Styling with classNames and styles
Widgets support fine-grained styling via classNames and styles props:
// Using classNames
<QuestWidget
configId="cfg_abc123"
classNames={{
root: 'my-quest-widget',
header: 'my-quest-header',
body: 'my-quest-body',
}}
/>
// Using inline styles
<QuestWidget
configId="cfg_abc123"
styles={{
root: { maxWidth: 400 },
header: { borderBottom: '1px solid #eee' },
body: { padding: 24 },
}}
/>
// Combining both
<QuestWidget
configId="cfg_abc123"
classNames={{ root: 'custom-widget' }}
styles={{ header: { backgroundColor: 'transparent' } }}
/>Each widget documents its available parts in its own API reference.
Usage Examples
// Example 1: Using cloud config (theme from Dashboard)
<TaskOnProvider config={{ apiKey: 'your-api-key' }}>
<QuestWidget configId="cfg_abc123" />
<TaskWidget configId="cfg_xyz789" />
</TaskOnProvider>
// Example 2: Using local theme (no cloud config)
<TaskOnProvider config={{ apiKey: 'your-api-key' }}>
<ThemeProvider theme={{ mode: 'dark', seed: { colorPrimary: '#6366f1' } }}>
<QuestWidget />
<TaskWidget />
</ThemeProvider>
</TaskOnProvider>
// Example 3: Different local themes for different areas
<TaskOnProvider config={{ apiKey: 'your-api-key' }}>
<ThemeProvider theme={{ mode: 'light' }}>
<TaskWidget />
</ThemeProvider>
<ThemeProvider theme={{ mode: 'dark', compact: true }}>
<QuestWidget />
</ThemeProvider>
</TaskOnProvider>
// Example 4: Mixed - some with cloud config, some with local theme
<TaskOnProvider config={{ apiKey: 'your-api-key' }}>
{/* Cloud config */}
<QuestWidget configId="cfg_abc123" />
{/* Local theme */}
<ThemeProvider theme={{ mode: 'dark' }}>
<TaskWidget />
</ThemeProvider>
</TaskOnProvider>Login Methods
TaskOn uses a unified login method with signature verification. All login methods require a backend signature for security.
Unified Login API
import { useTaskOnAuth } from "@taskon/widget-react";
const { login } = useTaskOnAuth();
// Login with any method
await login({
method: "evm_wallet", // Login method type
value: "0x1234...", // Address / email / OAuth token
sign: signatureFromBackend, // Backend signature
timestamp: 1234567890, // Signature timestamp (seconds)
});Supported Methods
| Method | method value | value parameter |
| ------------- | ----------------- | ----------------------- |
| EVM Wallet | evm_wallet | Wallet address (0x...) |
| Solana Wallet | solana_wallet | Wallet address (base58) |
| Email | email | Email address |
| Discord | discord | OAuth token |
| Twitter | twitter | OAuth token |
| Telegram | telegram | OAuth token |
LoginParams Type
interface LoginParams {
method: LoginMethod; // Login method type
value: string; // Address / email / OAuth token
sign: string; // Backend signature
timestamp: number; // Signature timestamp (seconds)
}
type LoginMethod =
| "evm_wallet"
| "solana_wallet"
| "email"
| "discord"
| "twitter"
| "telegram";Wallet Integration
When tasks or rewards involve blockchain operations (e.g., token rewards, NFT minting, on-chain verification), widgets need to interact with the user's wallet for:
- Connecting wallet - Get user's wallet address
- Signing messages - Verify wallet ownership
- Calling contracts - Claim on-chain rewards, execute transactions
Overview
TaskOn widgets include built-in wallet management, but also support external wallet providers for seamless integration with existing dApps. When wrapped in an external provider, the widget automatically detects and reuses your wallet management.
Wallet Management
TaskOn provides flexible wallet integration options:
| Setup | Behavior |
| -------------- | ------------------------------------- |
| Custom adapter | Uses your provided WalletAdapter |
| Default | Uses built-in window.ethereum adapter |
Custom Wallet Adapter (Recommended)
If you want full control over wallet connection (e.g., using RainbowKit, Web3Modal), provide a custom adapter:
import { createWalletAdapter } from "./my-wallet-adapter";
<TaskOnProvider
config={{
apiKey: "your-api-key",
walletConfig: {
evmAdapter: createWalletAdapter(),
},
}}
>
<App />
</TaskOnProvider>;Built-in Wallet Support
If no custom adapter is provided, TaskOn automatically uses window.ethereum to connect to browser wallets like MetaMask:
// No wallet config needed - uses window.ethereum by default
<TaskOnProvider config={{ apiKey: "your-api-key" }}>
<App />
</TaskOnProvider>Built-in Wallet Binding Dialog
When tasks require wallet binding (e.g., on-chain verification), TaskOn shows a built-in wallet selection dialog:
Desktop (without adapter):
- MetaMask
- ONTO Wallet
- Bitget Wallet
- OKX Wallet
- WalletConnect (requires
walletConnectProjectId)
Mobile (non-Dapp browser):
- WalletConnect only (requires
walletConnectProjectId)
Mobile (Dapp browser / wallet app):
- Uses injected provider directly
To enable WalletConnect in the dialog:
<TaskOnProvider
config={{
apiKey: "your-api-key",
walletConnectProjectId: "your-project-id", // Get from cloud.walletconnect.com
}}
>
<App />
</TaskOnProvider>Built-in Wallet Management
If no external provider is detected, the widget uses its built-in wallet management. No configuration needed:
<TaskOnProvider config={{ apiKey: "your-api-key" }}>
<App /> {/* Widget handles wallet connection internally */}
</TaskOnProvider>Configuration Options
interface WalletConfig {
// EVM wallet adapter (highest priority)
evmAdapter?: WalletAdapter;
// Solana wallet adapter (highest priority)
solanaAdapter?: WalletAdapter;
// Disable auto-detection of external providers
disableAutoDetect?: boolean;
}Custom Wallet Adapter
For projects with their own wallet management:
// Example: Custom wallet management
const useCustomWalletAdapter = (): WalletAdapter => {
const { openWalletModal, connectedAddress, chainId } = useYourWalletManager();
return {
connect: async () => {
// Open your wallet selection modal, return selected address
const address = await openWalletModal();
return address;
},
disconnect: async () => {
await yourDisconnectLogic();
},
signMessage: async (message) => {
return await yourSignMessageLogic(message);
},
getAddress: () => connectedAddress,
getChainId: () => chainId,
switchNetwork: async (chainId) => {
await yourSwitchNetworkLogic(chainId);
},
};
};
const App = () => {
const evmAdapter = useCustomWalletAdapter();
return (
<TaskOnProvider
config={{
apiKey: "your-api-key",
walletConfig: { evmAdapter },
}}
>
<YourApp />
</TaskOnProvider>
);
};Priority Order
When multiple options are available:
- Custom Adapter -
walletConfig.evmAdapter/solanaAdapter(highest) - Built-in Adapter - window.ethereum adapter for EVM wallets (lowest)
Hooks
useTaskOnAuth
Access TaskOn authentication with unified login method.
import { useTaskOnAuth } from "@taskon/widget-react";
const Component = () => {
const {
// State
userId, // TaskOn user ID (number | null)
isLoggedIn, // Whether user is logged in
isInitializing, // Whether provider is initializing
// Unified login method
login, // (params: LoginParams) => Promise<void>
// Logout
logout, // () => void
} = useTaskOnAuth();
return <div>TaskOn ID: {userId}</div>;
};Login Function
// Unified login method
login: (params: LoginParams) => Promise<void>;
// LoginParams
interface LoginParams {
method: LoginMethod; // Login method type
value: string; // Address / email / OAuth token
sign: string; // Backend signature
timestamp: number; // Signature timestamp (seconds)
}
// Logout
logout: () => void;useTaskOnTheme
Access current theme. Must be used within ThemeProvider.
import { useTaskOnTheme } from "@taskon/widget-react";
const Component = () => {
const theme = useTaskOnTheme();
return (
<div
style={{
background: theme.tokens.colorBg,
color: theme.tokens.colorText,
padding: theme.tokens.spacingMd,
borderRadius: theme.tokens.borderRadius,
}}
>
Current mode: {theme.mode}
</div>
);
};Integration Examples
// Example 1: With EVM Wallet
const EVMWalletIntegration = () => {
const { evmAddress } = useWallet(); // TaskOn wallet hook
const { userId, login } = useTaskOnAuth();
const handleLogin = async () => {
if (evmAddress && !userId) {
// Get signature from your backend
const { sign, timestamp } = await fetchSignatureFromBackend(evmAddress);
await login({
method: "evm_wallet",
value: evmAddress,
sign,
timestamp,
});
}
};
useEffect(() => {
handleLogin();
}, [evmAddress, userId]);
return <div>{userId ? `TaskOn: ${userId}` : "Not logged in"}</div>;
};
// Example 2: With Custom Wallet Adapter
const CustomWalletIntegration = () => {
const { evmAddress, connectEvm } = useWallet();
const { userId, login } = useTaskOnAuth();
const handleLogin = async () => {
if (publicKey && !userId) {
const address = publicKey.toBase58();
const { sign, timestamp } = await fetchSignatureFromBackend(address);
await login({
method: "solana_wallet",
value: address,
sign,
timestamp,
});
}
};
useEffect(() => {
handleLogin();
}, [publicKey, userId]);
return <div>{userId ? `TaskOn: ${userId}` : "Not logged in"}</div>;
};
// Example 3: Email login
const EmailLogin = () => {
const { login } = useTaskOnAuth();
const handleEmailLogin = async (email: string) => {
const { sign, timestamp } = await fetchSignatureFromBackend(email);
await login({
method: "email",
value: email,
sign,
timestamp,
});
};
return (
<button onClick={() => handleEmailLogin("[email protected]")}>Login</button>
);
};
// Example 4: Discord OAuth callback
const DiscordCallback = () => {
const { login } = useTaskOnAuth();
useEffect(() => {
// After your Discord OAuth flow
const processOAuth = async () => {
const oauthToken = getDiscordTokenFromOAuth();
const { sign, timestamp } = await fetchSignatureFromBackend(oauthToken);
await login({
method: "discord",
value: oauthToken,
sign,
timestamp,
});
};
processOAuth();
}, []);
return <div>Processing...</div>;
};WalletAdapter Interface
For custom wallet integration:
interface WalletAdapter {
// Required
connect: () => Promise<string>;
disconnect: () => Promise<void>;
signMessage: (message: string) => Promise<string>;
getAddress: () => string | null;
// Optional (EVM only)
getChainId?: () => number | null;
switchNetwork?: (chainId: number) => Promise<void>;
// Optional (Event subscriptions)
onAccountChange?: (callback: (address: string | null) => void) => () => void;
onChainChange?: (callback: (chainId: number) => void) => () => void;
}Types
TaskOnAuthState
interface TaskOnAuthState {
userId: number | null; // TaskOn user ID (null if not logged in)
isLoggedIn: boolean; // Whether user is logged in
isInitializing: boolean; // Whether provider is initializing
}LoginParams
interface LoginParams {
method: LoginMethod; // Login method type
value: string; // Address / email / OAuth token
sign: string; // Backend signature
timestamp: number; // Signature timestamp (seconds)
}
type LoginMethod =
| "evm_wallet"
| "solana_wallet"
| "email"
| "discord"
| "twitter"
| "telegram";TaskOnTheme
The resolved theme object returned by useTaskOnTheme():
interface TaskOnTheme {
mode: "light" | "dark";
compact: boolean;
// All tokens are resolved (seed + derived + overrides)
tokens: {
// Primary colors
colorPrimary: string;
colorPrimaryHover: string;
colorPrimaryActive: string;
colorPrimaryBg: string;
// Secondary colors
colorSecondary: string;
// Status colors
colorSuccess: string;
colorWarning: string;
colorError: string;
// Background
colorBg: string;
colorBgElevated: string;
colorBgSpotlight: string;
// Text
colorText: string;
colorTextSecondary: string;
colorTextTertiary: string;
colorTextDisabled: string;
// Border
colorBorder: string;
colorBorderSecondary: string;
// Layout
borderRadius: number;
borderRadiusSm: number;
borderRadiusLg: number;
// Typography
fontSize: number;
fontSizeSm: number;
fontSizeLg: number;
fontFamily: string;
// Spacing
spacing: number;
spacingXs: number;
spacingSm: number;
spacingMd: number;
spacingLg: number;
spacingXl: number;
};
}SSR Compatibility
This package is SSR-compatible:
- All components are marked with
'use client' - Browser APIs are safely wrapped
- No hydration mismatches
Works with:
- Next.js App Router
- Next.js Pages Router
- Remix
- Other React SSR frameworks
Peer Dependencies
react>= 18.0.0react-dom>= 18.0.0@taskon/core>= 0.0.0
Optional Peer Dependencies
For WalletConnect support in the built-in wallet binding dialog:
npm install @walletconnect/ethereum-provider @walletconnect/modalThen configure your project ID:
<TaskOnProvider
config={{
apiKey: "your-api-key",
walletConnectProjectId: "your-walletconnect-project-id",
}}
>
<App />
</TaskOnProvider>Get your WalletConnect Project ID at https://cloud.walletconnect.com
If not configured, the WalletConnect option will be disabled in the wallet binding dialog.
License
MIT
