fix(ios): address Pass B warnings from code audit

This commit is contained in:
null 2026-06-20 22:58:11 -05:00
parent cb54ed3079
commit 59c239694a
6 changed files with 133 additions and 177 deletions

View File

@ -85,9 +85,6 @@ struct DateMatchView: View {
.background(Color.closerBackground) .background(Color.closerBackground)
.navigationTitle("Date Ideas") .navigationTitle("Date Ideas")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.navigationDestination(isPresented: .constant(false)) {
DateBuilderView()
}
} }
private func swipe(_ direction: SwipeDirection) { private func swipe(_ direction: SwipeDirection) {

View File

@ -2,22 +2,28 @@ import SwiftUI
struct ContentView: View { struct ContentView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
var body: some View { var body: some View {
Group { NavigationStack {
switch appState.authState { rootView
case .loading: }
LoadingView(message: "Getting ready...") .environmentObject(appState)
case .unauthenticated: }
OnboardingFlow()
case .authenticated(_, let isAnonymous): @ViewBuilder
if isAnonymous { private var rootView: some View {
CreateProfileView() switch appState.authState {
} else if appState.currentUser?.coupleId == nil { case .loading:
PairPromptView() LoadingView(message: "Getting ready...")
} else { case .unauthenticated:
MainTabView() OnboardingFlow()
} case .authenticated(_, let isAnonymous):
if isAnonymous {
CreateProfileView()
} else if appState.currentUser?.coupleId == nil {
PairPromptView()
} else {
MainTabView()
} }
} }
} }
@ -28,52 +34,42 @@ struct ContentView: View {
struct MainTabView: View { struct MainTabView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@State private var selectedTab: Tab = .home @State private var selectedTab: Tab = .home
enum Tab: Hashable { enum Tab: Hashable {
case home, dailyQuestion, play, questionPacks, settings case home, dailyQuestion, play, questionPacks, settings
} }
var body: some View { var body: some View {
TabView(selection: $selectedTab) { TabView(selection: $selectedTab) {
NavigationStack { HomeView()
HomeView() .tabItem {
} Label("Home", systemImage: selectedTab == .home ? "house.fill" : "house")
.tabItem { }
Label("Home", systemImage: selectedTab == .home ? "house.fill" : "house") .tag(Tab.home)
}
.tag(Tab.home) DailyQuestionView()
.tabItem {
NavigationStack { Label("Today", systemImage: selectedTab == .dailyQuestion ? "heart.fill" : "heart")
DailyQuestionView() }
} .tag(Tab.dailyQuestion)
.tabItem {
Label("Today", systemImage: selectedTab == .dailyQuestion ? "heart.fill" : "heart") PlayHubView()
} .tabItem {
.tag(Tab.dailyQuestion) Label("Play", systemImage: selectedTab == .play ? "play.fill" : "play")
}
NavigationStack { .tag(Tab.play)
PlayHubView()
} QuestionPackLibraryView()
.tabItem { .tabItem {
Label("Play", systemImage: selectedTab == .play ? "play.fill" : "play") Label("Packs", systemImage: selectedTab == .questionPacks ? "star.fill" : "star")
} }
.tag(Tab.play) .tag(Tab.questionPacks)
NavigationStack { SettingsView()
QuestionPackLibraryView() .tabItem {
} Label("Settings", systemImage: selectedTab == .settings ? "gearshape.fill" : "gearshape")
.tabItem { }
Label("Packs", systemImage: selectedTab == .questionPacks ? "star.fill" : "star") .tag(Tab.settings)
}
.tag(Tab.questionPacks)
NavigationStack {
SettingsView()
}
.tabItem {
Label("Settings", systemImage: selectedTab == .settings ? "gearshape.fill" : "gearshape")
}
.tag(Tab.settings)
} }
.tint(.closerPrimary) .tint(.closerPrimary)
} }
@ -84,17 +80,15 @@ struct MainTabView: View {
struct OnboardingFlow: View { struct OnboardingFlow: View {
@State private var showLogin = false @State private var showLogin = false
@State private var showSignUp = false @State private var showSignUp = false
var body: some View { var body: some View {
NavigationStack { OnboardingView(showLogin: $showLogin, showSignUp: $showSignUp)
OnboardingView(showLogin: $showLogin, showSignUp: $showSignUp) .navigationDestination(isPresented: $showLogin) {
.navigationDestination(isPresented: $showLogin) { LoginView()
LoginView() }
} .navigationDestination(isPresented: $showSignUp) {
.navigationDestination(isPresented: $showSignUp) { SignUpView()
SignUpView() }
}
}
} }
} }

View File

@ -6,45 +6,45 @@ struct PairPromptView: View {
@State private var showCreateInvite = false @State private var showCreateInvite = false
@State private var showAcceptInvite = false @State private var showAcceptInvite = false
@State private var showEmailInvite = false @State private var showEmailInvite = false
var body: some View { var body: some View {
NavigationStack { NavigationStack {
VStack(spacing: CloserSpacing.xxl) { VStack(spacing: CloserSpacing.xxl) {
Spacer() Spacer()
Image(systemName: "link.circle.fill") Image(systemName: "link.circle.fill")
.font(.system(size: 72)) .font(.system(size: 72))
.foregroundColor(.closerPrimary) .foregroundColor(.closerPrimary)
Text("Connect with Your Partner") Text("Connect with Your Partner")
.font(CloserFont.title1) .font(CloserFont.title1)
.foregroundColor(.closerText) .foregroundColor(.closerText)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
Text("Create an invite code for your partner to join, or enter their code to connect.") Text("Create an invite code for your partner to join, or enter their code to connect.")
.font(CloserFont.callout) .font(CloserFont.callout)
.foregroundColor(.closerTextSecondary) .foregroundColor(.closerTextSecondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.closerPadding() .closerPadding()
VStack(spacing: CloserSpacing.lg) { VStack(spacing: CloserSpacing.lg) {
Button(action: { showCreateInvite = true }) { Button(action: { showCreateInvite = true }) {
Label("Create Invite Code", systemImage: "plus.circle.fill") Label("Create Invite Code", systemImage: "plus.circle.fill")
} }
.buttonStyle(PrimaryButtonStyle()) .buttonStyle(PrimaryButtonStyle())
Button(action: { showAcceptInvite = true }) { Button(action: { showAcceptInvite = true }) {
Label("Enter Partner's Code", systemImage: "key.fill") Label("Enter Partner's Code", systemImage: "key.fill")
} }
.buttonStyle(SecondaryButtonStyle()) .buttonStyle(SecondaryButtonStyle())
Button(action: { showEmailInvite = true }) { Button(action: { showEmailInvite = true }) {
Label("Invite by Email", systemImage: "envelope.fill") Label("Invite by Email", systemImage: "envelope.fill")
} }
.buttonStyle(SecondaryButtonStyle()) .buttonStyle(SecondaryButtonStyle())
} }
.closerPadding() .closerPadding()
Spacer() Spacer()
} }
.background(Color.closerBackground) .background(Color.closerBackground)
@ -68,7 +68,7 @@ struct CreateInviteView: View {
@State private var inviteCode = "" @State private var inviteCode = ""
@State private var isLoading = false @State private var isLoading = false
@State private var errorMessage: String? @State private var errorMessage: String?
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(spacing: CloserSpacing.xxl) { VStack(spacing: CloserSpacing.xxl) {
@ -85,7 +85,7 @@ struct CreateInviteView: View {
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
} }
.padding(.top, CloserSpacing.xxl) .padding(.top, CloserSpacing.xxl)
if !inviteCode.isEmpty { if !inviteCode.isEmpty {
VStack(spacing: CloserSpacing.md) { VStack(spacing: CloserSpacing.md) {
Text(inviteCode) Text(inviteCode)
@ -95,13 +95,13 @@ struct CreateInviteView: View {
.padding(CloserSpacing.xxl) .padding(CloserSpacing.xxl)
.background(Color.closerSurface) .background(Color.closerSurface)
.cornerRadius(CloserRadius.large) .cornerRadius(CloserRadius.large)
Button(action: copyCode) { Button(action: copyCode) {
Label("Copy Code", systemImage: "doc.on.doc") Label("Copy Code", systemImage: "doc.on.doc")
} }
.buttonStyle(SecondaryButtonStyle()) .buttonStyle(SecondaryButtonStyle())
.frame(maxWidth: 200) .frame(maxWidth: 200)
Button(action: shareCode) { Button(action: shareCode) {
Label("Share", systemImage: "square.and.arrow.up") Label("Share", systemImage: "square.and.arrow.up")
} }
@ -116,13 +116,13 @@ struct CreateInviteView: View {
} }
.buttonStyle(PrimaryButtonStyle()) .buttonStyle(PrimaryButtonStyle())
} }
if let error = errorMessage { if let error = errorMessage {
Text(error) Text(error)
.font(CloserFont.caption) .font(CloserFont.caption)
.foregroundColor(.closerDanger) .foregroundColor(.closerDanger)
} }
NavigationLink { NavigationLink {
InviteConfirmView() InviteConfirmView()
} label: { } label: {
@ -136,17 +136,23 @@ struct CreateInviteView: View {
.background(Color.closerBackground) .background(Color.closerBackground)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
private func generateInvite() { private func generateInvite() {
isLoading = true isLoading = true
errorMessage = nil errorMessage = nil
Task { Task {
do { do {
// TODO: Move invite creation to createInviteCallable Cloud Function.
// 6-character codes are enumerable; direct client writes to the invites
// collection expose them to enumeration. The iOS side should call
// createInviteCallable() once it exists in functions/src/invites/ and
// return the generated code instead of writing here. Leaving direct
// Firestore write as a placeholder until that function is implemented.
let userId = try FirestoreService.shared.userId() let userId = try FirestoreService.shared.userId()
let code = generateSixCharCode() let code = generateSixCharCode()
self.inviteCode = code self.inviteCode = code
let invite = Invite( let invite = Invite(
id: code, id: code,
code: code, code: code,
@ -159,7 +165,7 @@ struct CreateInviteView: View {
acceptedAt: nil, acceptedAt: nil,
acceptedByUserId: nil acceptedByUserId: nil
) )
let inviteRef = FirestoreService.shared.inviteDocument(code) let inviteRef = FirestoreService.shared.inviteDocument(code)
try await FirestoreService.shared.setDocument(invite, at: inviteRef, merge: false) try await FirestoreService.shared.setDocument(invite, at: inviteRef, merge: false)
} catch { } catch {
@ -168,11 +174,11 @@ struct CreateInviteView: View {
isLoading = false isLoading = false
} }
} }
private func copyCode() { private func copyCode() {
UIPasteboard.general.string = inviteCode UIPasteboard.general.string = inviteCode
} }
private func shareCode() { private func shareCode() {
let av = UIActivityViewController(activityItems: [inviteCode], applicationActivities: nil) let av = UIActivityViewController(activityItems: [inviteCode], applicationActivities: nil)
if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene, if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
@ -181,7 +187,7 @@ struct CreateInviteView: View {
root.present(av, animated: true) root.present(av, animated: true)
} }
} }
private func generateSixCharCode() -> String { private func generateSixCharCode() -> String {
let chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" // Avoid ambiguous 0/O, 1/I let chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" // Avoid ambiguous 0/O, 1/I
return String((0..<6).map { _ in chars.randomElement()! }) return String((0..<6).map { _ in chars.randomElement()! })
@ -195,7 +201,7 @@ struct AcceptInviteView: View {
@State private var code = "" @State private var code = ""
@State private var isLoading = false @State private var isLoading = false
@State private var errorMessage: String? @State private var errorMessage: String?
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(spacing: CloserSpacing.xxl) { VStack(spacing: CloserSpacing.xxl) {
@ -212,7 +218,7 @@ struct AcceptInviteView: View {
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
} }
.padding(.top, CloserSpacing.xxl) .padding(.top, CloserSpacing.xxl)
VStack(spacing: CloserSpacing.md) { VStack(spacing: CloserSpacing.md) {
TextField("XXXXXX", text: $code) TextField("XXXXXX", text: $code)
.font(.system(size: 32, weight: .bold, design: .monospaced)) .font(.system(size: 32, weight: .bold, design: .monospaced))
@ -228,13 +234,13 @@ struct AcceptInviteView: View {
code = String(newValue.prefix(6)) code = String(newValue.prefix(6))
} }
} }
if let error = errorMessage { if let error = errorMessage {
Text(error) Text(error)
.font(CloserFont.caption) .font(CloserFont.caption)
.foregroundColor(.closerDanger) .foregroundColor(.closerDanger)
} }
Button(action: acceptInvite) { Button(action: acceptInvite) {
if isLoading { if isLoading {
ProgressView().tint(.white) ProgressView().tint(.white)
@ -251,12 +257,12 @@ struct AcceptInviteView: View {
.background(Color.closerBackground) .background(Color.closerBackground)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
private func acceptInvite() { private func acceptInvite() {
guard code.count == 6 else { return } guard code.count == 6 else { return }
isLoading = true isLoading = true
errorMessage = nil errorMessage = nil
Task { Task {
do { do {
let coupleId = try await FirestoreService.shared.acceptInviteCallable(code: code) let coupleId = try await FirestoreService.shared.acceptInviteCallable(code: code)
@ -277,7 +283,7 @@ struct EmailInviteView: View {
@State private var isLoading = false @State private var isLoading = false
@State private var errorMessage: String? @State private var errorMessage: String?
@State private var successMessage: String? @State private var successMessage: String?
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(spacing: CloserSpacing.xxl) { VStack(spacing: CloserSpacing.xxl) {
@ -293,7 +299,7 @@ struct EmailInviteView: View {
.foregroundColor(.closerTextSecondary) .foregroundColor(.closerTextSecondary)
} }
.padding(.top, CloserSpacing.xxl) .padding(.top, CloserSpacing.xxl)
VStack(spacing: CloserSpacing.md) { VStack(spacing: CloserSpacing.md) {
TextField("partner@example.com", text: $email) TextField("partner@example.com", text: $email)
.textContentType(.emailAddress) .textContentType(.emailAddress)
@ -303,7 +309,7 @@ struct EmailInviteView: View {
.padding() .padding()
.background(Color.closerSurface) .background(Color.closerSurface)
.cornerRadius(CloserRadius.medium) .cornerRadius(CloserRadius.medium)
if let success = successMessage { if let success = successMessage {
Text(success) Text(success)
.font(CloserFont.callout) .font(CloserFont.callout)
@ -314,7 +320,7 @@ struct EmailInviteView: View {
.font(CloserFont.caption) .font(CloserFont.caption)
.foregroundColor(.closerDanger) .foregroundColor(.closerDanger)
} }
Button(action: sendInvite) { Button(action: sendInvite) {
if isLoading { if isLoading {
ProgressView().tint(.white) ProgressView().tint(.white)
@ -331,19 +337,22 @@ struct EmailInviteView: View {
.background(Color.closerBackground) .background(Color.closerBackground)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
private func sendInvite() { private func sendInvite() {
// Email invite sends via Cloud Function or mail service // Email invite sends via Cloud Function or mail service
// For MVP, generate invite code and share system share sheet // For MVP, generate invite code and share system share sheet
guard !email.isEmpty else { return } guard !email.isEmpty else { return }
isLoading = true isLoading = true
errorMessage = nil errorMessage = nil
Task { Task {
do { do {
// TODO: Use createInviteCallable Cloud Function instead of direct
// client writes to the invites collection. Leaving direct Firestore
// write as a placeholder until createInviteCallable is implemented.
let userId = try FirestoreService.shared.userId() let userId = try FirestoreService.shared.userId()
let code = generateSixCharCode() let code = generateSixCharCode()
let invite = Invite( let invite = Invite(
id: code, id: code,
code: code, code: code,
@ -356,7 +365,7 @@ struct EmailInviteView: View {
acceptedAt: nil, acceptedAt: nil,
acceptedByUserId: nil acceptedByUserId: nil
) )
try await FirestoreService.shared.setDocument(invite, at: FirestoreService.shared.inviteDocument(code), merge: false) try await FirestoreService.shared.setDocument(invite, at: FirestoreService.shared.inviteDocument(code), merge: false)
successMessage = "Invitation sent! Share this code: \(code)" successMessage = "Invitation sent! Share this code: \(code)"
} catch { } catch {
@ -365,7 +374,7 @@ struct EmailInviteView: View {
isLoading = false isLoading = false
} }
} }
private func generateSixCharCode() -> String { private func generateSixCharCode() -> String {
let chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" let chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
return String((0..<6).map { _ in chars.randomElement()! }) return String((0..<6).map { _ in chars.randomElement()! })
@ -376,22 +385,22 @@ struct EmailInviteView: View {
struct InviteConfirmView: View { struct InviteConfirmView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
var body: some View { var body: some View {
VStack(spacing: CloserSpacing.xxl) { VStack(spacing: CloserSpacing.xxl) {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.font(.system(size: 72)) .font(.system(size: 72))
.foregroundColor(.closerSuccess) .foregroundColor(.closerSuccess)
Text("Connected!") Text("Connected!")
.font(CloserFont.title1) .font(CloserFont.title1)
.foregroundColor(.closerText) .foregroundColor(.closerText)
Text("You and your partner are now connected. Start exploring questions and games together.") Text("You and your partner are now connected. Start exploring questions and games together.")
.font(CloserFont.callout) .font(CloserFont.callout)
.foregroundColor(.closerTextSecondary) .foregroundColor(.closerTextSecondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
Button("Let's Go!") { Button("Let's Go!") {
Task { await appState.refreshData() } Task { await appState.refreshData() }
} }
@ -411,21 +420,21 @@ struct RecoveryView: View {
Image(systemName: "key.icloud.fill") Image(systemName: "key.icloud.fill")
.font(.system(size: 48)) .font(.system(size: 48))
.foregroundColor(.closerPrimary) .foregroundColor(.closerPrimary)
Text("Unlock Answers") Text("Unlock Answers")
.font(CloserFont.title1) .font(CloserFont.title1)
.foregroundColor(.closerText) .foregroundColor(.closerText)
Text("Enter your recovery phrase to restore access to encrypted answers. This is a 12-word phrase generated when E2EE was first enabled.") Text("Enter your recovery phrase to restore access to encrypted answers. This is a 12-word phrase generated when E2EE was first enabled.")
.font(CloserFont.callout) .font(CloserFont.callout)
.foregroundColor(.closerTextSecondary) .foregroundColor(.closerTextSecondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
Text("Recovery phrase setup is available when E2EE is fully implemented.") Text("Recovery phrase setup is available when E2EE is fully implemented.")
.font(CloserFont.footnote) .font(CloserFont.footnote)
.foregroundColor(.closerTextSecondary) .foregroundColor(.closerTextSecondary)
.italic() .italic()
Spacer() Spacer()
} }
.closerPadding() .closerPadding()
@ -442,21 +451,21 @@ struct EncryptionUpgradeView: View {
Image(systemName: "lock.shield.fill") Image(systemName: "lock.shield.fill")
.font(.system(size: 48)) .font(.system(size: 48))
.foregroundColor(.closerPrimary) .foregroundColor(.closerPrimary)
Text("Secure Your Answers") Text("Secure Your Answers")
.font(CloserFont.title1) .font(CloserFont.title1)
.foregroundColor(.closerText) .foregroundColor(.closerText)
Text("End-to-end encryption ensures your answers are only visible to you and your partner.") Text("End-to-end encryption ensures your answers are only visible to you and your partner.")
.font(CloserFont.callout) .font(CloserFont.callout)
.foregroundColor(.closerTextSecondary) .foregroundColor(.closerTextSecondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
Text("E2EE upgrade is available when the full encryption layer is implemented.") Text("E2EE upgrade is available when the full encryption layer is implemented.")
.font(CloserFont.footnote) .font(CloserFont.footnote)
.foregroundColor(.closerTextSecondary) .foregroundColor(.closerTextSecondary)
.italic() .italic()
Spacer() Spacer()
} }
.closerPadding() .closerPadding()

View File

@ -263,24 +263,8 @@ struct ThisOrThatView: View {
.font(CloserFont.title3) .font(CloserFont.title3)
.foregroundColor(.closerText) .foregroundColor(.closerText)
ForEach(pairs[currentPair].0, pairs[currentPair].1, id: \.self) { option in let options = [pairs[currentPair].0, pairs[currentPair].1]
Button(action: { choose(option) }) { ForEach(options, id: \.self) { option in
Text(option)
.font(CloserFont.body)
.foregroundColor(.closerText)
.frame(maxWidth: .infinity)
.padding()
.background(Color.closerSurface)
.cornerRadius(CloserRadius.large)
.overlay(RoundedRectangle(cornerRadius: CloserRadius.large).stroke(Color.closerDivider))
}
}
Text("or")
.font(CloserFont.body)
.foregroundColor(.closerTextSecondary)
ForEach(pairs[currentPair].1, pairs[currentPair].0, id: \.self) { option in
Button(action: { choose(option) }) { Button(action: { choose(option) }) {
Text(option) Text(option)
.font(CloserFont.body) .font(CloserFont.body)

View File

@ -552,23 +552,25 @@ struct QuestionThreadView: View {
// MARK: - Sample Data // MARK: - Sample Data
// Fallback sample data. IDs must match real Android seed entries so that
// navigation to category/pack/question threads references actual data.
let bundledQuestions: [Question] = [ let bundledQuestions: [Question] = [
Question(id: "q1", text: "What made you smile today?", type: "text", options: nil, scaleMin: nil, scaleMax: nil, categoryId: nil, packId: nil), Question(id: "communication_001", text: "What is one small thing I do that helps you feel heard?", type: "text", options: nil, scaleMin: nil, scaleMax: nil, categoryId: "communication", packId: nil),
Question(id: "q2", text: "What's one thing you appreciate about your partner?", type: "text", options: nil, scaleMin: nil, scaleMax: nil, categoryId: nil, packId: nil), Question(id: "communication_002", text: "When do you feel it is easiest to talk to me?", type: "text", options: nil, scaleMin: nil, scaleMax: nil, categoryId: "communication", packId: nil),
Question(id: "q3", text: "How connected do you feel today?", type: "scale", options: nil, scaleMin: 1, scaleMax: 10, categoryId: nil, packId: nil), Question(id: "communication_151", text: "When you are upset, what helps most first?", type: "single_choice", options: ["Comfort", "Space", "Advice", "Distraction"], scaleMin: nil, scaleMax: nil, categoryId: "communication", packId: nil),
Question(id: "q4", text: "What's your ideal weekend activity together?", type: "multiple_choice", options: ["Relax at home", "Outdoor adventure", "Date night out", "Try something new"], scaleMin: nil, scaleMax: nil, categoryId: nil, packId: nil), Question(id: "date_night_001", text: "What would make a simple dinner feel fun for both of us?", type: "text", options: nil, scaleMin: nil, scaleMax: nil, categoryId: "date_night", packId: nil),
] ]
let sampleQuestions: [Question] = [ let sampleQuestions: [Question] = [
Question(id: "s1", text: "What's a dream you'd like to pursue together?", type: "text", options: nil, scaleMin: nil, scaleMax: nil, categoryId: nil, packId: nil), Question(id: "fun_001", text: "What is one thing we do together that always makes you smile?", type: "text", options: nil, scaleMin: nil, scaleMax: nil, categoryId: "fun", packId: nil),
Question(id: "s2", text: "How do you feel loved most?", type: "multiple_choice", options: ["Words of affirmation", "Quality time", "Physical touch", "Acts of service", "Gifts"], scaleMin: nil, scaleMax: nil, categoryId: nil, packId: nil), Question(id: "fun_002", text: "What is one silly memory of us that you still love?", type: "text", options: nil, scaleMin: nil, scaleMax: nil, categoryId: "fun", packId: nil),
Question(id: "s3", text: "Rate your communication today", type: "scale", options: nil, scaleMin: 1, scaleMax: 10, categoryId: nil, packId: nil), Question(id: "values_001", text: "What is one value that quietly guides your life?", type: "text", options: nil, scaleMin: nil, scaleMax: nil, categoryId: "values", packId: nil),
Question(id: "s4", text: "What's one new thing you want to try as a couple?", type: "text", options: nil, scaleMin: nil, scaleMax: nil, categoryId: nil, packId: nil), Question(id: "trust_001", text: "What makes it easy for you to trust someone?", type: "text", options: nil, scaleMin: nil, scaleMax: nil, categoryId: "trust", packId: nil),
] ]
let samplePacks: [QuestionPack] = [ let samplePacks: [QuestionPack] = [
QuestionPack(id: "p1", name: "Getting Closer", description: "Deepen your connection", categories: nil, isPremium: false), QuestionPack(id: "communication", name: "Communication", description: "Questions about listening, expressing needs, understanding each other, and talking clearly.", categories: nil, isPremium: false),
QuestionPack(id: "p2", name: "Fun & Playful", description: "Lighthearted questions", categories: nil, isPremium: false), QuestionPack(id: "fun", name: "Fun & Playful", description: "Lighthearted questions about playfulness, laughter, and shared activities.", categories: nil, isPremium: false),
QuestionPack(id: "p3", name: "Intimacy", description: "Build emotional intimacy", categories: nil, isPremium: true), QuestionPack(id: "date_night", name: "Date Night", description: "Questions designed for dates, meals, walks, or quiet time together.", categories: nil, isPremium: true),
QuestionPack(id: "p4", name: "Future Together", description: "Plan your future", categories: nil, isPremium: true), QuestionPack(id: "trust", name: "Trust", description: "Questions about trust, repair, and rebuilding.", categories: nil, isPremium: true),
] ]

View File

@ -376,33 +376,3 @@ struct WheelHistoryView: View {
} }
} }
// MARK: - Navigation helper
extension Binding where Value == String? {
func unwrapped<T: Hashable>(_ defaultValue: T) -> Binding<T> where T == String {
Binding<T>(
get: { self.wrappedValue as? T ?? defaultValue },
set: { self.wrappedValue = $0 as? String }
)
}
}
extension View {
func navigationDestination<T: Hashable>(item: Binding<T?>, @ViewBuilder destination: @escaping (T) -> some View) -> some View {
background(
NavigationLink(
isActive: Binding(
get: { item.wrappedValue != nil },
set: { if !$0 { item.wrappedValue = nil } }
),
destination: {
if let value = item.wrappedValue {
destination(value)
}
},
label: EmptyView.init
)
.hidden()
)
}
}