fix(ios): address Pass B warnings from code audit
This commit is contained in:
parent
cb54ed3079
commit
59c239694a
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
]
|
]
|
||||||
|
|
@ -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()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue