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