AI provider

Free · MITReact Native · Expo

The 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.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/ai-provider.json

Then 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

Use it for
  • 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.
Reach for something else
  • 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

PropTypeDefault
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

PropTypeDefault
provider*AiProvider<Output, Input>—
concurrency
Jobs that run at once.
number1
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

PropTypeDefault
initial*
Starting subscription credits.
number—
purchased
Starting bought credits, from your server.
number0
lowAt
The balance the app calls low. The meter warns at or below it.
number10

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.