Skip to content

feat(onboarding): first-launch profile setup sheet#1158

Merged
yewreeka merged 7 commits into
devfrom
jarod/onboarding-profile-sheet
Jul 10, 2026
Merged

feat(onboarding): first-launch profile setup sheet#1158
yewreeka merged 7 commits into
devfrom
jarod/onboarding-profile-sheet

Conversation

@yewreeka

@yewreeka yewreeka commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces the in-conversation "Add your name and pic" prompt with a "Hello / My name is" profile setup sheet shown once on first launch (Figma).

First-launch gating

  • Hooks into the same launch check as the found-device pairing prompt (ConversationsViewModel.checkForPairableDeviceIfNeeded). The sheet presents only when no private keys are detected in iCloud (pairableDeviceBackups() returns empty), so it can never race the "Pair device" sheet; deep-link/incoming pairing flows are guarded explicitly.
  • Waits for the global profile load (10s, mirroring ConversationOnboardingCoordinator) and never shows when a profile already exists.
  • Shows once (new hasShownFirstLaunchProfileSheet flag; cleared by the existing resetUserDefaults() path). Users who dismiss without saving fall back to the legacy in-conversation flow, which is left intact (also covers users who decline pairing).

Reused in two more places (My Info variant: no terms row, "Save" CTA)

  • Settings → My info now opens the sheet (was a pushed MyInfoView form).
  • Own profile from a conversation (your avatar / your member row) opens the same sheet — this now edits the global profile; the per-conversation override editor no longer exists.
  • MyInfoView is removed.

Sheet details (per Figma dev measurements)

  • 132pt lava band (text 40 from top / 28 below, 4 between lines), 36pt band→row gap, 24pt card margins, 52pt name row, 56pt CTA inset 16, medium-weight photo/camera.fill glyphs.
  • Empty state: "Name" placeholder, person.crop.circle.fill avatar → live monogram once a name is typed → photo once picked (avatar and photo icon open the library, camera icon the camera).
  • Terms toggle defaults on; "Come in" enables once a name is entered (toggling off disables it). Terms link opens hq.convos.org/privacy-and-terms.
  • The sheet keeps a local draft and writes to ProfileSettingsViewModel only on save — direct binding loses in-flight edits when the global-profile observation re-emits during first-launch inbox churn (reproduced before the fix).
  • Owns its presentation (plain .sheet + self-measured .height detent): the shared selfSizingSheet presents at .medium and iOS never re-snaps a presented sheet down to a smaller .height, which left dead space above content shorter than medium. That latent selfSizingSheet quirk affects any short sheet and deserves a separate follow-up; this PR leaves the shared modifier untouched.

QA

  • qa/tests/01-onboarding (md + structured yaml) rewritten around the sheet: empty-state assertions, no in-conversation re-prompt, documented dismiss-fallback (01b).
  • 14-profile: settings edit via the sheet; part 5 now verifies in-conversation edit updates the global profile.
  • 40-dm-members-as-contacts: devices set names via the first-launch sheet; 07/19 annotated as fallback-only; structured README screen map updated.
  • Structured test 01 executed twice via the CXDB harness (runs e28707d89147 on the initial build, db4473bb6732 on the final design): pass, 8/8 criteria, 0 app/XMTP errors, with the full event chain (found_device_check pairableCount=0first_launch_profile_sheet_shownprofile_sheet_savedprofile_auto_applied). HTML artifacts in qa/artifacts/ (gitignored, shareable as zips).

Not sim-driven: camera capture (no sim camera) and the pair-sheet-suppression path (needs an iCloud-keychain fixture; guarded by construction + test 38/44).

🤖 Generated with Claude Code

Note

Add first-launch profile setup sheet for onboarding

  • Introduces ProfileSetupSheet with firstLaunch and edit modes: first-launch shows terms and a 'Come in' CTA; edit mode is a standard profile update sheet.
  • On fresh install with no iCloud identity backup, ConversationsViewModel presents the sheet once via presentFirstLaunchProfileSetupIfNeeded(), which waits up to 10s for profile load and skips if a profile already exists.
  • Replaces the removed MyInfoView throughout: the App Settings 'My info' row and in-conversation profile prompt now open ProfileSetupSheet in edit mode.
  • Save is handled atomically inside the sheet via ProfileSettingsViewModel.saveAndAwait, reverting on failure; the old onProfileSettingsDismissed save path is removed.
  • QA structured tests updated to use profile-setup-* accessibility identifiers.

