Skip to main content

vetty — the pet-owner app

vetyy/ is the consumer-facing Flutter app. Pet owners sign up, add their pets, log vaccinations, hand records off to a vet through a QR share code, and search a directory of nearby providers (clinics, groomers, breeders, trainers).

Entry flow

Screens

Auth

  • LoginScreen — email + password; links to Forgot password? and Create account (owner registration). On success AuthStore is updated and the root rebuilds into HomeShell.
  • RegisterScreen — owner-specific registration: POST /auth/owner/register with {name, email, password, phone?}. device_name is hard-coded to vetty-mobile. The backend returns a token + user in the same shape as login.
  • ForgotPasswordScreen — posts {email, redirect_base: ${Env.webBase}/reset-password}. The email the backend sends links into the web app, which handles the actual reset — there's intentionally no native reset screen.

HomeShell (features/home/home_shell.dart)

A Scaffold with a NavigationBar:

  • Pets tab → PetsScreen
  • Nearby tab → ProvidersScreen

The app bar's profile icon opens a showModalBottomSheet with the signed-in user's name/email and a Sign out tile.

Pets

  • PetsScreenFutureBuilder<List<Map>>RefreshIndicator. Each tile shows name + species + breed and a trailing QR icon that opens ShareQrScreen. FAB adds a new pet.
  • PetFormScreen — name, species, breed, date of birth (shown via showDatePicker). Used for both create and edit.
  • PetDetailScreen — summary card + vaccinations list. A "+" action opens a showModalBottomSheet with a vaccination form (vaccine name, administered date, next due, notes).

Shares

  • ShareQrScreen — auto-creates a share code on mount (POST /owner/me/shares) with an optional TTL and note. Renders QrImageView that encodes a JSON payload:
    {"code":"A1B2C3D4","deep_link":"https://app.vetty.example/s/A1B2C3D4","v":1}
    Below the QR, the raw 8-char code and the deep link are copyable.

Providers

  • ProvidersScreen — a search bar, a type chip row (vet / groomer / breeder / trainer / all), and a list of results. Calls GET /providers?q=&business_type=&latitude=&longitude=&radius_km= and renders TenantResource cards.

Service layer (feature services)

features/
├── auth/auth_service.dart # login, register, refreshMe, forgot, verify, reset, logout
├── pets/pet_service.dart # list, create, update, vaccinations, addVaccination
├── shares/share_service.dart # list, create, revoke
└── providers/provider_service.dart # search

auth_service.dart highlights:

Future<void> login({required String email, required String password}) async {
final res = await _api.post('/auth/login', body: {
'email': email,
'password': password,
'device_name': 'vetty-mobile',
});
final token = res['token'] as String?;
if (token == null) {
throw ApiException(message: 'No token returned', statusCode: 500);
}
await _store.set(token: token, user: _userFrom(res));
}

Every service is constructed once in HomeShell and passed down via a small InheritedWidget (VettyServices) so screens under the tab bar can reach them without prop-drilling.

Theme

class VettyTheme {
static ThemeData light() => ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF059669)),
useMaterial3: true,
// rounded inputs, slate card borders, 12px default radius
);
}

Emerald keeps the app visually distinct from the indigo vetdoctor build.

Running it

cd vetyy
flutter pub get
flutter run \
--dart-define=VETTY_API_BASE=http://10.0.2.2:8000/api/v1 \
--dart-define=VETTY_WEB_BASE=http://localhost:3000

10.0.2.2 is the Android emulator's loopback; for iOS simulator use http://127.0.0.1:8000/api/v1. Physical devices need the host LAN IP.

Tests

test/widget_test.dart is a compile smoke test — it pumps MaterialApp(theme: VettyTheme.light()) with a simple child and asserts it builds. Running flutter test verifies the whole tree compiles.

Known non-goals

  • No visit/appointment history in vetty (yet) — those live under the clinic's tenant scope; the design calls for GET /owner/me/appointments to be read-only from the owner app in a later iteration.
  • No in-app password reset — the forgot-password email opens ${webBase}/reset-password, which the React app handles end-to-end.
  • No deep-link interception — share QRs include a deep link but vetty doesn't register a URL handler; the deep link is there for vetdoctor scanning + for the web landing page.