Skip to content

YES#88

Merged
B3ni15 merged 120 commits intomainfrom
dev
Feb 18, 2026
Merged

YES#88
B3ni15 merged 120 commits intomainfrom
dev

Conversation

@B3ni15
Copy link
Copy Markdown
Member

@B3ni15 B3ni15 commented Feb 18, 2026

This pull request introduces significant updates to the Syncre mobile project, focusing on a major feature and documentation update, build automation for Android releases, and several improvements to configuration and branding. The changes include a comprehensive rewrite and expansion of the README.md, the addition of a GitHub Actions workflow for automated Android APK builds and releases, updates to the app version and branding, and enhancements to the app's configuration for modern Android and Expo features.

The most important changes are:

1. Documentation and Developer Experience

  • Major rewrite and expansion of README.md to include detailed feature descriptions, updated technology stack, API integration guides, security architecture, project structure, development instructions, and contribution guidelines. This makes onboarding and development much easier for new contributors. [1] [2]

2. Build and Release Automation

  • Added a new GitHub Actions workflow (.github/workflows/android-build.yml) to automate Android APK building, signing (with dynamic keystore generation), and release uploading to GitHub on pushes to main that affect the Mobile directory.

3. App Versioning and Branding

  • Updated app version from 1.1.4 to 2.0.3 in both app.json and android/app/build.gradle, and changed the app name from syncre to Syncre across configuration files for consistent branding. [1] [2] [3] [4]

4. Configuration and Feature Enhancements

  • Enabled Expo's new architecture (newArchEnabled: true) and set the user interface style to dark mode by default in both app.json and android/gradle.properties. [1] [2]
  • Added new Android intent filters for deep linking and expanded the list of included Expo and React Native modules (e.g., expo-secure-store, expo-localization, @react-native-community/datetimepicker) for richer functionality. [1] [2]

5. Cleanup and Minor Changes

  • Removed the unused android/app/src/main/res/xml/file_paths.xml file.
  • Updated the app's display name and default UI mode in android/app/src/main/res/values/strings.xml to match new branding and dark mode preference.

These changes collectively modernize the Syncre mobile project, improve its developer experience, and automate the release process for Android.

Summary by CodeRabbit

  • New Features

    • Friends request system with accept/reject actions
    • Change password functionality
    • Disappearing messages with custom durations
    • Enhanced poll creation and voting
    • Scheduled messages (30 minutes, 1 hour, 3 hours, tomorrow, or custom)
    • Spotify deep link support on Android
  • Improvements

    • Dark UI mode enabled by default
    • Enhanced Spotify integration with real-time activity
    • Optimized chat list loading and performance
    • Improved app startup and authentication flow
    • Better error messaging and user notifications
    • Backup encryption system for added security
  • Updates

    • Version bumped to 2.0.3
    • Android build system modernized
    • iOS and Android platform updates

B3ni15 and others added 30 commits December 31, 2025 18:11
…rofile sections

- Added ChatContext for shared state management across tabs.
- Created TabLayout component to manage navigation and state for chats, friends, and profile.
- Developed FriendsTab for managing friend requests and searching for users.
- Implemented ChatsTab for displaying chat lists and handling chat interactions.
- Built ProfileTab for user profile management and settings access.
- Integrated WebSocket for real-time updates on friend statuses and chat messages.
- Enhanced user experience with notifications and loading states.
…t Native integration

- Modified project.pbxproj to include React and ReactNativeDependencies frameworks in the Embed Pods Frameworks build phase.
- Removed unnecessary privacy bundles from the Copy Pods Resources build phase.
- Updated AppDelegate.swift to set the window background color and override user interface style to dark mode.
- Changed Info.plist to enable the new architecture for React Native and set the UIUserInterfaceStyle to Dark.
Bumps [react-native-screens](https://github.com/software-mansion/react-native-screens) from 4.18.0 to 4.19.0.
- [Release notes](https://github.com/software-mansion/react-native-screens/releases)
- [Commits](software-mansion/react-native-screens@4.18.0...4.19.0)

---
updated-dependencies:
- dependency-name: react-native-screens
  dependency-version: 4.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [@react-native/codegen](https://github.com/facebook/react-native/tree/HEAD/packages/react-native-codegen) from 0.82.1 to 0.83.1.
- [Release notes](https://github.com/facebook/react-native/releases)
- [Changelog](https://github.com/facebook/react-native/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react-native/commits/v0.83.1/packages/react-native-codegen)

---
updated-dependencies:
- dependency-name: "@react-native/codegen"
  dependency-version: 0.83.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [expo-linear-gradient](https://github.com/expo/expo/tree/HEAD/packages/expo-linear-gradient) from 15.0.7 to 15.0.8.
- [Changelog](https://github.com/expo/expo/blob/main/packages/expo-linear-gradient/CHANGELOG.md)
- [Commits](https://github.com/expo/expo/commits/HEAD/packages/expo-linear-gradient)

---
updated-dependencies:
- dependency-name: expo-linear-gradient
  dependency-version: 15.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [expo-audio](https://github.com/expo/expo/tree/HEAD/packages/expo-audio) from 1.0.16 to 1.1.1.
- [Changelog](https://github.com/expo/expo/blob/main/packages/expo-audio/CHANGELOG.md)
- [Commits](https://github.com/expo/expo/commits/HEAD/packages/expo-audio)

---
updated-dependencies:
- dependency-name: expo-audio
  dependency-version: 1.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [expo-status-bar](https://github.com/expo/expo/tree/HEAD/packages/expo-status-bar) from 3.0.8 to 3.0.9.
- [Changelog](https://github.com/expo/expo/blob/main/packages/expo-status-bar/CHANGELOG.md)
- [Commits](https://github.com/expo/expo/commits/HEAD/packages/expo-status-bar)

---
updated-dependencies:
- dependency-name: expo-status-bar
  dependency-version: 3.0.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
…ralOptions, ProfileCard, and ScheduleMessageSheet

- Added SwiftUI support for iOS in CreatePollSheet, replacing existing components with SwiftUI counterparts.
- Implemented SwiftUI BottomSheet and VStack for better UI consistency and performance.
- Enhanced EphemeralOptions to utilize SwiftUI for displaying ephemeral message durations.
- Refactored ProfileCard to use SwiftUI for rendering user information and actions.
- Updated ScheduleMessageSheet to leverage SwiftUI DateTimePicker for scheduling messages.
- Added fallback to React Native components for Android and non-iOS platforms.
…creens

- Added new SwiftUI components and modifiers in ChatScreen for improved reaction handling.
- Updated ChatListWidget to always show the ProfileCard, even when user details are loading.
- Enhanced CreatePollSheet with additional SwiftUI components and improved layout.
- Improved EphemeralOptions with SwiftUI integration for better user experience.
- Refactored ProfileCard to use a shared content component and improved styling.
- Updated ScheduleMessageSheet to include new SwiftUI components and better layout.
…nents

- Integrated SwiftUI components in ProfileCard for improved UI consistency.
- Added functionality to display user initials and badges in ProfileCard.
- Refactored ScheduleMessageSheet to utilize SwiftUI for scheduling messages.
- Implemented a custom date and time picker using SwiftUI.
- Improved layout and styling for both components to enhance user experience.
B3ni15 and others added 23 commits February 8, 2026 23:05
Updated the REACT_NATIVE_PATH in the Xcode project configuration to reference @types/react version 19.1.17 instead of 19.2.7 for both debug and release configurations.
…ollSheet, EphemeralOptions, and ScheduleMessageSheet components
…ve SwiftUI support and implement BlurView for improved UI. Update styles for better visual consistency and adjust modal layouts.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 18, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

This pull request performs a major version bump (1.1.4 → 2.0.3) with substantial architectural changes: migrates from PIN-based identity bootstrapping to login-integrated E2EE initialization, introduces SwiftUI components for iOS, implements backup envelope encryption support, refactors tab-based navigation with real-time WebSocket integration, and systematically replaces GlassCard components with native View containers across the app.

Changes

Cohort / File(s) Summary
Version Bump & Manifest
app.json, android/app/build.gradle, package.json, ios/syncre/Info.plist
Version updated from 1.1.4 to 2.0.3; added new Expo plugins, enabled new architecture, updated app name capitalization to "Syncre".
Android Configuration
android/gradle.properties, android/settings.gradle, android/app/src/main/res/*, .github/workflows/android-build.yml
Enabled new architecture, switched JS engine from Hermes to (implied), capitalized root project name; added automated APK build/release workflow.
iOS Project Refactoring
ios/syncre.xcodeproj/*, ios/Podfile*, ios/syncre.xcworkspace/..., ios/syncre/*
Comprehensive rename from "syncre" to "Syncre" across Xcode project, targets, build configurations, and file references; switched JavaScript engine to Hermes; updated version strings and build settings.
Authentication & Navigation
app/_layout.tsx, app/index.tsx, app/identity.tsx, app/settings/change-password.tsx, app/share/index.tsx
Removed PIN-based identity screen; refactored token validation and local identity checks; updated navigation routes to redirect to new tab layout; added change-password route.
Tab-Based Layout & Navigation
app/(tabs)/_layout.tsx, app/(tabs)/index.tsx, app/(tabs)/friends.tsx, app/(tabs)/profile.tsx
Introduced new tab navigation system with ChatContext provider; added Chats, Friends, and Profile tabs with real-time data loading, WebSocket integration, and push notification handling.
Chat & Message System
app/chat/[id].tsx, components/ChatListWidget.tsx
Extensive refactor: added backup envelope decryption, decryption caching, poll hydration, ephemeral messaging, NativeContextMenu integration, and reduced animation jumps; enhanced participant-based user resolution and double-tap profile handling.
UI Component Modernization (GlassCard Replacement)
screens/EditProfileScreen.tsx, screens/HomeScreen.tsx, screens/LoginScreen.tsx, screens/MaintenanceScreen.tsx, screens/PasswordResetScreen.tsx, screens/PrivacyScreen.tsx, screens/RegisterScreen.tsx, screens/SettingsScreen.tsx, screens/VerifyScreen.tsx, app/wrap/[date].tsx, components/FriendRequestsWidget.tsx, components/SpotifyConnection.tsx
Replaced GlassCard components with plain View containers and inline styling (background, border, radius, padding) across screens; updated section wrappers and card containers.
SwiftUI Integration
components/GlassCard.tsx, components/NativeBlur.tsx, components/NativeContextMenu.tsx, components/GroupMemberPicker.tsx, components/ProfileCard.tsx, components/ProfileMenu.tsx
Added runtime SwiftUI detection and conditional rendering paths for iOS; implemented SwiftUI-based glass effects, context menus, bottom sheets, and blur components with React Native fallbacks; replaced Liquid Glass with SwiftUI approach.
Poll & Scheduling Features
components/CreatePollSheet.tsx, components/PollMessage.tsx, components/ScheduleMessageSheet.tsx, utils/pollPayload.ts
Refactored poll creation with ID-based option management; extended PollMessage with encryption fields; redesigned schedule sheet with predefined options and custom date picker; added poll payload encoding/decoding utilities.
Ephemeral Messaging
components/EphemeralOptions.tsx
Converted to externally-controlled modal via visible/onClose props; updated UI with BlurView overlay and header; simplified state management.
Encryption & Security Services
services/CryptoService.ts, services/PinService.ts, services/ReencryptionService.ts, utils/swiftUi.ts
Introduced password-based identity encryption/decryption, backup key initialization, backup envelope encryption; removed PIN service; added batch envelope posting; introduced SwiftUI detection utilities.
User Management & Caching
services/UserCacheService.ts, services/WebSocketService.ts, services/ApiService.ts
Added cache clearing; enhanced WebSocket state cleanup between accounts; extended API payload schemas for E2EE and backup envelopes.
Screen Additions & Refactoring
screens/ChangePasswordScreen.tsx, components/GlassySheet.tsx, components/UserAvatar.tsx, screens/LoginScreen.tsx
Added new change-password screen with identity re-encryption; introduced GlassySheet wrapper; extended UserAvatar with plain variant; enhanced login with E2EE initialization flow.
Documentation & Minor Updates
API.md, README.md, app/+not-found.tsx, app/profile.tsx
Added comprehensive API integration guide; expanded README with architecture, security, and development guidance; updated UI text and removed profile screen identity bootstrap.
Removed Resources
utils/platformUtils.ts, android/app/src/main/res/xml/file_paths.xml, ios/Syncre\ 2.icon/icon.json
Deleted platform detection utilities, FileProvider configuration, and legacy icon resource definitions.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant App
    participant LoginService
    participant CryptoService
    participant StorageService
    participant WebSocketService

    User->>App: Open app / Focus screen
    App->>App: validateTokenAndNavigate()
    
    alt Token exists
        App->>StorageService: Retrieve token
        App->>StorageService: Retrieve encrypted identity
        
        alt Local identity missing
            App->>App: Clear token & user data
            App->>App: Navigate to login
        else Local identity exists
            App->>CryptoService: initializeFromLogin(password, token, identityKey)
            activate CryptoService
            CryptoService->>CryptoService: Decrypt identity with password
            CryptoService->>StorageService: Persist identity
            CryptoService->>CryptoService: Attempt initializeBackupKey() (non-blocking)
            deactivate CryptoService
            App->>WebSocketService: Connect & register handlers
            App->>App: Navigate to /(tabs)
        end
    else No token
        App->>App: Navigate to login
    end
Loading
sequenceDiagram
    participant ChatComponent
    participant EnvelopeCache
    participant CryptoService
    participant Storage as StorageService

    ChatComponent->>ChatComponent: Fetch messages with envelopes
    
    loop For each message
        ChatComponent->>EnvelopeCache: Check decryptionCache[messageId]
        
        alt Cached
            EnvelopeCache-->>ChatComponent: Return decrypted content
        else Not cached
            ChatComponent->>CryptoService: Attempt decryptEnvelope()
            
            alt Success
                CryptoService-->>ChatComponent: Decrypted payload
                ChatComponent->>EnvelopeCache: Store in decryptionCache
            else Envelope decryption fails
                ChatComponent->>CryptoService: Attempt decryptFromBackup(backupEnvelopes)
                
                alt Backup success
                    CryptoService-->>ChatComponent: Decrypted from backup
                    ChatComponent->>EnvelopeCache: Cache result
                else Both fail
                    ChatComponent->>ChatComponent: Use fallback "[Encrypted message]"
                    ChatComponent->>ChatComponent: Mark for re-encrypt prompt
                end
            end
        end
    end
    
    ChatComponent->>ChatComponent: Render messages with decrypted content
Loading
sequenceDiagram
    participant TabLayout
    participant WebSocket as WebSocketService
    participant CryptoService
    participant ChatContext
    participant UIComponents

    TabLayout->>TabLayout: Initialize (hydrate cache, fetch user)
    TabLayout->>WebSocket: Connect with token
    activate WebSocket
    
    WebSocket->>WebSocket: Listen for message events
    
    alt User status update received
        WebSocket->>ChatContext: Update userStatuses
        ChatContext->>UIComponents: Re-render with new presence
    else New message received
        WebSocket->>TabLayout: Trigger loadChats()
        TabLayout->>ChatContext: Update chat list & unread count
        ChatContext->>UIComponents: Re-render chat list
    else Typing indicator
        WebSocket->>ChatContext: Update typingUsers
        ChatContext->>UIComponents: Show typing indicator
    end
    
    deactivate WebSocket
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • Fix1 #79 — Modifies message grouping logic and isLastInGroup behavior in app/chat/[id].tsx, directly overlapping with this PR's extensive chat message handling refactor.

Poem

🐰 Hoppy version two point oh!
Encryption blooms where envelopes flow,
SwiftUI renders, no cards made of glass,
Tab navigation makes journeys so fast!
Backup keys guard what we hold most dear,
Syncre evolves—a new era is here!

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@B3ni15 B3ni15 marked this pull request as ready for review February 18, 2026 07:39
@Syncre-App Syncre-App deleted a comment from coderabbitai bot Feb 18, 2026
@Syncre-App Syncre-App deleted a comment from chatgpt-codex-connector bot Feb 18, 2026
@Syncre-App Syncre-App deleted a comment from chatgpt-codex-connector bot Feb 18, 2026
@B3ni15 B3ni15 merged commit 5d448a1 into main Feb 18, 2026
3 of 4 checks passed
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
components/NativeContextMenu.tsx (1)

47-52: ⚠️ Potential issue | 🟡 Minor

Props onMenuWillShow, onMenuWillHide, and preview are declared but not used.

These props are defined in the interface but are not wired up in any of the three rendering paths (SwiftUI, LegacyContextMenu, or fallback). This could lead to caller confusion when callbacks are passed but never invoked.

💡 Options to address
  1. Remove unused props if they're not planned for immediate implementation
  2. Wire them up in the SwiftUI and LegacyContextMenu paths:
    • SwiftUI ContextMenu may support equivalent callbacks
    • react-native-context-menu-view supports onPreviewPress, onMenuWillShow, onMenuWillHide
 <LegacyContextMenu
   title={title}
   actions={contextMenuActions}
   onPress={handlePress}
   previewBackgroundColor="transparent"
   dropdownMenuMode={dropdownMenuMode}
   style={style}
+  onMenuWillShow={onMenuWillShow}
+  onMenuWillHide={onMenuWillHide}
 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/NativeContextMenu.tsx` around lines 47 - 52, The props preview,
onMenuWillShow, and onMenuWillHide declared on the NativeContextMenu component
are never used; either remove them from the interface or wire them into all
rendering paths (SwiftUI branch, LegacyContextMenu branch, and the fallback). If
wiring, forward preview into the SwiftUI ContextMenu/ContextMenuPreview and into
LegacyContextMenu props (and the react-native-context-menu-view equivalents such
as onPreviewPress/onMenuWillShow/onMenuWillHide) and invoke/forward callbacks at
the points where the menu is shown/dismissed so consumers receive the events;
ensure NativeContextMenu's prop names map to the underlying platform prop names
where they differ.
screens/PrivacyScreen.tsx (1)

35-35: ⚠️ Potential issue | 🟡 Minor

Remove unused isBootstrapping state.

The isBootstrapping state is declared at line 35 but never set to any value other than its initial false. It's referenced in the guard condition at line 226, in getRotateKeysText() at line 312, and in conditional rendering at line 424, but since it never changes, these checks are dead code. Remove the state declaration and its references.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@screens/PrivacyScreen.tsx` at line 35, The isBootstrapping useState
declaration is dead code and should be removed: delete the const
[isBootstrapping, setIsBootstrapping] = useState(false) declaration, remove all
references to isBootstrapping in the guard condition (where it’s checked around
line ~226), in getRotateKeysText() (referenced around line ~312), and in the
conditional rendering block (around line ~424), and update logic to rely on the
real bootstrap status variable(s) already used elsewhere; also remove useState
from the import list if it becomes unused. Ensure no other code expects
setIsBootstrapping to exist and adjust any boolean expressions accordingly.
services/CryptoService.ts (1)

95-110: ⚠️ Potential issue | 🔴 Critical

Reject Math.random fallback for cryptographic entropy.

The randomBytes function falls back to Math.random, which is cryptographically unsafe and produces predictable output. This directly affects identity key generation (via generateNewKeyPair), encryption nonces, and password salts. If Expo's secure RNG is unavailable, the entire encryption layer becomes predictable. Fail fast with a hard error instead of silently generating weak cryptographic material.

🔒 Proposed fix (fail fast on missing secure RNG)
   try {
     Crypto.getRandomValues(bytes);
     return bytes;
   } catch (error) {
-    console.warn('[CryptoService] Falling back to Math.random entropy', error);
-    for (let i = 0; i < bytes.length; i += 1) {
-      bytes[i] = Math.floor(Math.random() * 256);
-    }
-    return bytes;
+    console.error('[CryptoService] Secure RNG unavailable', error);
+    throw new Error('Secure RNG unavailable');
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/CryptoService.ts` around lines 95 - 110, The randomBytes function
currently falls back to Math.random which is insecure; modify randomBytes in
CryptoService.ts to remove the Math.random fallback and instead throw a
descriptive error when a secure RNG is not available (check
globalThis.crypto?.getRandomValues and the Crypto.getRandomValues branch), and
update any call sites like generateNewKeyPair to allow the error to propagate or
handle it so key generation fails fast rather than producing weak entropy.
screens/RegisterScreen.tsx (1)

58-90: ⚠️ Potential issue | 🟠 Major

Avoid logging emails/usernames and full API responses.

These logs can leak PII and potentially tokens in production logs. Prefer removing them or gating behind __DEV__ with redaction.

🛠️ Suggested change
-    console.log('Starting registration for:', e, '(username:', u, ')');
+    if (__DEV__) {
+      console.log('Starting registration');
+    }
...
-      console.log('Register response:', response);
+      if (__DEV__) {
+        console.log('Register response status:', { success: response.success, statusCode: response.statusCode });
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@screens/RegisterScreen.tsx` around lines 58 - 90, Remove console.log
statements that print PII (email `e`, username `u`) and full API responses
(`response`) in the registration flow inside RegisterScreen; instead either
fully remove them or wrap them in a development-only guard (if (__DEV__)) and
redact sensitive fields before logging. Specifically update usages around
ApiService.post call, the `response` handling, and the branches that call
notificationService.show and router.replace so that you only log non-sensitive
flags (e.g., `response.success` or `verified`) or sanitized messages, and never
include `e`, `u`, passwords, tokens, or the raw `response` object in production
logs. Ensure router.replace and notificationService.show calls are unchanged
except for removing any logging of PII.
🟡 Minor comments (10)
README.md-104-141 (1)

104-141: ⚠️ Potential issue | 🟡 Minor

Add a language to the fenced code block.

Markdownlint flags the project structure block as missing a language specifier. Adding one (e.g., text) will satisfy MD040 without changing content.

✅ Suggested fix
-```
+```text
 Mobile/
 ├── app/                      # Expo Router routes (screens)
 │   ├── (tabs)/              # Tab-based navigation screens
 │   │   ├── home.tsx         # Home/chat list screen
 │   │   ├── friends.tsx      # Friends management screen
 │   │   ├── profile.tsx      # User profile screen
 │   │   └── _layout.tsx      # Tab navigation layout
 │   ├── chat/                # Chat screens
 │   │   └── [id].tsx         # Individual chat screen (dynamic route)
 │   ├── login.tsx            # Login screen
 │   ├── register.tsx         # Registration screen
 │   ├── verify.tsx           # Email verification screen
 │   └── _layout.tsx          # Root layout with providers
 ├── components/              # Reusable UI components
 │   ├── MessageBubble.tsx    # Message display component
 │   ├── ChatList.tsx         # Chat list component
 │   └── ...
 ├── context/                 # React context providers
 │   ├── AuthContext.tsx      # Authentication state
 │   ├── ChatContext.tsx      # Chat/messaging state
 │   └── ...
 ├── hooks/                   # Custom React hooks
 │   ├── useAuth.ts           # Authentication hook
 │   ├── useChat.ts           # Chat management hook
 │   └── ...
 ├── services/                # Core services
 │   ├── ApiService.ts        # REST API client
 │   ├── WebSocketService.ts  # WebSocket communication
 │   ├── CryptoService.ts     # Encryption/decryption
 │   ├── IdentityService.ts   # Identity key management
 │   ├── ReencryptionService.ts # Message re-encryption
 │   ├── StorageService.ts    # Secure storage wrapper
 │   └── ...
 ├── types/                   # TypeScript type definitions
 ├── constants/               # App constants
 └── assets/                  # Static assets (images, fonts)
-```
+```
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 104 - 141, The fenced code block showing the project
tree in README.md is missing a language specifier which triggers MD040; update
the triple-backtick fence that wraps the Mobile/ tree to include a language like
text (i.e., change ``` to ```text) so the block is explicitly marked as plain
text—look for the block beginning with the "Mobile/" line and the closing triple
backticks and add the language specifier there.
screens/SettingsScreen.tsx-32-33 (1)

32-33: ⚠️ Potential issue | 🟡 Minor

Notification toggle has no effect; Dark Mode state is unused.

  • notificationsEnabled state only updates UI—there's no integration with NotificationService to actually enable/disable push notifications. This could mislead users into thinking they've toggled notifications.
  • darkModeEnabled state setter is connected to a disabled switch (line 181), making the setDarkModeEnabled call dead code.

Consider either:

  1. Wiring notificationsEnabled to actual notification permission/preference logic, or
  2. Removing/disabling the toggle until the feature is implemented (similar to Dark Mode).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@screens/SettingsScreen.tsx` around lines 32 - 33, notificationsEnabled is
only toggling UI and never calls into the app's notification logic, and
darkModeEnabled/setDarkModeEnabled are wired to a disabled switch (dead code);
update the SettingsScreen to either wire the notification toggle to the real
NotificationService (e.g., call NotificationService.requestPermission(),
NotificationService.enablePush() or NotificationService.updatePreference(...)
inside the onToggle handler where setNotificationsEnabled is used) and handle
async success/failure by reverting state if needed, or remove/disable the
notifications toggle until implemented; also remove or fully implement
darkModeEnabled/setDarkModeEnabled (used in the same component) so the setter
isn't dead—either connect it to your theme manager (e.g.,
ThemeContext.toggleDarkMode or ThemeService.setDarkMode) or remove the state and
disabled switch to avoid dead code.
services/UserCacheService.ts-113-124 (1)

113-124: ⚠️ Potential issue | 🟡 Minor

Potential race condition with in-progress hydration.

If clear() is called while startHydration() is still running, the async hydration will complete and set this.hydrated = true in its finally block, overwriting the false set here. This could leave the cache in an inconsistent state where hydrated is true but the cache is empty.

Consider awaiting or cancelling the in-progress hydration before clearing state:

Proposed fix
  clear(): void {
    if (this.persistTimer) {
      clearTimeout(this.persistTimer);
      this.persistTimer = null;
    }
    this.cache.clear();
-   this.hydrated = false;
-   this.hydrating = null;
+   // Only reset hydration state if not currently hydrating
+   // The next hydrate() call will re-trigger startHydration()
+   if (!this.hydrating) {
+     this.hydrated = false;
+   }
    StorageService.removeItem(STORAGE_KEY).catch((err) =>
      console.warn('[UserCache] Failed to remove persisted cache:', err)
    );
  }

Alternatively, track a cancellation token or await the hydration promise before clearing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/UserCacheService.ts` around lines 113 - 124, The clear() method can
race with an in-flight startHydration() because startHydration()'s finally will
set this.hydrated = true after clear() set it false; update clear() to cancel or
await any ongoing hydration by checking and handling this.hydrating: either
await this.hydrating before clearing state, or set a cancellation flag/token
that startHydration() checks (e.g., a this.hydrationCancelled boolean or a
CancellationToken) and have startHydration() skip setting this.hydrated when
cancelled; ensure you reference and update the this.hydrating promise and
this.hydrated in both clear() and startHydration() to avoid the overwrite race.
components/GroupMemberPicker.tsx-141-156 (1)

141-156: ⚠️ Potential issue | 🟡 Minor

Guard against invalid last_seen values.

Date.parse can return NaN, which currently produces NaNd ago. Add a simple check to fall back to offline when parsing fails.

🧪 Proposed fix
 const getPresenceLabel = (friend: Friend): string => {
   const lastSeen = friend.last_seen;
   if (friend.status === 'online') return 'online';
   if (friend.status === 'idle') return 'idle';
   if (lastSeen) {
-    const diff = Date.now() - Date.parse(lastSeen);
+    const parsed = Date.parse(lastSeen);
+    if (Number.isNaN(parsed)) return 'offline';
+    const diff = Date.now() - parsed;
     const minutes = Math.floor(diff / 60000);
     if (minutes < 3) return 'idle';
     if (minutes < 60) return `${minutes}m ago`;
     const hours = Math.floor(minutes / 60);
     if (hours < 24) return `${hours}h ago`;
     const days = Math.floor(hours / 24);
     return `${days}d ago`;
   }
   return 'offline';
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/GroupMemberPicker.tsx` around lines 141 - 156, The
getPresenceLabel function currently assumes friend.last_seen parses to a valid
date, which can yield NaN and produce "NaNd ago"; update getPresenceLabel (in
GroupMemberPicker.tsx) to validate the parsed timestamp: after computing const
parsed = Date.parse(lastSeen) check if Number.isNaN(parsed) (or
!isFinite(parsed)) and if so return 'offline'; otherwise use parsed for diff
calculation and continue the existing minutes/hours/days logic; keep existing
status checks for friend.status ('online'/'idle').
components/ScheduleMessageSheet.tsx-95-106 (1)

95-106: ⚠️ Potential issue | 🟡 Minor

Handle Android DateTimePicker dismiss events.

On Android, onChange fires even when the user dismisses the picker. Check event.type === 'dismissed' and close the picker instead of advancing to time mode.

🛠️ Proposed fix
-  const handleCustomDateChange = (_event: any, date?: Date) => {
+  const handleCustomDateChange = (event: any, date?: Date) => {
     if (Platform.OS === 'android') {
+      if (event?.type === 'dismissed') {
+        setShowCustomPicker(false);
+        return;
+      }
       if (pickerMode === 'date') {
         setPickerMode('time');
       } else {
         setShowCustomPicker(false);
       }
     }
     if (date) {
       setCustomDate(date);
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ScheduleMessageSheet.tsx` around lines 95 - 106, The Android
DateTimePicker dismiss isn't handled: inside handleCustomDateChange check the
incoming _event (use its type property) and if Platform.OS === 'android' and
event.type === 'dismissed' then close the picker via setShowCustomPicker(false)
(do not advance pickerMode or setCustomDate); otherwise keep the existing logic
that advances from 'date' to 'time' and sets setCustomDate(date) when date is
present. Ensure you reference pickerMode, setPickerMode, setShowCustomPicker,
setCustomDate and Platform.OS in the updated control flow.
app/chat/[id].tsx-4722-4726 (1)

4722-4726: ⚠️ Potential issue | 🟡 Minor

“Off” scheduling path doesn’t close the sheet.
The early return leaves the sheet open despite the inline comment.

✅ Suggested fix
     if (!scheduledFor) {
       // User selected "Off" - just close the sheet
+      setShowScheduleSheet(false);
       return;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/chat/`[id].tsx around lines 4722 - 4726, handleScheduleMessage currently
returns early when scheduledFor is null but doesn't close the scheduling sheet;
update handleScheduleMessage so the "Off" path invokes the component's
sheet-close logic (the same function or setter used elsewhere in this file—e.g.,
closeScheduleSheet(), schedulingSheetRef.current?.close(), or
setIsSchedulingOpen(false)) before returning so the sheet is dismissed when the
user selects "Off".
app/(tabs)/_layout.tsx-49-53 (1)

49-53: ⚠️ Potential issue | 🟡 Minor

Align the hook error message with the actual provider.

ChatProvider doesn’t exist in this file, so the error can mislead during debugging.

✅ Suggested fix
-    throw new Error('useChatContext must be used within ChatProvider');
+    throw new Error('useChatContext must be used within <ChatContext.Provider>');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(tabs)/_layout.tsx around lines 49 - 53, The hook useChatContext throws
a misleading error referencing a non-existent ChatProvider; update the thrown
message in useChatContext to reference the actual context provider (e.g.,
"useChatContext must be used within ChatContext.Provider" or the real provider
component name used in the codebase) so the error points to the correct symbol
(useChatContext / ChatContext) during debugging.
app/(tabs)/_layout.tsx-131-145 (1)

131-145: ⚠️ Potential issue | 🟡 Minor

Clear the unread debounce timer on unmount.

If the tab layout unmounts while a timeout is pending, the callback can still fire and update state on an unmounted component.

✅ Suggested fix
+  useEffect(() => {
+    return () => {
+      if (unreadTimeoutRef.current) {
+        clearTimeout(unreadTimeoutRef.current);
+      }
+    };
+  }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(tabs)/_layout.tsx around lines 131 - 145, The debounce timer in
loadUnreadSummary uses unreadTimeoutRef but isn't cleared on unmount; add a
cleanup effect in the component that checks unreadTimeoutRef.current and calls
clearTimeout(unreadTimeoutRef.current) (and nulls the ref) in the returned
cleanup function so the scheduled loadUnreadSummary won't run after unmount.
Place the useEffect cleanup alongside the other hooks in the same component that
defines loadUnreadSummary/unreadTimeoutRef.
app/(tabs)/index.tsx-56-69 (1)

56-69: ⚠️ Potential issue | 🟡 Minor

Await unread summary refresh to keep errors in the try/catch.

loadUnreadSummary(true) returns a promise; without await, any rejection escapes the handler and skips logging.

🔧 Suggested fix
-          loadUnreadSummary(true);
+          await loadUnreadSummary(true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(tabs)/index.tsx around lines 56 - 69, The handler handleMarkChatRead
doesn't await loadUnreadSummary(true), so any rejection escapes the try/catch;
update handleMarkChatRead to await the promise returned by
loadUnreadSummary(true) (i.e., await loadUnreadSummary(true)) inside the
existing try block after the ApiService.post call so errors are caught and
logged by the catch; ensure you still await ApiService.post and
StorageService.getAuthToken as currently implemented.
components/CreatePollSheet.tsx-60-72 (1)

60-72: ⚠️ Potential issue | 🟡 Minor

Guard add/remove against rapid taps to enforce min/max limits.

Using options.length from render can be stale if multiple taps land before re-render, letting the list exceed bounds. Prefer checking prev.length inside the updater.

🔧 Suggested fix
-  const handleAddOption = useCallback(() => {
-    if (options.length < MAX_OPTIONS) {
-      setOptions((prev) => [...prev, buildOption()]);
-      Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
-    }
-  }, [buildOption, options.length]);
+  const handleAddOption = useCallback(() => {
+    let didAdd = false;
+    setOptions((prev) => {
+      if (prev.length >= MAX_OPTIONS) return prev;
+      didAdd = true;
+      return [...prev, buildOption()];
+    });
+    if (didAdd) {
+      Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+    }
+  }, [buildOption]);
 
-  const handleRemoveOption = useCallback((optionId: string) => {
-    if (options.length > MIN_OPTIONS) {
-      setOptions((prev) => prev.filter((option) => option.id !== optionId));
-      Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
-    }
-  }, [options.length]);
+  const handleRemoveOption = useCallback((optionId: string) => {
+    let didRemove = false;
+    setOptions((prev) => {
+      if (prev.length <= MIN_OPTIONS) return prev;
+      didRemove = true;
+      return prev.filter((option) => option.id !== optionId);
+    });
+    if (didRemove) {
+      Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+    }
+  }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/CreatePollSheet.tsx` around lines 60 - 72, The add/remove handlers
(handleAddOption, handleRemoveOption) are using options.length from render which
can be stale; update them to use the functional updater's previous state: inside
setOptions(prev => { if (prev.length >= MAX_OPTIONS) return prev; return
[...prev, buildOption()]; }) for add, and for remove do setOptions(prev => { if
(prev.length <= MIN_OPTIONS) return prev; return prev.filter(o => o.id !==
optionId); }); only call Haptics.impactAsync when the updater actually changes
the array so rapid taps cannot bypass min/max limits.
🧹 Nitpick comments (19)
screens/SettingsScreen.tsx (2)

10-20: Remove unused imports.

TextInput (line 12) and ApiService (line 20) are imported but not used anywhere in this component.

🧹 Suggested cleanup
 import {
   Alert,
   Linking,
   ScrollView,
   StatusBar,
   StyleSheet,
   Switch as RNSwitch,
   Text,
-  TextInput,
   TouchableOpacity,
   View,
 } from 'react-native';
 import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
 import { NotificationService } from '../services/NotificationService';
 import { StorageService } from '../services/StorageService';
 import { UpdateService } from '../services/UpdateService';
-import { ApiService } from '../services/ApiService';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@screens/SettingsScreen.tsx` around lines 10 - 20, The SettingsScreen import
block contains unused symbols—remove TextInput from the React Native import list
and remove the ApiService import to clean up unused imports; specifically edit
the import that currently reads "Switch as RNSwitch, Text, TextInput,
TouchableOpacity, View" to drop TextInput and delete the separate "import {
ApiService } from '../services/ApiService';" line, then run a quick
lint/TypeScript check to ensure no references remain to TextInput or ApiService.

298-301: Unused style definition.

integrationsSection is defined but never used in the component. Consider removing it.

🧹 Remove dead style
-  integrationsSection: {
-    paddingHorizontal: spacing.lg,
-    paddingBottom: spacing.lg,
-  },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@screens/SettingsScreen.tsx` around lines 298 - 301, Remove the dead style key
integrationsSection from the styles object in SettingsScreen (the StyleSheet
used by the SettingsScreen component); locate the styles declaration (variable
named styles or StyleSheet.create in SettingsScreen.tsx), delete the
integrationsSection property and its values (paddingHorizontal/paddingBottom),
and run lint/format to ensure no references remain—if it was meant to be used
instead of another style, replace usages to reference the correct style name
rather than reintroducing integrationsSection.
ios/syncre.xcodeproj/xcshareddata/xcschemes/syncre.xcscheme (1)

18-20: Scheme content updates look consistent, but scheme filename may need renaming.

All BuildableReference entries are consistently updated to use the capitalized Syncre naming. However, the scheme file itself remains syncre.xcscheme while referencing Syncre.app and container:Syncre.xcodeproj. For full consistency, consider renaming the scheme file to Syncre.xcscheme.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/syncre.xcodeproj/xcshareddata/xcschemes/syncre.xcscheme` around lines 18
- 20, The scheme file name should match the capitalized project references;
rename the scheme file from syncre.xcscheme to Syncre.xcscheme and update any
references to the scheme in project metadata or CI configs accordingly so they
point to Syncre.xcscheme; ensure this aligns with the BuildableReference entries
like BuildableName "Syncre.app", BlueprintName "Syncre", and ReferencedContainer
"container:Syncre.xcodeproj".
ios/syncre.xcodeproj/project.pbxproj (1)

1-603: Run pod install after merging to regenerate Pod support files.

The project file references Pods-Syncre directories and scripts that need to be regenerated. After merging, ensure pod install is run in the ios/ directory to create the updated Pod support files matching the new Syncre naming.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/syncre.xcodeproj/project.pbxproj` around lines 1 - 603, Summary: The
Xcode project references Pod-generated files (e.g., Pods-Syncre entries,
ExpoModulesProvider.swift, Pods-Syncre-frameworks.sh, Pods-Syncre-resources.sh,
expo-configure-project.sh) that must be regenerated; run pod install after
merging to update them. Fix: after merging, run pod install in the iOS workspace
to regenerate Pod support files (which will update ExpoModulesProvider.swift,
Pods-Syncre-*.sh scripts and related resources referenced by the PBX project),
verify the generated files (including Manifest.lock/Podfile.lock and the scripts
referenced by "Bundle React Native code and images", "[Expo] Configure project",
"[CP] Embed Pods Frameworks", and "[CP] Copy Pods Resources") are present and
correct, then add and commit those regenerated Pod support files to the branch
before pushing.
screens/MaintenanceScreen.tsx (1)

70-79: Consider using radii token for border radius.

The card styling uses a hardcoded borderRadius: 20 instead of a design system token. For consistency with other components, consider using radii.lg (which equals 20):

Suggested change
+ import { font, palette, radii, spacing } from '../theme/designSystem';
- import { font, palette, spacing } from '../theme/designSystem';

  card: {
    alignSelf: 'center',
    width: '100%',
    maxWidth: 420,
    backgroundColor: 'rgba(255, 255, 255, 0.06)',
-   borderRadius: 20,
+   borderRadius: radii.lg,
    borderWidth: 1,
    borderColor: 'rgba(255, 255, 255, 0.1)',
    padding: spacing.lg,
  },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@screens/MaintenanceScreen.tsx` around lines 70 - 79, The card style object in
MaintenanceScreen.tsx uses a hardcoded borderRadius: 20; replace this with the
design-system token radii.lg to keep styling consistent—update the card style
(the card property in the StyleSheet) to use radii.lg instead of the literal 20
and ensure radii is imported/available in the file where the StyleSheet is
defined.
ios/syncre/Info.plist (1)

61-62: Inconsistent placeholder usage in permission string.

NSLocationWhenInUseUsageDescription uses a hardcoded "Syncre" while other permission strings (lines 55-66) use $(PRODUCT_NAME). Consider using the placeholder for consistency:

Suggested change
 <key>NSLocationWhenInUseUsageDescription</key>
-<string>Syncre uses your approximate location to keep message timestamps accurate.</string>
+<string>$(PRODUCT_NAME) uses your approximate location to keep message timestamps accurate.</string>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/syncre/Info.plist` around lines 61 - 62, Replace the hardcoded app name
in the NSLocationWhenInUseUsageDescription value with the product name
placeholder to match other permission strings; locate the Info.plist entry for
the key NSLocationWhenInUseUsageDescription and change "Syncre" to
$(PRODUCT_NAME) so the permission string uses the same placeholder pattern used
by the other permission keys.
screens/EditProfileScreen.tsx (2)

221-223: Misleading comment - this code path is used for all platforms.

The comment suggests this is Android-specific fallback, but the code below is the only render path and is used for both iOS and Android. Consider updating or removing the comment to avoid confusion.

Suggested change
  const displayImage = selectedImage?.uri || profilePicture;

-  // ═══════════════════════════════════════════════════════════════
-  // Android / Fallback: React Native components
-  // ═══════════════════════════════════════════════════════════════
-
   const renderSettingItem = (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@screens/EditProfileScreen.tsx` around lines 221 - 223, The comment block
labeled "Android / Fallback: React Native components" above the render path in
EditProfileScreen.tsx is misleading because that render path is used on all
platforms; update or remove that comment to accurately reflect its
cross-platform nature—either change the text to something like "Cross-platform:
React Native components" or delete the block; locate the comment near the main
render return in the EditProfileScreen component (the section demarcated with
the ═════════ line markers) and apply the change there.

410-418: Unused style definition.

The avatarSection style is defined but never referenced in the component. Consider removing it to reduce dead code.

Suggested removal
-  avatarSection: {
-    marginHorizontal: spacing.lg,
-    marginBottom: spacing.md,
-    backgroundColor: 'rgba(255, 255, 255, 0.04)',
-    borderRadius: radii.xl,
-    borderWidth: 1,
-    borderColor: 'rgba(255, 255, 255, 0.08)',
-    overflow: 'hidden',
-  },
   section: {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@screens/EditProfileScreen.tsx` around lines 410 - 418, Remove the dead style
object "avatarSection" from the styles in EditProfileScreen.tsx: locate the
styles definition containing avatarSection and delete that property (and any
trailing commas) since it's never referenced in the component; if you intend to
use it later, instead add a TODO comment and reference it from the appropriate
component (e.g., <Avatar /> container) to avoid unused-code warnings.
app/wrap/[date].tsx (1)

353-358: Consider using radii token for consistency.

The borderRadius: 20 is hardcoded here, while other GlassCard replacements in this PR use radii.xl or radii.lg from the design system. Using the token would maintain consistency.

Suggested change
  card: {
    width: '100%',
    maxWidth: layout.maxContentWidth,
    gap: spacing.md,
    backgroundColor: 'rgba(255, 255, 255, 0.06)',
-   borderRadius: 20,
+   borderRadius: radii.lg,
    borderWidth: 1,
    borderColor: 'rgba(255, 255, 255, 0.1)',
    padding: spacing.md,
  },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/wrap/`[date].tsx around lines 353 - 358, The hardcoded borderRadius: 20
in the style object (the block containing backgroundColor, borderWidth,
borderColor, padding) should be replaced with the design-system token (e.g.,
radii.xl or radii.lg) for consistency with other GlassCard replacements; locate
the style object in app/wrap/[date].tsx (the object that also defines
backgroundColor and padding) and swap borderRadius: 20 for the appropriate token
(radii.xl or radii.lg) and ensure the radii token is imported where needed.
app/_layout.tsx (1)

105-115: Consider removing or gating debug console.log statements for production.

These console.log statements are useful during development but may clutter production logs and expose internal routing decisions. Consider using a debug utility or environment-gated logging.

♻️ Suggested approach
+const DEBUG = __DEV__;
+const log = (...args: any[]) => DEBUG && console.log(...args);
+
 const responseSub = Notifications.addNotificationResponseReceivedListener((response) => {
-  console.log('📱 Notification tap received:', JSON.stringify(response?.notification?.request?.content?.data));
+  log('📱 Notification tap received:', JSON.stringify(response?.notification?.request?.content?.data));
   const chatId = extractChatIdFromNotification(response);
   const wrapDate = extractWrapDateFromNotification(response);
-  console.log('📱 Extracted - chatId:', chatId, 'wrapDate:', wrapDate);
+  log('📱 Extracted - chatId:', chatId, 'wrapDate:', wrapDate);
   if (wrapDate) {
-    console.log('📱 Navigating to wrap:', `/wrap/${wrapDate}`);
+    log('📱 Navigating to wrap:', `/wrap/${wrapDate}`);
     router.push(`/wrap/${wrapDate}` as any);
   } else if (chatId) {
-    console.log('📱 Navigating to chat:', `/chat/${chatId}`);
+    log('📱 Navigating to chat:', `/chat/${chatId}`);
     router.push(`/chat/${chatId}`);
   }
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/_layout.tsx` around lines 105 - 115, Remove or gate the console.log debug
output in the notification tap handler: replace the direct console.log calls
around extractChatIdFromNotification, extractWrapDateFromNotification and the
router.push branches with a controlled logger (e.g., debug/info) or wrap them
behind an environment check (NODE_ENV !== 'production' or a DEBUG flag). Ensure
logs still preserve the same messages when enabled but are suppressed in
production so routing decisions (router.push(`/wrap/${wrapDate}`) and
router.push(`/chat/${chatId}`)) remain unchanged except for logging.
utils/pollPayload.ts (1)

13-44: Consider extracting shared validation logic to reduce duplication.

Both decodePollPayload and decodePollPayloadFromJson share identical validation and object construction logic (lines 18-24 and 34-40). Extracting a shared validator would improve maintainability.

♻️ Proposed refactor
+const validateAndParsePollPayload = (parsed: any): PollPayload | null => {
+  if (!parsed || typeof parsed.question !== 'string' || !Array.isArray(parsed.options)) {
+    return null;
+  }
+  return {
+    question: parsed.question,
+    options: parsed.options.map((opt: any) => String(opt)),
+  };
+};
+
 export const decodePollPayload = (encoded: string): PollPayload | null => {
   if (!encoded) return null;
   try {
     const json = Buffer.from(encoded, 'base64').toString('utf8');
     const parsed = JSON.parse(json);
-    if (!parsed || typeof parsed.question !== 'string' || !Array.isArray(parsed.options)) {
-      return null;
-    }
-    return {
-      question: parsed.question,
-      options: parsed.options.map((opt: any) => String(opt)),
-    };
+    return validateAndParsePollPayload(parsed);
   } catch (error) {
     return null;
   }
 };

 export const decodePollPayloadFromJson = (raw: string): PollPayload | null => {
   if (!raw) return null;
   try {
     const parsed = JSON.parse(raw);
-    if (!parsed || typeof parsed.question !== 'string' || !Array.isArray(parsed.options)) {
-      return null;
-    }
-    return {
-      question: parsed.question,
-      options: parsed.options.map((opt: any) => String(opt)),
-    };
+    return validateAndParsePollPayload(parsed);
   } catch (error) {
     return null;
   }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@utils/pollPayload.ts` around lines 13 - 44, Both decodePollPayload and
decodePollPayloadFromJson duplicate the same validation/construct logic; extract
that shared logic into a single helper (e.g., parsePollPayload or
validateAndBuildPollPayload) that accepts a parsed object and returns
PollPayload | null after checking typeof question === 'string' and
Array.isArray(options') and mapping options to String; then have
decodePollPayload decode base64 to JSON and call the helper, and have
decodePollPayloadFromJson JSON.parse the raw string and call the same helper to
eliminate duplication while preserving current behavior and error handling.
screens/VerifyScreen.tsx (1)

229-238: Consider using theme tokens for the hardcoded card colors.

The backgroundColor and borderColor use hardcoded rgba values. For consistency with the design system and easier theme adjustments, consider extracting these to palette or a shared card style.

♻️ Example using theme tokens
 card: {
   width: '100%',
   maxWidth: 420,
   alignSelf: 'center',
   padding: spacing.lg,
-  backgroundColor: 'rgba(255, 255, 255, 0.04)',
+  backgroundColor: palette.surface, // or add a new token like palette.cardBackground
   borderRadius: radii.xl,
   borderWidth: 1,
-  borderColor: 'rgba(255, 255, 255, 0.08)',
+  borderColor: palette.border, // or a new token
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@screens/VerifyScreen.tsx` around lines 229 - 238, The card style in
VerifyScreen (the `card` object) uses hardcoded rgba values for
`backgroundColor` and `borderColor`; replace these with theme tokens (e.g., from
your `palette` or shared `card` style) so colors come from the design system.
Update the `card` style to reference the appropriate tokens (like
`palette.cardBackground` / `palette.cardBorder` or a shared `styles.card` token)
and ensure the VerifyScreen imports the theme/palette or shared style module and
uses those tokens instead of the rgba literals.
components/SpotifyConnection.tsx (1)

147-162: Consider gating debug console.log statements for production.

Similar to app/_layout.tsx, these debug logs are helpful during development but should be gated to avoid cluttering production logs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/SpotifyConnection.tsx` around lines 147 - 162, The debug
console.log calls around the OAuth flow (the logs before/after calling
WebBrowser.openAuthSessionAsync, the log of result.type, and the Success URL log
when checking result.type === 'success' in SpotifyConnection.tsx) should be
gated for non-production; wrap these console.log statements behind a
development-only flag (e.g. __DEV__ or an existing environment/logger utility)
or replace them with a logger that respects log-levels so they don't run in
production. Locate the block that calls WebBrowser.openAuthSessionAsync and the
subsequent `console.log('[Spotify] Auth session result:'...` and
`console.log('[Spotify] Success URL:'...)` and update them to be conditional on
the dev flag or routed through the centralized logger used elsewhere (matching
the pattern used in app/_layout.tsx).
components/GlassCard.tsx (1)

17-17: Unused import: padding modifier is imported but never used.

The padding modifier is imported from @expo/ui/swift-ui/modifiers (line 27) but is not used anywhere in the component. The padding is applied via React Native's style prop instead.

♻️ Remove unused import
 if (isIOS) {
   try {
     const swiftUI = require('@expo/ui/swift-ui');
     SwiftUIHost = swiftUI.Host;
     SwiftUIRoundedRectangle = swiftUI.RoundedRectangle;
     GlassEffectContainer = swiftUI.GlassEffectContainer;
     const modifiers = require('@expo/ui/swift-ui/modifiers');
     glassEffect = modifiers.glassEffect;
-    padding = modifiers.padding;
   } catch (e) {
     console.warn('SwiftUI components not available:', e);
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/GlassCard.tsx` at line 17, Remove the unused padding modifier:
delete the unused declaration "let padding: any = null;" and remove the unused
imported "padding" from the modifiers import in GlassCard (the import from
`@expo/ui/swift-ui/modifiers` and the local variable), ensuring padding is only
applied via the component's style prop and no references to the padding symbol
remain in GlassCard.tsx.
app/chat/[id].tsx (1)

641-642: Bound decryptionCache to avoid unbounded memory growth.
A size cap (or chat-scoped eviction) prevents long-lived sessions from accumulating decrypted payloads indefinitely.

♻️ Suggested size cap for the decryption cache
-const decryptionCache = new Map<string, string>();
+const DECRYPTION_CACHE_LIMIT = 500;
+const decryptionCache = new Map<string, string>();
-          if (decrypted) {
-            content = decrypted;
-            decryptionCache.set(cacheKey, decrypted);
-          } else {
+          if (decrypted) {
+            content = decrypted;
+            if (decryptionCache.size >= DECRYPTION_CACHE_LIMIT) {
+              const [oldestKey] = decryptionCache.keys();
+              if (oldestKey) {
+                decryptionCache.delete(oldestKey);
+              }
+            }
+            decryptionCache.set(cacheKey, decrypted);
+          } else {

Also applies to: 2378-2409

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/chat/`[id].tsx around lines 641 - 642, The global decryptionCache is
unbounded; replace it with a bounded cache (e.g., an LRU) or make it chat-scoped
and evict on chat change to prevent memory growth: implement a small LRUMap
wrapper (or use an existing LRU util) and instantiate it instead of new Map in
the declaration referenced by decryptionCache, enforce a reasonable maxEntries
(e.g., 250–1000) and evict the oldest entry on put when size > maxEntries, or
alternatively tie the cache lifecycle to the current chat id (clear or recreate
decryptionCache when the chat id changes) so decrypted payloads don’t accumulate
indefinitely.
app.json (1)

9-11: Verify New Architecture compatibility before release.

Enabling newArchEnabled: true can break native modules that aren’t compatible yet. Please validate builds and runtime on both platforms with your current dependency set.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app.json` around lines 9 - 11, The PR enables the new React Native
architecture via the JSON key newArchEnabled which may break native modules;
validate by building and running the app on both iOS and Android with
newArchEnabled set to true and false, exercise features that use native modules
(modules referenced by userInterfaceStyle and jsEngine changes may be impacted),
and if any native dependency fails, either revert newArchEnabled to false in
app.json or update/patch the incompatible native modules before merging; report
which platform(s), build errors or runtime crashes you observed and include
reproduction steps and the specific failing module names.
components/PollMessage.tsx (2)

30-44: Consider typing the new encrypted payload fields instead of any.

These fields are part of a public interface; any removes type safety and makes integration harder. If you already have payload/backup-envelope types (e.g., in poll payload utilities), wire them in here to catch mismatches at compile time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/PollMessage.tsx` around lines 30 - 44, The PollData interface
currently uses `any` for encryptedPayload and backupEnvelopes; replace those
with concrete types (e.g., the encrypted poll payload and envelope types used
elsewhere) to regain type safety: import the appropriate types from the poll
payload utilities (or the module that defines poll payload/envelope types) and
update `encryptedPayload?: <EncryptedPollPayloadType>` and `backupEnvelopes?:
<BackupEnvelopeType[]>` (and tighten `payloadVersion?: number`/`senderDeviceId?:
string | null` if needed) in the PollData declaration so the compiler enforces
structure instead of `any`.

115-148: Avoid indexOf inside map for fallback option labels.

Using indexOf makes this O(n²) and relies on reference equality. Prefer the map index instead.

♻️ Suggested change
-        {poll.options.map((option) => {
+        {poll.options.map((option, index) => {
           const optionLabel =
             typeof option.text === 'string' && option.text.trim().length > 0
               ? option.text
-              : `Option ${option.id ?? poll.options.indexOf(option) + 1}`;
+              : `Option ${option.id ?? index + 1}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/PollMessage.tsx` around lines 115 - 148, The fallback label
computation for optionLabel currently uses poll.options.indexOf(option) which is
O(n²) and relies on reference equality; inside the map that renders options
(where optionLabel, isVoted, voteCount, percentage, voters are computed) replace
the indexOf usage by using the map callback's index parameter to compute the
fallback: use `Option ${option.id ?? (index + 1)}` (ensure you capture the index
in the map signature). Update the occurrence in the optionLabel expression so it
no longer calls poll.options.indexOf(option).
.github/workflows/android-build.yml (1)

41-49: Gradle cache key should include nested Gradle files.

Current patterns don’t match android/app/build.gradle, so cache keys won’t update when app Gradle changes.

♻️ Suggested change
-          key: ${{ runner.os }}-gradle-${{ hashFiles('**/Mobile/android/*.gradle*', '**/Mobile/android/gradle-wrapper.properties') }}
+          key: ${{ runner.os }}-gradle-${{ hashFiles('**/Mobile/android/**/*.gradle*', '**/Mobile/android/**/gradle-wrapper.properties') }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/android-build.yml around lines 41 - 49, The "Cache Gradle"
step's key currently hashes only top-level patterns and misses nested files like
android/app/build.gradle; update the key expression used in that step (the key:
${{ runner.os }}-gradle-${{ hashFiles(...) }}) to include recursive patterns
such as '**/Mobile/android/**/*.gradle*' and
'**/Mobile/android/**/gradle-wrapper.properties' (or alternatively
'**/android/**/*.gradle*' if repo layout varies) so changes to nested Gradle
files update the cache key and invalidate the cache as expected.

Comment thread .github/workflows/android-build.yml
Comment thread app/(tabs)/_layout.tsx
Comment thread app/(tabs)/index.tsx
Comment thread app/chat/[id].tsx
const [showEphemeralSheet, setShowEphemeralSheet] = useState(false);
const [isCreatingPoll, setIsCreatingPoll] = useState(false);
const [pollsData, setPollsData] = useState<Map<string, { poll: PollData; userVotes: number[] }>>(new Map());
const pollDecryptAttemptedRef = useRef<Set<string>>(new Set());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Poll decryption can get stuck after a failed attempt.
The attempt key omits chatId and is recorded before a successful decrypt/parse, so a failed attempt (or another chat with the same messageId) can permanently block retries—even after re-encrypt.

🛠️ Suggested fix: scope the key to chatId and mark only on success
-      const attemptKey = `${messageId}:${poll.payloadVersion || 1}:${Array.isArray(poll.encryptedPayload) ? poll.encryptedPayload.length : 0}`;
-      if (pollDecryptAttemptedRef.current.has(attemptKey)) {
-        return;
-      }
-      pollDecryptAttemptedRef.current.add(attemptKey);
+      const attemptKey = `${chatId}:${messageId}:${poll.payloadVersion || 1}:${
+        Array.isArray(poll.encryptedPayload) ? poll.encryptedPayload.length : 0
+      }`;
+      if (pollDecryptAttemptedRef.current.has(attemptKey)) {
+        return;
+      }
...
-        if (!decoded) {
-          return;
-        }
+        if (!decoded) {
+          return;
+        }
+        pollDecryptAttemptedRef.current.add(attemptKey);

If you still want to avoid tight retry loops, consider a short cooldown or clearing the set when new envelopes are appended.

Also applies to: 4999-5017, 5073-5078

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/chat/`[id].tsx at line 1731, The poll decryption retry logic uses
pollDecryptAttemptedRef (a Set of keys) but builds keys without chatId and marks
attempts before successful decrypt/parse, causing permanent blocks across chats
or on failures; change the key to include chatId (e.g.,
`${chatId}:${messageId}`) and only add the key to pollDecryptAttemptedRef after
a successful decrypt/parse in the decrypt/parse success path (not before), and
optionally implement a short cooldown or clear the set when new envelopes are
appended to avoid permanent blocks.

Comment thread app/profile.tsx
Comment thread components/ProfileHeaderWidget.tsx
Comment thread ios/syncre.xcodeproj/project.pbxproj
Comment thread ios/syncre.xcworkspace/contents.xcworkspacedata
Comment thread services/CryptoService.ts
Comment thread services/ReencryptionService.ts
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.

1 participant