Integration Recipes
A handful of common screens, with the code and a sentence of why you’d use each. Take them, paste them, restyle them.
Sign‑in gate
Section titled “Sign‑in gate”When: on app load, you want to show a sign‑in screen until the user has authenticated.
import { useThinkworkAuth } from "@thinkwork/react-native-sdk";
function App() { const { status } = useThinkworkAuth(); if (status === "unknown") return <Splash />; if (status !== "signed-in") return <SignInScreen />; return <AppShell />;}
function SignInScreen() { const { signInWithGoogle } = useThinkworkAuth(); return <Button onPress={signInWithGoogle} title="Continue with Google" />;}Agent picker with persistent selection
Section titled “Agent picker with persistent selection”When: the user has several agents and you want their last choice to stick across app launches.
The SDK gives you the agent list but intentionally stays out of persistence — different apps want different storage (AsyncStorage, MMKV, whatever). This is the typical shape:
import AsyncStorage from "@react-native-async-storage/async-storage";import type { Agent } from "@thinkwork/react-native-sdk";
const KEY = "myapp.selectedAgentId";
export function useSelectedAgent(agents: Agent[]) { const [selectedId, setSelectedId] = React.useState<string | null>(null);
React.useEffect(() => { AsyncStorage.getItem(KEY).then(setSelectedId); }, []);
const selected = agents.find(a => a.id === selectedId) ?? agents[0] ?? null;
const setId = (id: string | null) => { setSelectedId(id); id ? AsyncStorage.setItem(KEY, id) : AsyncStorage.removeItem(KEY); };
return { selectedAgent: selected, setSelectedAgentId: setId };}Use it:
const { user } = useThinkworkAuth();const { agents } = useAgents({ tenantId: user?.tenantId });const { selectedAgent, setSelectedAgentId } = useSelectedAgent(agents);Thread list screen
Section titled “Thread list screen”When: you’re showing the user their threads.
const { threads, loading, refetch } = useThreads({ tenantId, agentId: selectedAgent?.id });
return ( <FlatList data={threads} keyExtractor={(t) => t.id} renderItem={({ item }) => <ThreadRow thread={item} />} refreshing={loading} onRefresh={refetch} />);Chat screen
Section titled “Chat screen”When: the user has tapped into a thread and you’re showing messages and a composer.
A minimal version:
function ChatScreen({ threadId }: { threadId: string }) { const { thread } = useThread(threadId); const { messages } = useMessages(threadId); const sendMessage = useSendMessage(); const [draft, setDraft] = React.useState("");
const submit = async () => { const text = draft.trim(); if (!text) return; setDraft(""); await sendMessage(threadId, text); };
return ( <> <Header title={thread?.title ?? "Chat"} /> <FlatList data={[...messages].reverse()} keyExtractor={(m) => m.id} renderItem={({ item }) => <Bubble role={item.role} text={item.content} />} /> <Composer value={draft} onChange={setDraft} onSend={submit} /> </> );}A full‑featured version with markdown rendering and auto‑scroll lives in examples/chat-ui/ThinkworkChat.tsx — copy it, restyle it.
Start a new thread (atomic)
Section titled “Start a new thread (atomic)”When: your user types a message in a “compose” box and you want both the thread and the first message created in one call.
const createThread = useCreateThread();
async function start(text: string) { const thread = await createThread({ tenantId, agentId: selectedAgent.id, title: text.slice(0, 60), channel: "CHAT", firstMessage: text, }); router.push(`/thread/${thread.id}`);}One round trip. The agent starts responding as soon as the thread is created.
Tab‑bar badge
Section titled “Tab‑bar badge”When: you have a “Chat” or “Brain” tab and want the standard red unread count dot.
function ChatTabBadge() { const { user } = useThinkworkAuth(); const { agents } = useAgents({ tenantId: user?.tenantId }); const { selectedAgent } = useSelectedAgent(agents); const { count } = useUnreadThreadCount({ tenantId: user?.tenantId, agentId: selectedAgent?.id, }); return count > 0 ? <Badge>{count}</Badge> : null;}When the user reads a thread, the count goes down automatically — no manual plumbing needed.
Optimistic “just read” (optional polish)
Section titled “Optimistic “just read” (optional polish)”When: you want the unread dot to disappear the instant a user taps a thread, before the server round trip returns.
Keep a local set alongside the server state:
const locallyRead = useRef(new Set<string>());
function openThread(t: Thread) { locallyRead.current.add(t.id); updateThread(t.id, { lastReadAt: new Date().toISOString() }); router.push(`/thread/${t.id}`);}
function isUnread(t: Thread) { if (locallyRead.current.has(t.id)) return false; return /* normal unread logic */;}The server truth still catches up; the local set just shortcuts the UX.
What to do when ThinkWork isn’t configured
Section titled “What to do when ThinkWork isn’t configured”When: your app ships to environments where ThinkWork isn’t set up (dev builds, pre‑activation, etc.) and you want to hide the features gracefully.
const isThinkworkConfigured = Boolean( process.env.EXPO_PUBLIC_THINKWORK_GRAPHQL_URL && process.env.EXPO_PUBLIC_COGNITO_USER_POOL_ID,);
return isThinkworkConfigured ? ( <ThinkworkProvider config={config}><App /></ThinkworkProvider>) : <App />;Hide tab‑bar entries that depend on ThinkWork behind the same check so users never see a broken tab.