Skip to content

Expo + Vettly

Add content moderation to your Expo / React Native app.

Installation

bash
npx expo install @nextauralabs/vettly-sdk

Quick Start

tsx
import { ModerationClient } from '@nextauralabs/vettly-sdk'

const vettly = new ModerationClient({
  apiKey: 'vettly_your_api_key' // Use env var in production
})

async function checkMessage(content: string) {
  const result = await vettly.check({
    content,
    contentType: 'text'
  })

  return result.action !== 'block'
}

Text Moderation

Chat Input Component

tsx
import React, { useState } from 'react'
import { View, TextInput, Button, Text, StyleSheet } from 'react-native'
import { ModerationClient } from '@nextauralabs/vettly-sdk'

const vettly = new ModerationClient({
  apiKey: process.env.EXPO_PUBLIC_VETTLY_API_KEY!
})

export function ChatInput({ onSend }) {
  const [message, setMessage] = useState('')
  const [error, setError] = useState('')
  const [checking, setChecking] = useState(false)

  const handleSend = async () => {
    if (!message.trim()) return

    setChecking(true)
    setError('')

    try {
      const result = await vettly.check({
        content: message,
        contentType: 'text'
      })

      if (result.action === 'block') {
        setError('Message contains inappropriate content')
        return
      }

      onSend(message)
      setMessage('')
    } catch (err) {
      console.error('Moderation error:', err)
      // Decide: block or allow on error
      onSend(message) // Fail open
      setMessage('')
    } finally {
      setChecking(false)
    }
  }

  return (
    <View style={styles.container}>
      <TextInput
        style={styles.input}
        value={message}
        onChangeText={setMessage}
        placeholder="Type a message..."
        multiline
      />
      {error ? <Text style={styles.error}>{error}</Text> : null}
      <Button
        title={checking ? 'Checking...' : 'Send'}
        onPress={handleSend}
        disabled={checking || !message.trim()}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  container: { padding: 16 },
  input: {
    borderWidth: 1,
    borderColor: '#ccc',
    borderRadius: 8,
    padding: 12,
    marginBottom: 8
  },
  error: { color: 'red', marginBottom: 8 }
})

Image Moderation

With expo-image-picker

tsx
import * as ImagePicker from 'expo-image-picker'
import * as FileSystem from 'expo-file-system'
import { ModerationClient } from '@nextauralabs/vettly-sdk'

const vettly = new ModerationClient({
  apiKey: process.env.EXPO_PUBLIC_VETTLY_API_KEY!
})

async function pickAndModerateImage() {
  // 1. Pick image
  const result = await ImagePicker.launchImageLibraryAsync({
    mediaTypes: ImagePicker.MediaTypeOptions.Images,
    quality: 0.8
  })

  if (result.canceled) return null

  const imageUri = result.assets[0].uri

  // 2. Convert to base64
  const base64 = await FileSystem.readAsStringAsync(imageUri, {
    encoding: FileSystem.EncodingType.Base64
  })

  // 3. 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 imageUri
}

Image Upload Component

tsx
import React, { useState } from 'react'
import { View, Image, Button, Alert, ActivityIndicator } from 'react-native'
import * as ImagePicker from 'expo-image-picker'
import * as FileSystem from 'expo-file-system'
import { ModerationClient } from '@nextauralabs/vettly-sdk'

const vettly = new ModerationClient({
  apiKey: process.env.EXPO_PUBLIC_VETTLY_API_KEY!
})