Macroscope summarized f6a53ec.

Replace the in-conversation "Add your name and pic" prompt with a
"Hello / My name is" sheet (Figma 5694-27769) shown once on first
launch. The sheet only presents when the pairable-device check finds no
other identity in the iCloud-synced keychain backup and no pairing UI
is on screen, so it can never race the found-device pairing sheet. The
in-conversation flow stays as a fallback for users who dismiss the
sheet without saving.

The same sheet (no terms row, "Save" CTA — Figma 5694-28789) replaces
the MyInfoView form in Settings > My info and the my-profile sheet
shown when tapping your own avatar/member row in a conversation;
MyInfoView is removed. The sheet keeps a local draft of the edit and
writes to ProfileSettingsViewModel only on save — binding the fields
directly loses in-flight edits when the global-profile observation
re-emits during first-launch inbox churn.

QA: rewrote tests 01 (first-launch sheet + no in-conversation
re-prompt) and 14 part 5 (own-profile edit from a conversation is now
global), updated 40 to name devices via the sheet, and noted the
fallback status of the quick-edit path in 07/19.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 10, 2026

Copy link
Copy Markdown

Claude finished @yewreeka's task —— View job


Code Review: First-Launch Profile Setup Sheet

I've completed my review of PR #1158. Overall, this is a well-structured implementation that properly addresses the previous review findings. The code follows Swift/SwiftUI best practices and the project's conventions. Here are my findings:


✅ Strengths

Architecture & Design:

  • Clean separation of concerns: ProfileSetupSheet handles its own state management with local draft pattern
  • Proper mode enum (ProfileSetupSheetMode) for differentiating first-launch vs. edit contexts
  • Smart rollback-on-failure pattern (ProfileSetupSheet:281-299) prevents stale data in shared view model
  • Well-designed cold-launch race prevention (ProfileSetupSheet:116-127) with hasEditedDraft guard

SwiftUI Conventions:

  • Correct use of @Observable pattern throughout
  • Proper button action extraction (ProfileSetupSheet:258-274) per project conventions
  • Clean view composition with extracted subviews
  • Appropriate accessibility identifiers on all interactive elements

Integration:

  • First-launch gating logic is sound - properly checks for pairable devices first
  • Waits for profile load with timeout (10s) to avoid cold-launch race
  • hasShownFirstLaunchProfileSheet flag prevents re-showing
  • Graceful fallback to in-conversation onboarding if dismissed without saving

Test Coverage:

  • Comprehensive QA test updates (01-onboarding.yaml, 14-profile.yaml)
  • Structured tests executed and passing per PR description
  • Clear test documentation with step-by-step verification

🟡 Areas for Improvement

1. Type-Check Performance Risk (Minor)

Location: ProfileSetupSheet.swift:88-94

The canSave computed property chains multiple conditions with ternaries. While currently safe, this pattern can blow the type-check budget if more conditions are added. Consider breaking it down:

private var canSave: Bool {
    let hasValidInput: Bool = mode == .edit ? (hasName || profileImage != nil) : hasName
    let hasTermsConsent: Bool = !mode.showsTermsRow || hasAgreedToTerms
    return hasValidInput && hasTermsConsent && !isSaving
}

2. Error Handling (Minor)

Location: ProfileSetupSheet.swift:295-300

Failed saves log errors but provide no user feedback. While the rollback prevents data corruption, users might be confused why the sheet didn't dismiss. Consider:

@State private var saveErrorMessage: String?

// In catch block:
saveErrorMessage = "Could not save profile. Please try again."

Then display an inline error or alert.

3. Terms Toggle Default (Acknowledged)

Location: ProfileSetupSheet.swift:50

The terms toggle defaults to true, which is noted as an intentional product decision. From a code perspective, this is correctly implemented. However, for compliance clarity, consider adding a code comment explaining the business decision:

/// Defaults to true per product decision (reduces first-launch friction).
/// Toggling off disables the CTA until re-enabled.
@State private var hasAgreedToTerms: Bool = true

4. Magic Numbers (Minor)

Location: ProfileSetupSheet.swift:73

Height values could use named constants for clarity:

private enum Constant {
    static let firstLaunchHeight: CGFloat = 398.0
    static let editModeHeight: CGFloat = 343.0
}

_contentHeight = State(initialValue: mode.showsTermsRow ? Constant.firstLaunchHeight : Constant.editModeHeight)

5. Optional Unwrapping (Best Practice)

Location: ConversationsViewModel.swift:982-983

The timeout check uses optional unwrapping without explicit handling of the nil case:

let profileLoaded = await ProfileSettingsViewModel.shared.waitForProfileLoad(
    timeout: Constant.firstLaunchProfileLoadTimeout
)
guard profileLoaded else {
    QAEvent.emit(.onboarding, "first_launch_profile_sheet_load_timeout")
    return
}

This is fine, but consider logging why it timed out for debugging purposes.


🟢 Previous Review Findings - Resolved

All high-severity issues from the previous Macroscope review have been properly addressed in commit ed67b9c4:

High: Conversation entry point overwrites saved photo - Fixed by removing onDismiss handler
High: Save failure leaves stale data - Fixed with rollback pattern (lines 284-298)
Medium: Cold-launch race - Fixed with hasEditedDraft guard (lines 119-127)
Medium: Photo-only edits blocked - Fixed in canSave logic (line 91)


🔒 Security Considerations

✅ No secrets or sensitive data exposed
✅ Terms & Privacy link properly validated before opening
✅ User input properly sanitized (trimmed whitespace in hasName)
✅ No force unwraps or unsafe optional handling


📊 Performance Considerations

✅ Efficient state management with local draft pattern
✅ No unnecessary view re-renders
✅ Async operations properly handled with Task
✅ Profile load timeout prevents indefinite hangs


🧪 Test Coverage Assessment

Strong coverage with:

  • Fresh onboarding flow (test 01)
  • Profile editing from settings (test 14)
  • Integration with existing flows (tests 07, 19, 40)
  • Both success and fallback paths documented

Missing coverage (acceptable for this PR):

  • Camera capture (no simulator support - noted in PR description)
  • Pairing sheet suppression (requires iCloud fixture - noted)
  • Save failure scenarios (error handling currently logs only)

📝 Code Quality Summary

Adherence to CLAUDE.md conventions:

  • ✅ SwiftUI state management (uses @Observable)
  • ✅ Button pattern (extracted actions)
  • ✅ Guard preference (proper early returns)
  • ✅ No force unwraps
  • ✅ No step counts or all-caps in comments
  • ✅ Accessibility identifiers on all UI elements
  • ✅ ConvosCore platform independence maintained

SwiftLint compliance: Should pass once linted (no obvious violations)


🎯 Recommendation

APPROVE with minor suggestions. This is a solid implementation that:

  • Properly addresses all previous review findings
  • Follows project conventions and Swift best practices
  • Has comprehensive test coverage
  • Handles edge cases gracefully

The suggested improvements are non-blocking and can be addressed in follow-up work if desired.


💡 Optional Follow-ups

  1. Add user-visible error feedback for failed saves
  2. Extract magic numbers to named constants
  3. Consider adding unit tests for ProfileSetupSheet view model logic (currently covered by integration tests)

Great work addressing the review feedback comprehensively! 🚀

Comment thread Convos/Profile/ProfileSetupSheet.swift
Comment thread Convos/Profile/ProfileSetupSheet.swift
Spacing/type corrections against the Figma dev measurements: 36pt gap
between the lava band and the name row (not 64 - the band overhangs its
text by 28), 132pt band (40 above / 28 below the text), 56pt CTA with a
16pt horizontal inset inside the card padding, and medium-weight
photo/camera glyphs. Dropped an experimental presentationCornerRadius
override that made the system inset the sheet content, leaving a white
cap above the lava band.

