React Native Integration
Add content moderation to your React Native app.
Using Expo? See the Expo integration guide for Expo-specific setup.
Installation
bash
npm install @nextauralabs/vettly-sdkQuick Start
tsx
import { ModerationClient } from '@nextauralabs/vettly-sdk'
const vettly = new ModerationClient({
apiKey: 'vettly_your_api_key'
})
async function moderateMessage(content: string) {
const result = await vettly.check({
content,
contentType: 'text'
})
if (result.action === 'block') {
return { allowed: false, reason: 'Content not allowed' }
}
return { allowed: true }
}Text Moderation
Moderated Input Component
tsx
import React, { useState } from 'react'
import {
View,
TextInput,
TouchableOpacity,
Text,
StyleSheet,
ActivityIndicator
} from 'react-native'
import { ModerationClient } from '@nextauralabs/vettly-sdk'
const vettly = new ModerationClient({
apiKey: 'vettly_your_api_key'
})
interface Props {
onSubmit: (text: string) => void
placeholder?: string
}
export function ModeratedInput({ onSubmit, placeholder }: Props) {
const [text, setText] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleSubmit = async () => {
if (!text.trim()) return
setLoading(true)
setError(null)
try {
const result = await vettly.check({
content: text,
contentType: 'text'
})
if (result.action === 'block') {
setError('This content is not allowed')
return
}
onSubmit(text)
setText('')
} catch (err) {
console.error('Moderation error:', err)
// Decide: fail open or closed
onSubmit(text) // Fail open
setText('')
} finally {
setLoading(false)
}
}
return (
<View style={styles.container}>
<TextInput
style={[styles.input, error && styles.inputError]}
value={text}
onChangeText={(t) => {
setText(t)
setError(null)
}}
placeholder={placeholder || 'Type something...'}
multiline
editable={!loading}
/>
{error && <Text style={styles.errorText}>{error}</Text>}
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleSubmit}
disabled={loading || !text.trim()}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>Send</Text>
)}
</TouchableOpacity>
</View>
)
}
const styles = StyleSheet.create({
container: {
padding: 16
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 12,
fontSize: 16,
minHeight: 80,
textAlignVertical: 'top'
},
inputError: {
borderColor: '#ef4444'
},
errorText: {
color: '#ef4444',
fontSize: 14,
marginTop: 4
},
button: {
backgroundColor: '#3b82f6',
borderRadius: 8,
padding: 14,
alignItems: 'center',
marginTop: 8
},
buttonDisabled: {
opacity: 0.6
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600'
}
})Image Moderation
With react-native-image-picker
bash
npm install react-native-image-picker react-native-fstsx
import { launchImageLibrary } from 'react-native-image-picker'
import RNFS from 'react-native-fs'
import { ModerationClient } from '@nextauralabs/vettly-sdk'
const vettly = new ModerationClient({
apiKey: 'vettly_your_api_key'
})
async function pickAndModerateImage() {
const result = await launchImageLibrary({
mediaType: 'photo',
quality: 0.8
})
if (result.didCancel || !result.assets?.[0]) {
return null
}
const asset = result.assets[0]
// Read as base64
const base64 = await RNFS.readFile(asset.uri!, 'base64')
// Moderate
const modResult = await vettly.check({
content: base64,
contentType: 'image'
})
if (modResult.action === 'block') {
Alert.alert('Error', 'This image is not allowed')
return null
}
return asset.uri
}Image Upload Component
tsx
import React, { useState } from 'react'
import {
View,
Image,
TouchableOpacity,
Text,
Alert,
ActivityIndicator,
StyleSheet
} from 'react-native'
import { launchImageLibrary } from 'react-native-image-picker'
import RNFS from 'react-native-fs'
import { ModerationClient } from '@nextauralabs/vettly-sdk'
const vettly = new ModerationClient({
apiKey: 'vettly_your_api_key'
})
interface Props {
onImageSelected: (uri: string, moderationId: string) => void
}
export function ModeratedImagePicker({ onImageSelected }: Props) {
const [imageUri, setImageUri] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const pickImage = async () => {
const result = await launchImageLibrary({
mediaType: 'photo',
quality: 0.8
})
if (result.didCancel || !result.assets?.[0]) return
const asset = result.assets[0]
setLoading(true)
try {
// Convert to base64
const base64 = await RNFS.readFile(asset.uri!, 'base64')
// Moderate
const modResult = await vettly.check({
content: base64,
contentType: 'image'
})
if (modResult.action === 'block') {
Alert.alert(
'Image Not Allowed',
'Please select a different image.'
)
return
}
setImageUri(asset.uri!)
onImageSelected(asset.uri!, modResult.decisionId)
} catch (error) {
Alert.alert('Error', 'Failed to process image')
} finally {
setLoading(false)
}
}
return (
<View style={styles.container}>
<TouchableOpacity
style={styles.picker}
onPress={pickImage}
disabled={loading}
>
{loading ? (
<ActivityIndicator size="large" color="#3b82f6" />
) : imageUri ? (
<Image source={{ uri: imageUri }} style={styles.image} />
) : (
<Text style={styles.placeholderText}>Tap to select image</Text>
)}
</TouchableOpacity>
</View>
)
}
const styles = StyleSheet.create({
container: {
alignItems: 'center'
},
picker: {
width: 200,
height: 200,
borderRadius: 12,
borderWidth: 2,
borderColor: '#ddd',
borderStyle: 'dashed',
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden'
},
image: {
width: '100%',
height: '100%'
},
placeholderText: {
color: '#666',
fontSize: 16
}
})Custom Hook
tsx
// hooks/useModeration.ts
import { useState, useCallback } from 'react'
import { ModerationClient, ModerationResult } from '@nextauralabs/vettly-sdk'
const vettly = new ModerationClient({
apiKey: 'vettly_your_api_key'
})
export function useModeration() {
const [loading, setLoading] = useState(false)
const [result, setResult] = useState<ModerationResult | null>(null)
const [error, setError] = useState<Error | null>(null)
const checkText = useCallback(async (content: string) => {
setLoading(true)
setError(null)
try {
const modResult = await vettly.check({
content,
contentType: 'text'
})
setResult(modResult)
return modResult
} catch (err) {
setError(err as Error)
throw err
} finally {
setLoading(false)
}
}, [])
const checkImage = useCallback(async (base64: string) => {
setLoading(true)
setError(null)
try {
const modResult = await vettly.check({
content: base64,
contentType: 'image'
})
setResult(modResult)
return modResult
} catch (err) {
setError(err as Error)
throw err
} finally {
setLoading(false)
}
}, [])
return {
checkText,
checkImage,
loading,
result,
error,
isBlocked: result?.action === 'block'
}
}Server-Side Moderation (Recommended)
Keep your API key secure by moderating on your backend:
tsx
// React Native app
async function sendMessage(content: string) {
const response = await fetch('https://your-api.com/api/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ content })
})
if (response.status === 403) {
const data = await response.json()
throw new Error(data.error)
}
return response.json()
}Error Handling
tsx
import { ModerationClient, VettlyError } from '@nextauralabs/vettly-sdk'
async function safeModerate(content: string) {
try {
const result = await vettly.check({
content,
contentType: 'text'
})
return result
} catch (error) {
if (error instanceof VettlyError) {
// API error - decide how to handle
console.warn('Moderation unavailable:', error.message)
return { action: 'allow' } // Fail open
}
throw error
}
}Offline Support
tsx
import NetInfo from '@react-native-community/netinfo'
import AsyncStorage from '@react-native-async-storage/async-storage'
async function moderateWithOfflineSupport(content: string) {
const netInfo = await NetInfo.fetch()
if (!netInfo.isConnected) {
// Queue for later
const pending = JSON.parse(
await AsyncStorage.getItem('pendingModeration') || '[]'
)
pending.push({ content, timestamp: Date.now() })
await AsyncStorage.setItem('pendingModeration', JSON.stringify(pending))
return { action: 'pending', offline: true }
}
return vettly.check({ content, contentType: 'text' })
}
// Process queue when back online
async function processPendingModeration() {
const pending = JSON.parse(
await AsyncStorage.getItem('pendingModeration') || '[]'
)
for (const item of pending) {
try {
await vettly.check({ content: item.content, contentType: 'text' })
} catch (err) {
console.error('Failed to process:', err)
}
}
await AsyncStorage.removeItem('pendingModeration')
}Next Steps
- Expo Integration - Expo-specific guide
- Custom Policies - Configure moderation rules
- Best Practices - Mobile moderation patterns