Develop#19
Conversation
Agent-Logs-Url: https://github.com/FLAiRistaken/firsthand/sessions/c39889d3-8be6-4438-a3c1-23c8c6a2fd0c Co-authored-by: FLAiRistaken <94542691+FLAiRistaken@users.noreply.github.com>
…694be4b feat: Implement Home Screen & LogModal
…nd path-specific quality guidelines
Fixed 2 file(s) based on 4 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
…fb0d3b1 feat: implement AI onboarding screen and fix Apple auth gate
…694be4b fix(auth): Fix TypeScript error with GoogleSignin.signIn() return type
- Add email and password inputs to AuthScreen - Integrate with Supabase signUp and signInWithPassword - Add form validation, loading states, and error handling - Update Build_plan.md
Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
…eyboard support and theme-based styling
…048971014011 Add Email/Password Authentication to AuthScreen
…ic from auth hooks and navigation
Fixed 4 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
…d standardize access across the application
Fixed 2 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Fixed 1 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Fixed 2 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
…37351245449143 Fix missing theme tokens and redesign the email/password form in AuthScreen
…e3f92c Add 30-second undo window for log entries
…rofile completion
…ession propagation delay in OnboardingScreen
…le refresh on OnboardingScreen
…n to immediately sync state after profile creation
…kers, and update log cancellation to perform hard deletes
…e RootNavigator loading screen
📝 WalkthroughWalkthroughThis PR introduces a comprehensive codebase configuration file, creates a new profile context for state management, refactors authentication and logging systems to use context/hooks composition, adds a new logging modal component, expands the design token system, and significantly enhances the home screen with interactive logging features, computed statistics, and a 30-second undo mechanism. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Auth as Auth Hook
participant Root as RootNavigator
participant Profile as ProfileContext
participant DB as Database
participant Session as Supabase Auth
User->>Auth: App loads
Auth->>Session: getSession()
Session-->>Auth: session or null
Auth-->>Root: {session, isLoading}
alt Session Exists
Root->>Profile: Provide userId
Profile->>DB: getProfile(userId)
DB-->>Profile: profile data
Profile-->>Root: {profile, isLoading}
alt Profile Onboarded
Root->>User: Render AppNavigator
else Profile Not Onboarded
Root->>User: Render Onboarding + Auth Screens
end
else No Session
Root->>User: Render Auth + Onboarding Screens
end
sequenceDiagram
participant User
participant Home as HomeScreen
participant Modal as LogModal
participant Logs as useLogs Hook
participant DB as Database
participant Undo as Undo Manager
User->>Home: Tap "I did it myself"
Home->>Modal: Open with type="win"
User->>Modal: Select category + note
User->>Modal: Tap Save
Modal->>Logs: addLog(entry)
Logs->>DB: insertLog(entry)
DB-->>Logs: LogEntry
Logs-->>Home: LogEntry
Home->>Undo: Start 30s timer for entry
Home->>User: Show undo button
alt User Taps Undo
User->>Home: Undo
Home->>Logs: deleteLog(id)
Logs->>DB: setLogCancelled(id)
DB-->>Logs: Success
Logs-->>Home: Removed
Home->>Undo: Clear timer
else Timer Expires
Undo->>Home: Undo expired
Home->>Undo: Clean up
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Review rate limit: 2/3 reviews remaining, refill in 20 minutes. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
src/components/PillButton.tsx (1)
75-78: 🧹 Nitpick | 🔵 Trivial | 💤 Low valueHardcoded spacing and border width values should use theme tokens.
Lines 75-78 contain hardcoded values (
paddingVertical: 7,paddingHorizontal: 15,borderWidth: 1.5) that should referenceSpacing.*andBorderWidths.*fromtheme.ts. The guideline requires all spacing to come from theme constants.Note:
paddingVertical: 7andpaddingHorizontal: 15don't match existingSpacingvalues. Consider adding appropriate tokens or using the nearest values.borderWidth: 1.5can useBorderWidths.md.♻️ Partial fix for borderWidth
+import { Colors, Fonts, FontSizes, Radius, BorderWidths } from '../constants/theme'; button: { paddingVertical: 7, paddingHorizontal: 15, borderRadius: Radius.pill, - borderWidth: 1.5, + borderWidth: BorderWidths.md, },As per coding guidelines: "All spacing must come from
src/constants/theme.tsSpacing and Radius — never hardcode px values".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/PillButton.tsx` around lines 75 - 78, Replace hardcoded paddingVertical, paddingHorizontal and borderWidth in PillButton styles with theme tokens: use Spacing.* values for vertical and horizontal padding (pick the nearest existing tokens or add new Spacing tokens if necessary) and replace borderWidth: 1.5 with BorderWidths.md; keep Radius.pill as-is. Update references in src/components/PillButton.tsx where paddingVertical, paddingHorizontal and borderWidth are defined so they import and use Spacing and BorderWidths from theme.ts instead of raw numbers.src/screens/AuthScreen.tsx (2)
168-173: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winFinish the token migration for radius and spacing.
This screen is mostly tokenised now, but
cornerRadius={14}andpaddingVertical: 14still bypasstheme.ts. Please lift those ontoRadius/Spacing/Sizesso future design changes stay centralised.As per coding guidelines "All design constants (colours, fonts, sizes, spacing) must be imported from
src/constants/theme.ts; never hardcode colours, fonts, sizes, or spacing values".Also applies to: 394-404
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/screens/AuthScreen.tsx` around lines 168 - 173, The Apple sign-in button and its style still hardcode sizing: replace cornerRadius={14} on AppleAuthentication.AppleAuthenticationButton and paddingVertical: 14 inside styles.appleButton with values imported from your theme (e.g., Radius.<appropriateName> and Spacing.<appropriateName> or Sizes.<appropriateName>) so all layout constants come from src/constants/theme.ts; update the import to pull Radius/Spacing/Sizes from theme and use those constants in the AppleAuthentication.AppleAuthenticationButton prop and the styles.appleButton style, and apply the same replacement for the other occurrences referenced around the bottom of the file (the block currently using hardcoded 14s).
124-132:⚠️ Potential issue | 🟠 Major | ⚡ Quick winFail fast when Google returns no ID token.
GoogleSignin.signIn()now makesdata?.idTokenoptional, but this branch just falls through when it is missing. That leaves the user on the auth screen with no feedback and no session.Suggested guard
await GoogleSignin.hasPlayServices(); const userInfo = await GoogleSignin.signIn(); const idToken = (userInfo as GoogleSignInResult).data?.idToken; - if (idToken) { - const { error } = await supabase.auth.signInWithIdToken({ - provider: 'google', - token: idToken, - }); - if (error) throw error; - } + if (!idToken) { + throw new Error('Google Sign-In failed: ID token is missing.'); + } + const { error } = await supabase.auth.signInWithIdToken({ + provider: 'google', + token: idToken, + }); + if (error) throw error;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/screens/AuthScreen.tsx` around lines 124 - 132, GoogleSignin.signIn() can return a result with no data.idToken, but the current branch silently falls through; update the sign-in flow in the AuthScreen (referencing GoogleSignin.signIn() and the GoogleSignInResult -> idToken usage) to fail fast when idToken is missing: detect when idToken is falsy, surface an explicit error (throw or return an error) and provide user feedback (e.g., show an error message/state) instead of proceeding to supabase.auth.signInWithIdToken; ensure the code path that calls supabase.auth.signInWithIdToken({ provider: 'google', token: idToken }) only runs when idToken is present.src/hooks/useLogs.ts (2)
161-180:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftUndo does not actually cancel logs created offline.
When
addLog()falls back to the optimistic entry, the real insert is persisted inOFFLINE_QUEUE_KEY. If the user taps undo before reconnecting,deleteLog()removes only local state and callsdbSetLogCancelled()with the temporary ID; the queued insert is never removed, so it will be flushed later and the "undone" log comes back.Based on learnings "Offline-first sync strategy: write to AsyncStorage queue if no connection, flush to Supabase on reconnect, on app foreground, and after successful adds".
Also applies to: 209-226
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/useLogs.ts` around lines 161 - 180, The offline undo bug: when addLog() queues an insert in OFFLINE_QUEUE_KEY (insertPayload/optimisticLog) and the user calls deleteLog(), the queued insert isn't removed so it later flushes; update deleteLog (and/or dbSetLogCancelled) to also read OFFLINE_QUEUE_KEY, find and remove or mark cancelled any queued items matching the optimistic/temp id or unique payload of insertPayload (and persist the trimmed/updated queue), ensuring queued inserts are cleaned up on undo; apply the same fix for the other queue-handling path referenced around the 209-226 block so all offline-insert flows remove queued entries on undo.
86-93:⚠️ Potential issue | 🟠 Major | ⚡ Quick winOne bad queued payload now blocks every later offline log.
flushOfflineQueue()breaks on every insert failure, including server-side validation and permission errors. Once one poisoned item reaches the front of the queue, nothing behind it can ever flush. Only stop on network failures; for server failures, drop or surface the bad item and continue.Suggested split between retryable and terminal failures
try { const insertedLog = await insertLog(itemToInsert); successfullyInserted.push(insertedLog); newQueue.shift(); // Remove the item we just successfully inserted } catch (err) { - console.error('Failed to flush log, keeping in queue:', err); - break; // Stop trying if we hit an error (likely still offline) + if (isNetworkError(err)) { + console.error('Failed to flush log, keeping in queue:', err); + break; + } + console.error('Discarding invalid queued log:', err); + newQueue.shift(); + continue; }Based on learnings "Distinguish network errors from server errors in optimistic updates: network errors should queue for retry, server errors should revert and rethrow".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/useLogs.ts` around lines 86 - 93, flushOfflineQueue currently breaks on any insertLog error which lets a single non-retryable (e.g. 4xx validation/permission) payload block the queue; change the error handling in flushOfflineQueue so you only break/keep the item for retry when the error looks like a network/retryable failure (no response object, status 0, or 5xx), and for terminal server errors (HTTP 4xx) remove the poisoned item from newQueue, record/log it (e.g. push to a failed list or processLogger.error with the item and error), and continue processing the rest; use the existing symbols insertLog, newQueue, successfullyInserted and ensure terminal errors are shifted off newQueue and not retried while retryable/network errors leave the item in front and cause a break.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/LogModal.tsx`:
- Around line 17-23: LogModal currently calls the onSave and onAddCategory props
synchronously and resets local state immediately; update it so onSave and
onAddCategory are treated as async (returning Promises), await their completion,
and only then clear/reset UI state and re-enable buttons, catching and surface
any rejection to avoid unhandled promise rejections. Concretely, modify the
handlers in the LogModal component that call props.onSave and
props.onAddCategory to: set a local "loading" (or "saving") flag, await
props.onSave(...) / await props.onAddCategory(...), clear or update state only
after the await, disable the submit/add buttons while loading, and catch errors
to show or propagate them instead of letting them become unhandled; reference
the LogModal component and its props onSave and onAddCategory as the places to
change.
In `@src/contexts/ProfileContext.tsx`:
- Around line 34-35: Replace untyped catch clauses in ProfileContext.tsx with
typed catches and proper narrowing: change catch (err) to catch (err: unknown)
in both occurrences (the one shown and the other at lines 73-74), then before
logging or accessing err.message narrow it (e.g., if (err instanceof Error) {
console.error('Failed to fetch profile:', err.message) } else {
console.error('Failed to fetch profile:', String(err)) }) — apply this pattern
in the same try/catch blocks (e.g., the fetch/profile-loading function(s)) so no
catch uses an untyped error or directly treats err as any.
- Line 45: The async function updateProfile currently lacks an explicit return
type; update its signature to include one (e.g. change "const updateProfile =
async (updates: Partial<Omit<UserProfile, 'id' | 'created_at'>>)" to "const
updateProfile = async (updates: Partial<Omit<UserProfile, 'id' |
'created_at'>>): Promise<void>" if it doesn't return a value, or ":
Promise<UserProfile>" (or the appropriate returned type) if it resolves with the
updated profile; ensure the declared return type matches the actual resolved
value of updateProfile.
- Line 4: Remove the unused import of "supabase" from ProfileContext.tsx; the
file currently imports { supabase } from '../lib/supabase' but never uses it, so
delete that import statement to clean up unused dependency and avoid lint errors
in the ProfileContext component/file.
In `@src/lib/db.ts`:
- Around line 56-71: setLogCancelled currently performs a hard delete which
violates the "no log deletion" rule; change it to a soft-delete by replacing the
.delete() call with an .update() that sets a cancelled flag (e.g., cancelled =
true) and cancelled_at timestamp (and optionally cancelled_by = userId) on the
logs row for the given id/userId, and retain the existing error handling
(console.error + throw). Ensure the update only succeeds when within the
30-second undo window by adding a conditional (e.g., where created_at or a
separate undo_deadline is within 30 seconds) in the query so logs become
permanent after that window; keep the function name setLogCancelled and its
signature intact.
In `@src/screens/AuthScreen.tsx`:
- Line 44: Replace the useNavigation<any>() call with the properly typed
navigation hook for this screen (e.g., useNavigation<YourStackNavigationProp>)
instead of any, add explicit return type annotations ": Promise<void>" to the
async handlers handleAppleSignIn, handleEmailAuth, and handleGoogleSignIn, and
change the catch clause in handleGoogleSignIn to use "catch (error: unknown)" to
match the other handlers; update imports/types as needed to reference your stack
navigation type so TypeScript knows the correct navigation type.
In `@src/screens/HomeScreen.tsx`:
- Around line 349-768: The style block uses hard-coded metrics and colours;
replace all literal values with theme tokens from src/constants/theme.ts (use
Spacing, FontSizes, Colors, Radius, Fonts, etc.) for entries such as header
(paddingBottom: 10), headerLeft (gap: 8), greetingName (fontSize: 30,
lineHeight: 36), winButton/winGhostIcon/winTitle/winSubtitle (shadow values,
font sizes, rgba white), sinButton/sinGhostIcon/sinTitle/sinSubtitle
(borderWidth: 1.5, colors), statsCard/statValue* (fontSize 22),
ratioBadge/ratioBadgeText (padding, fontSize),
dotContainer/dotCheck/dotTodayInner (width/height), dayLabel (fontSize), and
undo* styles (Spacing.xxl + 4 arithmetic); swap each literal with the
appropriate token (e.g., Spacing.md, FontSizes.lg/xl, Colors.primaryLight,
Radius.xl, Fonts.sansMedium) and remove raw rgba by adding an opacity-aware
color token or using a helper; keep the same style keys (header, greetingName,
winButton, sinButton, statsCard, ratioBarFill, dotTodayEmpty, dotPastEmpty,
dayLabelToday, undoCard, undoIconCircle, etc.) so the rest of the component
consumes theme tokens instead of hard-coded values.
- Around line 80-122: Remove the use of "any" in the undo error branch and add
explicit return types for the three async handlers: annotate handleUndo,
handleSaveLog, and handleAddCategory to return Promise<void>; inside
handleUndo's catch, replace "(err as any).code" with a safe type-guard (e.g.,
create or inline an isErrorWithCode(err): err is Error & { code?: string } that
checks err instanceof Error and 'code' in err) and then compare the code to
'EXPIRED' using that guarded type; ensure no any casts remain and adjust
imports/types if you add a small helper type guard function to locate behavior
around deleteLog/Alert handling.
In `@src/screens/OnboardingScreen.tsx`:
- Around line 197-213: Sanitise the onboarding answer before including it in the
Anthropic message history: create a sanitized variable by calling
sanitizePromptValue(userContent) and use that sanitized value when building
newMessages and when calling callClaude(…, ONBOARDING_SYSTEM). Keep the existing
profileRef.current assignments
(name/occupation/raw_tools/raw_uses/goal/success_definition) as needed, but
ensure any value sent to newMessages or passed into callClaude is the sanitized
version (refer to userContent, sanitizePromptValue, newMessages, setMessages,
callClaude, and ONBOARDING_SYSTEM).
- Line 60: Replace the loose navigation typing useNavigation<any>() with a
strongly-typed navigator using your RootStackParamList (e.g.
useNavigation<NativeStackNavigationProp<RootStackParamList, 'Onboarding'>> or
the appropriate screen key) so the screen gets correct route/param types; also
add explicit Promise<void> return types to the async functions initConversation,
handleCreateAccount, and handleSend to tighten TypeScript contracts and ensure
their signatures are explicit.
---
Outside diff comments:
In `@src/components/PillButton.tsx`:
- Around line 75-78: Replace hardcoded paddingVertical, paddingHorizontal and
borderWidth in PillButton styles with theme tokens: use Spacing.* values for
vertical and horizontal padding (pick the nearest existing tokens or add new
Spacing tokens if necessary) and replace borderWidth: 1.5 with BorderWidths.md;
keep Radius.pill as-is. Update references in src/components/PillButton.tsx where
paddingVertical, paddingHorizontal and borderWidth are defined so they import
and use Spacing and BorderWidths from theme.ts instead of raw numbers.
In `@src/hooks/useLogs.ts`:
- Around line 161-180: The offline undo bug: when addLog() queues an insert in
OFFLINE_QUEUE_KEY (insertPayload/optimisticLog) and the user calls deleteLog(),
the queued insert isn't removed so it later flushes; update deleteLog (and/or
dbSetLogCancelled) to also read OFFLINE_QUEUE_KEY, find and remove or mark
cancelled any queued items matching the optimistic/temp id or unique payload of
insertPayload (and persist the trimmed/updated queue), ensuring queued inserts
are cleaned up on undo; apply the same fix for the other queue-handling path
referenced around the 209-226 block so all offline-insert flows remove queued
entries on undo.
- Around line 86-93: flushOfflineQueue currently breaks on any insertLog error
which lets a single non-retryable (e.g. 4xx validation/permission) payload block
the queue; change the error handling in flushOfflineQueue so you only break/keep
the item for retry when the error looks like a network/retryable failure (no
response object, status 0, or 5xx), and for terminal server errors (HTTP 4xx)
remove the poisoned item from newQueue, record/log it (e.g. push to a failed
list or processLogger.error with the item and error), and continue processing
the rest; use the existing symbols insertLog, newQueue, successfullyInserted and
ensure terminal errors are shifted off newQueue and not retried while
retryable/network errors leave the item in front and cause a break.
In `@src/screens/AuthScreen.tsx`:
- Around line 168-173: The Apple sign-in button and its style still hardcode
sizing: replace cornerRadius={14} on
AppleAuthentication.AppleAuthenticationButton and paddingVertical: 14 inside
styles.appleButton with values imported from your theme (e.g.,
Radius.<appropriateName> and Spacing.<appropriateName> or
Sizes.<appropriateName>) so all layout constants come from
src/constants/theme.ts; update the import to pull Radius/Spacing/Sizes from
theme and use those constants in the
AppleAuthentication.AppleAuthenticationButton prop and the styles.appleButton
style, and apply the same replacement for the other occurrences referenced
around the bottom of the file (the block currently using hardcoded 14s).
- Around line 124-132: GoogleSignin.signIn() can return a result with no
data.idToken, but the current branch silently falls through; update the sign-in
flow in the AuthScreen (referencing GoogleSignin.signIn() and the
GoogleSignInResult -> idToken usage) to fail fast when idToken is missing:
detect when idToken is falsy, surface an explicit error (throw or return an
error) and provide user feedback (e.g., show an error message/state) instead of
proceeding to supabase.auth.signInWithIdToken; ensure the code path that calls
supabase.auth.signInWithIdToken({ provider: 'google', token: idToken }) only
runs when idToken is present.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 12e6b1a3-0781-4685-890c-6b37e148cc06
⛔ Files ignored due to path filters (1)
Build_plan.mdis excluded by!**/*.md
📒 Files selected for processing (17)
.coderabbit.yamlsrc/components/Card.tsxsrc/components/LogModal.tsxsrc/components/PillButton.tsxsrc/constants/theme.tssrc/contexts/ProfileContext.tsxsrc/hooks/useAuth.tssrc/hooks/useLogs.tssrc/hooks/useProfile.tssrc/lib/db.tssrc/lib/devConfig.tssrc/lib/googleSignIn.tssrc/lib/types.tssrc/navigation/RootNavigator.tsxsrc/screens/AuthScreen.tsxsrc/screens/HomeScreen.tsxsrc/screens/OnboardingScreen.tsx
💤 Files with no reviewable changes (2)
- src/lib/devConfig.ts
- src/hooks/useAuth.ts
| interface LogModalProps { | ||
| visible: boolean; | ||
| type: 'win' | 'sin'; | ||
| onSave: (entry: { type: 'win' | 'sin'; category: string; note: string; context: LogContext | undefined }) => void; | ||
| onClose: () => void; | ||
| customCategories: string[]; | ||
| onAddCategory: (category: string) => void; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "LogModal.tsx" -o -name "HomeScreen.tsx" | head -20Repository: FLAiRistaken/firsthand
Length of output: 125
🏁 Script executed:
git ls-files | grep -E "(LogModal|HomeScreen)" | head -20Repository: FLAiRistaken/firsthand
Length of output: 121
🏁 Script executed:
cat -n src/components/LogModal.tsx | head -100Repository: FLAiRistaken/firsthand
Length of output: 3667
🏁 Script executed:
cat -n src/screens/HomeScreen.tsx | grep -A 30 "onSave\|onAddCategory" | head -80Repository: FLAiRistaken/firsthand
Length of output: 1798
🏁 Script executed:
cat -n src/screens/HomeScreen.tsx | grep -B 5 -A 15 "const handleSaveLog\|const handleAddCategory"Repository: FLAiRistaken/firsthand
Length of output: 1300
🏁 Script executed:
cat -n src/components/LogModal.tsx | tail -200 | head -150Repository: FLAiRistaken/firsthand
Length of output: 4850
Update callback contracts to async and handle promise completion properly.
The callbacks accept async handlers (handleSaveLog and handleAddCategory in HomeScreen.tsx are async), but LogModal treats them as synchronous and resets state immediately. This allows buttons to remain tappable during requests, creating duplicate logs, and turns handler failures into unhandled promise rejections.
Required changes
interface LogModalProps {
visible: boolean;
type: 'win' | 'sin';
- onSave: (entry: { type: 'win' | 'sin'; category: string; note: string; context: LogContext | undefined }) => void;
+ onSave: (entry: { type: 'win' | 'sin'; category: string; note: string; context: LogContext | undefined }) => void | Promise<void>;
onClose: () => void;
customCategories: string[];
- onAddCategory: (category: string) => void;
+ onAddCategory: (category: string) => void | Promise<void>;
}
...
+ const [isSaving, setIsSaving] = useState<boolean>(false);
...
- const handleSave = () => {
+ const handleSave = async (): Promise<void> => {
if (!selectedCategory) return;
- onSave({
- type,
- category: selectedCategory,
- note: note.trim(),
- context: selectedContext,
- });
- // Reset state
- setSelectedCategory('');
- setNote('');
- setSelectedContext(undefined);
- setAddingCategory(false);
- setNewCategoryInput('');
+ setIsSaving(true);
+ try {
+ await onSave({
+ type,
+ category: selectedCategory,
+ note: note.trim(),
+ context: selectedContext,
+ });
+ resetState();
+ } finally {
+ setIsSaving(false);
+ }
};Also applies to: 83–93
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/LogModal.tsx` around lines 17 - 23, LogModal currently calls
the onSave and onAddCategory props synchronously and resets local state
immediately; update it so onSave and onAddCategory are treated as async
(returning Promises), await their completion, and only then clear/reset UI state
and re-enable buttons, catching and surface any rejection to avoid unhandled
promise rejections. Concretely, modify the handlers in the LogModal component
that call props.onSave and props.onAddCategory to: set a local "loading" (or
"saving") flag, await props.onSave(...) / await props.onAddCategory(...), clear
or update state only after the await, disable the submit/add buttons while
loading, and catch errors to show or propagate them instead of letting them
become unhandled; reference the LogModal component and its props onSave and
onAddCategory as the places to change.
| import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; | ||
| import { UserProfile } from '../lib/types'; | ||
| import { getProfile, upsertProfile } from '../lib/db'; | ||
| import { supabase } from '../lib/supabase'; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Remove unused import.
The supabase import is not used anywhere in this file.
🧹 Remove unused import
-import { supabase } from '../lib/supabase';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { supabase } from '../lib/supabase'; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/contexts/ProfileContext.tsx` at line 4, Remove the unused import of
"supabase" from ProfileContext.tsx; the file currently imports { supabase } from
'../lib/supabase' but never uses it, so delete that import statement to clean up
unused dependency and avoid lint errors in the ProfileContext component/file.
| } catch (err) { | ||
| console.error('Failed to fetch profile:', err); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Use catch (err: unknown) with type narrowing.
Catch blocks should explicitly type the error as unknown per coding guidelines, then narrow the type before using it.
♻️ Proposed fix
- } catch (err) {
+ } catch (err: unknown) {
console.error('Failed to fetch profile:', err);- } catch (err) {
+ } catch (err: unknown) {
console.error('Failed to update profile:', err);As per coding guidelines: "Use catch (error: unknown) with type narrowing, never catch (error: any)".
Also applies to: 73-74
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/contexts/ProfileContext.tsx` around lines 34 - 35, Replace untyped catch
clauses in ProfileContext.tsx with typed catches and proper narrowing: change
catch (err) to catch (err: unknown) in both occurrences (the one shown and the
other at lines 73-74), then before logging or accessing err.message narrow it
(e.g., if (err instanceof Error) { console.error('Failed to fetch profile:',
err.message) } else { console.error('Failed to fetch profile:', String(err)) })
— apply this pattern in the same try/catch blocks (e.g., the
fetch/profile-loading function(s)) so no catch uses an untyped error or directly
treats err as any.
| fetchProfile(); | ||
| }, [fetchProfile]); | ||
|
|
||
| const updateProfile = async (updates: Partial<Omit<UserProfile, 'id' | 'created_at'>>) => { |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Add explicit return type to async function.
The updateProfile async function is missing an explicit return type annotation.
♻️ Proposed fix
- const updateProfile = async (updates: Partial<Omit<UserProfile, 'id' | 'created_at'>>) => {
+ const updateProfile = async (updates: Partial<Omit<UserProfile, 'id' | 'created_at'>>): Promise<void> => {As per coding guidelines: "All async functions must have explicit return types".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const updateProfile = async (updates: Partial<Omit<UserProfile, 'id' | 'created_at'>>) => { | |
| const updateProfile = async (updates: Partial<Omit<UserProfile, 'id' | 'created_at'>>): Promise<void> => { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/contexts/ProfileContext.tsx` at line 45, The async function updateProfile
currently lacks an explicit return type; update its signature to include one
(e.g. change "const updateProfile = async (updates: Partial<Omit<UserProfile,
'id' | 'created_at'>>)" to "const updateProfile = async (updates:
Partial<Omit<UserProfile, 'id' | 'created_at'>>): Promise<void>" if it doesn't
return a value, or ": Promise<UserProfile>" (or the appropriate returned type)
if it resolves with the updated profile; ensure the declared return type matches
the actual resolved value of updateProfile.
| // setLogCancelled — called ONLY from the 30-second undo window in HomeScreen. | ||
| // No other code path should ever call this function. | ||
| // After 30 seconds, logs are permanent. No exceptions. | ||
| // This performs a direct table delete instead of soft-delete. | ||
| export const setLogCancelled = async (id: string, userId: string): Promise<void> => { | ||
| const { error } = await supabase | ||
| .from('logs') | ||
| .delete() | ||
| .eq('id', id) | ||
| .eq('user_id', userId); | ||
|
|
||
| if (error) { | ||
| console.error('Error cancelling log:', error); | ||
| throw error; | ||
| } | ||
| }; |
There was a problem hiding this comment.
Critical: setLogCancelled performs hard deletion, violating the "no log deletion" rule.
The coding guidelines and learnings explicitly state: "No deleteLog function exists; logs cannot be deleted—this is a hard rule enforced in src/lib/db.ts". However, setLogCancelled performs a hard .delete() operation, which directly contradicts this rule.
If the 30-second undo window is a product requirement, consider soft-delete semantics instead:
🐛 Alternative: Soft-delete implementation
export const setLogCancelled = async (id: string, userId: string): Promise<void> => {
- const { error } = await supabase
+ const { data, error } = await supabase
.from('logs')
- .delete()
+ .update({ cancelled: true })
.eq('id', id)
- .eq('user_id', userId);
+ .eq('user_id', userId)
+ .select()
+ .maybeSingle();
if (error) {
console.error('Error cancelling log:', error);
throw error;
}
+
+ if (!data) {
+ throw new Error('Log not found or already cancelled');
+ }
};As per coding guidelines: "No deleteLog function exists; logs cannot be deleted—this is a hard rule enforced in src/lib/db.ts".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/db.ts` around lines 56 - 71, setLogCancelled currently performs a
hard delete which violates the "no log deletion" rule; change it to a
soft-delete by replacing the .delete() call with an .update() that sets a
cancelled flag (e.g., cancelled = true) and cancelled_at timestamp (and
optionally cancelled_by = userId) on the logs row for the given id/userId, and
retain the existing error handling (console.error + throw). Ensure the update
only succeeds when within the 30-second undo window by adding a conditional
(e.g., where created_at or a separate undo_deadline is within 30 seconds) in the
query so logs become permanent after that window; keep the function name
setLogCancelled and its signature intact.
| const [email, setEmail] = useState(''); | ||
| const [password, setPassword] = useState(''); | ||
| const [emailLoading, setEmailLoading] = useState(false); | ||
| const navigation = useNavigation<any>(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's locate and examine the AuthScreen.tsx file
fd -t f "AuthScreen.tsx" src/screens/Repository: FLAiRistaken/firsthand
Length of output: 93
🏁 Script executed:
# Once we find it, let's examine the specific lines mentioned (44 and 63-138)
cat -n src/screens/AuthScreen.tsx | sed -n '1,150p'Repository: FLAiRistaken/firsthand
Length of output: 6990
🏁 Script executed:
# Check if RootStackParamList and NativeStackNavigationProp types exist
fd -t f "RootNavigator" src/Repository: FLAiRistaken/firsthand
Length of output: 99
🏁 Script executed:
# Search for existing navigation type usage patterns in the codebase
rg "useNavigation<" --type ts --type tsx -A 2 -B 2Repository: FLAiRistaken/firsthand
Length of output: 94
🏁 Script executed:
# Let's check the RootNavigator for the proper types
cat -n src/navigation/RootNavigator.tsx | head -50Repository: FLAiRistaken/firsthand
Length of output: 2081
🏁 Script executed:
# Let's also check what types are exported from RootNavigator
rg "RootStackParamList\|NativeStackNavigationProp" --type-add 'tsx:*.tsx' -t tsx src/navigation/Repository: FLAiRistaken/firsthand
Length of output: 48
🏁 Script executed:
# Double-check the exact lines with async handlers to confirm return type annotation status
cat -n src/screens/AuthScreen.tsx | sed -n '63,138p'Repository: FLAiRistaken/firsthand
Length of output: 2830
🏁 Script executed:
# Verify the imports needed for proper typing
rg "import.*NativeStackNavigationProp" src/Repository: FLAiRistaken/firsthand
Length of output: 48
🏁 Script executed:
# Check if there are other screens using proper navigation typing as reference
rg "useNavigation<" src/screens/ -A 1 -B 1Repository: FLAiRistaken/firsthand
Length of output: 465
Tighten the screen's TypeScript contracts.
Three issues need fixing:
- Line 44:
useNavigation<any>()violates the "no any type" rule. Use the proper stack type instead. - Lines 63, 96, 120: All three async handlers (
handleAppleSignIn,handleEmailAuth,handleGoogleSignIn) lack explicit return type annotations; add: Promise<void>. - Line 133: In
handleGoogleSignIn, the catch block uses untypederror. Change tocatch (error: unknown)(the other two handlers already have this correct).
Suggested tightening
+import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
+import type { RootStackParamList } from '../navigation/RootNavigator';
...
- const navigation = useNavigation<any>();
+ const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList, 'Auth'>>();
...
- const handleAppleSignIn = async () => {
+ const handleAppleSignIn = async (): Promise<void> => {
...
- const handleEmailAuth = async () => {
+ const handleEmailAuth = async (): Promise<void> => {
...
- const handleGoogleSignIn = async () => {
+ const handleGoogleSignIn = async (): Promise<void> => {
...
} catch (error) {
- console.error('Google sign in error:', error);
+ } catch (error: unknown) {
+ console.error('Google sign in error:', error);Per coding guidelines: "Never use any type in TypeScript — flag every instance" and "All async functions must have explicit return types".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/screens/AuthScreen.tsx` at line 44, Replace the useNavigation<any>() call
with the properly typed navigation hook for this screen (e.g.,
useNavigation<YourStackNavigationProp>) instead of any, add explicit return type
annotations ": Promise<void>" to the async handlers handleAppleSignIn,
handleEmailAuth, and handleGoogleSignIn, and change the catch clause in
handleGoogleSignIn to use "catch (error: unknown)" to match the other handlers;
update imports/types as needed to reference your stack navigation type so
TypeScript knows the correct navigation type.
| const handleUndo = async (entryId: string) => { | ||
| const target = undoTargets.get(entryId); | ||
| if (!target) return; | ||
|
|
||
| try { | ||
| await deleteLog(target.id); | ||
| // Success: clear this specific entry from undo UI and cancel its timer | ||
| setUndoTargets(prev => { | ||
| const next = new Map(prev); | ||
| next.delete(entryId); | ||
| return next; | ||
| }); | ||
| const timer = undoTimersRef.current.get(entryId); | ||
| if (timer) { | ||
| clearTimeout(timer); | ||
| undoTimersRef.current.delete(entryId); | ||
| } | ||
| } catch (err: unknown) { | ||
| if (err instanceof Error && (err as any).code === 'EXPIRED') { | ||
| Alert.alert('Too late to undo', 'Logs can only be undone within 30 seconds.'); | ||
| } else { | ||
| // Failure: keep the undo target and timer intact so user can retry | ||
| Alert.alert('Could not undo', 'The log could not be removed. Try again.'); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| const handleSaveLog = async (entry: { type: 'win' | 'sin'; category: string; note: string; context: LogContext | undefined }) => { | ||
| try { | ||
| const newLog = await addLog(entry); | ||
| showUndoToast(newLog); | ||
| setModalType(null); | ||
| } catch (e) { | ||
| console.error(e); | ||
| } | ||
| }; | ||
|
|
||
| const handleAddCategory = async (cat: string) => { | ||
| const currentCats = profile?.custom_categories ?? []; | ||
| if (!currentCats.includes(cat)) { | ||
| await updateProfile({ custom_categories: [...currentCats, cat] }); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check if the file exists and examine the specific lines
if [ -f "src/screens/HomeScreen.tsx" ]; then
echo "File found. Examining lines 80-122:"
sed -n '80,122p' src/screens/HomeScreen.tsx
echo -e "\n---\nTotal file line count:"
wc -l src/screens/HomeScreen.tsx
else
echo "File not found at src/screens/HomeScreen.tsx"
# Try to find similar files
find . -name "HomeScreen.tsx" -type f 2>/dev/null | head -5
fiRepository: FLAiRistaken/firsthand
Length of output: 1597
🏁 Script executed:
# Search for all async function declarations to check for return type annotations
echo "=== Async handlers in HomeScreen.tsx ==="
rg -n "const\s+\w+\s*=\s*async\s*" src/screens/HomeScreen.tsx -A 1 | head -30Repository: FLAiRistaken/firsthand
Length of output: 484
🏁 Script executed:
# Check for any casts in error handling
echo "=== Error handling with 'any' casts ==="
rg -n "as any" src/screens/HomeScreen.tsx -B 2 -A 2Repository: FLAiRistaken/firsthand
Length of output: 330
Remove the any cast from the undo error path and add explicit return types to all async handlers.
(err as any).code violates the repo's ban on any type and creates a type hole that makes this branch brittle against non-Error throws. Additionally, all three async handlers (handleUndo, handleSaveLog, handleAddCategory) lack explicit return type annotations.
Suggested refactor
+const hasErrorCode = (value: unknown): value is Error & { code: string } => (
+ value instanceof Error &&
+ 'code' in value &&
+ typeof (value as { code?: unknown }).code === 'string'
+);
...
- const handleUndo = async (entryId: string) => {
+ const handleUndo = async (entryId: string): Promise<void> => {
const target = undoTargets.get(entryId);
if (!target) return;
try {
await deleteLog(target.id);
setUndoTargets(prev => {
const next = new Map(prev);
next.delete(entryId);
return next;
});
const timer = undoTimersRef.current.get(entryId);
if (timer) {
clearTimeout(timer);
undoTimersRef.current.delete(entryId);
}
} catch (err: unknown) {
- if (err instanceof Error && (err as any).code === 'EXPIRED') {
+ if (hasErrorCode(err) && err.code === 'EXPIRED') {
Alert.alert('Too late to undo', 'Logs can only be undone within 30 seconds.');
} else {
Alert.alert('Could not undo', 'The log could not be removed. Try again.');
}
}
};
- const handleSaveLog = async (entry: { type: 'win' | 'sin'; category: string; note: string; context: LogContext | undefined }) => {
+ const handleSaveLog = async (entry: { type: 'win' | 'sin'; category: string; note: string; context: LogContext | undefined }): Promise<void> => {
try {
const newLog = await addLog(entry);
showUndoToast(newLog);
setModalType(null);
} catch (e) {
console.error(e);
}
};
- const handleAddCategory = async (cat: string) => {
+ const handleAddCategory = async (cat: string): Promise<void> => {
const currentCats = profile?.custom_categories ?? [];
if (!currentCats.includes(cat)) {
await updateProfile({ custom_categories: [...currentCats, cat] });
}
};Per coding guidelines: "Never use any type in TypeScript — flag every instance" and "All async functions must have explicit return types."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/screens/HomeScreen.tsx` around lines 80 - 122, Remove the use of "any" in
the undo error branch and add explicit return types for the three async
handlers: annotate handleUndo, handleSaveLog, and handleAddCategory to return
Promise<void>; inside handleUndo's catch, replace "(err as any).code" with a
safe type-guard (e.g., create or inline an isErrorWithCode(err): err is Error &
{ code?: string } that checks err instanceof Error and 'code' in err) and then
compare the code to 'EXPIRED' using that guarded type; ensure no any casts
remain and adjust imports/types if you add a small helper type guard function to
locate behavior around deleteLog/Alert handling.
| header: { | ||
| flexDirection: 'row', | ||
| justifyContent: 'space-between', | ||
| alignItems: 'center', | ||
| paddingHorizontal: Spacing.screen, | ||
| paddingBottom: 10, | ||
| backgroundColor: Colors.appBg, | ||
| }, | ||
| headerLeft: { | ||
| flexDirection: 'row', | ||
| alignItems: 'center', | ||
| gap: 8, | ||
| }, | ||
| greenDot: { | ||
| width: 8, | ||
| height: 8, | ||
| borderRadius: 4, | ||
| backgroundColor: Colors.primary, | ||
| }, | ||
| headerTitle: { | ||
| fontFamily: Fonts.serifSemiBold, | ||
| fontSize: FontSizes.lg, | ||
| color: Colors.textPrimary, | ||
| }, | ||
| headerRight: { | ||
| width: 34, | ||
| height: 34, | ||
| borderRadius: 17, | ||
| backgroundColor: Colors.streakEmpty, | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| }, | ||
| scrollContent: { | ||
| paddingHorizontal: Spacing.screen, | ||
| paddingTop: 10, | ||
| }, | ||
| title: { | ||
| greetingBlock: { | ||
| marginBottom: 22, | ||
| }, | ||
| greetingName: { | ||
| fontFamily: Fonts.serifSemiBold, | ||
| fontSize: 30, | ||
| color: Colors.textPrimary, | ||
| lineHeight: 36, | ||
| }, | ||
| greetingSubtitle: { | ||
| fontFamily: Fonts.sans, | ||
| fontWeight: '300', // sansLight approx | ||
| fontSize: FontSizes.md, | ||
| color: Colors.textMuted, | ||
| marginTop: 5, | ||
| }, | ||
| winButton: { | ||
| backgroundColor: Colors.primary, | ||
| borderRadius: Radius.xxl, | ||
| paddingVertical: 24, | ||
| paddingHorizontal: 22, | ||
| marginBottom: 10, | ||
| shadowColor: Colors.primary, | ||
| shadowOffset: { width: 0, height: 6 }, | ||
| shadowOpacity: 0.35, | ||
| shadowRadius: 14, | ||
| elevation: 8, | ||
| position: 'relative', | ||
| overflow: 'hidden', | ||
| }, | ||
| winGhostIcon: { | ||
| position: 'absolute', | ||
| right: 22, | ||
| top: '50%', | ||
| marginTop: -26, | ||
| opacity: 0.1, | ||
| }, | ||
| winTitle: { | ||
| fontFamily: Fonts.serifSemiBold, | ||
| fontSize: 21, | ||
| color: Colors.white, | ||
| marginBottom: 2, | ||
| marginTop: 9, | ||
| }, | ||
| winSubtitle: { | ||
| fontFamily: Fonts.sans, | ||
| fontWeight: '300', | ||
| fontSize: 13, | ||
| color: 'rgba(255,255,255,0.55)', | ||
| }, | ||
| sinButton: { | ||
| backgroundColor: Colors.sinBg, | ||
| borderWidth: 1.5, | ||
| borderColor: Colors.sinBorder, | ||
| borderRadius: Radius.xxl, | ||
| paddingVertical: 18, | ||
| paddingHorizontal: 22, | ||
| marginBottom: 14, | ||
| position: 'relative', | ||
| overflow: 'hidden', | ||
| }, | ||
| sinGhostIcon: { | ||
| position: 'absolute', | ||
| right: 22, | ||
| top: '50%', | ||
| marginTop: -23, | ||
| opacity: 0.1, | ||
| }, | ||
| sinTitle: { | ||
| fontFamily: Fonts.serifSemiBold, | ||
| fontSize: 19, | ||
| color: Colors.sinText, | ||
| marginBottom: 2, | ||
| marginTop: 8, | ||
| }, | ||
| sinSubtitle: { | ||
| fontFamily: Fonts.sans, | ||
| fontWeight: '300', | ||
| fontSize: 13, | ||
| color: Colors.sinTextLight, | ||
| }, | ||
| statsCard: { | ||
| flexDirection: 'row', | ||
| paddingVertical: 12, | ||
| paddingHorizontal: 16, | ||
| marginBottom: 10, | ||
| }, | ||
| statCol: { | ||
| flex: 1, | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| }, | ||
| statDivider: { | ||
| width: 1, | ||
| backgroundColor: Colors.border, | ||
| }, | ||
| statValueWin: { | ||
| fontFamily: Fonts.serifSemiBold, | ||
| fontSize: 22, | ||
| color: Colors.primary, | ||
| }, | ||
| statValueSin: { | ||
| fontFamily: Fonts.serifSemiBold, | ||
| fontSize: 22, | ||
| color: Colors.amber, | ||
| }, | ||
| statValueStreak: { | ||
| fontFamily: Fonts.serifSemiBold, | ||
| fontSize: 22, | ||
| color: Colors.textSecondary, | ||
| }, | ||
| statLabel: { | ||
| fontFamily: Fonts.sans, | ||
| fontSize: FontSizes.sm, | ||
| color: Colors.textHint, | ||
| marginTop: 3, | ||
| }, | ||
| ratioCard: { | ||
| paddingVertical: 12, | ||
| paddingHorizontal: 14, | ||
| marginBottom: 10, | ||
| }, | ||
| ratioRow: { | ||
| flexDirection: 'row', | ||
| justifyContent: 'space-between', | ||
| alignItems: 'center', | ||
| marginBottom: 8, | ||
| }, | ||
| ratioLabel: { | ||
| fontFamily: Fonts.sans, | ||
| fontSize: FontSizes.sm, | ||
| color: Colors.textHint, | ||
| }, | ||
| ratioRight: { | ||
| flexDirection: 'row', | ||
| alignItems: 'center', | ||
| gap: 8, | ||
| }, | ||
| ratioBadge: { | ||
| paddingVertical: 2, | ||
| paddingHorizontal: 7, | ||
| borderRadius: 8, | ||
| }, | ||
| ratioBadgeAbove: { | ||
| backgroundColor: Colors.primaryLight, | ||
| }, | ||
| ratioBadgeBelow: { | ||
| backgroundColor: Colors.amberLight, | ||
| }, | ||
| ratioBadgeText: { | ||
| fontSize: FontSizes.xs, | ||
| fontFamily: Fonts.sansMedium, | ||
| }, | ||
| ratioBadgeTextAbove: { | ||
| color: Colors.primary, | ||
| }, | ||
| ratioBadgeTextBelow: { | ||
| color: Colors.amber, | ||
| }, | ||
| ratioValue: { | ||
| fontFamily: Fonts.serifSemiBold, | ||
| fontSize: 20, | ||
| color: Colors.primary, | ||
| }, | ||
| ratioBarBg: { | ||
| height: 4, | ||
| backgroundColor: Colors.border, | ||
| borderRadius: 2, | ||
| width: '100%', | ||
| }, | ||
| ratioBarFill: { | ||
| height: '100%', | ||
| backgroundColor: Colors.primary, | ||
| borderRadius: 2, | ||
| }, | ||
| ratioPlaceholder: { | ||
| fontFamily: Fonts.sans, | ||
| fontSize: FontSizes.sm, | ||
| color: Colors.textHint, | ||
| textAlign: 'center', | ||
| marginVertical: 4, | ||
| }, | ||
| ratioDivider: { | ||
| height: 1, | ||
| backgroundColor: Colors.border, | ||
| marginVertical: 10, | ||
| }, | ||
| dotsRow: { | ||
| flexDirection: 'row', | ||
| gap: 6, | ||
| paddingHorizontal: 2, | ||
| }, | ||
| dayCol: { | ||
| flex: 1, | ||
| alignItems: 'center', | ||
| gap: 4, | ||
| }, | ||
| dotContainer: { | ||
| width: '100%', | ||
| aspectRatio: 1, | ||
| borderRadius: Radius.full, | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| }, | ||
| dotWin: { | ||
| backgroundColor: Colors.primary, | ||
| borderWidth: 1, | ||
| borderColor: Colors.primary, | ||
| }, | ||
| dotCheck: { | ||
| width: 10, | ||
| height: 10, | ||
| }, | ||
| dotTodayEmpty: { | ||
| backgroundColor: 'transparent', | ||
| borderWidth: 1, | ||
| borderColor: Colors.streakToday, | ||
| }, | ||
| dotTodayInner: { | ||
| width: 3, | ||
| height: 3, | ||
| borderRadius: 1.5, | ||
| backgroundColor: Colors.streakToday, | ||
| }, | ||
| dotPastEmpty: { | ||
| backgroundColor: 'transparent', | ||
| borderWidth: 1, | ||
| borderColor: Colors.streakEmpty, | ||
| }, | ||
| dayLabel: { | ||
| fontSize: FontSizes.xs, | ||
| }, | ||
| dayLabelToday: { | ||
| color: Colors.primary, | ||
| fontFamily: Fonts.sansMedium, | ||
| }, | ||
| dayLabelWin: { | ||
| color: Colors.textMuted, | ||
| fontFamily: Fonts.sans, | ||
| }, | ||
| dayLabelEmpty: { | ||
| color: Colors.streakEmpty, | ||
| fontFamily: Fonts.sans, | ||
| }, | ||
| logsSection: { | ||
| marginTop: 10, | ||
| }, | ||
| logsHeader: { | ||
| flexDirection: 'row', | ||
| justifyContent: 'space-between', | ||
| paddingVertical: 10, | ||
| }, | ||
| logsHeaderTitle: { | ||
| fontFamily: Fonts.sansMedium, | ||
| fontSize: FontSizes.sm, | ||
| color: Colors.textHint, | ||
| letterSpacing: 0.08, | ||
| }, | ||
| logsHeaderAction: { | ||
| fontFamily: Fonts.sans, | ||
| fontSize: FontSizes.sm, | ||
| color: Colors.textHint, | ||
| }, | ||
| logsList: { | ||
| marginTop: 5, | ||
| }, | ||
| logItem: { | ||
| flexDirection: 'row', | ||
| alignItems: 'center', | ||
| gap: 10, | ||
| paddingVertical: 9, | ||
| paddingHorizontal: 12, | ||
| borderRadius: Radius.md, | ||
| marginBottom: 5, | ||
| }, | ||
| logItemWin: { | ||
| backgroundColor: Colors.primaryLight, | ||
| borderWidth: 1, | ||
| borderColor: Colors.primaryBorder, | ||
| }, | ||
| logItemSin: { | ||
| backgroundColor: Colors.sinBg, | ||
| borderWidth: 1, | ||
| borderColor: Colors.sinBorder, | ||
| }, | ||
| logIconCircle: { | ||
| width: 24, | ||
| height: 24, | ||
| borderRadius: 12, | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| }, | ||
| logIconCircleWin: { | ||
| backgroundColor: Colors.primary, | ||
| }, | ||
| logIconCircleSin: { | ||
| backgroundColor: Colors.sinIconBg, | ||
| }, | ||
| logCategoryText: { | ||
| flex: 1, | ||
| fontFamily: Fonts.sansMedium, | ||
| fontSize: FontSizes.base, | ||
| }, | ||
| logCategoryTextWin: { | ||
| color: Colors.primary, | ||
| }, | ||
| logCategoryTextSin: { | ||
| color: Colors.sinCategoryText, | ||
| }, | ||
| logNoteText: { | ||
| fontFamily: Fonts.sans, | ||
| color: Colors.textMuted, | ||
| }, | ||
| logTimeText: { | ||
| fontFamily: Fonts.sans, | ||
| fontSize: FontSizes.sm, | ||
| color: Colors.textHint, | ||
| }, | ||
| bottomSpacer: { | ||
| height: Spacing.screen, | ||
| }, | ||
|
|
||
| undoToastWrapper: { | ||
| position: 'absolute', | ||
| left: Spacing.lg, | ||
| right: Spacing.lg, | ||
| zIndex: 100, | ||
| }, | ||
| undoCard: { | ||
| flexDirection: 'row', | ||
| alignItems: 'center', | ||
| paddingVertical: Spacing.md, | ||
| paddingHorizontal: Spacing.lg, | ||
| gap: Spacing.md, | ||
| borderRadius: Radius.lg, | ||
| backgroundColor: Colors.cardBg, | ||
| borderColor: Colors.border, | ||
| borderWidth: 1, | ||
| shadowColor: Colors.black, | ||
| shadowOffset: { width: 0, height: 2 }, | ||
| shadowOpacity: 0.1, | ||
| shadowRadius: 8, | ||
| elevation: 4, | ||
| }, | ||
| undoIconCircle: { | ||
| width: Spacing.xxl + 4, | ||
| height: Spacing.xxl + 4, | ||
| borderRadius: Radius.full, | ||
| flexShrink: 0, | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| }, | ||
| undoIconCircleWin: { | ||
| backgroundColor: Colors.primary, | ||
| }, | ||
| undoIconCircleSin: { | ||
| backgroundColor: Colors.sinIconBg, | ||
| }, | ||
| undoTextBlock: { | ||
| flex: 1, | ||
| marginTop: Spacing.xs / 2, | ||
| }, | ||
| undoTitleText: { | ||
| fontFamily: Fonts.sansMedium, | ||
| fontSize: FontSizes.base, | ||
| color: Colors.textPrimary, | ||
| }, | ||
| }) | ||
| undoHintText: { | ||
| fontFamily: Fonts.sans, | ||
| fontSize: FontSizes.sm, | ||
| color: Colors.textHint, | ||
| }, | ||
| undoButton: { | ||
| paddingVertical: Spacing.xs + 2, | ||
| paddingHorizontal: Spacing.md, | ||
| borderRadius: Radius.md, | ||
| backgroundColor: Colors.primaryLight, | ||
| }, | ||
| undoButtonText: { | ||
| fontFamily: Fonts.sansMedium, | ||
| fontSize: FontSizes.sm, | ||
| color: Colors.primary, | ||
| }, | ||
| }); No newline at end of file |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift
This style block has drifted away from the theme token system.
There are still many raw metrics and literal colours here (paddingBottom: 10, gap: 8, fontSize: 30, borderWidth: 1.5, 'rgba(255,255,255,0.55)', etc.). That makes the home screen harder to retune alongside the rest of the app and cuts across the repo's design-system rule.
As per coding guidelines "All design constants (colours, fonts, sizes, spacing) must be imported from src/constants/theme.ts; never hardcode colours, fonts, sizes, or spacing values".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/screens/HomeScreen.tsx` around lines 349 - 768, The style block uses
hard-coded metrics and colours; replace all literal values with theme tokens
from src/constants/theme.ts (use Spacing, FontSizes, Colors, Radius, Fonts,
etc.) for entries such as header (paddingBottom: 10), headerLeft (gap: 8),
greetingName (fontSize: 30, lineHeight: 36),
winButton/winGhostIcon/winTitle/winSubtitle (shadow values, font sizes, rgba
white), sinButton/sinGhostIcon/sinTitle/sinSubtitle (borderWidth: 1.5, colors),
statsCard/statValue* (fontSize 22), ratioBadge/ratioBadgeText (padding,
fontSize), dotContainer/dotCheck/dotTodayInner (width/height), dayLabel
(fontSize), and undo* styles (Spacing.xxl + 4 arithmetic); swap each literal
with the appropriate token (e.g., Spacing.md, FontSizes.lg/xl,
Colors.primaryLight, Radius.xl, Fonts.sansMedium) and remove raw rgba by adding
an opacity-aware color token or using a helper; keep the same style keys
(header, greetingName, winButton, sinButton, statsCard, ratioBarFill,
dotTodayEmpty, dotPastEmpty, dayLabelToday, undoCard, undoIconCircle, etc.) so
the rest of the component consumes theme tokens instead of hard-coded values.
|
|
||
| export default function OnboardingScreen() { | ||
| const insets = useSafeAreaInsets(); | ||
| const navigation = useNavigation<any>(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "OnboardingScreen.tsx" -type fRepository: FLAiRistaken/firsthand
Length of output: 101
🏁 Script executed:
cat -n ./src/screens/OnboardingScreen.tsx | head -250Repository: FLAiRistaken/firsthand
Length of output: 11096
🏁 Script executed:
rg "RootStackParamList" -A 5Repository: FLAiRistaken/firsthand
Length of output: 929
Tighten the screen's TypeScript contracts.
useNavigation<any>() removes route safety. Type the navigator against RootStackParamList instead. Additionally, initConversation, handleCreateAccount, and handleSend are async functions without explicit return types — add Promise<void> to each.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/screens/OnboardingScreen.tsx` at line 60, Replace the loose navigation
typing useNavigation<any>() with a strongly-typed navigator using your
RootStackParamList (e.g.
useNavigation<NativeStackNavigationProp<RootStackParamList, 'Onboarding'>> or
the appropriate screen key) so the screen gets correct route/param types; also
add explicit Promise<void> return types to the async functions initConversation,
handleCreateAccount, and handleSend to tighten TypeScript contracts and ensure
their signatures are explicit.
| const userContent = input.trim(); | ||
| setInput(''); | ||
| setLoading(true); | ||
|
|
||
| if (step === 0) profileRef.current.name = userContent.split(' ')[0]; | ||
| else if (step === 1) profileRef.current.occupation = userContent; | ||
| else if (step === 2) profileRef.current.raw_tools = userContent; | ||
| else if (step === 3) profileRef.current.raw_uses = userContent; | ||
| else if (step === 4) profileRef.current.goal = userContent; | ||
| else if (step === 5) profileRef.current.success_definition = userContent; | ||
|
|
||
| const newMessages = [...messages, { role: 'user' as const, content: userContent }]; | ||
| setMessages(newMessages); | ||
|
|
||
| try { | ||
| const response = await callClaude(newMessages, ONBOARDING_SYSTEM); | ||
|
|
There was a problem hiding this comment.
Sanitise onboarding answers before sending them back to Claude.
userContent is pushed straight into the Anthropic message history. That gives prompt-injection input a direct path into every later completion. Please run each answer through sanitizePromptValue() before appending it to newMessages.
Suggested hardening
+import { sanitizePromptValue } from '../lib/prompts';
...
- const userContent = input.trim();
+ const userContent = input.trim();
+ const safeUserContent = sanitizePromptValue(userContent);
...
- if (step === 0) profileRef.current.name = userContent.split(' ')[0];
- else if (step === 1) profileRef.current.occupation = userContent;
- else if (step === 2) profileRef.current.raw_tools = userContent;
- else if (step === 3) profileRef.current.raw_uses = userContent;
- else if (step === 4) profileRef.current.goal = userContent;
- else if (step === 5) profileRef.current.success_definition = userContent;
+ if (step === 0) profileRef.current.name = safeUserContent.split(' ')[0];
+ else if (step === 1) profileRef.current.occupation = safeUserContent;
+ else if (step === 2) profileRef.current.raw_tools = safeUserContent;
+ else if (step === 3) profileRef.current.raw_uses = safeUserContent;
+ else if (step === 4) profileRef.current.goal = safeUserContent;
+ else if (step === 5) profileRef.current.success_definition = safeUserContent;
- const newMessages = [...messages, { role: 'user' as const, content: userContent }];
+ const newMessages = [...messages, { role: 'user' as const, content: safeUserContent }];As per coding guidelines "All user-provided values passed into Anthropic prompts must go through sanitizePromptValue()".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const userContent = input.trim(); | |
| setInput(''); | |
| setLoading(true); | |
| if (step === 0) profileRef.current.name = userContent.split(' ')[0]; | |
| else if (step === 1) profileRef.current.occupation = userContent; | |
| else if (step === 2) profileRef.current.raw_tools = userContent; | |
| else if (step === 3) profileRef.current.raw_uses = userContent; | |
| else if (step === 4) profileRef.current.goal = userContent; | |
| else if (step === 5) profileRef.current.success_definition = userContent; | |
| const newMessages = [...messages, { role: 'user' as const, content: userContent }]; | |
| setMessages(newMessages); | |
| try { | |
| const response = await callClaude(newMessages, ONBOARDING_SYSTEM); | |
| const userContent = input.trim(); | |
| const safeUserContent = sanitizePromptValue(userContent); | |
| setInput(''); | |
| setLoading(true); | |
| if (step === 0) profileRef.current.name = safeUserContent.split(' ')[0]; | |
| else if (step === 1) profileRef.current.occupation = safeUserContent; | |
| else if (step === 2) profileRef.current.raw_tools = safeUserContent; | |
| else if (step === 3) profileRef.current.raw_uses = safeUserContent; | |
| else if (step === 4) profileRef.current.goal = safeUserContent; | |
| else if (step === 5) profileRef.current.success_definition = safeUserContent; | |
| const newMessages = [...messages, { role: 'user' as const, content: safeUserContent }]; | |
| setMessages(newMessages); | |
| try { | |
| const response = await callClaude(newMessages, ONBOARDING_SYSTEM); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/screens/OnboardingScreen.tsx` around lines 197 - 213, Sanitise the
onboarding answer before including it in the Anthropic message history: create a
sanitized variable by calling sanitizePromptValue(userContent) and use that
sanitized value when building newMessages and when calling callClaude(…,
ONBOARDING_SYSTEM). Keep the existing profileRef.current assignments
(name/occupation/raw_tools/raw_uses/goal/success_definition) as needed, but
ensure any value sent to newMessages or passed into callClaude is the sanitized
version (refer to userContent, sanitizePromptValue, newMessages, setMessages,
callClaude, and ONBOARDING_SYSTEM).
…onstants and strengthen type safety
Summary by CodeRabbit
New Features
Refactor