@vaiftech/sdk-expo
v1.0.9
Published
VAIF SDK for React Native and Expo applications
Downloads
777
Maintainers
Readme
@vaiftech/sdk-expo
React Native and Expo SDK for VAIF Studio - a Backend-as-a-Service platform.
Installation
npm install @vaiftech/sdk-expo @vaiftech/client @react-native-async-storage/async-storage
# or
pnpm add @vaiftech/sdk-expo @vaiftech/client @react-native-async-storage/async-storage
# or
yarn add @vaiftech/sdk-expo @vaiftech/client @react-native-async-storage/async-storageQuick Start
import { VaifProvider } from '@vaiftech/sdk-expo';
import { createVaifClient } from '@vaiftech/client';
import AsyncStorage from '@react-native-async-storage/async-storage';
const client = createVaifClient({
baseUrl: 'https://api.myproject.vaif.io',
apiKey: 'vaif_pk_xxx',
});
export default function App() {
return (
<VaifProvider client={client} storage={AsyncStorage}>
<MainApp />
</VaifProvider>
);
}Hooks Overview
| Category | Hooks |
|----------|-------|
| Auth | useAuth, useUser, useToken, usePasswordReset, useEmailVerification, useMagicLink, useOAuth, useMFA |
| Query | useQuery, useQueryById, useQueryFirst, usePaginatedQuery, useInfiniteQuery, useCount |
| Mutation | useCreate, useUpdate, useDelete, useUpsert, useBatchCreate, useBatchUpdate, useBatchDelete, useOptimisticMutation |
| Realtime | useSubscription, useChannel, usePresence, useRealtimeConnection, useBroadcast |
| Storage | useUpload, useDownload, useFile, useFiles, usePublicUrl |
| Functions | useFunction, useRpc, useFunctionQuery, useFunctionList, useBatchInvoke, useScheduledFunction |
| MongoDB | useMongoFind, useMongoFindOne, useMongoAggregate, useMongoInsertOne, useMongoInsertMany, useMongoUpdateOne, useMongoUpdateMany, useMongoDeleteOne, useMongoDeleteMany, useMongoInfiniteFind, useMongoCount, useMongoDistinct, useMongoCollection |
| Observability | useMetrics, useAuditLogs, useIncidents, useSystemHealth, useRealtimeStats, useErrorTracking |
Features
Persistent Sessions
Sessions are automatically persisted using AsyncStorage, so users stay logged in:
import { useAuth } from '@vaiftech/sdk-expo';
function LoginScreen() {
const { user, isLoading, signIn } = useAuth();
// Session restored from AsyncStorage on app start
if (isLoading) return <ActivityIndicator />;
if (user) return <Redirect to="/home" />;
return (
<Button
title="Sign In"
onPress={() => signIn({ email, password })}
/>
);
}Image Upload from Camera/Gallery
Upload images directly from Expo ImagePicker:
import { useUpload } from '@vaiftech/sdk-expo';
import * as ImagePicker from 'expo-image-picker';
function ImageUpload() {
const { upload, isUploading, progress } = useUpload();
const pickAndUpload = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 0.8,
});
if (!result.canceled) {
const { uri } = result.assets[0];
// Convert URI to blob and upload
const response = await fetch(uri);
const blob = await response.blob();
const uploaded = await upload(blob, `photos/${Date.now()}.jpg`);
console.log('Uploaded:', uploaded?.url);
}
};
return (
<View>
<Button
title={isUploading ? `Uploading ${progress}%` : 'Select Image'}
onPress={pickAndUpload}
disabled={isUploading}
/>
</View>
);
}Authentication
import { useAuth, useUser, useOAuth, useMFA } from '@vaiftech/sdk-expo';
function AuthScreen() {
const { user, isLoading, signIn, signUp, signOut } = useAuth();
if (isLoading) return <ActivityIndicator />;
if (!user) {
return (
<View>
<Button title="Sign In" onPress={() => signIn({ email, password })} />
<Button title="Sign Up" onPress={() => signUp({ email, password, name })} />
</View>
);
}
return (
<View>
<Text>Welcome, {user.email}</Text>
<Button title="Sign Out" onPress={signOut} />
</View>
);
}
// OAuth login
function OAuthLogin() {
const { signInWithProvider, isLoading } = useOAuth();
return (
<View>
<Button
title="Sign in with Google"
onPress={() => signInWithProvider('google')}
disabled={isLoading}
/>
<Button
title="Sign in with Apple"
onPress={() => signInWithProvider('apple')}
disabled={isLoading}
/>
</View>
);
}
// MFA setup
function MFASetup() {
const { enroll, verify, qrCode } = useMFA();
return (
<View>
{qrCode && <Image source={{ uri: qrCode }} style={{ width: 200, height: 200 }} />}
<Button title="Enable MFA" onPress={() => enroll({ type: 'totp' })} />
</View>
);
}Standalone Auth Provider
For auth-only applications:
import { AuthProvider, useAuth, useSession } from '@vaiftech/sdk-expo';
function App() {
return (
<AuthProvider config={{ url: 'https://api.vaif.studio', apiKey: 'your-key' }}>
<Navigation />
</AuthProvider>
);
}Data Fetching
import { useQuery, useQueryById, usePaginatedQuery, useInfiniteQuery } from '@vaiftech/sdk-expo';
interface Task {
id: string;
title: string;
completed: boolean;
}
// Basic query
function TaskList() {
const { data: tasks, isLoading, error, refetch } = useQuery<Task>('tasks', {
filters: [{ field: 'completed', operator: 'eq', value: false }],
orderBy: [{ field: 'createdAt', direction: 'desc' }],
});
if (isLoading) return <ActivityIndicator />;
if (error) return <Text>Error: {error.message}</Text>;
return (
<FlatList
data={tasks}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <Text>{item.title}</Text>}
refreshing={isLoading}
onRefresh={refetch}
/>
);
}
// Query by ID
function TaskDetail({ taskId }: { taskId: string }) {
const { data: task, isLoading } = useQueryById<Task>('tasks', taskId);
if (isLoading) return <ActivityIndicator />;
return <Text>{task?.title}</Text>;
}
// Paginated query
function PaginatedTasks() {
const { data, page, totalPages, nextPage, prevPage } = usePaginatedQuery<Task>('tasks', {
pageSize: 20,
});
return (
<View>
<FlatList data={data} renderItem={({ item }) => <Text>{item.title}</Text>} />
<View style={{ flexDirection: 'row' }}>
<Button title="Prev" onPress={prevPage} disabled={page === 1} />
<Text>{page} / {totalPages}</Text>
<Button title="Next" onPress={nextPage} disabled={page === totalPages} />
</View>
</View>
);
}
// Infinite scroll
function InfiniteTasks() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery<Task>('tasks');
return (
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <Text>{item.title}</Text>}
onEndReached={() => hasNextPage && fetchNextPage()}
ListFooterComponent={isFetchingNextPage ? <ActivityIndicator /> : null}
/>
);
}Mutations
import { useCreate, useUpdate, useDelete } from '@vaiftech/sdk-expo';
// Create
function CreateTask() {
const { create, isCreating } = useCreate<Task>('tasks');
return (
<Button
title={isCreating ? 'Creating...' : 'Add Task'}
onPress={() => create({ title: 'New Task', completed: false })}
disabled={isCreating}
/>
);
}
// Update
function ToggleTask({ task }: { task: Task }) {
const { update, isUpdating } = useUpdate<Task>('tasks');
return (
<Switch
value={task.completed}
onValueChange={(completed) => update(task.id, { completed })}
disabled={isUpdating}
/>
);
}
// Delete
function DeleteTask({ taskId }: { taskId: string }) {
const { remove, isDeleting } = useDelete('tasks');
return (
<Button
title="Delete"
onPress={() => remove(taskId)}
disabled={isDeleting}
color="red"
/>
);
}Realtime
import { useSubscription, usePresence, useBroadcast, useChannel } from '@vaiftech/sdk-expo';
// Listen for new messages
function MessageListener() {
useSubscription<Message>('messages', {
filters: [{ field: 'room_id', operator: 'eq', value: roomId }],
onInsert: (message) => console.log('New:', message),
onUpdate: (message) => console.log('Updated:', message),
onDelete: (message) => console.log('Deleted:', message.id),
});
return null;
}
// Track online users
function OnlineUsers({ roomId }: { roomId: string }) {
const { users, track, leave } = usePresence(`room-${roomId}`);
useEffect(() => {
track({ status: 'online', name: currentUser.name });
return () => leave();
}, []);
return (
<View>
<Text>Online: {users.length}</Text>
{users.map(u => <Text key={u.id}>{u.name}</Text>)}
</View>
);
}
// Broadcast typing indicator
function TypingIndicator({ roomId }: { roomId: string }) {
const { send, subscribe } = useBroadcast(`room-${roomId}`);
const [typing, setTyping] = useState<string[]>([]);
useEffect(() => {
subscribe('typing', (event) => {
setTyping(prev => [...prev, event.userName]);
setTimeout(() => {
setTyping(prev => prev.filter(n => n !== event.userName));
}, 3000);
});
}, []);
return (
<Text>{typing.length > 0 ? `${typing.join(', ')} typing...` : ''}</Text>
);
}
// Channel messaging
function ChatRoom({ roomId }: { roomId: string }) {
const channel = useChannel(`room-${roomId}`);
useEffect(() => {
channel.on('message', (msg) => {
// Handle incoming message
});
return () => channel.off('message');
}, [channel]);
const sendMessage = (text: string) => {
channel.send('message', { text, userId: currentUser.id });
};
return <View>{/* Chat UI */}</View>;
}Storage
import { useUpload, useDownload, useFile, useFiles, usePublicUrl } from '@vaiftech/sdk-expo';
// Upload with progress
function FileUpload() {
const { upload, isUploading, progress } = useUpload();
return (
<View>
<Button
title={isUploading ? `${progress}%` : 'Upload'}
onPress={async () => {
const result = await upload(fileBlob, 'uploads/file.pdf');
Alert.alert('Uploaded!', result?.url);
}}
disabled={isUploading}
/>
{isUploading && <ProgressBar progress={progress / 100} />}
</View>
);
}
// Download file
function DownloadFile({ path }: { path: string }) {
const { download, isDownloading } = useDownload();
return (
<Button
title={isDownloading ? 'Downloading...' : 'Download'}
onPress={async () => {
const blob = await download(path);
// Save to device or open
}}
/>
);
}
// Display image
function ProfileImage({ path }: { path: string }) {
const url = usePublicUrl(path);
return url ? <Image source={{ uri: url }} style={styles.avatar} /> : null;
}
// List files
function FileList({ folder }: { folder: string }) {
const { files, isLoading } = useFiles({ prefix: folder });
if (isLoading) return <ActivityIndicator />;
return (
<FlatList
data={files}
keyExtractor={(f) => f.path}
renderItem={({ item }) => (
<Text>{item.name} ({item.size} bytes)</Text>
)}
/>
);
}Edge Functions
import { useFunction, useRpc, useBatchInvoke } from '@vaiftech/sdk-expo';
// Invoke function
function ProcessImage() {
const { invoke, isInvoking, result, error } = useFunction('process-image');
return (
<Button
title={isInvoking ? 'Processing...' : 'Process'}
onPress={() => invoke({ imageUrl: 'https://...' })}
disabled={isInvoking}
/>
);
}
// RPC-style call
function Calculator() {
const { call, isLoading, data } = useRpc<{ result: number }>('calculate');
return (
<View>
<Button title="Calculate" onPress={() => call({ a: 5, b: 3, op: 'add' })} />
{data && <Text>Result: {data.result}</Text>}
</View>
);
}
// Batch operations
function BatchProcessor() {
const { invoke, isInvoking, results } = useBatchInvoke();
const processAll = async () => {
await invoke([
{ name: 'resize', payload: { url: 'img1.jpg' } },
{ name: 'resize', payload: { url: 'img2.jpg' } },
]);
};
return <Button title="Process All" onPress={processAll} disabled={isInvoking} />;
}MongoDB Hooks
import {
useMongoFind,
useMongoFindOne,
useMongoInsertOne,
useMongoUpdateOne,
useMongoDeleteOne,
useMongoAggregate,
useMongoInfiniteFind,
useMongoCollection,
} from '@vaiftech/sdk-expo';
// Find documents
function UserList() {
const { data: users, isLoading, refetch } = useMongoFind<User>('users', {
filter: { status: 'active' },
sort: { createdAt: -1 },
limit: 20,
});
if (isLoading) return <ActivityIndicator />;
return (
<FlatList
data={users}
keyExtractor={(item) => item._id}
renderItem={({ item }) => <Text>{item.name}</Text>}
refreshing={isLoading}
onRefresh={refetch}
/>
);
}
// Find single document
function UserProfile({ id }: { id: string }) {
const { data: user, isLoading } = useMongoFindOne<User>('users', { _id: id });
return user ? <Text>{user.name}</Text> : null;
}
// Insert document
function CreateUser() {
const { insertOne, isInserting } = useMongoInsertOne<User>('users');
return (
<Button
title={isInserting ? 'Creating...' : 'Create User'}
onPress={() => insertOne({ name: 'New User', email: '[email protected]' })}
disabled={isInserting}
/>
);
}
// Update document
function UpdateUser({ id }: { id: string }) {
const { updateOne, isUpdating } = useMongoUpdateOne('users');
return (
<Button
title="Update"
onPress={() => updateOne({ _id: id }, { $set: { updatedAt: new Date() } })}
disabled={isUpdating}
/>
);
}
// Delete document
function DeleteUser({ id }: { id: string }) {
const { deleteOne, isDeleting } = useMongoDeleteOne('users');
return (
<Button
title="Delete"
onPress={() => deleteOne({ _id: id })}
disabled={isDeleting}
color="red"
/>
);
}
// Aggregation
function UserStats() {
const { data: stats } = useMongoAggregate<{ _id: string; count: number }>('users', [
{ $match: { status: 'active' } },
{ $group: { _id: '$country', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
]);
return (
<View>
{stats?.map(stat => (
<Text key={stat._id}>{stat._id}: {stat.count}</Text>
))}
</View>
);
}
// Infinite scroll
function InfiniteUsers() {
const { data, fetchNextPage, hasNextPage } = useMongoInfiniteFind<User>('users', {
filter: { status: 'active' },
limit: 20,
});
return (
<FlatList
data={data}
keyExtractor={(item) => item._id}
renderItem={({ item }) => <Text>{item.name}</Text>}
onEndReached={() => hasNextPage && fetchNextPage()}
/>
);
}
// Full collection access
function BulkOps() {
const collection = useMongoCollection<User>('users');
const handleBulk = async () => {
const count = await collection.count({ status: 'active' });
const countries = await collection.distinct('country');
await collection.bulkWrite([
{ insertOne: { document: { name: 'Bulk User' } } },
]);
};
return <Button title="Run Bulk Ops" onPress={handleBulk} />;
}Observability Hooks
import {
useMetrics,
useAuditLogs,
useIncidents,
useSystemHealth,
useRealtimeStats,
useErrorTracking,
} from '@vaiftech/sdk-expo';
// Metrics dashboard
function MetricsDashboard() {
const { metrics, timeRange, setTimeRange, refresh } = useMetrics({
sources: ['api', 'database'],
interval: '1h',
});
return (
<View>
<Picker selectedValue={timeRange} onValueChange={setTimeRange}>
<Picker.Item label="Last Hour" value="1h" />
<Picker.Item label="Last 24 Hours" value="24h" />
<Picker.Item label="Last 7 Days" value="7d" />
</Picker>
{metrics?.map(m => (
<Text key={m.name}>{m.name}: {m.value}</Text>
))}
</View>
);
}
// Audit logs
function AuditLogs() {
const { logs, hasMore, loadMore } = useAuditLogs({ limit: 50 });
return (
<FlatList
data={logs}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<Text>{item.timestamp}: {item.action} by {item.userId}</Text>
)}
onEndReached={() => hasMore && loadMore()}
/>
);
}
// Incidents
function IncidentList() {
const { incidents, activeCount, acknowledge, resolve } = useIncidents();
return (
<View>
<Text>Active: {activeCount}</Text>
{incidents?.map(incident => (
<View key={incident.id}>
<Text>{incident.title} - {incident.severity}</Text>
{incident.status === 'open' && (
<Button title="Ack" onPress={() => acknowledge(incident.id)} />
)}
{incident.status === 'acknowledged' && (
<Button title="Resolve" onPress={() => resolve(incident.id)} />
)}
</View>
))}
</View>
);
}
// System health
function SystemHealth() {
const { isHealthy, services, lastCheck } = useSystemHealth({ pollInterval: 30000 });
return (
<View>
<Text>Status: {isHealthy ? 'Healthy' : 'Degraded'}</Text>
<Text>Last: {lastCheck?.toLocaleString()}</Text>
{services?.map(svc => (
<Text key={svc.name}>{svc.name}: {svc.status} ({svc.latency}ms)</Text>
))}
</View>
);
}
// Realtime stats
function RealtimeStats() {
const { connections, requestsPerSecond, activeUsers } = useRealtimeStats();
return (
<View>
<Text>Connections: {connections}</Text>
<Text>Requests/sec: {requestsPerSecond}</Text>
<Text>Active Users: {activeUsers}</Text>
</View>
);
}
// Error tracking
function ErrorTracker() {
const { errorRate, topErrors, trackError } = useErrorTracking({ timeRange: '24h' });
return (
<View>
<Text>Error Rate: {errorRate}%</Text>
{topErrors?.map(err => (
<Text key={err.message}>{err.message} ({err.count})</Text>
))}
</View>
);
}TypeScript Support
All hooks support TypeScript generics:
interface User {
id: string;
email: string;
name: string;
}
const { data } = useQuery<User>('users');
// data is User[] | undefined
const { data: user } = useMongoFindOne<User>('users', { _id: id });
// user is User | null | undefinedRelated Packages
- @vaiftech/client - Core client SDK
- @vaiftech/auth - Standalone auth client
- @vaiftech/react - React (web) hooks
- @vaiftech/cli - CLI tools
License
MIT