Empty state per design: placeholder is "Name" (not "Somebody"), the
avatar shows person.crop.circle.fill on an inverted circle until a name
is typed (monogram) or a photo is chosen, the terms toggle starts OFF,
and the CTA renders gray while disabled.

QA: test 01 and 40 now enable the terms toggle before saving; empty
state assertions added to 01.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread Convos/Profile/ProfileSetupSheet.swift
Comment thread Convos/Profile/ProfileSetupSheet.swift
yewreeka and others added 2 commits July 10, 2026 09:03
… card

The self-sizing detent can settle taller than the measured content, and
rubber-banding exposes the area beyond it - both showed the ultra-thin
material as a white cap above the lava band. Paint the presentation
background lava on top / card gray below so any exposed region reads as
the card continuing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The selfSizingSheet approach presents at .medium and swaps to a
.height() detent after measuring, but iOS never re-snaps a presented
sheet down from medium - content shorter than medium (this sheet, 398pt
vs medium's ~425pt) floats centered with dead space, which showed as a
white cap above the lava band. ProfileSetupSheet now presents via plain
.sheet and supplies its own detent, seeded per mode and corrected by
measurement, so the detent only receives same-kind .height updates.
Also: camera.fill glyph per design.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread Convos/Conversation Detail/ConversationView.swift Outdated
Come in is gated only on entering a name; turning the toggle off still
disables it. QA tests 01/40 updated (no toggle tap needed before save).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@yewreeka yewreeka marked this pull request as ready for review July 10, 2026 16:54
@macroscopeapp

macroscopeapp Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a new first-launch profile setup sheet that changes the user onboarding experience. New user-facing features with new components, workflows, and behavioral changes warrant human review.

You can customize Macroscope's approvability policy. Learn more.

- Drop the ConversationView onDismiss handler (and the now-unused
  onProfileSettingsDismissed): it re-saved from the stale
  myProfileViewModel, clobbering the just-saved profile photo. Also
  fixes the SwiftLint multiple_closures_with_trailing_closure failure.
- Roll the shared view model back to its pre-save values when
  saveAndAwait throws, so a failed save doesn't present never-persisted
  edits as stored.
- Reseed a pristine draft when the global profile load resolves, so a
  sheet opened during cold launch can't save a stale empty draft over
  the stored avatar.
- Allow photo-only saves in edit mode (empty name falls back to the
  stored one in saveAndAwait).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@yewreeka

Copy link
Copy Markdown
Contributor Author

Review findings addressed in ed67b9c4:

  • High — conversation entry point overwrites saved photo: removed the onDismissonProfileSettingsDismissed hook (and the now-unused method). The sheet owns the full save; the old handler re-saved from the stale myProfileViewModel. This also fixes the SwiftLint multiple_closures_with_trailing_closure failure.
  • High — save failure leaves stale data in the shared view model: the save now snapshots the view model and rolls back in the catch.
  • Medium — cold-launch race: a pristine draft reseeds from the view model when the global profile load resolves (gated on the user not having edited), so a sheet opened before load can't save an empty draft over the stored avatar.
  • Medium — photo-only edits blocked in edit mode: canSave now accepts a photo without a name in .edit (empty name falls back to the stored one in saveAndAwait).
  • Terms toggle default: intentional product decision (defaults on; toggling off still disables the CTA) — see inline reply.
  • Low — QAEvent ordering / error toast on failed save: deferred; failed saves currently roll back + log. Happy to add user-visible error feedback in a follow-up if desired.

🤖 Generated with Claude Code

Mirrors the profile sheet's name row (Figma 5694-28455): 36pt avatar
(person.crop.circle.fill placeholder until a name or photo exists),
display name ("Name" placeholder in tertiary when unset), and a
trailing pencil glyph instead of the old lanyard icon + trailing
name/avatar. A "Your name and pic" footer sits under the row. Tapping
still opens the profile sheet. Accessibility: label stays "My info"
(id my-info-row) with the name exposed as the value; QA test 14
annotated accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@yewreeka yewreeka merged commit 28fc4a5 into dev Jul 10, 2026
10 checks passed
@yewreeka yewreeka deleted the jarod/onboarding-profile-sheet branch July 10, 2026 18:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant