Skip to main content

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​

TypeDescriptionUse Case
successPositive notificationSuccessful actions, confirmations
warningWarning notificationCaution alerts, important notices
errorError notificationFailed actions, validation errors
lightLight impactButton taps, selections
mediumMedium impactToggle switches, sliders
heavyHeavy impactSignificant 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​

  1. 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();
}
  1. Request at point of use - Don't request all permissions upfront. Request when the user triggers a feature that needs it.

  2. 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:

  1. From the mini-app: Tap the menu (⋮) → "Manage Permissions"
  2. 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 immediately
  • playsInline: Required for iOS to play inline (not fullscreen)
  • muted: Prevents audio feedback when showing local camera
  • scaleX(-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​

ValueDescription
image/*All image types (JPEG, PNG, GIF, etc.)
video/*All video types (MP4, MOV, etc.)
image/*,video/*Both images and videos
image/jpeg,image/pngSpecific image formats only

Best Practices​

  1. Revoke object URLs when done to prevent memory leaks
  2. Use hidden inputs with custom styled buttons for better UX
  3. Reset input value after selection to allow re-selecting the same file
  4. Show file name to confirm selection to users

Platform Notes​

iOS​

  • Camera/microphone permissions are prompted automatically on first getUserMedia call
  • 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

See Also​