@thewhateverapp/tile-sdk
v0.10.1
Published
SDK for building interactive tiles on The Whatever App platform
Maintainers
Readme
@thewhateverapp/tile-sdk
⚠️ Beta Release (v0.0.1): This package is in early preview. Requires integration with The Whatever App parent window (thewhatever.app). Use for development and testing.
SDK for building interactive tiles on The Whatever App platform. Provides secure parent-tile communication, authentication, storage, and more.
Features
- 🔐 Authentication: OAuth-like permission system for accessing user data
- 💾 Storage: Parent-managed persistent storage (100KB per tile)
- 📋 Clipboard: Secure clipboard access with user permission
- 🔄 Navigation: Navigate to full page or open external URLs
- 📊 Analytics: Track events and user interactions
- 🎨 React Hooks: Easy-to-use React hooks and components
- 🔒 Security: Sandboxed iframe with validated message bridge
Installation
npm install @thewhateverapp/tile-sdkQuick Start
React Component
import { TileProvider, useTile } from '@thewhateverapp/tile-sdk/react';
function MyTileApp() {
return (
<TileProvider fallback={<div>Loading...</div>}>
<TileContent />
</TileProvider>
);
}
function TileContent() {
const tile = useTile();
const handleClick = async () => {
// Navigate to full page
tile.navigateToPage();
// Track event
tile.trackEvent('button_click', { action: 'open_page' });
};
return (
<div className="w-full h-full">
<h1>My Tile</h1>
<button onClick={handleClick}>Open Full App</button>
</div>
);
}Vanilla JavaScript
import { getTileBridge } from '@thewhateverapp/tile-sdk';
const bridge = getTileBridge();
// Wait for bridge to be ready
await bridge.waitForReady();
// Use bridge APIs
bridge.navigateToPage();
bridge.trackEvent('page_view', { page: 'home' });Authentication
The tile SDK implements an OAuth-like permission system where apps start with minimal access (user ID only) and must request additional permissions.
Permission Scopes
id- User's internal ID (always granted)profile- Username, avatar, display nameemail- Email addresswallet- Connected wallet addressesanalytics:read- View user's analyticsanalytics:write- Track analytics (auto-granted)storage:read- Read user's storage (auto-granted)storage:write- Write to storage (auto-granted)
Request User Authentication
const tile = useTile();
// Request user with specific scopes
const handleLogin = async () => {
const user = await tile.auth.getUser({
scopes: ['profile', 'email'],
reason: 'We need your email to send you updates'
});
if (user) {
console.log('User granted access:', user);
// { id, username, email, scopes }
} else {
console.log('User denied permission');
}
};Get Current User
const tile = useTile();
// Get current user (if already authenticated)
const user = await tile.auth.getCurrentUser();
if (user) {
console.log(`Hello, ${user.username || 'User'}!`);
}Request Additional Scopes
const tile = useTile();
// Later in the app, request wallet access
const handleConnectWallet = async () => {
const granted = await tile.auth.requestScopes(['wallet']);
if (granted) {
const user = await tile.auth.getCurrentUser();
console.log('Wallet addresses:', user.walletAddresses);
}
};Storage
Tiles get 100KB of persistent storage managed by the parent window.
const tile = useTile();
// Store data
await tile.storage.set('user_preferences', {
theme: 'dark',
notifications: true
});
// Retrieve data
const prefs = await tile.storage.get('user_preferences');
console.log('Theme:', prefs.theme);Clipboard
Clipboard access requires user permission (one-time prompt).
const tile = useTile();
const handleCopy = async () => {
try {
await tile.clipboard.write('Hello from my tile!');
console.log('Copied to clipboard!');
} catch (error) {
console.error('User denied clipboard access');
}
};Navigation
Navigate to Full Page
const tile = useTile();
// Opens the full page view of your app
tile.navigateToPage();Open External URL
const tile = useTile();
// Opens URL with user confirmation
tile.openUrl('https://example.com', '_blank');Analytics
Track user events and interactions.
const tile = useTile();
// Track custom event
tile.trackEvent('button_click', {
button: 'subscribe',
location: 'header'
});
// Track page view
tile.trackEvent('page_view', { page: 'home' });Configuration
Access tile configuration from parent.
const tile = useTile();
if (tile.config) {
console.log('App ID:', tile.config.tileId);
console.log('Position:', tile.config.position);
console.log('Theme:', tile.config.theme);
}Advanced: Using TileBridge Directly
For non-React apps or advanced use cases:
import { getTileBridge } from '@thewhateverapp/tile-sdk';
const bridge = getTileBridge();
// Wait for ready
const config = await bridge.waitForReady();
// Listen for events
bridge.on('theme:change', (theme) => {
console.log('Theme changed:', theme);
});
// Request resize (max 512x512)
bridge.requestResize(400, 300);
// Get user with auth
const user = await bridge.getUser({
scopes: ['profile'],
reason: 'Show your name'
});
// Storage operations
await bridge.setStorage('key', { data: 'value' });
const data = await bridge.getStorage('key');
// Navigate
bridge.navigateToPage();
bridge.openUrl('https://example.com');
// Track events
bridge.trackEvent('custom_event', { foo: 'bar' });
// Clipboard
await bridge.writeToClipboard('text to copy');Security Model
Sandboxed Iframe
All tiles run in sandboxed iframes with strict policies:
<iframe
sandbox="allow-scripts allow-pointer-lock"
referrerpolicy="no-referrer"
allow="clipboard-read; clipboard-write"
/>Origin Validation
All messages are validated against the parent origin:
- Production:
https://thewhatever.app - Development:
http://localhost:3000
Permission System
- Apps start with minimal access (user ID only)
- Additional permissions require explicit user consent
- Users can revoke permissions anytime in settings
- Permission grants are audited for security
Best Practices
1. Request Minimal Scopes
❌ Bad:
await tile.auth.getUser({
scopes: ['profile', 'email', 'wallet']
});✅ Good:
// Start minimal
await tile.auth.getUser({
scopes: ['profile']
});
// Request more later when needed
await tile.auth.requestScopes(['email']);2. Explain Why
await tile.auth.getUser({
scopes: ['email'],
reason: 'Send you order confirmation'
});3. Handle Denials Gracefully
const user = await tile.auth.getUser({ scopes: ['email'] });
if (!user || !user.email) {
// Provide alternative flow
return <ManualEmailInput />;
}4. Check Scopes Before Using
const user = await tile.auth.getCurrentUser();
if (user?.scopes.includes('email')) {
sendEmail(user.email);
} else {
console.log('Email permission not granted');
}API Reference
useTile() Hook
Returns the tile context with all available methods.
Returns: TileContextValue
TileContextValue
interface TileContextValue {
config: TileConfig | null;
bridge: TileBridge;
isReady: boolean;
// Navigation
navigateToPage: () => void;
openUrl: (url: string, target?: '_blank' | '_self') => void;
// Analytics
trackEvent: (eventName: string, data?: any) => void;
// Storage
storage: {
get: (key: string) => Promise<any>;
set: (key: string, value: any) => Promise<void>;
};
// Clipboard
clipboard: {
write: (text: string) => Promise<void>;
};
// Authentication
auth: {
getUser: (options?: {
scopes?: string[];
reason?: string;
}) => Promise<UserContext | null>;
requestScopes: (scopes: string[]) => Promise<boolean>;
getCurrentUser: () => Promise<UserContext | null>;
};
}TileConfig
interface TileConfig {
tileId: string;
position: { row: number; col: number };
tileUrl: string;
apiEndpoint?: string;
theme?: 'light' | 'dark';
debug?: boolean;
}UserContext
interface UserContext {
authenticated: boolean;
userId: string;
scopes: string[];
// Only if 'profile' scope granted
username?: string;
avatar?: string;
displayName?: string;
// Only if 'email' scope granted
email?: string;
// Only if 'wallet' scope granted
walletAddresses?: string[];
}Examples
Example: Simple Counter Tile
import { TileProvider, useTile } from '@thewhateverapp/tile-sdk/react';
import { useState, useEffect } from 'react';
function CounterTile() {
return (
<TileProvider>
<Counter />
</TileProvider>
);
}
function Counter() {
const tile = useTile();
const [count, setCount] = useState(0);
// Load count from storage on mount
useEffect(() => {
tile.storage.get('count').then(savedCount => {
if (savedCount !== undefined) setCount(savedCount);
});
}, []);
// Save count to storage when it changes
useEffect(() => {
tile.storage.set('count', count);
}, [count]);
const increment = () => {
setCount(c => c + 1);
tile.trackEvent('counter_increment', { value: count + 1 });
};
return (
<div className="flex flex-col items-center justify-center h-full">
<h1 className="text-4xl font-bold">{count}</h1>
<button onClick={increment} className="mt-4 px-6 py-2 bg-blue-500">
Increment
</button>
<button onClick={() => tile.navigateToPage()} className="mt-2">
View Full App
</button>
</div>
);
}Example: User Profile Tile
import { TileProvider, useTile } from '@thewhateverapp/tile-sdk/react';
import { useState, useEffect } from 'react';
function ProfileTile() {
return (
<TileProvider>
<Profile />
</TileProvider>
);
}
function Profile() {
const tile = useTile();
const [user, setUser] = useState(null);
const handleLogin = async () => {
const userData = await tile.auth.getUser({
scopes: ['profile', 'email'],
reason: 'Display your profile information'
});
if (userData) {
setUser(userData);
tile.trackEvent('user_logged_in');
}
};
if (!user) {
return (
<div className="flex items-center justify-center h-full">
<button onClick={handleLogin} className="px-6 py-3 bg-purple-500">
Sign In
</button>
</div>
);
}
return (
<div className="p-4">
<img src={user.avatar} className="w-16 h-16 rounded-full" />
<h2 className="text-xl font-bold mt-2">{user.username}</h2>
{user.email && <p className="text-gray-600">{user.email}</p>}
<button onClick={() => tile.navigateToPage()} className="mt-4">
View Full Profile
</button>
</div>
);
}TypeScript Support
Full TypeScript support with type definitions included.
import type {
TileContextValue,
TileConfig,
UserContext,
TileBridge
} from '@thewhateverapp/tile-sdk';Development
Local Testing
# Install dependencies
npm install
# Build
npm run build
# Run type checking
npm run typecheck
# Lint
npm run lintTesting with Parent
To test your tile locally, you'll need the parent window running:
# In parent project
npm run dev
# Your tile will be loaded at:
# http://localhost:3000/tile/[your-app-id]Resources
License
Proprietary - All rights reserved
