Skip to content

iOS: add buddy session host/join flow with deep links#220

Merged
auerbachb merged 14 commits into
mainfrom
cursor/ios-buddy-video-support-f581
Apr 24, 2026
Merged

iOS: add buddy session host/join flow with deep links#220
auerbachb merged 14 commits into
mainfrom
cursor/ios-buddy-video-support-f581

Conversation

@auerbachb
Copy link
Copy Markdown
Owner

@auerbachb auerbachb commented Apr 24, 2026

Summary

  • add buddy session DTOs and APIClient methods for create/join/snapshot/ready/start/leave/cancel/meeting-token/record-personal-session
  • add iOS buddy session navigation states and deep-link handling for stillpoint://buddy/TOKEN plus ?buddy= links
  • implement buddy hub, waiting room, active session, completion handling, and container orchestration with polling
  • add iOS video embedding path via WKWebView using Daily room URL + meeting token
  • update Info.plist with URL scheme and camera/mic/background mode entries for partner video sessions
  • add compile robustness fix for buddy views by importing UIKit where UIPasteboard/UIViewRepresentable are used

Follow-up review fixes

  • remove unsupported voip background mode from iOS Info.plist (leave audio)
  • fix AppView.Equatable for .buddySession(sessionId:) so different sessions are not treated as equal
  • force buddy container re-init per session with .id(sessionId) in RootView
  • harden buddy polling loop cancellation handling and prevent stale snapshot overwrite races
  • harden meeting token fetch state handling and retry behavior
  • ensure waiting room ready toggle/leave/cancel behaviors only exit on successful server updates
  • support stillpoint://buddy/TOKEN parsing in hub token extraction
  • always replace Daily t query parameter in buddy web video view
  • disable and guard "Return home without saving" while completion save is active
  • remove unreachable/dead completion callback path from BuddyActiveSessionView
  • isolate AppViewModel on @MainActor to keep observable state mutations on main thread
  • clear stale meeting token immediately when buddy snapshot revision/key changes
  • throttle active-session timer end refresh to one in-flight completion refresh request

Final cleanup pass

  • centralize invite token parsing into a shared BuddyInviteTokenParser and reuse it from both AppViewModel and BuddySessionHubView
  • constrain ?token= parsing to buddy-specific routes only (prevents misclassifying unrelated deep links)
  • configure ISO parsing for fractional-second timestamps with fallback parser for non-fractional server values
  • harden completion handoff checks in BuddySessionContainerView to prevent stale async completion from overriding newer navigation state
  • remove raw Daily room URL from the user-facing active session UI
  • avoid deep-link interruption of active sessions by queueing invite tokens while in-session and consuming them when returning home/after leave
  • simplify redundant main-actor wrappers in meeting-token fetch updates
  • clarify completion UI messaging from "Saving..." to "Preparing to save..." when save has not started yet
  • fix completion-screen regression by validating SessionDTO.buddySessionId against buddy snapshot id (instead of comparing mismatched entity ids)
  • remove unused participantCompleteAndLeave dead code from buddy session view model
  • prevent WKWebView infinite reload loops by tracking the last requested URL in a UIViewRepresentable coordinator
  • remove unused iOS API client buddyParticipantComplete method after dead-code cleanup

Latest unresolved-thread pass

  • surface deep-link join errors in buddy hub by syncing local error state from appVM.buddyInviteError and initializing existing error state when navigating into the hub
  • tighten raw-string token fallback behavior by disabling raw fallback for app-level deep link parsing while keeping join input fallback in hub UI
  • replace diagnostic timer subtitle with clear end-user session phrasing
  • add WKUIDelegate media-capture permission handling and set uiDelegate on WKWebView with explicit main-actor decision handling

Notes

  • CodeRabbit local/github rule files referenced in the issue were not present in this repository, so implementation proceeded without those local rule docs.
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Shared meditation: hub, waiting room, and active session flows with host controls, join/create via invite link or token, and in-app video playback.
    • Mind-state tracking, note capture, and personal-session save/export; home adds “Meditate with a friend”.
    • Deep-link handling: incoming invite URLs are processed and can auto-join after login; invite link copy/share UI.
  • Permissions / Background

    • Camera & microphone usage strings added; background audio enabled.
  • Bug Fixes / UX

    • Improved invite error feedback and join flow resilience.

Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
still-point Ignored Ignored Preview Apr 24, 2026 4:37am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Buddy Sessions feature: Xcode project entries and Info.plist updates, new shared API endpoints and DTOs, a BuddySessionViewModel with polling and session actions, five SwiftUI views (hub, waiting room, active, container, video), and AppViewModel/Root/Home navigation and URL handling.

Changes

Cohort / File(s) Summary
Project & App Configuration
ios/StillPoint.xcodeproj/project.pbxproj, ios/StillPointApp/Info.plist
Added Xcode entries/groups for new Buddy Swift files; registered stillpoint URL scheme; added NSCameraUsageDescription and NSMicrophoneUsageDescription; enabled UIBackgroundModes (audio).
App Navigation / URL Handling
ios/StillPointApp/ViewModels/AppViewModel.swift, ios/StillPointApp/Views/RootView.swift, ios/StillPointApp/Views/HomeView.swift
Added .buddyHub and .buddySession(sessionId:) app views, deep-link handling and invite token lifecycle, HomeView button to begin buddy sessions and surface invite errors.
Buddy Session ViewModel
ios/StillPointApp/ViewModels/BuddySessionViewModel.swift
New @MainActor observable VM: periodic snapshot polling with stale-response guards, meeting-token fetching/deduping, mind-state & thought logging, session actions (ready/start/leave/cancel/participant-complete), and personal-session recording flow.
Buddy Session UI Views
ios/StillPointApp/Views/BuddySession/...
BuddyActiveSessionView.swift, BuddySessionContainerView.swift, BuddySessionHubView.swift, BuddyVideoWebView.swift, BuddyWaitingRoomView.swift
Added five SwiftUI components: hub (create/join/share), waiting room (ready toggle, host controls), active session (timer/participants/controls/note capture), WKWebView wrapper injecting meeting token, and container orchestrating polling, state transitions, and completion save flow.
API Client & Error Model
ios/StillPointShared/Sources/StillPointShared/APIClient.swift
Added public async APIs for buddy session lifecycle (create/join/snapshot/meeting-token/ready/start/leave/cancel/participant-complete/record-personal-session). APIError now includes optional code decoded from server error responses.
DTOs / Data Models
ios/StillPointShared/Sources/StillPointShared/DTOs/DTOs.swift
Extended SessionDTO with buddySessionId: String?. Added request/response DTOs for buddy session creation, joining, snapshot, meeting token, readiness/start/cancel, and personal-session recording.

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as "SwiftUI Views"
    participant AppVM as "AppViewModel"
    participant VM as "BuddySessionViewModel"
    participant API as "APIClient"
    participant Server as "Server"

    User->>UI: Tap "Meditate with a friend"
    UI->>AppVM: beginBuddySession()
    AppVM->>UI: navigate to BuddySessionHubView

    UI->>API: createBuddySession() / joinBuddySession(token)
    API->>Server: POST /api/buddy/sessions
    Server-->>API: sessionId / DTO
    API-->>AppVM: return sessionId
    AppVM->>UI: enterBuddySession(sessionId)
    UI->>VM: init(sessionId) / startPolling()

    loop periodic polling (every ~1.5s)
      VM->>API: getBuddySnapshot(sessionId)
      API->>Server: GET /api/buddy/sessions/{id}/snapshot
      Server-->>API: BuddySnapshotDTO
      API-->>VM: snapshot
      VM->>UI: update view (waiting / active / completed)
    end

    alt active & dailyRoomUrl present
      VM->>API: getBuddyMeetingToken(sessionId)
      API->>Server: GET /meeting-token
      Server-->>API: token
      API-->>VM: meeting token
      VM->>UI: BuddyVideoWebView loads URL with ?t=token
    end

    UI->>VM: setReady()/startSession()/leaveSession()/savePersonalSession()
    VM->>API: setBuddyReady()/startBuddySession()/leaveBuddySession()/recordBuddyPersonalSession()
    API->>Server: POST/DELETE endpoints
    Server-->>API: success / SessionDTO
    API-->>VM: response
    VM->>AppVM: notify completion
    AppVM->>UI: navigate back home
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hop to join, a tiny cheer, a token shared, a friend is near.
Timers tick and snapshots hum, together breathe — the calm will come.
Notes I tuck, thoughts softly penned, buddy sessions bloom — code and friend.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective: adding buddy session host/join functionality with deep link support, which aligns with the core changes across DTOs, APIClient, view models, and UI components.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 cursor/ios-buddy-video-support-f581

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

@auerbachb auerbachb marked this pull request as ready for review April 24, 2026 02:22
Comment thread ios/StillPointApp/Views/RootView.swift
Comment thread ios/StillPointApp/ViewModels/BuddySessionViewModel.swift Outdated
Comment thread ios/StillPointApp/ViewModels/BuddySessionViewModel.swift Outdated
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: 10

🧹 Nitpick comments (1)
ios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swift (1)

53-57: This completion-save path is unreachable from BuddyActiveSessionView.

BuddySessionContainerView only instantiates this screen from its "active" branch, so snapshot.state here never becomes "completed". That makes the .task(id: snapshot.state) block and handleCompletionSave() dead code. I'd either delete this path or keep completion handling solely in the container.

Also applies to: 266-275

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

In `@ios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swift` around
lines 53 - 57, BuddyActiveSessionView contains a .task(id: snapshot.state) block
that checks for snapshot.state == "completed" and calls handleCompletionSave(),
but BuddySessionContainerView only presents BuddyActiveSessionView from its
"active" branch so snapshot.state never becomes "completed" here; remove the
unreachable .task(id: snapshot.state) block and the handleCompletionSave() call
(and any related dead helper code) from BuddyActiveSessionView, or alternatively
move/centralize completion handling into BuddySessionContainerView where the
"completed" path is reachable; target symbols: BuddyActiveSessionView, .task(id:
snapshot.state), handleCompletionSave(), BuddySessionContainerView.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ios/StillPointApp/Info.plist`:
- Around line 46-50: Remove the unsupported VoIP background mode: edit
Info.plist's UIBackgroundModes array and delete the <string>voip</string> entry
(leaving <string>audio</string> intact) so the app no longer declares VoIP
support; confirm there are no PushKit/CallKit references elsewhere before
submitting to App Review.

In `@ios/StillPointApp/ViewModels/AppViewModel.swift`:
- Around line 20-21: The Equatable implementation currently treats all
.buddySession enum cases as equal which hides transitions; update the equality
logic in the enum used by AppViewModel (the type that implements Equatable and
has the .buddySession case) so that when comparing two .buddySession values you
compare their associated sessionId instead of returning true for any pair, i.e.,
match both cases to .buddySession(let a) and .buddySession(let b) and compare
a.sessionId == b.sessionId; leave other case comparisons unchanged.

In `@ios/StillPointApp/ViewModels/BuddySessionViewModel.swift`:
- Around line 60-74: refreshSnapshot() can race: an older slow response may
overwrite a newer snapshot; solve by adding a monotonic request-sequence token
on the ViewModel (e.g., refreshRequestCounter: Int) that you increment at the
start of refreshSnapshot(), capture into a local let requestId, then after
awaiting APIClient.shared.getBuddySnapshot(sessionId:) check that requestId ==
refreshRequestCounter before mutating snapshot, pollError, isLoading, or calling
handleSnapshotUpdate(snapshot); this ensures only the latest response applies
(alternatively implement a single-flight bool like isRefreshing and skip/start
accordingly), reference refreshSnapshot(), snapshot, sessionId,
handleSnapshotUpdate(snapshot) and update state only when the sequence check
passes.
- Around line 270-300: maybeFetchMeetingToken currently commits
lastMeetingTokenFetchKey before the network call which suppresses retries and
allows stale responses to win; change this so the key is NOT assigned before the
Task: compute let requestKey =
"\(snapshot.id):\(snapshot.revision):\(dailyRoomURL)" and do not set
lastMeetingTokenFetchKey = requestKey upfront, start the Task, then on success
assign lastMeetingTokenFetchKey = requestKey and update
meetingToken/meetingTokenError only if lastMeetingTokenFetchKey is nil or equal
to requestKey (to avoid overwriting newer results); on failure do not set
lastMeetingTokenFetchKey so retries are allowed, and similarly only apply error
state if lastMeetingTokenFetchKey is nil or equals requestKey. Ensure you
reference maybeFetchMeetingToken, lastMeetingTokenFetchKey, meetingToken,
meetingTokenError, and APIClient.shared.getBuddyMeetingToken(sessionId:).
- Around line 45-50: The polling loop in pollTask uses "try? await
Task.sleep(...)" which swallows CancellationError so the loop still calls await
refreshSnapshot() once more after stopPolling() cancels the task; change the
sleep to either do { try await Task.sleep(...) } catch is CancellationError {
return } or after the sleep check "if Task.isCancelled { return }" before
calling await refreshSnapshot(), referencing the pollTask Task and
refreshSnapshot() method so cancellation is handled immediately and no extra
network call runs.

In `@ios/StillPointApp/Views/BuddySession/BuddySessionContainerView.swift`:
- Around line 76-111: Disable the "Return home without saving" button while
vm.isSavingCompletion is true and make its action fully await vm.leaveSession();
only call appVM.leaveBuddySession() if leaveSession() succeeds (handle failures
instead of ignoring), and ensure saveCompletionIfPossible() (or the completion
callback that calls appVM.completeSession(...)) checks a shared state/flag
(e.g., vm.didLeave or appVM.isInBuddySession) before calling
appVM.completeSession so a concurrently finishing save cannot complete the
session after navigation. Reference: saveCompletionIfPossible(),
vm.isSavingCompletion, vm.leaveSession(), appVM.leaveBuddySession(),
appVM.completeSession(...).

In `@ios/StillPointApp/Views/BuddySession/BuddySessionHubView.swift`:
- Around line 201-220: The extractBuddyToken(_:) function currently doesn't
handle deep-link invites like stillpoint://buddy/TOKEN; update it to recognize
that format by parsing the URL's scheme/host/path and returning the path segment
after "/buddy/" (or the first path component when host == "buddy") as the token
(trimmed of whitespace), before falling back to the existing query-based and
raw-string logic; keep the existing behavior for queryItems ("buddy=") and raw
fallthrough intact and ensure empty results still return "".

In `@ios/StillPointApp/Views/BuddySession/BuddyVideoWebView.swift`:
- Around line 23-26: The code currently only appends a "t" URLQueryItem when
missing, so updates to meetingToken are ignored; modify the logic around query
(used in BuddyVideoWebView.swift where hasToken, query and meetingToken are
referenced) to always replace or set the "t" parameter: find the existing
URLQueryItem with name "t" and update its value to meetingToken if present,
otherwise append a new URLQueryItem(name: "t", value: meetingToken), ensuring
the final query array always contains the current token.

In `@ios/StillPointApp/Views/BuddySession/BuddyWaitingRoomView.swift`:
- Around line 39-50: The Toggle currently optimistically sets myReady before
awaiting vm.setReady(newValue) which can leave the UI showing ready when the API
call fails; modify the binding so either (a) the Toggle's value is derived
solely from vm.snapshot (e.g., use me.ready from vm.snapshot) removing the
optimistic myReady, or (b) detect failure from vm.setReady(newValue) (use the
returned Bool or try/catch if it throws) and on failure run MainActor to set
myReady back to its previous value and clear isUpdatingReady; specifically
update the Toggle Binding (get/set) around myReady, vm.setReady(_:) and
isUpdatingReady to perform rollback on error and ensure all state updates run on
MainActor.
- Around line 92-120: The Button actions call onExit() unconditionally which
hides vm.actionError when cancelSession() or leaveSession() fails; change both
Task blocks that call vm.cancelSession() and vm.leaveSession() to await the Bool
result and only call onExit() when the result is true (do not call onExit() on
false), leaving vm.actionError visible for the UI to surface the failure;
reference the async methods vm.cancelSession(), vm.leaveSession() and the exit
callback onExit() when making this change.

---

Nitpick comments:
In `@ios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swift`:
- Around line 53-57: BuddyActiveSessionView contains a .task(id: snapshot.state)
block that checks for snapshot.state == "completed" and calls
handleCompletionSave(), but BuddySessionContainerView only presents
BuddyActiveSessionView from its "active" branch so snapshot.state never becomes
"completed" here; remove the unreachable .task(id: snapshot.state) block and the
handleCompletionSave() call (and any related dead helper code) from
BuddyActiveSessionView, or alternatively move/centralize completion handling
into BuddySessionContainerView where the "completed" path is reachable; target
symbols: BuddyActiveSessionView, .task(id: snapshot.state),
handleCompletionSave(), BuddySessionContainerView.
🪄 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: CHILL

Plan: Pro

Run ID: 4d6fcd49-3568-4eed-84d3-6dc8e069c076

📥 Commits

Reviewing files that changed from the base of the PR and between eb9f13a and 9c66aa9.

📒 Files selected for processing (13)
  • ios/StillPoint.xcodeproj/project.pbxproj
  • ios/StillPointApp/Info.plist
  • ios/StillPointApp/ViewModels/AppViewModel.swift
  • ios/StillPointApp/ViewModels/BuddySessionViewModel.swift
  • ios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swift
  • ios/StillPointApp/Views/BuddySession/BuddySessionContainerView.swift
  • ios/StillPointApp/Views/BuddySession/BuddySessionHubView.swift
  • ios/StillPointApp/Views/BuddySession/BuddyVideoWebView.swift
  • ios/StillPointApp/Views/BuddySession/BuddyWaitingRoomView.swift
  • ios/StillPointApp/Views/HomeView.swift
  • ios/StillPointApp/Views/RootView.swift
  • ios/StillPointShared/Sources/StillPointShared/APIClient.swift
  • ios/StillPointShared/Sources/StillPointShared/DTOs/DTOs.swift

Comment thread ios/StillPointApp/Info.plist
Comment thread ios/StillPointApp/ViewModels/AppViewModel.swift Outdated
Comment thread ios/StillPointApp/ViewModels/BuddySessionViewModel.swift
Comment thread ios/StillPointApp/ViewModels/BuddySessionViewModel.swift
Comment thread ios/StillPointApp/ViewModels/BuddySessionViewModel.swift
Comment thread ios/StillPointApp/Views/BuddySession/BuddySessionHubView.swift Outdated
Comment thread ios/StillPointApp/Views/BuddySession/BuddyVideoWebView.swift Outdated
Comment thread ios/StillPointApp/Views/BuddySession/BuddyWaitingRoomView.swift
Comment thread ios/StillPointApp/Views/BuddySession/BuddyWaitingRoomView.swift Outdated
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
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.

♻️ Duplicate comments (1)
ios/StillPointApp/Views/BuddySession/BuddyVideoWebView.swift (1)

24-27: ⚠️ Potential issue | 🟠 Major

Always replace the t query item, don’t only append when missing.

Line 24-27 keeps an existing t value, so token refreshes can be ignored and the web view may continue loading with a stale meeting token.

Proposed fix
-        let hasToken = query.contains { $0.name == "t" }
-        if !hasToken {
-            query.append(URLQueryItem(name: "t", value: meetingToken))
-        }
+        query.removeAll { $0.name == "t" }
+        query.append(URLQueryItem(name: "t", value: meetingToken))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/StillPointApp/Views/BuddySession/BuddyVideoWebView.swift` around lines 24
- 27, The code currently only appends a URLQueryItem "t" when missing, which
preserves stale tokens; modify the logic around the query array so that any
existing URLQueryItem with name "t" is removed or replaced and a new
URLQueryItem(name: "t", value: meetingToken) is always added/updated (update the
block using hasToken, query and URLQueryItem to filter out existing "t" items or
map them to the new meetingToken), ensuring the web view always gets the fresh
meetingToken.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@ios/StillPointApp/Views/BuddySession/BuddyVideoWebView.swift`:
- Around line 24-27: The code currently only appends a URLQueryItem "t" when
missing, which preserves stale tokens; modify the logic around the query array
so that any existing URLQueryItem with name "t" is removed or replaced and a new
URLQueryItem(name: "t", value: meetingToken) is always added/updated (update the
block using hasToken, query and URLQueryItem to filter out existing "t" items or
map them to the new meetingToken), ensuring the web view always gets the fresh
meetingToken.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2b1e4305-4871-4c1f-9fd7-6c54f92bee44

📥 Commits

Reviewing files that changed from the base of the PR and between 9c66aa9 and bde6cc0.

📒 Files selected for processing (2)
  • ios/StillPointApp/Views/BuddySession/BuddySessionHubView.swift
  • ios/StillPointApp/Views/BuddySession/BuddyVideoWebView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • ios/StillPointApp/Views/BuddySession/BuddySessionHubView.swift

cursoragent and others added 2 commits April 24, 2026 03:04
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
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: 3

♻️ Duplicate comments (1)
ios/StillPointApp/Views/BuddySession/BuddySessionContainerView.swift (1)

156-168: ⚠️ Potential issue | 🟠 Major

Guard the completion handoff against stale in-flight saves.

This only checks didExitWithoutSaving. If the container leaves for any other reason while savePersonalSession() is running, the old task can still call appVM.completeSession(...) and overwrite the newer screen state.

Suggested fix
     private func saveCompletionIfPossible() async {
         guard vm.snapshot?.state == "completed", !vm.isSavingCompletion else { return }
         guard !didExitWithoutSaving else { return }
         guard let saved = await vm.savePersonalSession() else { return }
-        guard !didExitWithoutSaving else { return }
+        guard !didExitWithoutSaving,
+              appVM.currentView == .buddySession(sessionId: sessionId) else { return }
         appVM.completeSession(
             sessionId: saved.id,
             clearPercent: saved.clearPercent,
             thoughtCount: saved.thoughtCount,
             thoughts: vm.capturedThoughts,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/StillPointApp/Views/BuddySession/BuddySessionContainerView.swift` around
