iOS: add buddy session host/join flow with deep links#220
Conversation
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (1)
ios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swift (1)
53-57: This completion-save path is unreachable fromBuddyActiveSessionView.
BuddySessionContainerViewonly instantiates this screen from its"active"branch, sosnapshot.statehere never becomes"completed". That makes the.task(id: snapshot.state)block andhandleCompletionSave()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
📒 Files selected for processing (13)
ios/StillPoint.xcodeproj/project.pbxprojios/StillPointApp/Info.plistios/StillPointApp/ViewModels/AppViewModel.swiftios/StillPointApp/ViewModels/BuddySessionViewModel.swiftios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swiftios/StillPointApp/Views/BuddySession/BuddySessionContainerView.swiftios/StillPointApp/Views/BuddySession/BuddySessionHubView.swiftios/StillPointApp/Views/BuddySession/BuddyVideoWebView.swiftios/StillPointApp/Views/BuddySession/BuddyWaitingRoomView.swiftios/StillPointApp/Views/HomeView.swiftios/StillPointApp/Views/RootView.swiftios/StillPointShared/Sources/StillPointShared/APIClient.swiftios/StillPointShared/Sources/StillPointShared/DTOs/DTOs.swift
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
ios/StillPointApp/Views/BuddySession/BuddyVideoWebView.swift (1)
24-27:⚠️ Potential issue | 🟠 MajorAlways replace the
tquery item, don’t only append when missing.Line 24-27 keeps an existing
tvalue, 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
📒 Files selected for processing (2)
ios/StillPointApp/Views/BuddySession/BuddySessionHubView.swiftios/StillPointApp/Views/BuddySession/BuddyVideoWebView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
- ios/StillPointApp/Views/BuddySession/BuddySessionHubView.swift
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
ios/StillPointApp/Views/BuddySession/BuddySessionContainerView.swift (1)
156-168:⚠️ Potential issue | 🟠 MajorGuard the completion handoff against stale in-flight saves.
This only checks
didExitWithoutSaving. If the container leaves for any other reason whilesavePersonalSession()is running, the old task can still callappVM.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
AppViewModelwould 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
📒 Files selected for processing (9)
ios/StillPointApp/Info.plistios/StillPointApp/ViewModels/AppViewModel.swiftios/StillPointApp/ViewModels/BuddySessionViewModel.swiftios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swiftios/StillPointApp/Views/BuddySession/BuddySessionContainerView.swiftios/StillPointApp/Views/BuddySession/BuddySessionHubView.swiftios/StillPointApp/Views/BuddySession/BuddyVideoWebView.swiftios/StillPointApp/Views/BuddySession/BuddyWaitingRoomView.swiftios/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
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
|
@cursor review |
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
ios/StillPointApp/ViewModels/AppViewModel.swiftios/StillPointApp/ViewModels/BuddySessionViewModel.swiftios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swiftios/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
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
There was a problem hiding this comment.
🧹 Nitpick comments (2)
ios/StillPointApp/ViewModels/BuddySessionViewModel.swift (1)
305-327: RedundantMainActor.runblocks within@MainActorclass.Since
BuddySessionViewModelis marked@MainActor, unstructuredTask { }blocks created from within this class inherit MainActor isolation. Theawait 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.isSavingCompletionis false andvm.completionSaveErroris nil, this branch displays "Saving your personal session..." even though saving hasn't started yet (e.g., waiting forsaveCompletionIfPossibleto 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
📒 Files selected for processing (5)
ios/StillPointApp/ViewModels/AppViewModel.swiftios/StillPointApp/ViewModels/BuddySessionViewModel.swiftios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swiftios/StillPointApp/Views/BuddySession/BuddySessionContainerView.swiftios/StillPointApp/Views/BuddySession/BuddySessionHubView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
- ios/StillPointApp/Views/BuddySession/BuddyActiveSessionView.swift
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
|
@cursor review |
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
|
@cursor review |
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
|
@cursor review |
Greptile SummaryThis PR adds the full iOS buddy (shared meditation) session flow: hub, waiting room, active session with video, and completion handoff, including
Confidence Score: 4/5Safe 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
Sequence DiagramsequenceDiagram
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
Reviews (1): Last reviewed commit: "Resolve latest buddy video and API revie..." | Re-trigger Greptile |
| } 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 | ||
| } |
There was a problem hiding this comment.
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.
| .tint(SPColor.green) | ||
| } | ||
| .padding(SPSpacing.s3) |
There was a problem hiding this comment.
| 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) | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ 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 | ||
| } |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit a555ec1. Configure here.
| buddyInviteError = "Could not open buddy invite." | ||
| if case .auth = currentView {} else { | ||
| currentView = .buddyHub | ||
| } |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit a555ec1. Configure here.
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>


Summary
stillpoint://buddy/TOKENplus?buddy=linksWKWebViewusing Daily room URL + meeting tokenUIPasteboard/UIViewRepresentableare usedFollow-up review fixes
voipbackground mode from iOS Info.plist (leaveaudio)AppView.Equatablefor.buddySession(sessionId:)so different sessions are not treated as equal.id(sessionId)inRootViewstillpoint://buddy/TOKENparsing in hub token extractiontquery parameter in buddy web video viewBuddyActiveSessionViewAppViewModelon@MainActorto keep observable state mutations on main threadFinal cleanup pass
BuddyInviteTokenParserand reuse it from bothAppViewModelandBuddySessionHubView?token=parsing to buddy-specific routes only (prevents misclassifying unrelated deep links)BuddySessionContainerViewto prevent stale async completion from overriding newer navigation stateSessionDTO.buddySessionIdagainst buddy snapshot id (instead of comparing mismatched entity ids)participantCompleteAndLeavedead code from buddy session view modelWKWebViewinfinite reload loops by tracking the last requested URL in aUIViewRepresentablecoordinatorbuddyParticipantCompletemethod after dead-code cleanupLatest unresolved-thread pass
appVM.buddyInviteErrorand initializing existing error state when navigating into the hubWKUIDelegatemedia-capture permission handling and setuiDelegateonWKWebViewwith explicit main-actor decision handlingNotes
Summary by CodeRabbit
New Features
Permissions / Background
Bug Fixes / UX