336 lines
12 KiB
Swift
336 lines
12 KiB
Swift
import SwiftUI
|
|
|
|
// MARK: - Pair Prompt
|
|
|
|
struct PairPromptView: View {
|
|
@State private var showCreateInvite = false
|
|
@State private var showAcceptInvite = false
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
VStack(spacing: CloserSpacing.xxl) {
|
|
Spacer()
|
|
|
|
CloserIllustrationView(imageName: "illustration-couple-invite", size: 190)
|
|
|
|
Text("Connect with Your Partner")
|
|
.font(CloserFont.title1)
|
|
.foregroundColor(.closerText)
|
|
.multilineTextAlignment(.center)
|
|
|
|
Text("Create an invite code for your partner to join, or enter their code to connect.")
|
|
.font(CloserFont.callout)
|
|
.foregroundColor(.closerTextSecondary)
|
|
.multilineTextAlignment(.center)
|
|
.closerPadding()
|
|
|
|
VStack(spacing: CloserSpacing.lg) {
|
|
Button(action: { showCreateInvite = true }) {
|
|
Label("Invite my partner", systemImage: "plus.circle.fill")
|
|
}
|
|
.buttonStyle(PrimaryButtonStyle())
|
|
|
|
Button(action: { showAcceptInvite = true }) {
|
|
Label("Enter a code", systemImage: "key.fill")
|
|
}
|
|
.buttonStyle(SecondaryButtonStyle())
|
|
}
|
|
.closerPadding()
|
|
|
|
Spacer()
|
|
}
|
|
.background(Color.closerBackground)
|
|
.navigationDestination(isPresented: $showCreateInvite) {
|
|
CreateInviteView()
|
|
}
|
|
.navigationDestination(isPresented: $showAcceptInvite) {
|
|
AcceptInviteView()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Create Invite
|
|
|
|
struct CreateInviteView: View {
|
|
@EnvironmentObject var appState: AppState
|
|
@State private var inviteCode = ""
|
|
@State private var isLoading = false
|
|
@State private var errorMessage: String?
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(spacing: CloserSpacing.xxl) {
|
|
VStack(spacing: CloserSpacing.sm) {
|
|
Image(systemName: "square.and.arrow.up.fill")
|
|
.font(.system(size: 44))
|
|
.foregroundColor(.closerPrimary)
|
|
Text("Share Your Code")
|
|
.font(CloserFont.title1)
|
|
.foregroundColor(.closerText)
|
|
Text("Share this code with your partner so they can connect with you")
|
|
.font(CloserFont.callout)
|
|
.foregroundColor(.closerTextSecondary)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
.padding(.top, CloserSpacing.xxl)
|
|
|
|
if !inviteCode.isEmpty {
|
|
VStack(spacing: CloserSpacing.md) {
|
|
Text(inviteCode)
|
|
.font(.system(size: 40, weight: .bold, design: .monospaced))
|
|
.foregroundColor(.closerPrimary)
|
|
.tracking(8)
|
|
.padding(CloserSpacing.xxl)
|
|
.background(Color.closerSurface)
|
|
.cornerRadius(CloserRadius.large)
|
|
|
|
Button(action: copyCode) {
|
|
Label("Copy Code", systemImage: "doc.on.doc")
|
|
}
|
|
.buttonStyle(SecondaryButtonStyle())
|
|
.frame(maxWidth: 200)
|
|
|
|
Button(action: shareCode) {
|
|
Label("Share", systemImage: "square.and.arrow.up")
|
|
}
|
|
.buttonStyle(PrimaryButtonStyle())
|
|
}
|
|
} else if isLoading {
|
|
ProgressView()
|
|
.tint(.closerPrimary)
|
|
} else {
|
|
Button("Generate Invite Code") {
|
|
generateInvite()
|
|
}
|
|
.buttonStyle(PrimaryButtonStyle())
|
|
}
|
|
|
|
if let error = errorMessage {
|
|
Text(error)
|
|
.font(CloserFont.caption)
|
|
.foregroundColor(.closerDanger)
|
|
}
|
|
|
|
NavigationLink {
|
|
InviteConfirmView()
|
|
} label: {
|
|
Text("Your partner will see this after entering your code")
|
|
.font(CloserFont.footnote)
|
|
.foregroundColor(.closerTextSecondary)
|
|
}
|
|
}
|
|
.closerPadding()
|
|
}
|
|
.background(Color.closerBackground)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
}
|
|
|
|
private func generateInvite() {
|
|
isLoading = true
|
|
errorMessage = nil
|
|
|
|
Task {
|
|
do {
|
|
let (code, _) = try await FirestoreService.shared.createInviteCallable()
|
|
self.inviteCode = code
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
isLoading = false
|
|
}
|
|
}
|
|
|
|
private func copyCode() {
|
|
UIPasteboard.general.string = inviteCode
|
|
}
|
|
|
|
private func shareCode() {
|
|
let av = UIActivityViewController(activityItems: [inviteCode], applicationActivities: nil)
|
|
if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
|
let window = scene.windows.first,
|
|
let root = window.rootViewController {
|
|
root.present(av, animated: true)
|
|
}
|
|
}
|
|
|
|
private func generateSixCharCode() -> String {
|
|
let chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" // Avoid ambiguous 0/O, 1/I
|
|
return String((0..<6).map { _ in chars.randomElement()! })
|
|
}
|
|
}
|
|
|
|
// MARK: - Accept Invite
|
|
|
|
struct AcceptInviteView: View {
|
|
@EnvironmentObject var appState: AppState
|
|
@State private var code = ""
|
|
@State private var isLoading = false
|
|
@State private var errorMessage: String?
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(spacing: CloserSpacing.xxl) {
|
|
VStack(spacing: CloserSpacing.sm) {
|
|
Image(systemName: "key.fill")
|
|
.font(.system(size: 44))
|
|
.foregroundColor(.closerPrimary)
|
|
Text("Enter Invite Code")
|
|
.font(CloserFont.title1)
|
|
.foregroundColor(.closerText)
|
|
Text("Ask your partner for their invite code and enter it below")
|
|
.font(CloserFont.callout)
|
|
.foregroundColor(.closerTextSecondary)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
.padding(.top, CloserSpacing.xxl)
|
|
|
|
VStack(spacing: CloserSpacing.md) {
|
|
TextField("XXXXXX", text: $code)
|
|
.font(.system(size: 32, weight: .bold, design: .monospaced))
|
|
.multilineTextAlignment(.center)
|
|
.tracking(8)
|
|
.autocapitalization(.allCharacters)
|
|
.disableAutocorrection(true)
|
|
.padding()
|
|
.background(Color.closerSurface)
|
|
.cornerRadius(CloserRadius.large)
|
|
.onChange(of: code) { oldValue, newValue in
|
|
if newValue.count > 6 {
|
|
code = String(newValue.prefix(6))
|
|
}
|
|
}
|
|
|
|
if let error = errorMessage {
|
|
Text(error)
|
|
.font(CloserFont.caption)
|
|
.foregroundColor(.closerDanger)
|
|
}
|
|
|
|
Button(action: acceptInvite) {
|
|
if isLoading {
|
|
ProgressView().tint(.white)
|
|
} else {
|
|
Text("Connect")
|
|
}
|
|
}
|
|
.buttonStyle(PrimaryButtonStyle(isDisabled: isLoading || code.count != 6))
|
|
.disabled(isLoading || code.count != 6)
|
|
}
|
|
}
|
|
.closerPadding()
|
|
}
|
|
.background(Color.closerBackground)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
}
|
|
|
|
private func acceptInvite() {
|
|
guard code.count == 6 else { return }
|
|
isLoading = true
|
|
errorMessage = nil
|
|
|
|
Task {
|
|
do {
|
|
let coupleId = try await FirestoreService.shared.acceptInviteCallable(code: code)
|
|
await appState.refreshData()
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
isLoading = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Invite Confirm
|
|
|
|
struct InviteConfirmView: View {
|
|
@EnvironmentObject var appState: AppState
|
|
|
|
var body: some View {
|
|
VStack(spacing: CloserSpacing.xxl) {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.font(.system(size: 72))
|
|
.foregroundColor(.closerSuccess)
|
|
|
|
Text("Connected!")
|
|
.font(CloserFont.title1)
|
|
.foregroundColor(.closerText)
|
|
|
|
Text("You and your partner are now connected. Start exploring questions and games together.")
|
|
.font(CloserFont.callout)
|
|
.foregroundColor(.closerTextSecondary)
|
|
.multilineTextAlignment(.center)
|
|
|
|
Button("Let's Go!") {
|
|
Task { await appState.refreshData() }
|
|
}
|
|
.buttonStyle(PrimaryButtonStyle())
|
|
}
|
|
.closerPadding()
|
|
.background(Color.closerBackground)
|
|
.navigationBarBackButtonHidden()
|
|
}
|
|
}
|
|
|
|
// MARK: - Recovery
|
|
|
|
struct RecoveryView: View {
|
|
var body: some View {
|
|
VStack(spacing: CloserSpacing.xxl) {
|
|
Image(systemName: "key.icloud.fill")
|
|
.font(.system(size: 48))
|
|
.foregroundColor(.closerPrimary)
|
|
|
|
Text("Unlock Answers")
|
|
.font(CloserFont.title1)
|
|
.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.")
|
|
.font(CloserFont.callout)
|
|
.foregroundColor(.closerTextSecondary)
|
|
.multilineTextAlignment(.center)
|
|
|
|
Text("Recovery phrase setup is available when E2EE is fully implemented.")
|
|
.font(CloserFont.footnote)
|
|
.foregroundColor(.closerTextSecondary)
|
|
.italic()
|
|
|
|
Spacer()
|
|
}
|
|
.closerPadding()
|
|
.background(Color.closerBackground)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
}
|
|
}
|
|
|
|
// MARK: - Encryption Upgrade
|
|
|
|
struct EncryptionUpgradeView: View {
|
|
var body: some View {
|
|
VStack(spacing: CloserSpacing.xxl) {
|
|
Image(systemName: "lock.shield.fill")
|
|
.font(.system(size: 48))
|
|
.foregroundColor(.closerPrimary)
|
|
|
|
Text("Secure Your Answers")
|
|
.font(CloserFont.title1)
|
|
.foregroundColor(.closerText)
|
|
|
|
Text("End-to-end encryption ensures your answers are only visible to you and your partner.")
|
|
.font(CloserFont.callout)
|
|
.foregroundColor(.closerTextSecondary)
|
|
.multilineTextAlignment(.center)
|
|
|
|
Text("E2EE upgrade is available when the full encryption layer is implemented.")
|
|
.font(CloserFont.footnote)
|
|
.foregroundColor(.closerTextSecondary)
|
|
.italic()
|
|
|
|
Spacer()
|
|
}
|
|
.closerPadding()
|
|
.background(Color.closerBackground)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
}
|
|
}
|