lines 156 - 168, The completion handoff can run with stale state because only
didExitWithoutSaving is rechecked; update saveCompletionIfPossible to
re-validate that the container is still the active/completed session before
calling appVM.completeSession by adding post-await checks: after awaiting
vm.savePersonalSession() re-check vm.snapshot?.state == "completed",
!vm.isSavingCompletion, !didExitWithoutSaving (or compare a captured snapshot
id/token from vm.snapshot before the await to the current vm.snapshot?.id after
the await) and ensure the saved.id matches the current snapshot id; only call
appVM.completeSession(...) when these validations pass to avoid overwriting
newer screen state.
🧹 Nitpick comments (2)
ios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swift (1)

130-135: Remove the raw room URL from the user-facing card.

This looks like debug output and exposes the meeting endpoint without adding product value. I’d keep it out of the production UI unless it’s behind a debug-only flag.

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

In `@ios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swift` around
lines 130 - 135, The UI currently renders the raw meeting endpoint via
Text(roomURL) in BuddyActiveSessionView; remove this user-facing debug output or
gate it behind a non-production flag. Either delete the Text(roomURL) view block
(and its modifiers) from the BuddyActiveSessionView body, or wrap it with a
compile/runtime guard such as `#if` DEBUG (or an injected showDebugInfo Bool) so
the raw roomURL is only visible in debug builds; ensure no other UI references
rely on that Text view.
ios/StillPointApp/Views/BuddySession/BuddySessionHubView.swift (1)

202-235: Centralize invite-token parsing instead of maintaining two copies.

This is now duplicating the deep-link parsing logic in ios/StillPointApp/ViewModels/AppViewModel.swift. The next invite-format tweak will be easy to fix in one place and miss in the other.

Suggested direction
-    private func extractBuddyToken(_ raw: String) -> String {
-        ...
-    }
+    private func extractBuddyToken(_ raw: String) -> String {
+        BuddyInviteParser.extractToken(from: raw) ?? ""
+    }

A small shared helper used by both the hub and AppViewModel would keep the accepted formats in sync.

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

In `@ios/StillPointApp/Views/BuddySession/BuddySessionHubView.swift` around lines
202 - 235, The invite-token parsing logic in extractBuddyToken is duplicated in
AppViewModel; extract that logic into a single shared helper (e.g.,
InviteTokenParser.parse(_:) or an extension on String like String.inviteToken())
and replace calls from BuddySessionHubView.extractBuddyToken and the parsing in
AppViewModel to use that helper; ensure the new helper preserves existing
behaviors (trim, URL query "buddy"/"token", stillpoint://buddy path, and
fallback "buddy=" substring or raw string) and update both places to remove the
duplicate parsing code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ios/StillPointApp/ViewModels/AppViewModel.swift`:
- Around line 84-87: The AppViewModel's observable state must be isolated to the
main actor to avoid off-main mutations after await points; mark the view model
class as `@MainActor` (or mark mutating methods as `@MainActor`) and ensure any Task
closures that call joinBuddySession(token:) or mutate currentView /
buddyInviteError (e.g. didLogin, handleIncomingURL,
consumePendingBuddyInviteIfNeeded) perform those mutations on the main
actor—either by making the functions async `@MainActor` or wrapping state changes
in await MainActor.run { ... }—so all updates to currentUser, currentView, and
buddyInviteError happen on the main thread.

In `@ios/StillPointApp/ViewModels/BuddySessionViewModel.swift`:
- Around line 281-315: maybeFetchMeetingToken can leave a stale meetingToken
while a new request is in-flight; ensure that when you detect a new requestKey
you immediately clear cached state. In maybeFetchMeetingToken, after computing
requestKey and before setting latestMeetingTokenRequestKey (or immediately when
they differ), set meetingToken = nil and meetingTokenError = nil so the UI can't
use the previous token; then set latestMeetingTokenRequestKey = requestKey and
proceed with the Task that fetches via
APIClient.shared.getBuddyMeetingToken(sessionId:). Use the existing symbols
latestMeetingTokenRequestKey, meetingToken, meetingTokenError, requestKey and
keep the existing MainActor guards in the Task.

In `@ios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swift`:
- Around line 32-39: The timer handler currently calls vm.refreshSnapshot() on
every tick once vm.currentRemainingSeconds(at:) == 0, which can spam the
endpoint; add a one-shot guard (e.g., a `@State` Bool like didRequestRefresh) and
only start the Task when didRequestRefresh is false, then set didRequestRefresh
= true immediately after scheduling the Task; ensure the flag is reset when the
view model indicates the session is active again (for example in the snapshot
update path or when vm.currentRemainingSeconds(at:) > 0) so refreshSnapshot()
can be called again on a future expiry.

---

Duplicate comments:
In `@ios/StillPointApp/Views/BuddySession/BuddySessionContainerView.swift`:
- Around line 156-168: The completion handoff can run with stale state because
only didExitWithoutSaving is rechecked; update saveCompletionIfPossible to
re-validate that the container is still the active/completed session before
calling appVM.completeSession by adding post-await checks: after awaiting
vm.savePersonalSession() re-check vm.snapshot?.state == "completed",
!vm.isSavingCompletion, !didExitWithoutSaving (or compare a captured snapshot
id/token from vm.snapshot before the await to the current vm.snapshot?.id after
the await) and ensure the saved.id matches the current snapshot id; only call
appVM.completeSession(...) when these validations pass to avoid overwriting
newer screen state.

---

Nitpick comments:
In `@ios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swift`:
- Around line 130-135: The UI currently renders the raw meeting endpoint via
Text(roomURL) in BuddyActiveSessionView; remove this user-facing debug output or
gate it behind a non-production flag. Either delete the Text(roomURL) view block
(and its modifiers) from the BuddyActiveSessionView body, or wrap it with a
compile/runtime guard such as `#if` DEBUG (or an injected showDebugInfo Bool) so
the raw roomURL is only visible in debug builds; ensure no other UI references
rely on that Text view.

In `@ios/StillPointApp/Views/BuddySession/BuddySessionHubView.swift`:
- Around line 202-235: The invite-token parsing logic in extractBuddyToken is
duplicated in AppViewModel; extract that logic into a single shared helper
(e.g., InviteTokenParser.parse(_:) or an extension on String like
String.inviteToken()) and replace calls from
BuddySessionHubView.extractBuddyToken and the parsing in AppViewModel to use
that helper; ensure the new helper preserves existing behaviors (trim, URL query
"buddy"/"token", stillpoint://buddy path, and fallback "buddy=" substring or raw
string) and update both places to remove the duplicate parsing code.
🪄 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: CHILL

Plan: Pro

Run ID: 206aebe3-1f3e-4f9b-be88-42c88d84f208

📥 Commits

Reviewing files that changed from the base of the PR and between bde6cc0 and d550fba.

📒 Files selected for processing (9)
  • ios/StillPointApp/Info.plist
  • ios/StillPointApp/ViewModels/AppViewModel.swift
  • ios/StillPointApp/ViewModels/BuddySessionViewModel.swift
  • ios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swift
  • ios/StillPointApp/Views/BuddySession/BuddySessionContainerView.swift
  • ios/StillPointApp/Views/BuddySession/BuddySessionHubView.swift
  • ios/StillPointApp/Views/BuddySession/BuddyVideoWebView.swift
  • ios/StillPointApp/Views/BuddySession/BuddyWaitingRoomView.swift
  • ios/StillPointApp/Views/RootView.swift
✅ Files skipped from review due to trivial changes (1)
  • ios/StillPointApp/Info.plist
🚧 Files skipped from review as they are similar to previous changes (1)
  • ios/StillPointApp/Views/RootView.swift

Comment thread ios/StillPointApp/ViewModels/AppViewModel.swift
Comment thread ios/StillPointApp/ViewModels/BuddySessionViewModel.swift
Comment thread ios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swift
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
@auerbachb
Copy link
Copy Markdown
Owner Author

@cursor review

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ios/StillPointApp/ViewModels/AppViewModel.swift`:
- Around line 175-181: The extractBuddyToken(from:) function currently treats
any ?token= query as a buddy invite; update it so it only falls back to reading
the token query when the incoming URL is actually a buddy route (e.g., check
URLComponents.host and/or components.path for your buddy-specific host/path
segment such as "/buddy" or "invite/buddy"); keep the existing ?buddy= parsing
as-is, and only after failing that, check for ?token= if and only if the
host/path matches the buddy route (use the existing extractBuddyToken function
and URLComponents to perform the additional host/path validation before
returning the token).

In `@ios/StillPointApp/ViewModels/BuddySessionViewModel.swift`:
- Line 33: The ISO8601DateFormatter instance (isoFormatter) is not configured to
parse fractional seconds, so parseISO returns nil for timestamps like
"2026-04-24T01:23:58.000Z" and breaks startedAt/serverNow and active-session
anchor initialization; fix this by configuring isoFormatter.formatOptions to
include .withFractionalSeconds (e.g., set isoFormatter.formatOptions =
[.withInternetDateTime, .withFractionalSeconds]) and ensure parseISO uses that
isoFormatter when parsing startedAt and serverNow so fractional-second
timestamps are accepted.
🪄 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: CHILL

Plan: Pro

Run ID: 7dafa0ab-ea93-4b0a-8965-41b32746faf5

📥 Commits

Reviewing files that changed from the base of the PR and between d550fba and 094fc55.

📒 Files selected for processing (4)
  • ios/StillPointApp/ViewModels/AppViewModel.swift
  • ios/StillPointApp/ViewModels/BuddySessionViewModel.swift
  • ios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swift
  • ios/StillPointApp/Views/BuddySession/BuddyWaitingRoomView.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • ios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swift
  • ios/StillPointApp/Views/BuddySession/BuddyWaitingRoomView.swift

Comment thread ios/StillPointApp/ViewModels/AppViewModel.swift
Comment thread ios/StillPointApp/ViewModels/BuddySessionViewModel.swift Outdated
Comment thread ios/StillPointApp/ViewModels/AppViewModel.swift
Comment thread ios/StillPointApp/ViewModels/AppViewModel.swift
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
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.

🧹 Nitpick comments (2)
ios/StillPointApp/ViewModels/BuddySessionViewModel.swift (1)

305-327: Redundant MainActor.run blocks within @MainActor class.

Since BuddySessionViewModel is marked @MainActor, unstructured Task { } blocks created from within this class inherit MainActor isolation. The await MainActor.run { } calls at lines 309, 315, and 321 are unnecessary and add minor overhead.

Suggested simplification
         Task { [weak self] in
             guard let self else { return }
             do {
                 let token = try await APIClient.shared.getBuddyMeetingToken(sessionId: sessionId)
-                await MainActor.run {
-                    guard self.latestMeetingTokenRequestKey == requestKey else { return }
-                    self.meetingToken = token
-                    self.meetingTokenError = nil
-                }
+                guard self.latestMeetingTokenRequestKey == requestKey else { return }
+                self.meetingToken = token
+                self.meetingTokenError = nil
             } catch let error as APIError {
-                await MainActor.run {
-                    guard self.latestMeetingTokenRequestKey == requestKey else { return }
-                    self.meetingToken = nil
-                    self.meetingTokenError = error.message
-                }
+                guard self.latestMeetingTokenRequestKey == requestKey else { return }
+                self.meetingToken = nil
+                self.meetingTokenError = error.message
             } catch {
-                await MainActor.run {
-                    guard self.latestMeetingTokenRequestKey == requestKey else { return }
-                    self.meetingToken = nil
-                    self.meetingTokenError = "Could not get video token."
-                }
+                guard self.latestMeetingTokenRequestKey == requestKey else { return }
+                self.meetingToken = nil
+                self.meetingTokenError = "Could not get video token."
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/StillPointApp/ViewModels/BuddySessionViewModel.swift` around lines 305 -
327, Inside the `@MainActor` BuddySessionViewModel, remove the unnecessary await
MainActor.run { ... } wrappers inside the Task block and directly perform the UI
state updates; specifically, in the Task that calls
APIClient.shared.getBuddyMeetingToken(sessionId:), replace the three await
MainActor.run blocks by directly checking guard
self.latestMeetingTokenRequestKey == requestKey and then assigning
self.meetingToken, self.meetingTokenError (or nil) in the do, catch APIError,
and catch blocks respectively so the same logic remains but without redundant
MainActor.run calls.
ios/StillPointApp/Views/BuddySession/BuddySessionContainerView.swift (1)

90-94: Clarify the "waiting to save" UI state text.

When vm.isSavingCompletion is false and vm.completionSaveError is nil, this branch displays "Saving your personal session..." even though saving hasn't started yet (e.g., waiting for saveCompletionIfPossible to trigger). Consider using clearer text like "Preparing to save..." or simply showing a progress indicator without text to avoid implying an active save operation.

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

In `@ios/StillPointApp/Views/BuddySession/BuddySessionContainerView.swift` around
lines 90 - 94, The UI text currently shows "Saving your personal session..."
when vm.isSavingCompletion is false and vm.completionSaveError is nil, which
wrongly implies an active save; update the branch that renders Text("Saving your
personal session...") (in BuddySessionContainerView) to use a clearer message
like "Preparing to save..." or remove the text and show only a progress
indicator, and ensure logic still reflects vm.isSavingCompletion and
vm.completionSaveError (and interactions with saveCompletionIfPossible) so the
label only indicates an active save when vm.isSavingCompletion is true.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@ios/StillPointApp/ViewModels/BuddySessionViewModel.swift`:
- Around line 305-327: Inside the `@MainActor` BuddySessionViewModel, remove the
unnecessary await MainActor.run { ... } wrappers inside the Task block and
directly perform the UI state updates; specifically, in the Task that calls
APIClient.shared.getBuddyMeetingToken(sessionId:), replace the three await
MainActor.run blocks by directly checking guard
self.latestMeetingTokenRequestKey == requestKey and then assigning
self.meetingToken, self.meetingTokenError (or nil) in the do, catch APIError,
and catch blocks respectively so the same logic remains but without redundant
MainActor.run calls.

In `@ios/StillPointApp/Views/BuddySession/BuddySessionContainerView.swift`:
- Around line 90-94: The UI text currently shows "Saving your personal
session..." when vm.isSavingCompletion is false and vm.completionSaveError is
nil, which wrongly implies an active save; update the branch that renders
Text("Saving your personal session...") (in BuddySessionContainerView) to use a
clearer message like "Preparing to save..." or remove the text and show only a
progress indicator, and ensure logic still reflects vm.isSavingCompletion and
vm.completionSaveError (and interactions with saveCompletionIfPossible) so the
label only indicates an active save when vm.isSavingCompletion is true.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 20f61066-5805-4614-b1fc-c267d5a968b7

📥 Commits

Reviewing files that changed from the base of the PR and between 094fc55 and aa6e036.

📒 Files selected for processing (5)
  • ios/StillPointApp/ViewModels/AppViewModel.swift
  • ios/StillPointApp/ViewModels/BuddySessionViewModel.swift
  • ios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swift
  • ios/StillPointApp/Views/BuddySession/BuddySessionContainerView.swift
  • ios/StillPointApp/Views/BuddySession/BuddySessionHubView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • ios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swift

cursoragent and others added 2 commits April 24, 2026 03:47
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
@auerbachb
Copy link
Copy Markdown
Owner Author

@cursor review

Comment thread ios/StillPointApp/Views/BuddySession/BuddySessionContainerView.swift Outdated
Comment thread ios/StillPointApp/ViewModels/BuddySessionViewModel.swift Outdated
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
@auerbachb
Copy link
Copy Markdown
Owner Author

@cursor review

Comment thread ios/StillPointApp/Views/BuddySession/BuddyVideoWebView.swift
Comment thread ios/StillPointShared/Sources/StillPointShared/APIClient.swift
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
@auerbachb
Copy link
Copy Markdown
Owner Author

@cursor review

@auerbachb
Copy link
Copy Markdown
Owner Author

@greptileai

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 24, 2026

Greptile Summary

This PR adds the full iOS buddy (shared meditation) session flow: hub, waiting room, active session with video, and completion handoff, including stillpoint://buddy/TOKEN deep-link handling with invite queuing while in-session. The implementation is well-structured with @MainActor isolation, stale-snapshot guards, meeting-token retry fencing, and a coordinator-based WKWebView reload guard.

  • P1 — error swallowed after deep-link join failure: joinBuddySession on error sets buddyInviteError and navigates to .buddyHub, but BuddySessionHubView never reads appVM.buddyInviteError — only its own local errorMessage — so users sent to the hub after a failed invite link see no explanation. Either surface appVM.buddyInviteError in the hub view, or stay on the current view and let HomeView's existing error display do the work.
  • P2 — debug label in production UI: BuddyActiveSessionView.timerCard renders \"\\(snapshot.durationSeconds)s shared timer synced from server\" which looks like a development diagnostic string rather than user-facing copy.

Confidence Score: 4/5

Safe to merge after fixing the silent deep-link error path; no data loss or security concerns found.

One P1 issue: error feedback is broken for the deep-link join failure path — the user is silently redirected to the buddy hub with no message. All other findings are P2 (debug label, edge-case substring matching in the raw-string parser). The core session lifecycle, race-condition guards, and @mainactor isolation are sound.

ios/StillPointApp/ViewModels/AppViewModel.swift (joinBuddySession error path) and ios/StillPointApp/Views/BuddySession/BuddySessionHubView.swift (not reading appVM.buddyInviteError)

Important Files Changed

Filename Overview
ios/StillPointApp/ViewModels/AppViewModel.swift Adds buddy session navigation states, deep-link handling, and invite token queuing; has a P1 bug where deep-link join errors are silently swallowed after navigating to the hub, and a P2 raw-string substring-match issue in BuddyInviteTokenParser
ios/StillPointApp/ViewModels/BuddySessionViewModel.swift New view model for buddy session lifecycle: polling, mind-state tracking, meeting-token fetch with stale-guard, completion save; race conditions and concurrent-save guards are well-handled with @mainactor isolation
ios/StillPointApp/Views/BuddySession/BuddySessionContainerView.swift Orchestrates waiting room / active / completion states with polling and completion-handoff guards; snapshot-state fencing and currentView checks before navigating to completion are sound
ios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swift Timer-driven active session UI with mind-state and thought capture; one-shot completion-refresh throttle is correct; contains a debug label that will be visible to end users
ios/StillPointApp/Views/BuddySession/BuddySessionHubView.swift Hub for creating/joining sessions; displays its own local errorMessage but does not surface appVM.buddyInviteError, so deep-link join failures navigated here are invisible to the user
ios/StillPointApp/Views/BuddySession/BuddyVideoWebView.swift UIViewRepresentable wrapping WKWebView for Daily video calls; infinite-reload loop prevented with coordinator URL tracking; autoplay enabled without user action as expected for a video call
ios/StillPointApp/Views/BuddySession/BuddyWaitingRoomView.swift Waiting room with ready toggle, host start/cancel, and server-confirmed exit; isUpdatingReady disables the toggle while in-flight
ios/StillPointShared/Sources/StillPointShared/APIClient.swift Adds buddy session API endpoints; extends APIError with optional code field; implementation is clean and consistent with existing patterns
ios/StillPointShared/Sources/StillPointShared/DTOs/DTOs.swift Adds BuddySnapshotDTO, BuddyParticipantDTO, request/response wrappers; SessionDTO gains optional buddySessionId used for completion handoff validation
ios/StillPointApp/Info.plist Registers stillpoint:// URL scheme, camera/mic usage strings, and background audio mode; voip mode correctly omitted
ios/StillPointApp/Views/RootView.swift Adds buddyHub and buddySession cases to the view switcher; .id(sessionId) forces container re-init per session; onOpenURL wired to appVM.handleIncomingURL
ios/StillPointApp/Views/HomeView.swift Adds Meditate with a friend button and buddy invite error display; straightforward UI addition

Sequence Diagram

sequenceDiagram
    participant User
    participant RootView
    participant AppViewModel
    participant BuddySessionHubView
    participant BuddySessionContainerView
    participant BuddySessionViewModel
    participant APIClient

    User->>RootView: tap Meditate with a friend
    RootView->>AppViewModel: beginBuddySession()
    AppViewModel-->>RootView: currentView = .buddyHub

    User->>BuddySessionHubView: tap Start shared session
    BuddySessionHubView->>APIClient: createBuddySession()
    APIClient-->>BuddySessionHubView: BuddySessionCreatedDTO
    User->>BuddySessionHubView: tap Enter waiting room
    BuddySessionHubView->>AppViewModel: enterBuddySession(sessionId)
    AppViewModel-->>RootView: currentView = .buddySession(sessionId)

    BuddySessionContainerView->>BuddySessionViewModel: startPolling()
    loop every 1.5s
        BuddySessionViewModel->>APIClient: getBuddySnapshot(sessionId)
        APIClient-->>BuddySessionViewModel: BuddySnapshotDTO
    end

    Note over BuddySessionViewModel: state == active
    BuddySessionViewModel->>APIClient: getBuddyMeetingToken(sessionId)
    APIClient-->>BuddySessionViewModel: token

    Note over BuddySessionViewModel: state == completed
    BuddySessionContainerView->>APIClient: recordBuddyPersonalSession()
    APIClient-->>BuddySessionContainerView: SessionDTO
    BuddySessionContainerView->>AppViewModel: completeSession(...)
    AppViewModel-->>RootView: currentView = .completion

    Note over AppViewModel: Deep-link path
    User->>RootView: open stillpoint://buddy/TOKEN
    RootView->>AppViewModel: handleIncomingURL(url)
    alt not logged in or in session
        AppViewModel->>AppViewModel: queue pendingBuddyInviteToken
    else logged in and idle
        AppViewModel->>APIClient: joinBuddySession(token)
        APIClient-->>AppViewModel: sessionId
        AppViewModel-->>RootView: currentView = .buddySession(sessionId)
    end
Loading

Reviews (1): Last reviewed commit: "Resolve latest buddy video and API revie..." | Re-trigger Greptile

Comment on lines +169 to +178
} catch let error as APIError {
buddyInviteError = error.message
if case .auth = currentView {} else {
currentView = .buddyHub
}
} catch {
buddyInviteError = "Could not open buddy invite."
if case .auth = currentView {} else {
currentView = .buddyHub
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Deep-link join error silently lost when navigated to buddy hub

When joinBuddySession fails (e.g. expired or invalid token from a deep link), buddyInviteError is set and then currentView is immediately changed to .buddyHub. However, BuddySessionHubView only reads its own local @State var errorMessage — it never reads appVM.buddyInviteError — so the user arrives at the hub with no indication of what went wrong or why they were sent there.

A user tapping a shared invite link that has expired will be silently redirected to the buddy hub with no error message visible. The simplest fix is to stay on the current view when an error occurs (removing the navigation), since buddyInviteError is already surfaced on HomeView.

Comment on lines +73 to +75
.tint(SPColor.green)
}
.padding(SPSpacing.s3)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Debug label exposed in production UI

"\(snapshot.durationSeconds)s shared timer synced from server" reads as a development diagnostic string rather than end-user copy. It will render as e.g. "1800s shared timer synced from server" alongside the countdown timer in the production session view.

Comment on lines +196 to +202
if let range = trimmed.range(of: "buddy=") {
let rest = String(trimmed[range.upperBound...])
if let amp = rest.firstIndex(of: "&") {
return String(rest[..<amp]).trimmingCharacters(in: .whitespacesAndNewlines)
}
return rest.trimmingCharacters(in: .whitespacesAndNewlines)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Raw string fallback uses substring match, not query-parameter match

trimmed.range(of: "buddy=") matches the literal substring buddy= at any position. A string like notabuddy=abc would match at the buddy= portion and extract abc as the token instead of returning nil. This edge case only applies when the user pastes something that isn't parseable as a URL, but a mis-scanned QR code or partial paste could silently produce an incorrect token rather than a clear error.

Consider using a more precise boundary check (e.g. "?buddy=" or "&buddy="), or simply reject raw strings that can't be parsed as URLs and show a validation error.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit a555ec1. Configure here.

webView.backgroundColor = .clear
webView.scrollView.isScrollEnabled = false
return webView
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WKWebView missing UIDelegate prevents camera/mic access

High Severity

BuddyVideoWebView never sets a uiDelegate on the WKWebView and doesn't implement WKUIDelegate.webView(_:requestMediaCapturePermissionFor:initiatedByFrame:type:decisionHandler:). On iOS 15+, without this delegate, getUserMedia() calls from Daily's web page are denied or produce repeated system prompts that reset on each page load. Camera and microphone access for partner video sessions will not work reliably.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a555ec1. Configure here.

buddyInviteError = "Could not open buddy invite."
if case .auth = currentView {} else {
currentView = .buddyHub
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Deep-link join error invisible on buddy hub

Medium Severity

When joinBuddySession fails (from a deep link or pending invite), the error is stored in buddyInviteError and the user is navigated to .buddyHub. However, buddyInviteError is only rendered in HomeView, not in BuddySessionHubView. The user lands on the hub with no visible error feedback explaining why the invite failed.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a555ec1. Configure here.

cursoragent and others added 4 commits April 24, 2026 04:31
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
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.

2 participants