Skip to content

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-sdk

Quick 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-fs
tsx
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'
  }
}

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