feat(ios): add native SwiftUI iOS app scaffold under /iphone/ (batch 1-6)
- ARCHITECTURE_AUDIT.md: full audit covering 49 screens, Firestore schema,
35 domain models, 17 Cloud Functions, RevenueCat integration, auth flow,
E2EE skip-for-MVP decision
- Project config: project.yml (XcodeGen), Info.plist, Closer.entitlements, Package.swift (SPM)
- Core: AuthService (rate limiter), FirestoreService (callable wrappers),
BillingService (RevenueCat), NotificationService (FCM)
- Models: AuthState (ObservableObject), FirestoreModels (20+ codable types),
DomainModels (35 structs)
- Theme: CloserTheme (50+ colors, typography, spacing), CommonViews
- Screens: Onboarding, pairing, home, daily questions, play hub + games
(ThisOrThat, HowWell, DesireSync, ConnectionChallenges), spin wheel
(animated 8-slice), dates (swipe cards, bucket list), settings + paywall
(RevenueCatUI)
2026-06-20 17:15:25 -05:00
import SwiftUI
import RevenueCat
import RevenueCatUI
// MARK: - S e t t i n g s
struct SettingsView : View {
@ EnvironmentObject var appState : AppState
@ State private var showPaywall = false
@ State private var showLogoutConfirm = false
@ State private var showDeleteConfirm = false
@ State private var isPairingActive = false
var body : some View {
NavigationStack {
2026-06-22 18:14:55 -05:00
ScrollView {
VStack ( alignment : . leading , spacing : CloserSpacing . lg ) {
Text ( " Manage your space, reminders, privacy, and account. " )
. font ( CloserFont . callout )
. foregroundColor ( . closerTextSecondary )
. fixedSize ( horizontal : false , vertical : true )
. padding ( . horizontal , CloserSpacing . xl )
2026-06-21 17:37:14 -05:00
if isLoggedInAnonymously {
NavigationLink {
SignUpView ( )
} label : {
SettingsProfileHeader (
initials : initials ,
name : appState . currentUser ? . displayName ? ? " You " ,
2026-06-21 17:44:56 -05:00
detail : " Create an account to save your Closer space " ,
imageUrl : appState . currentUser ? . photoUrl
2026-06-21 17:37:14 -05:00
)
}
2026-06-22 18:14:55 -05:00
. buttonStyle ( . plain )
. closerPadding ( )
2026-06-21 17:37:14 -05:00
} else {
NavigationLink {
EditProfileView ( )
} label : {
SettingsProfileHeader (
initials : initials ,
name : appState . currentUser ? . displayName ? ? " You " ,
2026-06-21 17:44:56 -05:00
detail : appState . currentUser ? . email ? ? " Edit your profile " ,
imageUrl : appState . currentUser ? . photoUrl
feat(ios): add native SwiftUI iOS app scaffold under /iphone/ (batch 1-6)
- ARCHITECTURE_AUDIT.md: full audit covering 49 screens, Firestore schema,
35 domain models, 17 Cloud Functions, RevenueCat integration, auth flow,
E2EE skip-for-MVP decision
- Project config: project.yml (XcodeGen), Info.plist, Closer.entitlements, Package.swift (SPM)
- Core: AuthService (rate limiter), FirestoreService (callable wrappers),
BillingService (RevenueCat), NotificationService (FCM)
- Models: AuthState (ObservableObject), FirestoreModels (20+ codable types),
DomainModels (35 structs)
- Theme: CloserTheme (50+ colors, typography, spacing), CommonViews
- Screens: Onboarding, pairing, home, daily questions, play hub + games
(ThisOrThat, HowWell, DesireSync, ConnectionChallenges), spin wheel
(animated 8-slice), dates (swipe cards, bucket list), settings + paywall
(RevenueCatUI)
2026-06-20 17:15:25 -05:00
)
}
2026-06-22 18:14:55 -05:00
. buttonStyle ( . plain )
. closerPadding ( )
feat(ios): add native SwiftUI iOS app scaffold under /iphone/ (batch 1-6)
- ARCHITECTURE_AUDIT.md: full audit covering 49 screens, Firestore schema,
35 domain models, 17 Cloud Functions, RevenueCat integration, auth flow,
E2EE skip-for-MVP decision
- Project config: project.yml (XcodeGen), Info.plist, Closer.entitlements, Package.swift (SPM)
- Core: AuthService (rate limiter), FirestoreService (callable wrappers),
BillingService (RevenueCat), NotificationService (FCM)
- Models: AuthState (ObservableObject), FirestoreModels (20+ codable types),
DomainModels (35 structs)
- Theme: CloserTheme (50+ colors, typography, spacing), CommonViews
- Screens: Onboarding, pairing, home, daily questions, play hub + games
(ThisOrThat, HowWell, DesireSync, ConnectionChallenges), spin wheel
(animated 8-slice), dates (swipe cards, bucket list), settings + paywall
(RevenueCatUI)
2026-06-20 17:15:25 -05:00
}
2026-06-21 17:37:14 -05:00
2026-06-22 18:14:55 -05:00
SettingsSectionCard ( title : " For the two of you " , accent : . closerSecondary ) {
if let partner = appState . currentPartner {
SettingsLinkRow (
icon : " heart.fill " ,
title : " Connected with \( partner . displayName . isEmpty ? " Partner " : partner . displayName ) " ,
subtitle : " Your shared Closer space is active " ,
tint : . closerPrimary ,
imageUrl : partner . photoUrl ,
showsChevron : false
)
} else {
Button ( action : { isPairingActive = true } ) {
SettingsLinkRow (
icon : " heart " ,
title : " Invite your partner " ,
subtitle : " Start answering together " ,
tint : . closerPrimary
)
}
. buttonStyle ( . plain )
}
SettingsDivider ( )
NavigationLink {
AnswerHistoryView ( )
} label : {
SettingsLinkRow (
icon : " checkmark.circle " ,
title : " Answer History " ,
subtitle : " Revisit the moments you have shared "
2026-06-21 17:37:14 -05:00
)
feat(ios): add native SwiftUI iOS app scaffold under /iphone/ (batch 1-6)
- ARCHITECTURE_AUDIT.md: full audit covering 49 screens, Firestore schema,
35 domain models, 17 Cloud Functions, RevenueCat integration, auth flow,
E2EE skip-for-MVP decision
- Project config: project.yml (XcodeGen), Info.plist, Closer.entitlements, Package.swift (SPM)
- Core: AuthService (rate limiter), FirestoreService (callable wrappers),
BillingService (RevenueCat), NotificationService (FCM)
- Models: AuthState (ObservableObject), FirestoreModels (20+ codable types),
DomainModels (35 structs)
- Theme: CloserTheme (50+ colors, typography, spacing), CommonViews
- Screens: Onboarding, pairing, home, daily questions, play hub + games
(ThisOrThat, HowWell, DesireSync, ConnectionChallenges), spin wheel
(animated 8-slice), dates (swipe cards, bucket list), settings + paywall
(RevenueCatUI)
2026-06-20 17:15:25 -05:00
}
2026-06-21 17:37:14 -05:00
. buttonStyle ( . plain )
feat(ios): add native SwiftUI iOS app scaffold under /iphone/ (batch 1-6)
- ARCHITECTURE_AUDIT.md: full audit covering 49 screens, Firestore schema,
35 domain models, 17 Cloud Functions, RevenueCat integration, auth flow,
E2EE skip-for-MVP decision
- Project config: project.yml (XcodeGen), Info.plist, Closer.entitlements, Package.swift (SPM)
- Core: AuthService (rate limiter), FirestoreService (callable wrappers),
BillingService (RevenueCat), NotificationService (FCM)
- Models: AuthState (ObservableObject), FirestoreModels (20+ codable types),
DomainModels (35 structs)
- Theme: CloserTheme (50+ colors, typography, spacing), CommonViews
- Screens: Onboarding, pairing, home, daily questions, play hub + games
(ThisOrThat, HowWell, DesireSync, ConnectionChallenges), spin wheel
(animated 8-slice), dates (swipe cards, bucket list), settings + paywall
(RevenueCatUI)
2026-06-20 17:15:25 -05:00
}
2026-06-22 18:14:55 -05:00
. closerPadding ( )
2026-06-21 17:37:14 -05:00
2026-06-22 18:14:55 -05:00
SettingsSectionCard ( title : " Your rhythm " , accent : . closerPrimary ) {
NavigationLink {
NavigationSettingsView ( )
} label : {
SettingsLinkRow (
icon : " bell.badge " ,
title : " Notifications " ,
subtitle : " Set gentle reminders that fit your day "
)
}
. buttonStyle ( . plain )
feat(ios): add native SwiftUI iOS app scaffold under /iphone/ (batch 1-6)
- ARCHITECTURE_AUDIT.md: full audit covering 49 screens, Firestore schema,
35 domain models, 17 Cloud Functions, RevenueCat integration, auth flow,
E2EE skip-for-MVP decision
- Project config: project.yml (XcodeGen), Info.plist, Closer.entitlements, Package.swift (SPM)
- Core: AuthService (rate limiter), FirestoreService (callable wrappers),
BillingService (RevenueCat), NotificationService (FCM)
- Models: AuthState (ObservableObject), FirestoreModels (20+ codable types),
DomainModels (35 structs)
- Theme: CloserTheme (50+ colors, typography, spacing), CommonViews
- Screens: Onboarding, pairing, home, daily questions, play hub + games
(ThisOrThat, HowWell, DesireSync, ConnectionChallenges), spin wheel
(animated 8-slice), dates (swipe cards, bucket list), settings + paywall
(RevenueCatUI)
2026-06-20 17:15:25 -05:00
}
2026-06-22 18:14:55 -05:00
. closerPadding ( )
2026-06-21 17:37:14 -05:00
2026-06-22 18:14:55 -05:00
SettingsSectionCard ( title : " Premium " , accent : . closerSecondary ) {
Button {
showPaywall = true
} label : {
SettingsLinkRow (
icon : " heart.fill " ,
title : " Upgrade to Premium " ,
subtitle : " One subscription for both partners. " ,
tint : . closerPrimary
)
}
. disabled ( isLoggedInAnonymously )
. opacity ( isLoggedInAnonymously ? 0.55 : 1 )
. buttonStyle ( . plain )
feat(ios): add native SwiftUI iOS app scaffold under /iphone/ (batch 1-6)
- ARCHITECTURE_AUDIT.md: full audit covering 49 screens, Firestore schema,
35 domain models, 17 Cloud Functions, RevenueCat integration, auth flow,
E2EE skip-for-MVP decision
- Project config: project.yml (XcodeGen), Info.plist, Closer.entitlements, Package.swift (SPM)
- Core: AuthService (rate limiter), FirestoreService (callable wrappers),
BillingService (RevenueCat), NotificationService (FCM)
- Models: AuthState (ObservableObject), FirestoreModels (20+ codable types),
DomainModels (35 structs)
- Theme: CloserTheme (50+ colors, typography, spacing), CommonViews
- Screens: Onboarding, pairing, home, daily questions, play hub + games
(ThisOrThat, HowWell, DesireSync, ConnectionChallenges), spin wheel
(animated 8-slice), dates (swipe cards, bucket list), settings + paywall
(RevenueCatUI)
2026-06-20 17:15:25 -05:00
}
2026-06-22 18:14:55 -05:00
. closerPadding ( )
2026-06-21 17:37:14 -05:00
2026-06-22 18:14:55 -05:00
SettingsSectionCard ( title : " Privacy and safety " , accent : . closerPrimary ) {
NavigationLink {
DataExportView ( )
} label : {
SettingsLinkRow (
icon : " square.and.arrow.up " ,
title : " Export Data " ,
subtitle : " Download a copy of your Closer data "
)
}
. buttonStyle ( . plain )
2026-06-21 17:37:14 -05:00
2026-06-22 18:14:55 -05:00
SettingsDivider ( )
2026-06-21 17:37:14 -05:00
2026-06-22 18:14:55 -05:00
NavigationLink {
Text ( " Privacy Policy " )
} label : {
SettingsLinkRow (
icon : " hand.raised " ,
title : " Privacy Policy " ,
subtitle : " How your data is handled "
)
}
. buttonStyle ( . plain )
2026-06-21 17:37:14 -05:00
2026-06-22 18:14:55 -05:00
SettingsDivider ( )
2026-06-21 17:37:14 -05:00
2026-06-22 18:14:55 -05:00
NavigationLink {
Text ( " Terms of Service " )
} label : {
SettingsLinkRow (
icon : " doc.text " ,
title : " Terms of Service " ,
subtitle : " The agreement for using Closer "
)
}
. buttonStyle ( . plain )
feat(ios): add native SwiftUI iOS app scaffold under /iphone/ (batch 1-6)
- ARCHITECTURE_AUDIT.md: full audit covering 49 screens, Firestore schema,
35 domain models, 17 Cloud Functions, RevenueCat integration, auth flow,
E2EE skip-for-MVP decision
- Project config: project.yml (XcodeGen), Info.plist, Closer.entitlements, Package.swift (SPM)
- Core: AuthService (rate limiter), FirestoreService (callable wrappers),
BillingService (RevenueCat), NotificationService (FCM)
- Models: AuthState (ObservableObject), FirestoreModels (20+ codable types),
DomainModels (35 structs)
- Theme: CloserTheme (50+ colors, typography, spacing), CommonViews
- Screens: Onboarding, pairing, home, daily questions, play hub + games
(ThisOrThat, HowWell, DesireSync, ConnectionChallenges), spin wheel
(animated 8-slice), dates (swipe cards, bucket list), settings + paywall
(RevenueCatUI)
2026-06-20 17:15:25 -05:00
}
2026-06-22 18:14:55 -05:00
. closerPadding ( )
2026-06-21 17:37:14 -05:00
2026-06-22 18:14:55 -05:00
SettingsSectionCard ( title : " Support " , accent : . closerSecondary ) {
NavigationLink {
HelpCenterView ( )
} label : {
SettingsLinkRow (
icon : " questionmark.circle " ,
title : " Help Center " ,
subtitle : " Answers for common questions "
)
}
. buttonStyle ( . plain )
2026-06-21 17:37:14 -05:00
2026-06-22 18:14:55 -05:00
SettingsDivider ( )
2026-06-21 17:37:14 -05:00
2026-06-22 18:14:55 -05:00
NavigationLink {
Text ( " Contact Us " )
} label : {
SettingsLinkRow (
icon : " envelope " ,
title : " Contact Us " ,
subtitle : " Get help from the Closer team "
)
}
. buttonStyle ( . plain )
SettingsDivider ( )
SettingsVersionRow ( version : appVersion )
feat(ios): add native SwiftUI iOS app scaffold under /iphone/ (batch 1-6)
- ARCHITECTURE_AUDIT.md: full audit covering 49 screens, Firestore schema,
35 domain models, 17 Cloud Functions, RevenueCat integration, auth flow,
E2EE skip-for-MVP decision
- Project config: project.yml (XcodeGen), Info.plist, Closer.entitlements, Package.swift (SPM)
- Core: AuthService (rate limiter), FirestoreService (callable wrappers),
BillingService (RevenueCat), NotificationService (FCM)
- Models: AuthState (ObservableObject), FirestoreModels (20+ codable types),
DomainModels (35 structs)
- Theme: CloserTheme (50+ colors, typography, spacing), CommonViews
- Screens: Onboarding, pairing, home, daily questions, play hub + games
(ThisOrThat, HowWell, DesireSync, ConnectionChallenges), spin wheel
(animated 8-slice), dates (swipe cards, bucket list), settings + paywall
(RevenueCatUI)
2026-06-20 17:15:25 -05:00
}
2026-06-22 18:14:55 -05:00
. closerPadding ( )
2026-06-21 17:37:14 -05:00
2026-06-22 18:14:55 -05:00
SettingsSectionCard ( title : " Account " , accent : . closerDanger . opacity ( 0.5 ) ) {
Button ( role : . destructive , action : { showLogoutConfirm = true } ) {
SettingsLinkRow (
icon : " rectangle.portrait.and.arrow.right " ,
title : " Sign Out " ,
subtitle : " Leave this device while keeping your data saved " ,
tint : . closerDanger ,
isDestructive : true ,
showsChevron : false
)
}
. buttonStyle ( . plain )
SettingsDivider ( )
Button ( role : . destructive , action : { showDeleteConfirm = true } ) {
SettingsLinkRow (
icon : " trash " ,
title : " Delete Account " ,
subtitle : " Permanently remove your Closer account " ,
tint : . closerDanger ,
isDestructive : true ,
showsChevron : false
)
}
. buttonStyle ( . plain )
feat(ios): add native SwiftUI iOS app scaffold under /iphone/ (batch 1-6)
- ARCHITECTURE_AUDIT.md: full audit covering 49 screens, Firestore schema,
35 domain models, 17 Cloud Functions, RevenueCat integration, auth flow,
E2EE skip-for-MVP decision
- Project config: project.yml (XcodeGen), Info.plist, Closer.entitlements, Package.swift (SPM)
- Core: AuthService (rate limiter), FirestoreService (callable wrappers),
BillingService (RevenueCat), NotificationService (FCM)
- Models: AuthState (ObservableObject), FirestoreModels (20+ codable types),
DomainModels (35 structs)
- Theme: CloserTheme (50+ colors, typography, spacing), CommonViews
- Screens: Onboarding, pairing, home, daily questions, play hub + games
(ThisOrThat, HowWell, DesireSync, ConnectionChallenges), spin wheel
(animated 8-slice), dates (swipe cards, bucket list), settings + paywall
(RevenueCatUI)
2026-06-20 17:15:25 -05:00
}
2026-06-22 18:14:55 -05:00
. closerPadding ( )
feat(ios): add native SwiftUI iOS app scaffold under /iphone/ (batch 1-6)
- ARCHITECTURE_AUDIT.md: full audit covering 49 screens, Firestore schema,
35 domain models, 17 Cloud Functions, RevenueCat integration, auth flow,
E2EE skip-for-MVP decision
- Project config: project.yml (XcodeGen), Info.plist, Closer.entitlements, Package.swift (SPM)
- Core: AuthService (rate limiter), FirestoreService (callable wrappers),
BillingService (RevenueCat), NotificationService (FCM)
- Models: AuthState (ObservableObject), FirestoreModels (20+ codable types),
DomainModels (35 structs)
- Theme: CloserTheme (50+ colors, typography, spacing), CommonViews
- Screens: Onboarding, pairing, home, daily questions, play hub + games
(ThisOrThat, HowWell, DesireSync, ConnectionChallenges), spin wheel
(animated 8-slice), dates (swipe cards, bucket list), settings + paywall
(RevenueCatUI)
2026-06-20 17:15:25 -05:00
}
2026-06-22 18:14:55 -05:00
. padding ( . top , CloserSpacing . md )
. padding ( . bottom , CloserSpacing . xxl )
feat(ios): add native SwiftUI iOS app scaffold under /iphone/ (batch 1-6)
- ARCHITECTURE_AUDIT.md: full audit covering 49 screens, Firestore schema,
35 domain models, 17 Cloud Functions, RevenueCat integration, auth flow,
E2EE skip-for-MVP decision
- Project config: project.yml (XcodeGen), Info.plist, Closer.entitlements, Package.swift (SPM)
- Core: AuthService (rate limiter), FirestoreService (callable wrappers),
BillingService (RevenueCat), NotificationService (FCM)
- Models: AuthState (ObservableObject), FirestoreModels (20+ codable types),
DomainModels (35 structs)
- Theme: CloserTheme (50+ colors, typography, spacing), CommonViews
- Screens: Onboarding, pairing, home, daily questions, play hub + games
(ThisOrThat, HowWell, DesireSync, ConnectionChallenges), spin wheel
(animated 8-slice), dates (swipe cards, bucket list), settings + paywall
(RevenueCatUI)
2026-06-20 17:15:25 -05:00
}
. background ( Color . closerBackground )
. navigationTitle ( " Settings " )
2026-06-22 18:14:55 -05:00
. navigationBarTitleDisplayMode ( . large )
feat(ios): add native SwiftUI iOS app scaffold under /iphone/ (batch 1-6)
- ARCHITECTURE_AUDIT.md: full audit covering 49 screens, Firestore schema,
35 domain models, 17 Cloud Functions, RevenueCat integration, auth flow,
E2EE skip-for-MVP decision
- Project config: project.yml (XcodeGen), Info.plist, Closer.entitlements, Package.swift (SPM)
- Core: AuthService (rate limiter), FirestoreService (callable wrappers),
BillingService (RevenueCat), NotificationService (FCM)
- Models: AuthState (ObservableObject), FirestoreModels (20+ codable types),
DomainModels (35 structs)
- Theme: CloserTheme (50+ colors, typography, spacing), CommonViews
- Screens: Onboarding, pairing, home, daily questions, play hub + games
(ThisOrThat, HowWell, DesireSync, ConnectionChallenges), spin wheel
(animated 8-slice), dates (swipe cards, bucket list), settings + paywall
(RevenueCatUI)
2026-06-20 17:15:25 -05:00
. fullScreenCover ( isPresented : $ showPaywall ) {
PaywallView ( )
}
. fullScreenCover ( isPresented : $ isPairingActive ) {
CreateInviteView ( )
}
. alert ( " Sign Out " , isPresented : $ showLogoutConfirm ) {
Button ( " Cancel " , role : . cancel ) { }
Button ( " Sign Out " , role : . destructive ) {
Task { await AuthService . shared . signOut ( ) }
}
} message : {
Text ( " Are you sure you want to sign out? Your data will be saved. " )
}
. alert ( " Delete Account " , isPresented : $ showDeleteConfirm ) {
Button ( " Cancel " , role : . cancel ) { }
Button ( " Delete " , role : . destructive ) {
Task { await deleteAccount ( ) }
}
} message : {
Text ( " This will permanently delete your account and all data. This action cannot be undone. " )
}
}
}
private var initials : String {
let name = appState . currentUser ? . displayName ? ? " U "
let parts = name . split ( separator : " " )
let initials = parts . prefix ( 2 ) . compactMap { $0 . first . map ( String . init ) } . joined ( )
return initials . isEmpty ? " U " : initials . uppercased ( )
}
private var isLoggedInAnonymously : Bool {
AuthService . shared . currentUser ? . isAnonymous ? ? true
}
private var appVersion : String {
Bundle . main . infoDictionary ? [ " CFBundleShortVersionString " ] as ? String ? ? " 1.0.0 "
}
private func deleteAccount ( ) async {
do {
try await FirestoreService . shared . deleteUserCallable ( )
await AuthService . shared . signOut ( )
} catch {
// E r r o r h a n d l e d u p s t r e a m
}
}
}
// MARK: - E d i t P r o f i l e
struct EditProfileView : View {
@ State private var displayName = " "
@ State private var bio = " "
@ State private var isLoading = false
var body : some View {
Form {
Section ( " Profile " ) {
TextField ( " Display Name " , text : $ displayName )
TextField ( " Bio (optional) " , text : $ bio , axis : . vertical )
. lineLimit ( 3 )
}
Section {
Button ( action : saveProfile ) {
if isLoading {
ProgressView ( )
} else {
Text ( " Save " )
}
}
. disabled ( displayName . isEmpty || isLoading )
. frame ( maxWidth : . infinity )
}
}
. background ( Color . closerBackground )
. navigationTitle ( " Edit Profile " )
. navigationBarTitleDisplayMode ( . inline )
. task {
displayName = AuthService . shared . currentUser ? . displayName ? ? " "
}
}
private func saveProfile ( ) {
isLoading = true
Task {
try ? await FirestoreService . shared . updateUserCallable ( displayName : displayName , bio : bio . isEmpty ? nil : bio )
isLoading = false
}
}
}
// MARK: - N a v i g a t i o n S e t t i n g s
struct NavigationSettingsView : View {
@ AppStorage ( " dailyReminderEnabled " ) private var dailyReminders = true
@ AppStorage ( " reminderHour " ) private var reminderHour = 20
@ AppStorage ( " reminderMinute " ) private var reminderMinute = 0
@ AppStorage ( " quietHoursEnabled " ) private var quietHoursEnabled = false
@ AppStorage ( " quietHourStart " ) private var quietHourStart = 22
@ AppStorage ( " quietHourEnd " ) private var quietHourEnd = 8
var body : some View {
Form {
Section ( " Daily Question " ) {
Toggle ( " Daily Reminder " , isOn : $ dailyReminders )
if dailyReminders {
DatePicker ( " Reminder Time " ,
selection : Binding (
get : {
Calendar . current . date ( from : DateComponents ( hour : reminderHour , minute : reminderMinute ) ) ? ? Date ( )
} ,
set : { date in
let comps = Calendar . current . dateComponents ( [ . hour , . minute ] , from : date )
reminderHour = comps . hour ? ? 20
reminderMinute = comps . minute ? ? 0
}
) ,
displayedComponents : . hourAndMinute
)
}
}
Section ( " Quiet Hours " ) {
Toggle ( " Quiet Hours " , isOn : $ quietHoursEnabled )
if quietHoursEnabled {
HStack {
Text ( " From " )
Spacer ( )
Picker ( " Start " , selection : $ quietHourStart ) {
ForEach ( 0. . < 24 ) { hour in
Text ( " \( hour ) :00 " ) . tag ( hour )
}
}
. pickerStyle ( . menu )
}
HStack {
Text ( " To " )
Spacer ( )
Picker ( " End " , selection : $ quietHourEnd ) {
ForEach ( 0. . < 24 ) { hour in
Text ( " \( hour ) :00 " ) . tag ( hour )
}
}
. pickerStyle ( . menu )
}
}
}
Section ( " Partner Activity " ) {
Toggle ( " Partner Activity Alerts " , isOn : . constant ( true ) )
Toggle ( " When partner answers question " , isOn : . constant ( true ) )
Toggle ( " When partner sends gentle reminder " , isOn : . constant ( true ) )
Toggle ( " When new date match " , isOn : . constant ( true ) )
}
}
. background ( Color . closerBackground )
. navigationTitle ( " Notification Settings " )
. navigationBarTitleDisplayMode ( . inline )
}
}
// MARK: - D a t a E x p o r t
struct DataExportView : View {
@ State private var isExporting = false
@ State private var exportComplete = false
@ State private var showError = false
var body : some View {
VStack ( spacing : CloserSpacing . xl ) {
Image ( systemName : " square.and.arrow.up " )
. font ( . system ( size : 64 ) )
. foregroundColor ( . closerPrimary )
Text ( " Export Your Data " )
. font ( CloserFont . title2 )
. foregroundColor ( . closerText )
Text ( " Download a copy of all your Closer data including answers, memories, and preferences. " )
. font ( CloserFont . callout )
. foregroundColor ( . closerTextSecondary )
. multilineTextAlignment ( . center )
if exportComplete {
HStack ( spacing : CloserSpacing . sm ) {
Image ( systemName : " checkmark.circle.fill " )
. foregroundColor ( . closerSuccess )
Text ( " Export completed — check your downloads " )
. font ( CloserFont . callout )
. foregroundColor ( . closerSuccess )
}
. padding ( )
. background ( Color . closerSuccess . opacity ( 0.1 ) )
. cornerRadius ( CloserRadius . medium )
}
Button ( action : exportData ) {
if isExporting {
ProgressView ( )
. tint ( . white )
} else {
Text ( exportComplete ? " Export Again " : " Export My Data " )
}
}
. buttonStyle ( PrimaryButtonStyle ( isDisabled : isExporting ) )
. disabled ( isExporting )
. closerPadding ( )
if exportComplete {
Button ( " Done " ) {
exportComplete = false
}
. font ( CloserFont . callout )
. foregroundColor ( . closerPrimary )
}
}
. closerPadding ( )
. background ( Color . closerBackground )
. navigationTitle ( " Export Data " )
. navigationBarTitleDisplayMode ( . inline )
. alert ( " Export Failed " , isPresented : $ showError ) {
Button ( " OK " , role : . cancel ) { }
} message : {
Text ( " Unable to export your data. Please try again later. " )
}
}
private func exportData ( ) {
isExporting = true
Task {
do {
try await FirestoreService . shared . exportDataCallable ( )
await MainActor . run {
isExporting = false
exportComplete = true
}
} catch {
await MainActor . run {
isExporting = false
showError = true
}
}
}
}
}
// MARK: - H e l p C e n t e r
struct HelpCenterView : View {
var body : some View {
List {
Section ( " FAQs " ) {
NavigationLink ( " How does the daily question work? " ) {
DailyQuestionHelpView ( )
}
NavigationLink ( " What happens if I sign out? " ) {
SignOutHelpView ( )
}
NavigationLink ( " How does pairing work? " ) {
PairingHelpView ( )
}
NavigationLink ( " What is Premium? " ) {
PremiumHelpView ( )
}
NavigationLink ( " How does encryption work? " ) {
EncryptionHelpView ( )
}
}
Section ( " Troubleshooting " ) {
NavigationLink ( " Notifications not working " ) {
GenericHelpView ( title : " Notifications " , body : " Make sure push notifications are enabled in your device Settings > Closer. If you've denied permission, go to Settings > Closer > Notifications and enable them. " )
}
NavigationLink ( " Can't pair with partner " ) {
GenericHelpView ( title : " Pairing Issues " , body : " Confirm your partner has created an account. Check that your invite code is entered correctly (hypens are optional). If the code expired, generate a new one from Settings > Connection. " )
}
NavigationLink ( " Restore purchases " ) {
GenericHelpView ( title : " Restore Purchases " , body : " Open Settings and tap 'Upgrade to Premium', then tap 'Restore Purchases'. If your subscription was purchased with a different Apple ID, sign in to that Apple ID and try again. " )
}
}
}
. listStyle ( . insetGrouped )
. background ( Color . closerBackground )
. navigationTitle ( " Help Center " )
. navigationBarTitleDisplayMode ( . inline )
}
}
// MARK: - H e l p D e t a i l V i e w s
struct DailyQuestionHelpView : View {
var body : some View {
ScrollView {
VStack ( alignment : . leading , spacing : CloserSpacing . md ) {
Text ( " Every day, you and your partner receive the same question. Answer independently — your answer is private until both of you respond. Once both answers are in, you can reveal each other's answers together. It's a simple way to deepen your connection one day at a time. " )
. font ( CloserFont . body )
. foregroundColor ( . closerText )
Text ( " Tips: " )
. font ( CloserFont . headline )
. foregroundColor ( . closerText )
. padding ( . top )
BulletPoint ( " Be honest — your partner sees only what you share " )
BulletPoint ( " Set a daily reminder in Settings > Notifications " )
BulletPoint ( " Send a gentle reminder if your partner hasn't answered yet " )
BulletPoint ( " Past answers live in Answer History " )
}
. closerPadding ( )
}
. background ( Color . closerBackground )
. navigationTitle ( " Daily Questions " )
. navigationBarTitleDisplayMode ( . inline )
}
}
struct SignOutHelpView : View {
var body : some View {
ScrollView {
VStack ( alignment : . leading , spacing : CloserSpacing . md ) {
Text ( " Your data is saved securely in the cloud. When you sign back in, everything will be right where you left it — your answers, memories, and connection will all be restored. " )
. font ( CloserFont . body )
. foregroundColor ( . closerText )
Text ( " Note: " )
. font ( CloserFont . headline )
. foregroundColor ( . closerText )
. padding ( . top )
BulletPoint ( " Your partner can still access shared data " )
BulletPoint ( " Push notifications may stop until you sign back in " )
BulletPoint ( " If you're the only one in the couple, you may need to re-invite your partner " )
}
. closerPadding ( )
}
. background ( Color . closerBackground )
. navigationTitle ( " Sign Out " )
. navigationBarTitleDisplayMode ( . inline )
}
}
struct PairingHelpView : View {
var body : some View {
ScrollView {
VStack ( alignment : . leading , spacing : CloserSpacing . md ) {
Text ( " After creating your account, go to Settings > Connection to generate a unique 6-character invite code. Share this code with your partner — they enter it on their end to connect your accounts. " )
. font ( CloserFont . body )
. foregroundColor ( . closerText )
BulletPoint ( " Codes expire after a set time — generate a new one if needed " )
BulletPoint ( " Each account can only be in one couple at a time " )
BulletPoint ( " Both of you need a Closer account first " )
}
. closerPadding ( )
}
. background ( Color . closerBackground )
. navigationTitle ( " Pairing " )
. navigationBarTitleDisplayMode ( . inline )
}
}
struct PremiumHelpView : View {
var body : some View {
ScrollView {
VStack ( alignment : . leading , spacing : CloserSpacing . md ) {
Text ( " Closer Premium unlocks deeper connection tools: " )
. font ( CloserFont . body )
. foregroundColor ( . closerText )
FeatureBullet ( " sparkles " , " Desire Sync " , " Align your desires and dreams " )
FeatureBullet ( " mountain.2.fill " , " Connection Challenges " , " Multi-day programs " )
FeatureBullet ( " clock.fill " , " Memory Lane " , " Time capsules for your relationship " )
FeatureBullet ( " questionmark.bubble.fill " , " Unlimited Packs " , " All premium question packs " )
FeatureBullet ( " chart.bar.fill " , " Advanced Insights " , " Deeper relationship analytics " )
FeatureBullet ( " heart.fill " , " Priority Support " , " Faster customer support " )
Text ( " \n $4.99/month. Cancel anytime through your Apple ID subscription settings. " )
. font ( CloserFont . callout )
. foregroundColor ( . closerTextSecondary )
}
. closerPadding ( )
}
. background ( Color . closerBackground )
. navigationTitle ( " Premium " )
. navigationBarTitleDisplayMode ( . inline )
}
}
struct EncryptionHelpView : View {
var body : some View {
ScrollView {
VStack ( alignment : . leading , spacing : CloserSpacing . md ) {
Text ( " Your answers and private data are encrypted end-to-end using industry-standard encryption. Only you and your partner have the keys to read your shared content — not even Closer's servers can access it. " )
. font ( CloserFont . body )
. foregroundColor ( . closerText )
BulletPoint ( " Encryption keys stay on your device " )
BulletPoint ( " Data is encrypted before it leaves your phone " )
BulletPoint ( " Your partner's device decrypts it on arrival " )
BulletPoint ( " Your encryption key is backed up securely for recovery " )
}
. closerPadding ( )
}
. background ( Color . closerBackground )
. navigationTitle ( " Encryption " )
. navigationBarTitleDisplayMode ( . inline )
}
}
struct GenericHelpView : View {
let title : String
let body : String
var body : some View {
ScrollView {
VStack ( alignment : . leading , spacing : CloserSpacing . md ) {
Text ( body )
. font ( CloserFont . body )
. foregroundColor ( . closerText )
}
. closerPadding ( )
}
. background ( Color . closerBackground )
. navigationTitle ( title )
. navigationBarTitleDisplayMode ( . inline )
}
}
// MARK: - P a y w a l l
struct PaywallView : View {
@ Environment ( \ . dismiss ) private var dismiss
@ State private var isPurchasing = false
@ State private var showError = false
@ State private var showSuccess = false
@ State private var selectedPlan : Plan = . monthly
enum Plan : String , CaseIterable {
case monthly = " monthly "
case yearly = " yearly "
var price : String {
switch self {
case . monthly : return " $4.99 "
case . yearly : return " $29.99 "
}
}
var period : String {
switch self {
case . monthly : return " /month "
case . yearly : return " /year "
}
}
var savings : String ? {
switch self {
case . monthly : return nil
case . yearly : return " Save 50% "
}
}
}
var body : some View {
NavigationStack {
ScrollView {
VStack ( spacing : CloserSpacing . xxl ) {
VStack ( spacing : CloserSpacing . md ) {
2026-06-21 17:04:40 -05:00
CloserIllustrationView ( imageName : " illustration-couple-paywall " , size : 184 )
Text ( " Go deeper together " )
feat(ios): add native SwiftUI iOS app scaffold under /iphone/ (batch 1-6)
- ARCHITECTURE_AUDIT.md: full audit covering 49 screens, Firestore schema,
35 domain models, 17 Cloud Functions, RevenueCat integration, auth flow,
E2EE skip-for-MVP decision
- Project config: project.yml (XcodeGen), Info.plist, Closer.entitlements, Package.swift (SPM)
- Core: AuthService (rate limiter), FirestoreService (callable wrappers),
BillingService (RevenueCat), NotificationService (FCM)
- Models: AuthState (ObservableObject), FirestoreModels (20+ codable types),
DomainModels (35 structs)
- Theme: CloserTheme (50+ colors, typography, spacing), CommonViews
- Screens: Onboarding, pairing, home, daily questions, play hub + games
(ThisOrThat, HowWell, DesireSync, ConnectionChallenges), spin wheel
(animated 8-slice), dates (swipe cards, bucket list), settings + paywall
(RevenueCatUI)
2026-06-20 17:15:25 -05:00
. font ( CloserFont . title1 )
. foregroundColor ( . closerText )
2026-06-21 17:04:40 -05:00
. multilineTextAlignment ( . center )
Text ( " Unlock everything Closer has built for couples. " )
feat(ios): add native SwiftUI iOS app scaffold under /iphone/ (batch 1-6)
- ARCHITECTURE_AUDIT.md: full audit covering 49 screens, Firestore schema,
35 domain models, 17 Cloud Functions, RevenueCat integration, auth flow,
E2EE skip-for-MVP decision
- Project config: project.yml (XcodeGen), Info.plist, Closer.entitlements, Package.swift (SPM)
- Core: AuthService (rate limiter), FirestoreService (callable wrappers),
BillingService (RevenueCat), NotificationService (FCM)
- Models: AuthState (ObservableObject), FirestoreModels (20+ codable types),
DomainModels (35 structs)
- Theme: CloserTheme (50+ colors, typography, spacing), CommonViews
- Screens: Onboarding, pairing, home, daily questions, play hub + games
(ThisOrThat, HowWell, DesireSync, ConnectionChallenges), spin wheel
(animated 8-slice), dates (swipe cards, bucket list), settings + paywall
(RevenueCatUI)
2026-06-20 17:15:25 -05:00
. font ( CloserFont . callout )
. foregroundColor ( . closerTextSecondary )
. multilineTextAlignment ( . center )
}
. padding ( . top , CloserSpacing . xxl )
// F e a t u r e s
VStack ( alignment : . leading , spacing : CloserSpacing . lg ) {
PremiumFeatureRow ( icon : " sparkles " , title : " Desire Sync " , description : " Align your desires and dreams " )
PremiumFeatureRow ( icon : " mountain.2.fill " , title : " Connection Challenges " , description : " Multi-day programs to strengthen your bond " )
PremiumFeatureRow ( icon : " clock.fill " , title : " Memory Lane " , description : " Create and unlock time capsules " )
PremiumFeatureRow ( icon : " questionmark.bubble.fill " , title : " Unlimited Packs " , description : " Access all premium question packs " )
PremiumFeatureRow ( icon : " chart.bar.fill " , title : " Advanced Insights " , description : " Deeper relationship analytics and trends " )
PremiumFeatureRow ( icon : " heart.fill " , title : " Premium Support " , description : " Priority customer support " )
}
. padding ( )
. closerCard ( )
. closerPadding ( )
// P l a n s e l e c t o r
VStack ( spacing : CloserSpacing . md ) {
ForEach ( Plan . allCases , id : \ . self ) { plan in
Button ( action : { selectedPlan = plan } ) {
HStack {
VStack ( alignment : . leading , spacing : 2 ) {
HStack ( spacing : 6 ) {
Text ( " \( plan . price ) \( plan . period ) " )
. font ( CloserFont . title3 )
. foregroundColor ( . closerText )
if let savings = plan . savings {
Text ( savings )
. font ( CloserFont . caption )
. foregroundColor ( . closerSuccess )
. padding ( . horizontal , 6 )
. padding ( . vertical , 2 )
. background ( Color . closerSuccess . opacity ( 0.1 ) )
. cornerRadius ( CloserRadius . full )
}
}
if plan = = . yearly {
Text ( " Billed annually — cancel anytime " )
. font ( CloserFont . caption )
. foregroundColor ( . closerTextSecondary )
}
}
Spacer ( )
if selectedPlan = = plan {
Image ( systemName : " checkmark.circle.fill " )
. foregroundColor ( . closerPrimary )
} else {
Circle ( )
. stroke ( Color . closerDivider )
. frame ( width : 22 , height : 22 )
}
}
. padding ( )
. background ( selectedPlan = = plan ? Color . closerPrimary . opacity ( 0.05 ) : Color . closerSurface )
. cornerRadius ( CloserRadius . medium )
. overlay (
RoundedRectangle ( cornerRadius : CloserRadius . medium )
. stroke ( selectedPlan = = plan ? Color . closerPrimary : Color . closerDivider )
)
}
. buttonStyle ( . plain )
}
}
. closerPadding ( )
if showSuccess {
HStack ( spacing : CloserSpacing . sm ) {
Image ( systemName : " checkmark.circle.fill " )
. foregroundColor ( . closerSuccess )
Text ( " Welcome to Premium! " )
. font ( CloserFont . callout )
. foregroundColor ( . closerSuccess )
}
. padding ( )
. background ( Color . closerSuccess . opacity ( 0.1 ) )
. cornerRadius ( CloserRadius . medium )
}
// S u b s c r i b e b u t t o n
Button ( action : purchase ) {
if isPurchasing {
ProgressView ( )
. tint ( . white )
} else {
Text ( " Try Premium Free " )
}
}
. buttonStyle ( PrimaryButtonStyle ( isDisabled : isPurchasing ) )
. disabled ( isPurchasing )
. closerPadding ( )
// R e s t o r e
Button ( " Restore Purchases " ) {
Task {
do {
let _ = try await Purchases . shared . restorePurchases ( )
showSuccess = true
} catch {
showError = true
}
}
}
. font ( CloserFont . callout )
. foregroundColor ( . closerPrimary )
// T e r m s
Text ( " Subscription automatically renews unless cancelled at least 24 hours before the end of the current period. Manage in your Apple ID Settings. " )
. font ( CloserFont . caption2 )
. foregroundColor ( . closerTextSecondary )
. multilineTextAlignment ( . center )
. closerPadding ( )
}
}
. background ( Color . closerBackground )
. toolbar {
ToolbarItem ( placement : . confirmationAction ) {
Button ( " Close " ) { dismiss ( ) }
}
}
. alert ( " Purchase Error " , isPresented : $ showError ) {
Button ( " OK " , role : . cancel ) { }
} message : {
Text ( " Unable to process your purchase. Please try again later. " )
}
}
}
private func purchase ( ) {
isPurchasing = true
Task {
do {
let _ = try await BillingService . shared . purchase ( )
await MainActor . run {
isPurchasing = false
showSuccess = true
}
} catch {
await MainActor . run {
isPurchasing = false
showError = true
}
}
}
}
}
2026-06-21 17:37:14 -05:00
// MARK: - S e t t i n g s R o w H e l p e r s
2026-06-22 18:14:55 -05:00
struct SettingsSectionCard < Content : View > : View {
let title : String
let accent : Color
let content : Content
init ( title : String , accent : Color , @ ViewBuilder content : ( ) -> Content ) {
self . title = title
self . accent = accent
self . content = content ( )
}
var body : some View {
VStack ( alignment : . leading , spacing : CloserSpacing . sm ) {
Text ( title )
. font ( CloserFont . caption )
. foregroundColor ( . closerTextSecondary )
. textCase ( . uppercase )
VStack ( spacing : 0 ) {
content
}
2026-06-23 12:31:59 -05:00
. background ( Color . closerSurface . opacity ( 0.72 ) )
2026-06-22 18:14:55 -05:00
. clipShape ( RoundedRectangle ( cornerRadius : CloserRadius . large , style : . continuous ) )
}
. padding ( CloserSpacing . md )
. background (
LinearGradient (
colors : [
Color . closerSurface ,
accent . opacity ( 0.18 )
] ,
startPoint : . topLeading ,
endPoint : . bottomTrailing
)
)
. clipShape ( RoundedRectangle ( cornerRadius : CloserRadius . xlarge , style : . continuous ) )
. closerShadow ( level : . small )
}
}
2026-06-21 17:37:14 -05:00
struct SettingsProfileHeader : View {
let initials : String
let name : String
let detail : String
2026-06-21 17:44:56 -05:00
var imageUrl : String ? = nil
2026-06-21 17:37:14 -05:00
var body : some View {
VStack ( spacing : CloserSpacing . sm ) {
2026-06-21 17:44:56 -05:00
SettingsAvatarView (
imageUrl : imageUrl ,
fallbackIcon : nil ,
fallbackText : initials ,
size : 72 ,
tint : . closerPrimary
)
2026-06-21 17:37:14 -05:00
Text ( name )
. font ( CloserFont . title3 )
. foregroundColor ( . closerText )
Text ( detail )
. font ( CloserFont . callout )
. foregroundColor ( . closerTextSecondary )
. multilineTextAlignment ( . center )
}
. frame ( maxWidth : . infinity )
2026-06-22 18:14:55 -05:00
. padding ( CloserSpacing . lg )
. background (
LinearGradient (
colors : [
Color . closerSurface ,
Color . closerSecondary . opacity ( 0.14 )
] ,
startPoint : . topLeading ,
endPoint : . bottomTrailing
)
)
. clipShape ( RoundedRectangle ( cornerRadius : CloserRadius . xlarge , style : . continuous ) )
. closerShadow ( level : . small )
}
}
struct SettingsLinkRow : View {
let icon : String
let title : String
let subtitle : String
var tint : Color = . closerTextSecondary
var imageUrl : String ? = nil
var isDestructive : Bool = false
var showsChevron : Bool = true
var body : some View {
HStack ( spacing : CloserSpacing . md ) {
if let imageUrl , ! imageUrl . isEmpty {
SettingsAvatarView (
imageUrl : imageUrl ,
fallbackIcon : icon ,
fallbackText : nil ,
size : 40 ,
tint : tint
)
} else {
SettingsIconTile ( icon : icon , tint : tint , isDestructive : isDestructive )
}
VStack ( alignment : . leading , spacing : 3 ) {
Text ( title )
. font ( CloserFont . body )
. foregroundColor ( isDestructive ? . closerDanger : . closerText )
. lineLimit ( 1 )
Text ( subtitle )
. font ( CloserFont . caption )
. foregroundColor ( . closerTextSecondary )
. lineLimit ( 2 )
}
Spacer ( minLength : CloserSpacing . sm )
if showsChevron {
Image ( systemName : " chevron.right " )
. font ( . footnote . weight ( . semibold ) )
. foregroundColor ( . closerTextSecondary . opacity ( 0.72 ) )
}
}
. padding ( . horizontal , CloserSpacing . md )
. padding ( . vertical , CloserSpacing . sm )
. contentShape ( Rectangle ( ) )
}
}
struct SettingsIconTile : View {
let icon : String
let tint : Color
var isDestructive : Bool = false
var body : some View {
ZStack {
RoundedRectangle ( cornerRadius : 14 , style : . continuous )
. fill ( tint . opacity ( isDestructive ? 0.12 : 0.14 ) )
Image ( systemName : icon )
. font ( . headline )
. foregroundColor ( tint )
}
. frame ( width : 40 , height : 40 )
. accessibilityHidden ( true )
}
}
struct SettingsDivider : View {
var body : some View {
Rectangle ( )
. fill ( Color . closerDivider . opacity ( 0.8 ) )
. frame ( height : 1 )
. padding ( . leading , 64 )
. padding ( . trailing , CloserSpacing . md )
}
}
struct SettingsVersionRow : View {
let version : String
var body : some View {
SettingsLinkRow (
icon : " info.circle " ,
title : " Version " ,
subtitle : version ,
showsChevron : false
)
2026-06-21 17:37:14 -05:00
}
}
struct SettingsLinkLabel : View {
let icon : String
let title : String
let subtitle : String
var tint : Color = . closerTextSecondary
2026-06-21 17:44:56 -05:00
var imageUrl : String ? = nil
2026-06-21 17:37:14 -05:00
var body : some View {
HStack ( spacing : CloserSpacing . md ) {
2026-06-21 17:44:56 -05:00
SettingsAvatarView (
imageUrl : imageUrl ,
fallbackIcon : icon ,
fallbackText : nil ,
size : 34 ,
tint : tint
)
2026-06-21 17:37:14 -05:00
VStack ( alignment : . leading , spacing : 2 ) {
Text ( title )
. font ( CloserFont . body )
. foregroundColor ( . closerText )
Text ( subtitle )
. font ( CloserFont . caption )
. foregroundColor ( . closerTextSecondary )
}
}
. padding ( . vertical , CloserSpacing . xs )
}
}
2026-06-21 17:44:56 -05:00
struct SettingsAvatarView : View {
let imageUrl : String ?
let fallbackIcon : String ?
let fallbackText : String ?
let size : CGFloat
let tint : Color
var body : some View {
ZStack {
Circle ( )
. fill ( tint . opacity ( 0.16 ) )
if let urlString = imageUrl , ! urlString . isEmpty , let url = URL ( string : urlString ) {
AsyncImage ( url : url ) { phase in
switch phase {
case . success ( let image ) :
image
. resizable ( )
. scaledToFill ( )
default :
fallback
}
}
} else {
fallback
}
}
. frame ( width : size , height : size )
. clipShape ( Circle ( ) )
. overlay ( Circle ( ) . stroke ( Color . closerSurface , lineWidth : 1.5 ) )
. accessibilityHidden ( true )
}
@ ViewBuilder
private var fallback : some View {
if let fallbackText {
Text ( fallbackText )
. font ( size >= 60 ? CloserFont . title1 : CloserFont . caption )
. foregroundColor ( tint )
} else if let fallbackIcon {
Image ( systemName : fallbackIcon )
. font ( size >= 44 ? . title3 : . headline )
. foregroundColor ( tint )
}
}
}
2026-06-21 17:04:40 -05:00
// MARK: - P r e m i u m S e t t i n g s C T A
struct PremiumSettingsCTA : View {
var body : some View {
HStack ( spacing : CloserSpacing . md ) {
Image ( " illustration-couple-subscription " )
. resizable ( )
. scaledToFill ( )
. frame ( width : 68 , height : 68 )
. clipShape ( RoundedRectangle ( cornerRadius : CloserRadius . large , style : . continuous ) )
. accessibilityHidden ( true )
VStack ( alignment : . leading , spacing : CloserSpacing . xs ) {
Text ( " Upgrade to Premium " )
. font ( CloserFont . headline )
. foregroundColor ( . closerText )
Text ( " One subscription for both partners. " )
. font ( CloserFont . caption )
. foregroundColor ( . closerTextSecondary )
}
Spacer ( )
Image ( systemName : " chevron.right " )
. font ( . footnote . weight ( . semibold ) )
. foregroundColor ( . closerTextSecondary )
}
. padding ( . vertical , CloserSpacing . xs )
}
}
feat(ios): add native SwiftUI iOS app scaffold under /iphone/ (batch 1-6)
- ARCHITECTURE_AUDIT.md: full audit covering 49 screens, Firestore schema,
35 domain models, 17 Cloud Functions, RevenueCat integration, auth flow,
E2EE skip-for-MVP decision
- Project config: project.yml (XcodeGen), Info.plist, Closer.entitlements, Package.swift (SPM)
- Core: AuthService (rate limiter), FirestoreService (callable wrappers),
BillingService (RevenueCat), NotificationService (FCM)
- Models: AuthState (ObservableObject), FirestoreModels (20+ codable types),
DomainModels (35 structs)
- Theme: CloserTheme (50+ colors, typography, spacing), CommonViews
- Screens: Onboarding, pairing, home, daily questions, play hub + games
(ThisOrThat, HowWell, DesireSync, ConnectionChallenges), spin wheel
(animated 8-slice), dates (swipe cards, bucket list), settings + paywall
(RevenueCatUI)
2026-06-20 17:15:25 -05:00
// MARK: - P r e m i u m F e a t u r e R o w
struct PremiumFeatureRow : View {
let icon : String
let title : String
let description : String
var body : some View {
HStack ( spacing : CloserSpacing . md ) {
Image ( systemName : icon )
. font ( . title3 )
. foregroundColor ( . closerGold )
. frame ( width : 32 )
VStack ( alignment : . leading , spacing : 2 ) {
Text ( title )
. font ( CloserFont . body )
. foregroundColor ( . closerText )
Text ( description )
. font ( CloserFont . caption )
. foregroundColor ( . closerTextSecondary )
}
}
}
}
// MARK: - H e l p e r V i e w s
struct BulletPoint : View {
let text : String
init ( _ text : String ) {
self . text = text
}
var body : some View {
HStack ( alignment : . top , spacing : CloserSpacing . sm ) {
Text ( " \ u{2022} " )
. foregroundColor ( . closerPrimary )
Text ( text )
. font ( CloserFont . body )
. foregroundColor ( . closerText )
}
}
}
struct FeatureBullet : View {
let icon : String
let title : String
let description : String
init ( _ icon : String , _ title : String , _ description : String ) {
self . icon = icon
self . title = title
self . description = description
}
var body : some View {
HStack ( spacing : CloserSpacing . md ) {
Image ( systemName : icon )
. font ( . callout )
. foregroundColor ( . closerGold )
. frame ( width : 24 )
VStack ( alignment : . leading , spacing : 2 ) {
Text ( title )
. font ( CloserFont . body )
. foregroundColor ( . closerText )
Text ( description )
. font ( CloserFont . caption )
. foregroundColor ( . closerTextSecondary )
}
}
. padding ( . vertical , 4 )
}
}