# Phase 1B FE prep — local-backend testing via `--dart-define`

Frontend only. `AppConfig.apiBaseUrl` is now compile-time injectable. Default behavior (no override) is unchanged — production URL is still embedded.

**Build verification (Flutter 3.38.5 / Dart 3.10.4 on Windows 11):**

| Command | Result |
|---|---|
| `flutter pub get` | ✅ |
| `flutter analyze` | ✅ **0 errors**, 19 warnings, 555 infos (unchanged from Phase 1B FE1) |
| `flutter build web` (default) | ✅ `Built build\web` (54.5s) — prod URL embedded |
| `flutter build web --dart-define=API_BASE_URL=http://localhost:8000` | ✅ `Built build\web` (47.9s) — localhost embedded |

---

## 1. `lib/config.dart` — make `apiBaseUrl` injectable

```diff
 class AppConfig {
-  // Set your base URL here
-  static const String apiBaseUrl = 'https://mathapi.allgifted.com';
-  //static const String apiBaseUrl = 'http://localhost:8000';
+  // Backend base URL. Override at compile time with:
+  //   flutter run --dart-define=API_BASE_URL=http://localhost:8000
+  // Default is production. See CHANGES.md for the full invocation matrix
+  // (Android emulator → 10.0.2.2, iOS sim → localhost, LAN device → host IP).
+  static const String apiBaseUrl = String.fromEnvironment(
+    'API_BASE_URL',
+    defaultValue: 'https://mathapi.allgifted.com',
+  );
+
   // Stripe (mobile only)
```

| Aspect | Before | After |
|---|---|---|
| Symbol | `static const String apiBaseUrl` | `static const String apiBaseUrl` (still `const`) |
| Value (default) | `'https://mathapi.allgifted.com'` | `'https://mathapi.allgifted.com'` (byte-for-byte identical — no trailing slash, no `/api` suffix) |
| Source | Hardcoded literal | `String.fromEnvironment('API_BASE_URL', defaultValue: ...)` |

`String.fromEnvironment` is `const`-compatible with a `const` default, so all 30+ existing call sites that interpolate `${AppConfig.apiBaseUrl}/api/...` continue to compile unchanged.

The commented-out `//static const String apiBaseUrl = 'http://localhost:8000';` was removed — `--dart-define` replaces the need for that toggle.

---

## 2. Hardcoded-URL audit

### Verbatim grep output

```
$ grep -rn "mathapi.allgifted.com" lib/
lib/config.dart:8:    defaultValue: 'https://mathapi.allgifted.com',

$ grep -rn "https://allgifted" lib/
lib/config.dart:24:  static const String appDownloadUrl = 'https://allgifted.com/app';

$ grep -rn "Uri.parse('http" lib/
(no output)

$ grep -rn "Uri.https" lib/
(no output)

$ grep -rn "Uri.http" lib/
(no output)
```

### Broader sanity sweep

`grep -rn "['\"]http" lib/` (any `'http...` or `"http...` literal anywhere):

```
lib/config.dart:8:    defaultValue: 'https://mathapi.allgifted.com',
lib/config.dart:24:  static const String appDownloadUrl = 'https://allgifted.com/app';
lib/models/diagnostic_question.dart:23:    if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
lib/screens/question_screen.dart:373:    if (!videoLink.startsWith('http://') && !videoLink.startsWith('https://')) {
lib/widgets/common_question_widgets.dart:391:      if (answerImage.startsWith('http://') ||
lib/widgets/common_question_widgets.dart:392:          answerImage.startsWith('https://')) {
lib/widgets/question_display_widgets.dart:94:      if (!videoUrl.startsWith('http://') && !videoUrl.startsWith('https://')) {
lib/widgets/question_layout.dart:69:                  "http://127.0.0.1:8000${q['question_image']}",   ← FIXED
```

### Classification

| Match | Location | Classification | Action |
|---|---|---|---|
| `'https://mathapi.allgifted.com'` | `config.dart:8` | The new `defaultValue` for `String.fromEnvironment`. **This is the prod URL source of truth.** | Leave. |
| `'https://allgifted.com/app'` | `config.dart:24` | `appDownloadUrl` — App Store / Play Store smart-link placeholder for web users tapping subscribe / buy-lives. **Genuinely external** (marketing site → store), not a backend API call. | Leave. Phase 1.5 will replace with a real smart link. |
| `'http://'` / `'https://'` literals | `diagnostic_question.dart:23`, `question_screen.dart:373`, `common_question_widgets.dart:391–392`, `question_display_widgets.dart:94` | Each is `if (path.startsWith('http://') \|\| path.startsWith('https://')) {` — a passthrough check that decides whether to prefix `${AppConfig.apiBaseUrl}/` to a relative path returned by the backend, or treat it as already-absolute. | Leave. These are **scheme detection**, not URL literals. |
| `"http://127.0.0.1:8000${q['question_image']}"` | `question_layout.dart:69` | **Hardcoded localhost dev URL inside `Image.network`** — clearly leftover from manual testing against a Laravel dev server. Bypassed `AppConfig` and would 404 in production. Originally missed by the prompt's 5 grep patterns because the URL is a string literal inside `Image.network(...)`, not inside a `Uri.parse(...)` or `Uri.http*`. | **Fixed** — see §3. |

---

## 3. Hardcoded-URL fix applied

### `lib/widgets/question_layout.dart`