export function AvatarUpload({ onUpload }) {
  const [image, setImage] = useState(null)
  const [loading, setLoading] = useState(false)

  const pickImage = async () => {
    const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync()

    if (status !== 'granted') {
      Alert.alert('Permission needed', 'Please allow photo access')
      return
    }

    const result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.Images,
      allowsEditing: true,
      aspect: [1, 1],
      quality: 0.8
    })

    if (result.canceled) return

    setLoading(true)

    try {
      const uri = result.assets[0].uri

      // Convert to base64
      const base64 = await FileSystem.readAsStringAsync(uri, {
        encoding: FileSystem.EncodingType.Base64
      })

      // Moderate
      const modResult = await vettly.check({
        content: base64,
        contentType: 'image'
      })

      if (modResult.action === 'block') {
        Alert.alert(
          'Image Not Allowed',
          'Please choose a different image'
        )
        return
      }

      setImage(uri)
      onUpload(uri, modResult.decisionId)
    } catch (error) {
      console.error('Error:', error)
      Alert.alert('Error', 'Failed to process image')
    } finally {
      setLoading(false)
    }
  }

  return (
    <View style={{ alignItems: 'center' }}>
      {loading ? (
        <ActivityIndicator size="large" />
      ) : image ? (
        <Image
          source={{ uri: image }}
          style={{ width: 150, height: 150, borderRadius: 75 }}
        />
      ) : null}
      <Button
        title={image ? 'Change Photo' : 'Upload Photo'}
        onPress={pickImage}
        disabled={loading}
      />
    </View>
  )
}

Custom Hook

tsx
// hooks/useModeration.ts
import { useState, useCallback } from 'react'
import { ModerationClient, ModerationResult } from '@nextauralabs/vettly-sdk'

const vettly = new ModerationClient({
  apiKey: process.env.EXPO_PUBLIC_VETTLY_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 check = useCallback(async (
    content: string,
    contentType: 'text' | 'image' = 'text'
  ) => {
    setLoading(true)
    setError(null)

    try {
      const modResult = await vettly.check({ content, contentType })
      setResult(modResult)
      return modResult
    } catch (err) {
      setError(err as Error)
      throw err
    } finally {
      setLoading(false)
    }
  }, [])

  const isBlocked = result?.action === 'block'
  const isFlagged = result?.action === 'flag'

  return { check, result, loading, error, isBlocked, isFlagged }
}

Usage:

tsx
function CommentForm() {
  const [comment, setComment] = useState('')
  const { check, loading, isBlocked } = useModeration()

  const handleSubmit = async () => {
    const result = await check(comment)

    if (result.action !== 'block') {
      // Submit comment
      await submitComment(comment)
    }
  }

  return (
    <View>
      <TextInput value={comment} onChangeText={setComment} />
      {isBlocked && <Text style={{ color: 'red' }}>Not allowed</Text>}
      <Button
        title={loading ? 'Checking...' : 'Submit'}
        onPress={handleSubmit}
        disabled={loading}
      />
    </View>
  )
}

For production, moderate on your backend to keep API keys secure:

tsx
// Mobile app
async function sendMessage(content: string) {
  const response = await fetch('https://your-api.com/messages', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${userToken}`
    },
    body: JSON.stringify({ content })
  })

  if (response.status === 403) {
    const data = await response.json()
    Alert.alert('Not Allowed', data.error)
    return
  }

  return response.json()
}
ts
// Your backend (Express/Next.js/etc)
app.post('/messages', async (req, res) => {
  const { content } = req.body

  const result = await vettly.check({ content, contentType: 'text' })

  if (result.action === 'block') {
    return res.status(403).json({ error: 'Content not allowed' })
  }

  // Save message
  await db.messages.create({ content, userId: req.user.id })
  res.json({ success: true })
})

Environment Variables

bash
# .env
EXPO_PUBLIC_VETTLY_API_KEY=vettly_your_api_key

Access in code:

ts
const apiKey = process.env.EXPO_PUBLIC_VETTLY_API_KEY

Offline Handling

tsx
import NetInfo from '@react-native-community/netinfo'

async function moderateWithFallback(content: string) {
  const netInfo = await NetInfo.fetch()

  if (!netInfo.isConnected) {
    // Queue for later moderation
    await AsyncStorage.setItem(
      'pendingModeration',
      JSON.stringify([...pending, { content, timestamp: Date.now() }])
    )
    return { action: 'pending' }
  }

  return vettly.check({ content, contentType: 'text' })
}

Next Steps