Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
75f4800
feat(mobile): Sign in with Apple
VoMinhKhoii Jun 11, 2026
377fb21
feat(mobile): account section — export, sign out, delete
VoMinhKhoii Jun 11, 2026
3f5e255
feat(mobile): forgot-password recovery flow
VoMinhKhoii Jun 11, 2026
22eb797
feat(mobile): branded cold start — cream launch screen + wordmark splash
VoMinhKhoii Jun 11, 2026
9236f1d
feat(mobile): replace nav drawer with bottom tab bar
VoMinhKhoii Jun 11, 2026
43b8a6b
feat(mobile): localized time-of-day greeting on the dashboard
VoMinhKhoii Jun 11, 2026
3516148
feat(mobile): honest over-target state + consumed-fills rings + local…
VoMinhKhoii Jun 11, 2026
6074f40
feat(mobile): never destroy typed meal on failure — retry card + reas…
VoMinhKhoii Jun 11, 2026
1fbdf79
feat(mobile): native haptic vocabulary
VoMinhKhoii Jun 11, 2026
8c6c4c6
feat(mobile): streaming-reveal morph — answer lands in place, no vani…
VoMinhKhoii Jun 12, 2026
fa6ecfd
feat(mobile): logging ergonomics — 44pt taps, full-row expand, pull-t…
VoMinhKhoii Jun 12, 2026
aaa9ebe
feat(mobile): real week paging — replace the dead carousel transform …
VoMinhKhoii Jun 12, 2026
f338602
feat(mobile): port the partial-day + legacy-macro notices
VoMinhKhoii Jun 12, 2026
6884182
feat(mobile): swipe-to-remove a saved meal with undo — close the corr…
VoMinhKhoii Jun 12, 2026
1e3fa0c
refactor(mobile): delete the dead weight-verdict engine
VoMinhKhoii Jun 12, 2026
286b269
feat(mobile): craft pass — Lora-never-bold, real ellipsis, branded to…
VoMinhKhoii Jun 12, 2026
1539097
refactor(mobile): Lucide glyphs in the logging surface (one icon DNA)
VoMinhKhoii Jun 12, 2026
2948676
feat(mobile): dashboard paged day-viewer + first-run collapse
VoMinhKhoii Jun 12, 2026
df279a1
fix(mobile): suppress dashboard "% on track" until a day is logged
VoMinhKhoii Jun 12, 2026
f0bfebb
fix(mobile): nutrition iOS hands — bounce + pull-to-refresh, keep con…
VoMinhKhoii Jun 12, 2026
5c703ec
feat(mobile): real nutrient detail route — sparkline + full food rows
VoMinhKhoii Jun 12, 2026
0c1ea22
refactor(mobile): re-clothe nutrition in dashboard_tokens
VoMinhKhoii Jun 12, 2026
088494d
feat(mobile): felt save + honest profile-load error in settings
VoMinhKhoii Jun 12, 2026
94a5d55
fix(mobile): stop fabricating 65/165/25 body-metric defaults in settings
VoMinhKhoii Jun 12, 2026
548a521
feat(mobile): strike a meal-item row stepped to zero
VoMinhKhoii Jun 12, 2026
2bbc61e
refactor(mobile): Lucide glyphs across the settings surface
VoMinhKhoii Jun 12, 2026
9ef4c52
feat(mobile): pre-auth welcome screen + real confirm-email state
VoMinhKhoii Jun 12, 2026
8475f5a
fix(mobile): de-leak onboarding step 2 — strings, flag emoji, sex str…
VoMinhKhoii Jun 12, 2026
c92d73c
feat(mobile): country picker as a bottom sheet with pinned Việt Nam
VoMinhKhoii Jun 12, 2026
b790589
feat(mobile): /welcome counts the computed target up in Lora 40
VoMinhKhoii Jun 12, 2026
0e482ea
refactor(mobile): Lucide glyphs across the auth + onboarding surfaces
VoMinhKhoii Jun 12, 2026
8ab364d
refactor(mobile): Lucide glyphs on the dashboard meal FAB
VoMinhKhoii Jun 12, 2026
476a000
feat(mobile): add once-daily partial-yesterday nudge to logging
VoMinhKhoii Jun 12, 2026
986cc57
refactor(mobile): flatten settings into grouped rows with focused edi…
VoMinhKhoii Jun 12, 2026
c49d768
fix(mobile): localized neutral auth errors rendered inline
VoMinhKhoii Jun 12, 2026
a2deeac
fix(mobile): drop the confidence clause from the Patterns trust line
VoMinhKhoii Jun 12, 2026
0e58dcc
fix(mobile): meal delete heals every cache and undo can't race
VoMinhKhoii Jun 12, 2026
20ce94e
fix(mobile): reveal lifecycle — no vanished answers, softer seam, dat…
VoMinhKhoii Jun 12, 2026
a4064a2
fix(mobile): instant-commit never drops in-flight edits or desyncs lo…
VoMinhKhoii Jun 12, 2026
bf107b1
fix(mobile): honest nutrient detail band + reduced-motion gates
VoMinhKhoii Jun 12, 2026
ed9028e
fix(mobile): design-system + craft sweep
VoMinhKhoii Jun 12, 2026
60c496e
perf(mobile): bundle Lora + DM Sans so cold starts render brand type
VoMinhKhoii Jun 12, 2026
13f6d6b
fix: polish mobile ux review blockers
VoMinhKhoii Jun 18, 2026
96334f9
Merge remote-tracking branch 'origin/main' into feat/ux-wave0-mobile
VoMinhKhoii Jun 18, 2026
34b65c0
Merge remote-tracking branch 'origin/main' into feat/ux-wave0-mobile
VoMinhKhoii Jun 26, 2026
7860fd1
feat(mobile-flutter): overhaul logging UX + global Be Vietnam Pro font
VoMinhKhoii Jun 28, 2026
64bdf9c
feat(nutrition): redesign mobile Nutrition tab into a single dense view
VoMinhKhoii Jun 28, 2026
eb9c318
feat(nutrition): compact suggested-foods sheet + clearer drawer hiera…
VoMinhKhoii Jun 28, 2026
482ba13
fix(nutrition): default to sex-neutral average targets when sex is un…
VoMinhKhoii Jun 28, 2026
4f415d9
feat(nutrition): derive food suggestions from the composition DB for …
VoMinhKhoii Jun 28, 2026
df07081
feat(nutrition): mobile foods sheet — DB-derived foods, reserve + cycle
VoMinhKhoii Jun 28, 2026
fdfb9c9
feat(nutrition): filter impractical foods from candidate suggestions
VoMinhKhoii Jun 28, 2026
638138c
fix(nutrition): type resolved target unit as the mg|mcg union
VoMinhKhoii Jun 28, 2026
6e14403
fix(nutrition-mobile): clamp food chip width to stop Wrap overflow
VoMinhKhoii Jun 28, 2026
831cd6a
fix(nutrition-mobile): add missing vi 'one' plural for partialNote
VoMinhKhoii Jun 28, 2026
4360bca
refactor(nutrition): model VN iron target as age-banded
VoMinhKhoii Jun 28, 2026
0ecba5b
refactor(nutrition): drop vestigial supportsCandidates + table-drive …
VoMinhKhoii Jun 28, 2026
e4833dd
fix(dashboard-mobile): equalize weight field and button height
VoMinhKhoii Jun 28, 2026
e7c676a
refactor(mobile): flatten onboarding nudge card
VoMinhKhoii Jun 28, 2026
e23ef82
Merge branch 'main' into feat/ux-wave0-mobile
VoMinhKhoii Jun 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
235 changes: 184 additions & 51 deletions apps/mobile-flutter/assets/l10n/en.json

Large diffs are not rendered by default.

236 changes: 185 additions & 51 deletions apps/mobile-flutter/assets/l10n/vi.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions apps/mobile-flutter/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
PRODUCT_BUNDLE_IDENTIFIER = com.khoivo.nham;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
Expand Down Expand Up @@ -673,6 +674,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
PRODUCT_BUNDLE_IDENTIFIER = com.khoivo.nham;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
Expand All @@ -695,6 +697,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
PRODUCT_BUNDLE_IDENTIFIER = com.khoivo.nham;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="backgroundColor" red="0.99607843137254903" green="0.98431372549019602" blue="0.96470588235294119" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
Expand Down
10 changes: 10 additions & 0 deletions apps/mobile-flutter/ios/Runner/Runner.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
</dict>
</plist>
61 changes: 44 additions & 17 deletions apps/mobile-flutter/lib/data/api_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ import 'dart:convert';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:supabase_flutter/supabase_flutter.dart' show Session;

import '../models/streaming.dart';
import '../services/supabase_service.dart';
import 'env.dart';

const _authRefreshSkew = Duration(minutes: 5);

/// Client-side mirror of the server `ApiError` envelope.
///
/// Re-implemented (not imported) exactly as the RN client does, because the
Expand All @@ -40,7 +43,8 @@ class ApiError implements Exception {
]);

