Skip to content

Develop#19

Merged
FLAiRistaken merged 42 commits into
mainfrom
develop
Apr 30, 2026
Merged

Develop#19
FLAiRistaken merged 42 commits into
mainfrom
develop

Conversation

@FLAiRistaken

@FLAiRistaken FLAiRistaken commented Apr 30, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added interactive logging modal for quick "win" and "sin" entries with custom categories, notes, and context options.
    • Introduced comprehensive home dashboard displaying statistics, 7-day activity ratio, streaks, and daily logged entries.
    • Added email/password authentication as an alternative sign-in method.
    • Implemented chat-based onboarding experience with streamlined account creation.
    • Added 30-second undo functionality for logged entries.
    • Enabled custom category creation during logging.
  • Refactor

    • Simplified internal authentication flow and profile state management.

FLAiRistaken and others added 30 commits April 29, 2026 16:59
…694be4b

feat: Implement Home Screen & LogModal
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>
…048971014011

Add Email/Password Authentication to AuthScreen
Fixed 4 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>
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
Copilot AI review requested due to automatic review settings April 30, 2026 13:43
@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
Configuration
.coderabbit.yaml
Adds CodeRabbit configuration with British English locale, assertive auto-review rules for main/develop branches, path-specific constraints (styling for screens, product/data for logging, reliability/security for API calls), and auto-reply enablement.
Component Type Refactoring
src/components/Card.tsx, src/components/PillButton.tsx
Updates component type annotations from React.FC<Props> to explicit parameter destructuring; makes Card component's children prop optional.
New Logging Component
src/components/LogModal.tsx
Introduces new modal component for logging "win"/"sin" entries with category selection, custom category creation, optional note/context (work/personal), and state reset on visibility/type changes.
Design System Expansion
src/constants/theme.ts
Adds new colour tokens (primary/amber dark variants, sin-related UI colours, Google brand palette, overlay), expands spacing scale (smLg, xxxl), and introduces Sizes and BorderWidths object exports.
Profile State Management
src/contexts/ProfileContext.tsx, src/hooks/useProfile.ts
Creates new ProfileContext with provider that handles profile fetching, upsert, and refresh; refactors useProfile hook to delegate to context rather than managing state independently.
Authentication Flow Refactor
src/hooks/useAuth.ts, src/lib/devConfig.ts, src/navigation/RootNavigator.tsx
Removes development authentication bypass (DEV_BYPASS_AUTH, DEV_USER); refactors RootNavigator to use useAuth() hook and ProfileProvider context; eliminates direct Supabase auth subscription logic.
Logging & Data Layer
src/hooks/useLogs.ts, src/lib/db.ts, src/lib/types.ts
Adds deleteLog() method with optimistic deletion and undo-window expiry detection; updates addLog() to return inserted LogEntry; introduces setLogCancelled() database function; extends LogEntry type with optional cancelled property.
Type Definitions
src/lib/googleSignIn.ts
Adds GoogleSignInResult interface and updates GoogleSignin.signIn() return type from Promise<never> to Promise<GoogleSignInResult>.
Screen Implementations
src/screens/AuthScreen.tsx
Adds client-side email/password authentication with validation, keyboard-aware layout wrapping, input fields, and loading states; refactors styling to use theme constants; tightens idToken extraction typing.
Interactive Dashboard
src/screens/HomeScreen.tsx
Transforms from placeholder into full dashboard: displays greeting, win/sin logging CTAs via modal, computed stats (wins, sins, streak, 7-day ratio), today's entries with toggle/timestamps, and per-entry 30-second undo system with expiry handling and custom category support.
AI-Driven Onboarding
src/screens/OnboardingScreen.tsx
Replaces static screen with interactive step-based conversation flow: integrates callClaude for AI assistant messages, implements account creation with email/password sign-up and session polling, profile construction, and completion token detection.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 hops through commit with delight
Auth flows refactored, now shining bright,
A logging modal blooms like spring clover,
With profiles managed as AI chats take over,
Undo systems hop—thirty seconds to recover! 🥕✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Develop' is vague and generic, providing no meaningful information about the substantial changes across multiple files and features in this changeset. Replace with a descriptive title summarising the primary changes, e.g. 'Add logging modal and profile context with authentication improvements' or similar, to clearly communicate the main objectives.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Review rate limit: 2/3 reviews remaining, refill in 20 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 value

Hardcoded spacing and border width values should use theme tokens.

Lines 75-78 contain hardcoded values (paddingVertical: 7, paddingHorizontal: 15, borderWidth: 1.5) that should reference Spacing.* and BorderWidths.* from theme.ts. The guideline requires all spacing to come from theme constants.

Note: paddingVertical: 7 and paddingHorizontal: 15 don't match existing Spacing values. Consider adding appropriate tokens or using the nearest values. borderWidth: 1.5 can use BorderWidths.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.ts Spacing 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 win

Finish the token migration for radius and spacing.

This screen is mostly tokenised now, but cornerRadius={14} and paddingVertical: 14 still bypass theme.ts. Please lift those onto Radius/Spacing/Sizes so 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 win

Fail fast when Google returns no ID token.

GoogleSignin.signIn() now makes data?.idToken optional, 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 lift

Undo does not actually cancel logs created offline.

When addLog() falls back to the optimistic entry, the real insert is persisted in OFFLINE_QUEUE_KEY. If the user taps undo before reconnecting, deleteLog() removes only local state and calls dbSetLogCancelled() 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 win

One 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

📥 Commits

Reviewing files that changed from the base of the PR and between ff69734 and ee2343f.

⛔ Files ignored due to path filters (1)
  • Build_plan.md is excluded by !**/*.md
📒 Files selected for processing (17)
  • .coderabbit.yaml
  • src/components/Card.tsx
  • src/components/LogModal.tsx
  • src/components/PillButton.tsx
  • src/constants/theme.ts
  • src/contexts/ProfileContext.tsx
  • src/hooks/useAuth.ts
  • src/hooks/useLogs.ts
  • src/hooks/useProfile.ts
  • src/lib/db.ts
  • src/lib/devConfig.ts
  • src/lib/googleSignIn.ts
  • src/lib/types.ts
  • src/navigation/RootNavigator.tsx
  • src/screens/AuthScreen.tsx
  • src/screens/HomeScreen.tsx
  • src/screens/OnboardingScreen.tsx
💤 Files with no reviewable changes (2)
  • src/lib/devConfig.ts
  • src/hooks/useAuth.ts

Comment thread src/components/LogModal.tsx Outdated
Comment on lines +17 to +23
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "LogModal.tsx" -o -name "HomeScreen.tsx" | head -20

Repository: FLAiRistaken/firsthand

Length of output: 125


🏁 Script executed:

git ls-files | grep -E "(LogModal|HomeScreen)" | head -20

Repository: FLAiRistaken/firsthand

Length of output: 121


🏁 Script executed:

cat -n src/components/LogModal.tsx | head -100

Repository: FLAiRistaken/firsthand

Length of output: 3667


🏁 Script executed:

cat -n src/screens/HomeScreen.tsx | grep -A 30 "onSave\|onAddCategory" | head -80

Repository: 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 -150

Repository: 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.

Comment thread src/contexts/ProfileContext.tsx Outdated
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';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
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.

Comment thread src/contexts/ProfileContext.tsx Outdated
Comment on lines +34 to +35
} catch (err) {
console.error('Failed to fetch profile:', err);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/contexts/ProfileContext.tsx Outdated
fetchProfile();
}, [fetchProfile]);

const updateProfile = async (updates: Partial<Omit<UserProfile, 'id' | 'created_at'>>) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
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.

Comment thread src/lib/db.ts
Comment on lines +56 to +71
// 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;
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

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.

Comment thread src/screens/AuthScreen.tsx Outdated
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [emailLoading, setEmailLoading] = useState(false);
const navigation = useNavigation<any>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 2

Repository: FLAiRistaken/firsthand

Length of output: 94


🏁 Script executed:

# Let's check the RootNavigator for the proper types
cat -n src/navigation/RootNavigator.tsx | head -50

Repository: 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 1

Repository: FLAiRistaken/firsthand

Length of output: 465


Tighten the screen's TypeScript contracts.

Three issues need fixing:

  1. Line 44: useNavigation<any>() violates the "no any type" rule. Use the proper stack type instead.
  2. Lines 63, 96, 120: All three async handlers (handleAppleSignIn, handleEmailAuth, handleGoogleSignIn) lack explicit return type annotations; add : Promise<void>.
  3. Line 133: In handleGoogleSignIn, the catch block uses untyped error. Change to catch (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.

Comment thread src/screens/HomeScreen.tsx Outdated
Comment on lines +80 to +122
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] });
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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
fi

Repository: 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 -30

Repository: 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 2

Repository: 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.

Comment on lines +349 to +768
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

find . -name "OnboardingScreen.tsx" -type f

Repository: FLAiRistaken/firsthand

Length of output: 101


🏁 Script executed:

cat -n ./src/screens/OnboardingScreen.tsx | head -250

Repository: FLAiRistaken/firsthand

Length of output: 11096


🏁 Script executed:

rg "RootStackParamList" -A 5

Repository: 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.

Comment on lines +197 to +213
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants