Device Features
Mini Apps can access native device capabilities through the MiniKit SDK and standard web APIs. This guide covers haptic feedback and camera/microphone access.
Haptic Feedback
Haptic feedback provides tactile responses to user interactions, enhancing the user experience.
Usage
import { minikit } from 'watchee-minikit';
// Trigger success haptic
await minikit.hapticFeedback('success');
// Trigger error haptic
await minikit.hapticFeedback('error');
// Trigger light impact
await minikit.hapticFeedback('light');
Available Types
| Type | Description | Use Case |
|---|---|---|
success | Positive notification | Successful actions, confirmations |
warning | Warning notification | Caution alerts, important notices |
error | Error notification | Failed actions, validation errors |
light | Light impact | Button taps, selections |
medium | Medium impact | Toggle switches, sliders |
heavy | Heavy impact | Significant actions, deletions |
Best Practices
- Use notification types (
success,warning,error) for outcomes - Use impact types (
light,medium,heavy) for interactions - Don't overuse haptics - they should enhance, not distract
- Always provide visual feedback alongside haptic feedback
Device Permissions
The MiniKit SDK provides a comprehensive permission system similar to World App, allowing mini-apps to request and manage device permissions with beautiful UI and user control.
Permission Types
import { Permission, PermissionStatus } from 'watchee-minikit';
// Available permissions
Permission.Camera // Camera access
Permission.Microphone // Microphone access
Permission.Notifications // Push notifications
Permission.MediaLibrary // Photo library access
// Permission states
PermissionStatus.Granted // User allowed
PermissionStatus.Denied // User denied
PermissionStatus.NotRequested // Never requested
Requesting Permissions
When you request a permission, a beautiful modal is shown to the user:
import { minikit, Permission, PermissionStatus } from 'watchee-minikit';
// Request camera permission
const response = await minikit.requestPermission(Permission.Camera);
if (response.status === PermissionStatus.Granted) {
// Permission granted - proceed with camera access
await minikit.hapticFeedback('success');
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
} else {
// Permission denied
showMessage('Camera permission is required for this feature');
}
Checking Permission Status
Check permissions without showing a prompt:
// Check single permission
const cameraStatus = await minikit.checkPermission(Permission.Camera);
// Get all permissions at once
const allPerms = await minikit.getPermissions();
console.log(allPerms.permissions.camera); // 'granted' | 'denied' | 'not_requested'
Permission Flow Best Practices
- Check first, request second:
const status = await minikit.checkPermission(Permission.Camera);
if (status.status === PermissionStatus.NotRequested) {
// Show explanation UI, then request
const result = await minikit.requestPermission(Permission.Camera);
} else if (status.status === PermissionStatus.Denied) {
// Guide user to settings
showMessage('Enable camera in Settings → Spaces → YourApp');
} else {
// Already granted - proceed
startCamera();
}
-
Request at point of use - Don't request all permissions upfront. Request when the user triggers a feature that needs it.
-
Provide feedback:
const result = await minikit.requestPermission(Permission.Microphone);
if (result.status === PermissionStatus.Granted) {
await minikit.hapticFeedback('success');
} else {
await minikit.hapticFeedback('error');
}
User Permission Management
Users can manage your mini-app's permissions at any time:
- From the mini-app: Tap the menu (⋮) → "Manage Permissions"
- From Settings: Settings → Spaces → Your App → Device Permissions
Permission toggles allow users to:
- Enable/disable individual permissions
- See current status (Allowed/Denied/Not requested)
- Revoke all permissions at once
React Hook Example
function usePermission(permission: Permission) {
const [status, setStatus] = useState<PermissionStatus>(PermissionStatus.NotRequested);
const [loading, setLoading] = useState(false);
useEffect(() => {
minikit.checkPermission(permission).then(r => setStatus(r.status));
}, [permission]);
const request = async () => {
setLoading(true);
try {
const result = await minikit.requestPermission(permission);
setStatus(result.status);
return result.status === PermissionStatus.Granted;
} finally {
setLoading(false);
}
};
return { status, loading, request, isGranted: status === PermissionStatus.Granted };
}
// Usage
function CameraButton() {
const { status, loading, request, isGranted } = usePermission(Permission.Camera);
const handleClick = async () => {
if (!isGranted) {
const granted = await request();
if (!granted) return;
}
startCamera();
};
return (
<button onClick={handleClick} disabled={loading}>
{loading ? 'Requesting...' : 'Start Camera'}
</button>
);
}
Camera & Microphone Access
Mini Apps can access the device camera and microphone using the standard WebRTC getUserMedia API. This enables features like:
- Video calls and conferencing
- QR code scanning
- Photo capture
- Audio recording
- Live streaming
Basic Usage
// Request camera and microphone access
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
// Display in a video element
const videoElement = document.querySelector('video');
videoElement.srcObject = stream;
await videoElement.play();
Camera Options
// Front camera (selfie)
const frontCamera = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'user' }
});
// Back camera
const backCamera = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' }
});
// High resolution
const hdStream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 30 }
}
});
Audio Options
const audioStream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
Permission Handling
Always handle permission errors gracefully:
async function startCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
// Success - use the stream
return stream;
} catch (error) {
switch (error.name) {
case 'NotAllowedError':
// User denied permission
showMessage('Please allow camera access to use this feature');
break;
case 'NotFoundError':
// No camera available
showMessage('No camera found on this device');
break;
case 'NotReadableError':
// Camera is in use by another app
showMessage('Camera is being used by another application');
break;
default:
showMessage('Unable to access camera');
}
return null;
}
}
Cleanup
Always stop media tracks when done to release device resources:
function stopMedia(stream: MediaStream) {
stream.getTracks().forEach(track => {
track.stop();
});
}
// Example: Stop on component unmount
useEffect(() => {
return () => {
if (streamRef.current) {
stopMedia(streamRef.current);
}
};
}, []);
Video Element Best Practices
When displaying camera preview:
<video
autoPlay
playsInline
muted
style={{ transform: facingMode === 'user' ? 'scaleX(-1)' : 'none' }}
/>
autoPlay: Start playing immediatelyplaysInline: Required for iOS to play inline (not fullscreen)muted: Prevents audio feedback when showing local camerascaleX(-1): Mirror the front camera so movements feel natural (like a mirror)
React Example
import { useRef, useState, useEffect } from 'react';
function CameraComponent() {
const videoRef = useRef<HTMLVideoElement>(null);
const streamRef = useRef<MediaStream | null>(null);
const [isActive, setIsActive] = useState(false);
const [facingMode, setFacingMode] = useState<'user' | 'environment'>('user');
const startCamera = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode }
});
streamRef.current = stream;
if (videoRef.current) {
videoRef.current.srcObject = stream;
await videoRef.current.play();
}
setIsActive(true);
} catch (error) {
console.error('Failed to start camera:', error);
}
};
const stopCamera = () => {
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
streamRef.current = null;
}
setIsActive(false);
};
useEffect(() => {
return () => stopCamera();
}, []);
return (
<div>
{/* Mirror front camera for natural feel */}
<video
ref={videoRef}
autoPlay
playsInline
muted
style={{ transform: facingMode === 'user' ? 'scaleX(-1)' : 'none' }}
/>
<button onClick={isActive ? stopCamera : startCamera}>
{isActive ? 'Stop' : 'Start'} Camera
</button>
</div>
);
}
Media Picker (Camera Roll)
Mini-apps can let users select images and videos from their device using the standard HTML file input.
Basic Usage
// Image picker
<input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
const url = URL.createObjectURL(file);
// Use the URL to display the image
}
}}
/>
// Video picker
<input
type="file"
accept="video/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
const url = URL.createObjectURL(file);
// Use the URL to display the video
}
}}
/>
React Example
function MediaPicker() {
const [media, setMedia] = useState<{ url: string; type: string } | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const handleSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// Revoke previous URL to prevent memory leaks
if (media?.url) {
URL.revokeObjectURL(media.url);
}
setMedia({
url: URL.createObjectURL(file),
type: file.type.startsWith('video/') ? 'video' : 'image',
});
};
// Cleanup on unmount
useEffect(() => {
return () => {
if (media?.url) URL.revokeObjectURL(media.url);
};
}, []);
return (
<div>
<input
ref={inputRef}
type="file"
accept="image/*,video/*"
className="hidden"
onChange={handleSelect}
/>
<button onClick={() => inputRef.current?.click()}>
Pick Media
</button>
{media && (
media.type === 'video'
? <video src={media.url} controls />
: <img src={media.url} alt="Selected" />
)}
</div>
);
}
Accept Attribute Options
| Value | Description |
|---|---|
image/* | All image types (JPEG, PNG, GIF, etc.) |
video/* | All video types (MP4, MOV, etc.) |
image/*,video/* | Both images and videos |
image/jpeg,image/png | Specific image formats only |
Best Practices
- Revoke object URLs when done to prevent memory leaks
- Use hidden inputs with custom styled buttons for better UX
- Reset input value after selection to allow re-selecting the same file
- Show file name to confirm selection to users
Platform Notes
iOS
- Camera/microphone permissions are prompted automatically on first
getUserMediacall - The Watchee app has the required usage descriptions in Info.plist
Android
- Camera/microphone permissions are granted automatically within the WebView
- The Watchee app has the required permissions in AndroidManifest.xml