@override
String toString() => 'ApiError($code, $status, retryable=$retryable): $message';
String toString() =>
'ApiError($code, $status, retryable=$retryable): $message';
}

/// Parse a `Retry-After` header: numeric seconds first, else HTTP-date delta.
Expand Down Expand Up @@ -96,33 +100,45 @@ class StreamAnalyzeInput {
});

Map<String, dynamic> toJson() => {
'message': message,
'loggedDate': loggedDate,
'timezoneOffset': timezoneOffset,
if (locale != null) 'locale': locale,
};
'message': message,
'loggedDate': loggedDate,
'timezoneOffset': timezoneOffset,
if (locale != null) 'locale': locale,
};
}

class ApiClient {
ApiClient({http.Client? httpClient})
: _http = httpClient ?? http.Client(),
_baseUrl = Env.apiBaseUrl;
: _http = httpClient ?? http.Client(),
_baseUrl = Env.apiBaseUrl;

final http.Client _http;
final String _baseUrl;

bool _expiresSoon(Session session) {
final expiresAt = session.expiresAt;
if (expiresAt == null) return false;
final expiry = DateTime.fromMillisecondsSinceEpoch(expiresAt * 1000);
return DateTime.now().add(_authRefreshSkew).isAfter(expiry);
}

/// Bearer header from the current Supabase session token, or empty when
/// signed out. Mirrors `authHeaders()` in the RN client.
Future<Map<String, String>> _authHeaders() async {
final token = SupabaseService.client.auth.currentSession?.accessToken;
final auth = SupabaseService.client.auth;
var session = auth.currentSession;
if (session != null && _expiresSoon(session)) {
try {
session = (await auth.refreshSession()).session ?? auth.currentSession;
} catch (_) {
session = auth.currentSession;
}
}
final token = session?.accessToken;
return token != null ? {'Authorization': 'Bearer $token'} : {};
}

Future<T> _request<T>(
String method,
String path, [
Object? body,
]) async {
Future<T> _request<T>(String method, String path, [Object? body]) async {
final headers = await _authHeaders();
if (body != null) {
headers['Content-Type'] = 'application/json';
Expand Down Expand Up @@ -158,6 +174,16 @@ class ApiClient {
/// DELETE.
Future<T> delete<T>(String path) => _request<T>('DELETE', path);

/// Permanently delete the signed-in user's account and all their data
/// (`DELETE /api/v1/account`). The server removes the Supabase auth user,
/// which cascades to every app row. There is no undo.
Future<void> deleteAccount() => delete<dynamic>('/api/v1/account');

/// Fetch a complete JSON snapshot of the user's data
/// (`GET /api/v1/account`): profile, meals (with items), and weights.
Future<Map<String, dynamic>> exportMyData() =>
get<Map<String, dynamic>>('/api/v1/account');

/// Fire-and-forget ping to wake a scale-to-zero backend on launch. Failures
/// are swallowed (offline / unreachable is fine). Mirrors `warmupApi()`.
Future<void> warmup() async {
Expand All @@ -182,9 +208,10 @@ class ApiClient {
headers['Content-Type'] = 'application/json';
headers['Accept'] = 'text/event-stream';

final req = http.Request('POST', Uri.parse('$_baseUrl/api/analyze-meal'))
..headers.addAll(headers)
..body = jsonEncode(input.toJson());
final req =
http.Request('POST', Uri.parse('$_baseUrl/api/analyze-meal'))
..headers.addAll(headers)
..body = jsonEncode(input.toJson());

http.StreamedResponse streamed;
try {
Expand Down
Loading
Loading