AI provider
Free · MITReact Native · ExpoThe logic layer behind the AI components, with no UI: a typed provider adapter with an offline mock, a credit ledger that keeps subscription and bought credits apart, and a generation queue that charges, refunds and retries.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/ai-provider.jsonThen run npx expo install --fix so dependencies match your Expo SDK. Files land in components/goodui-native/ai-provider.
Usage
import { Button } from "@/components/goodui-native/button/button";
import { createMockProvider, useCredits, useGenerationQueue } from "@/components/goodui-native/ai-provider/ai-provider";
import { GenerationQueue } from "@/components/goodui-native/generation-queue/generation-queue";
const provider = createMockProvider<{ url: string }>({
durationMs: 4000,
stages: ["Reading your photo", "Painting", "Finishing"],
output: () => ({ url: "https://example.com/result.jpg" }),
});
export function Studio() {
const credits = useCredits({ initial: 12 });
const queue = useGenerationQueue({ provider, credits });
return (
<>
<Button
disabled={!credits.canAfford(4)}
onPress={() => queue.enqueue("Portrait", { prompt: "oil painting" }, 4)}
>
Generate
</Button>
<GenerationQueue jobs={queue.jobs} onCancel={queue.cancel} onRetry={queue.retry} />
</>
);
}When to use
- Building an AI screen before the backend exists: the mock runs with no keys and no network.
- Running long jobs one or a few at a time, with progress, cancel and retry.
- As your source of truth for credits: useCredits is a display cache, and the server has to enforce the balance.
- Calling a model provider directly from the app: keys in an app can be extracted, so call your own backend.
API reference
createMockProvider
| Prop | Type | Default |
|---|---|---|
| output Result for a request. Runs after the fake wait. | (request: AiRequest<Input>) => Output | — |
| reply Text for `stream`. | (request: AiRequest<Input>) => string | — |
| durationMs How long one generation takes. | number | — |
| stages Stage names reported along the way, evenly spaced. | string[] | — |
| failRate Fail this share of generations (0 to 1), to exercise the error states. | number | — |
| tokenMs Delay between streamed words. | number | — |
useGenerationQueue
| Prop | Type | Default |
|---|---|---|
| provider* | AiProvider<Output, Input> | — |
| concurrency Jobs that run at once. | number | 1 |
| onDone A job finished. Good moment for a notification. | (job: GenerationJob<Output, Input>) => void | — |
| credits Pass useCredits' result and the queue does the money: enqueue and retry charge the job's cost, and a failed or cancelled job is refunded once, to the balances it came from. Without it, `cost` is only a number on the job. | Pick<Credits, "spend" | "refund"> | — |
| onRefund A job failed or was cancelled. With `credits` the refund is already done. | (job: GenerationJob<Output, Input>) => void | — |
| onShort Retry could not be paid for. Send the person to the store: a silent failed tap looks like a broken button. | (job: GenerationJob<Output, Input>) => void | — |
useCredits
| Prop | Type | Default |
|---|---|---|
| initial* Starting subscription credits. | number | — |
| purchased Starting bought credits, from your server. | number | 0 |
| lowAt The balance the app calls low. The meter warns at or below it. | number | 10 |
Motion and haptics
- None. The mock eases its progress out so progress bars look like a real render.
- Each progress report re-renders the hook's owner, so report a few times a second at most.
- Keep useGenerationQueue above your navigator if jobs must survive leaving the screen. Unmounting it aborts running jobs and refunds running and queued ones.
Accessibility
- No UI of its own. GenerationQueue, CreditsMeter and StreamText announce what this layer reports.
Notes for AI
- Replace the mock with one object that implements generate and stream and calls your backend. Do not change the screens.
- Reject with AiAbortError when the signal aborts; any other rejection marks the job failed and calls onRefund.
- Pass `credits` to useGenerationQueue and it does the money: enqueue and retry charge, and a failed or cancelled job is refunded once to the balance it came from. Both return null or false when the balance is short.
- Keep the two balances apart: renew(allowance) resets subscription credits each period, and addPurchased adds bought credits, which never expire (App Store rule 3.1.1). spend uses subscription credits first.
- Stage names are shown as they are, so pass translated text or map keys to text in the UI.
Related
Cards for long AI jobs: each one shows its status, a fill and a light sweep while running, a rolling percentage, and cancel, retry and open actions.
A balance pill for an app bar whose digits roll when the balance changes and which turns warning colored when low, plus CreditsCost, a live price line that says what a job costs and what is left after it.
Text that arrives word by word: new words fade up and a caret pulses at the end, and when streaming stops it becomes one plain text block.
The input for AI apps: a field that grows with the text on a spring, chips that add phrases to the prompt, and a send button that morphs into a stop square while a job runs.