```diff
 import 'package:flutter/material.dart';
 import 'package:flutter_math_fork/flutter_math.dart';
+import '../config.dart';
 import '../theme/app_colors.dart';
 import '../theme/app_button_styles.dart';
 import '../theme/app_font_styles.dart';
```

```diff
                 child: Image.network(
-                  "http://127.0.0.1:8000${q['question_image']}",
+                  "${AppConfig.apiBaseUrl}${q['question_image']}",
                   height: 180,
                 ),
```

`q['question_image']` is the path returned by the backend (typically begins with `/`); concatenating it with `AppConfig.apiBaseUrl` produces the same shape as every other image fetch in the app (e.g. `subject_select_screen.dart` track tiles, which already use `${AppConfig.apiBaseUrl}/media/${track['image']}`). With the `--dart-define` override active, the URL becomes `http://localhost:8000<path>` automatically.

> **Note:** `question_layout.dart` itself is currently dead code from a Plan 1.5 audit perspective — `flutter analyze` previously reported `_buildProgressBar` and other declarations as unused — but the file *is* referenced (its export is alive in the widget tree). Fixed regardless: a dev-stub localhost URL in the binary is a footgun even if unreached.

---

## 4. Build verification

### Default build (no `--dart-define`)

```
$ flutter build web
✓ Built build\web (54.5s)

$ grep -c "mathapi.allgifted.com" build/web/main.dart.js
21

$ grep -c "localhost:8000" build/web/main.dart.js
0
```

**Prod URL embedded 21 times. Localhost: 0. ✅**

### Override build (`--dart-define=API_BASE_URL=http://localhost:8000`)

```
$ flutter build web --dart-define=API_BASE_URL=http://localhost:8000
✓ Built build\web (47.9s)

$ grep -c "mathapi.allgifted.com" build/web/main.dart.js
0

$ grep -c "localhost:8000" build/web/main.dart.js
21
```

**Prod URL: 0 occurrences. Localhost embedded 21 times. ✅**

The 21-vs-21 symmetry confirms the swap is total — every site that interpolates `${AppConfig.apiBaseUrl}` got the override value baked in at compile time.

---

## 5. Operational invocations

`--dart-define` is **compile-time**. Switching the base URL requires a rebuild — hot reload preserves the constant value from the original compile.

### Invocation matrix

| Target | Command |
|---|---|
| **Default (production)** — any platform | `flutter run` &nbsp;·&nbsp; `flutter build apk` &nbsp;·&nbsp; `flutter build web` |
| **iOS simulator** | `flutter run --dart-define=API_BASE_URL=http://localhost:8000` |
| **Web (default port)** | `flutter run -d chrome --dart-define=API_BASE_URL=http://localhost:8000` |
| **Web (pinned port for stable Stripe / OAuth redirects)** | `flutter run -d chrome --web-port 3000 --dart-define=API_BASE_URL=http://localhost:8000` |
| **Android emulator** | `flutter run --dart-define=API_BASE_URL=http://10.0.2.2:8000` |
| **Real Android device on LAN** | `flutter run --dart-define=API_BASE_URL=http://<windows-LAN-IP>:8000` &nbsp;·&nbsp; replace `<windows-LAN-IP>` with `ipconfig` output (e.g. `192.168.1.42`); device must be on the same LAN; Windows Firewall must allow inbound on port 8000 |
| **Production build** (no override needed) | `flutter build web` &nbsp;·&nbsp; `flutter build apk --release` |

### Why each platform needs a different host

- **iOS simulator** shares the host's loopback, so `localhost` works.
- **Android emulator** runs in a NAT'd VM; `10.0.2.2` is the magic address that resolves to the host's loopback. `localhost` from inside the emulator is the emulator itself.
- **Real device on LAN** can't reach the host's loopback at all — has to use the host's LAN IP address. Laravel's `php artisan serve` binds to `127.0.0.1` by default and won't accept LAN traffic; you'll want `php artisan serve --host=0.0.0.0 --port=8000` so it listens on all interfaces.

### Reload semantics reminder

| Action | Effect on `apiBaseUrl` |
|---|---|
| Save a `.dart` file → hot reload | **Constant unchanged.** Whatever was baked in at compile time stays. |
| Stop and re-run with same flags | **Constant unchanged.** Same compile inputs → same value. |
| Stop and re-run with different `--dart-define` | **Constant changes.** New compile, new value. |

---

## 6. Files changed at a glance

```
modified:   lib/config.dart                      (apiBaseUrl → String.fromEnvironment)
modified:   lib/widgets/question_layout.dart     (localhost stub → AppConfig.apiBaseUrl + new import)
added:      CHANGES.md                           (this file)
```

---

## Phase 1.5 follow-ups (deferred per prompt)

1. **Dev-mode visible banner / app bar suffix** when `AppConfig.apiBaseUrl != 'https://mathapi.allgifted.com'` — to prevent "wait, is this prod?" confusion during dev. Suggested check: `if (!AppConfig.apiBaseUrl.contains('mathapi.allgifted.com')) ...` rendered as a small chip on the app bar / a translucent bottom band.
2. **README update** with the §5 invocation matrix.
3. **Runtime-switchable env** (e.g. `flutter_dotenv` or in-app debug screen) — only if a workflow ever needs to swap base URL without a rebuild. Compile-time is sufficient for now and avoids shipping a runtime knob that could be flipped to a non-prod URL on a real user device.
4. **`appDownloadUrl`** still a placeholder (`https://allgifted.com/app`). Real App Store / Play Store smart link tracked from Phase 1.2.
