Customer Management
Customer Management pattern for React Native.
A native screen for listing, creating, and editing customers — the data plane that complements the Payment Form recipe. No card data is involved here, so this pattern is independent of Basis Theory and runs as soon as EasyProvider mounts.
Goal
Build a screen that:
- Loads existing customers with
getCustomers. - Creates new customers with
createCustomer. - Edits an existing customer with
updateCustomer. - Uses a single form for both create and edit, toggled by selection state.
This is the same shape as the ProfileScreen in the Expo example — adapted here as a standalone recipe.
Implementation
import type { CustomerData } from '@easylabs/react-native';
import { useEasy } from '@easylabs/react-native';
import { useCallback, useEffect, useState } from 'react';
import {
ActivityIndicator,
Alert,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
export default function CustomersScreen() {
const { getCustomers, createCustomer, updateCustomer } = useEasy();
const [customers, setCustomers] = useState<CustomerData[]>([]);
const [selected, setSelected] = useState<CustomerData | null>(null);
const [editing, setEditing] = useState(false);
const [loading, setLoading] = useState(false);
const [form, setForm] = useState({ firstName: '', lastName: '', email: '', phone: '' });
const fillFormFrom = useCallback((c: CustomerData) => {
setForm({
firstName: c.entity.first_name,
lastName: c.entity.last_name,
email: c.entity.email ?? '',
phone: c.entity.phone ?? '',
});
}, []);
const load = useCallback(async () => {
setLoading(true);
try {
const result = await getCustomers({ limit: 20 });
if (result.success) {
setCustomers(result.data);
if (result.data.length > 0) {
setSelected(result.data[0]);
fillFormFrom(result.data[0]);
}
}
} catch (err) {
Alert.alert('Error', err instanceof Error ? err.message : 'Failed to load');
} finally {
setLoading(false);
}
}, [getCustomers, fillFormFrom]);
useEffect(() => {
load();
}, [load]);
const handleCreate = async () => {
if (!form.firstName || !form.lastName) {
Alert.alert('Missing info', 'First and last name are required.');
return;
}
setLoading(true);
try {
const result = await createCustomer({
first_name: form.firstName,
last_name: form.lastName,
email: form.email,
phone: form.phone,
});
if (result.success) {
Alert.alert('Created', `Customer ${result.data.id}`);
await load();
setForm({ firstName: '', lastName: '', email: '', phone: '' });
}
} catch (err) {
Alert.alert('Error', err instanceof Error ? err.message : 'Failed to create');
} finally {
setLoading(false);
}
};
const handleUpdate = async () => {
if (!selected) return;
setLoading(true);
try {
const result = await updateCustomer(selected.id, {
first_name: form.firstName,
last_name: form.lastName,
email: form.email,
phone: form.phone,
});
if (result.success) {
Alert.alert('Saved', 'Customer updated');
await load();
setEditing(false);
}
} catch (err) {
Alert.alert('Error', err instanceof Error ? err.message : 'Failed to save');
} finally {
setLoading(false);
}
};
return (
<ScrollView contentContainerStyle={styles.container}>
{customers.length > 0 && (
<View>
<Text style={styles.sectionTitle}>Customers</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
{customers.map((c) => (
<TouchableOpacity
key={c.id}
style={[styles.chip, selected?.id === c.id && styles.chipSelected]}
onPress={() => {
setSelected(c);
fillFormFrom(c);
setEditing(false);
}}
>
<Text style={[styles.chipText, selected?.id === c.id && styles.chipTextSelected]}>
{c.entity.first_name} {c.entity.last_name}
</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
)}
<Text style={styles.sectionTitle}>
{selected && !editing ? 'Customer details' : selected ? 'Edit customer' : 'New customer'}
</Text>
<Text style={styles.label}>First name</Text>
<TextInput
style={styles.input}
value={form.firstName}
onChangeText={(v) => setForm({ ...form, firstName: v })}
editable={!loading && (!selected || editing)}
/>
<Text style={styles.label}>Last name</Text>
<TextInput
style={styles.input}
value={form.lastName}
onChangeText={(v) => setForm({ ...form, lastName: v })}
editable={!loading && (!selected || editing)}
/>
<Text style={styles.label}>Email</Text>
<TextInput
style={styles.input}
value={form.email}
onChangeText={(v) => setForm({ ...form, email: v })}
keyboardType="email-address"
autoCapitalize="none"
editable={!loading && (!selected || editing)}
/>
<Text style={styles.label}>Phone</Text>
<TextInput
style={styles.input}
value={form.phone}
onChangeText={(v) => setForm({ ...form, phone: v })}
keyboardType="phone-pad"
editable={!loading && (!selected || editing)}
/>
{selected && !editing ? (
<TouchableOpacity style={styles.button} onPress={() => setEditing(true)}>
<Text style={styles.buttonText}>Edit</Text>
</TouchableOpacity>
) : selected && editing ? (
<TouchableOpacity style={styles.button} onPress={handleUpdate} disabled={loading}>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.buttonText}>Save</Text>}
</TouchableOpacity>
) : (
<TouchableOpacity style={styles.button} onPress={handleCreate} disabled={loading}>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.buttonText}>Create</Text>}
</TouchableOpacity>
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { padding: 16, gap: 8 },
sectionTitle: { fontSize: 18, fontWeight: '600', marginTop: 16, marginBottom: 8 },
chip: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, backgroundColor: '#f0f0f0', marginRight: 8 },
chipSelected: { backgroundColor: '#007AFF' },
chipText: { color: '#333' },
chipTextSelected: { color: '#fff' },
label: { fontSize: 14, fontWeight: '500', marginTop: 12 },
input: { borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 12, fontSize: 16 },
button: { backgroundColor: '#007AFF', borderRadius: 8, padding: 16, alignItems: 'center', marginTop: 16 },
buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
});Related calls
useEasy() exposes the rest of the customer surface for follow-on screens:
const {
// Single record
getCustomer,
// Subresources
getCustomerPaymentInstruments,
getCustomerOrders,
getCustomerSubscriptions,
getCustomerWallets,
} = useEasy();
const { data: instruments } = await getCustomerPaymentInstruments('cust_…');
const { data: orders } = await getCustomerOrders('cust_…', { limit: 20 });Tradeoffs
- All customer reads happen client-side under the publishable key. This is fine for a logged-in customer viewing their own profile or for admin tooling shipped behind your own auth, but it's not appropriate for surfacing other customers' data in a consumer app. For multi-tenant or operator-facing tooling, proxy customer reads through your server using the backend SDK.
updateCustomerisPATCH-style. Only fields you pass are updated; omitted fields keep their current value. To clear a field, pass an explicitnullrather than omitting it.- No client-side pagination state. The example loads 20 records and stops. For large lists, hold an
offsetin component state and bump it on a "Load more" button —getCustomers({ limit, offset })is paginated. - Tag schemas are unenforced.
tagsisRecord<string, unknown>at the API level. Define your tag shape in your own type and cast at the call site (see TypeScript → Module augmentation).