From 5cf32a437071223e97693c48370b6a2a5b9b109f Mon Sep 17 00:00:00 2001 From: kutesir Date: Sun, 21 Jun 2026 03:03:28 +0300 Subject: [PATCH] Calendar: fix Day/3-Day/Week/List task rendering; lighter checkbox; confirm future completes Calendar timeline views only populated in Month. Fixes: - Day/3-Day time grid dropped untimed (midnight) tasks and ignored recurrence. Now uses occurrences(on:) + hasTime, and adds an all-day band for untimed tasks, all-day events, and the day's workout. - Week showed abstract color bars; now lists real task/event titles (up to 4/day + "N more") via a shared cvAgendaItems helper. - List view and the task-list toggle now use occurrences(on:) so recurring tasks appear. TodayView: - Slimmer task checkbox (18pt open ring vs 22pt filled donut). - Confirm before completing a future-dated task (Next 7 Days / Later) to prevent accidental completion; today/overdue still toggle instantly. Bump build to 2.0 (9). Add App Store support-site one-pager. Co-Authored-By: Claude Opus 4.8 --- KisaniCal.xcodeproj/project.pbxproj | 42 +++----- .../xcshareddata/xcschemes/KisaniCal.xcscheme | 16 ++- KisaniCal/Views/CalendarView.swift | 96 ++++++++++++++---- KisaniCal/Views/TodayView.swift | 45 ++++++--- project.yml | 2 +- support-site/index.html | 98 +++++++++++++++++++ 6 files changed, 235 insertions(+), 64 deletions(-) create mode 100644 support-site/index.html diff --git a/KisaniCal.xcodeproj/project.pbxproj b/KisaniCal.xcodeproj/project.pbxproj index 0390d8c..3429f50 100644 --- a/KisaniCal.xcodeproj/project.pbxproj +++ b/KisaniCal.xcodeproj/project.pbxproj @@ -82,7 +82,7 @@ 0506183945D16EC443A69651 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = ""; }; 0513EB8C7F6B6853B9E93E09 /* TaskLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskLiveActivity.swift; sourceTree = ""; }; 0C96EC00F6B021DBA3CC1A79 /* ProfileSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSetupView.swift; sourceTree = ""; }; - 0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = KisaniCalWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = KisaniCalWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 106EEF572C6F8990408329F0 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; 1AA7498EFED7692022F3E7E1 /* KisaniCal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KisaniCal.entitlements; sourceTree = ""; }; 208F82348DEBD9FF7B0DCF17 /* MyTasksWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyTasksWidget.swift; sourceTree = ""; }; @@ -108,7 +108,7 @@ 9100804DB1E61EA882CC54DA /* FloatingTabState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingTabState.swift; sourceTree = ""; }; 92824ED40ECD41EFD4F78BEC /* ISSUES.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ISSUES.md; sourceTree = ""; }; 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; - 9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; }; ADF6CCD95A587E26E30F5712 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = ""; }; AF905C574F34B4EE51A8D21E /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = ""; }; B4CFFDFE4653A9E901CEF28D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; @@ -326,7 +326,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 2620; + LastUpgradeCheck = 1500; TargetAttributes = { 28B43516AD88946E21D9BFE0 = { DevelopmentTeam = K8BLMMR883; @@ -456,7 +456,6 @@ CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = Wenza; @@ -468,8 +467,8 @@ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -516,13 +515,11 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 5; - DEAD_CODE_STRIPPING = YES; + CURRENT_PROJECT_VERSION = 9; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = K8BLMMR883; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -536,7 +533,6 @@ MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; - STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; @@ -549,15 +545,10 @@ APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_ENTITLEMENTS = KisaniCalWidgets/KisaniCalWidgets.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = KisaniCalWidgets/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 18.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.KisaniCalWidgets; SDKROOT = iphoneos; SWIFT_VERSION = 5.9; @@ -599,13 +590,11 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 5; - DEAD_CODE_STRIPPING = YES; + CURRENT_PROJECT_VERSION = 9; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = K8BLMMR883; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -626,7 +615,6 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; - STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -639,7 +627,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = WenzaWatch/WenzaWatch.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = WenzaWatch/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Wenza; @@ -658,15 +645,10 @@ APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_ENTITLEMENTS = KisaniCalWidgets/KisaniCalWidgets.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = KisaniCalWidgets/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 18.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.KisaniCalWidgets; SDKROOT = iphoneos; SWIFT_VERSION = 5.9; @@ -680,7 +662,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = WenzaWatch/WenzaWatch.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = WenzaWatch/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Wenza; @@ -701,7 +682,6 @@ CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = Wenza; @@ -713,8 +693,8 @@ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/KisaniCal.xcodeproj/xcshareddata/xcschemes/KisaniCal.xcscheme b/KisaniCal.xcodeproj/xcshareddata/xcschemes/KisaniCal.xcscheme index 675309e..9db359c 100644 --- a/KisaniCal.xcodeproj/xcshareddata/xcschemes/KisaniCal.xcscheme +++ b/KisaniCal.xcodeproj/xcshareddata/xcschemes/KisaniCal.xcscheme @@ -1,10 +1,11 @@ + LastUpgradeVersion = "1500" + version = "1.7"> + buildImplicitDependencies = "YES" + runPostActionsOnFailure = "NO"> + shouldUseLaunchSchemeArgsEnv = "YES" + onlyGenerateCoverageForSpecifiedTargets = "NO"> + + + + + + diff --git a/KisaniCal/Views/CalendarView.swift b/KisaniCal/Views/CalendarView.swift index 1b3ae92..4e3868d 100644 --- a/KisaniCal/Views/CalendarView.swift +++ b/KisaniCal/Views/CalendarView.swift @@ -482,10 +482,7 @@ struct CalendarView: View { // MARK: - Task list mode (toggle view) private var calDayTaskListView: some View { - let all = taskVM.tasks.filter { t in - guard let d = t.dueDate else { return false } - return cal.isDate(d, inSameDayAs: selectedDate) - } + let all = taskVM.occurrences(on: selectedDate) let incomplete = all.filter { !$0.isComplete } let complete = all.filter { $0.isComplete } @@ -633,10 +630,7 @@ struct CalendarView: View { Divider().background(AppColors.border(cs)).padding(.horizontal, 16) ScrollView(showsIndicators: false) { VStack(spacing: 0) { - let dayTasks = taskVM.tasks.filter { t in - guard !t.isComplete, let d = t.dueDate else { return false } - return cal.isDate(d, inSameDayAs: selectedDate) - } + let dayTasks = taskVM.occurrences(on: selectedDate).filter { !$0.isComplete } let events = calStore.isAuthorized ? calStore.events(for: selectedDate) : [] if dayTasks.isEmpty && events.isEmpty { cvEmptyDay @@ -805,7 +799,7 @@ struct CalendarView: View { ForEach(cvWeekDays(), id: \.self) { date in let isTod = cal.isDateInToday(date) let isSel = cal.isDate(date, inSameDayAs: selectedDate) - let dots = eventDots(for: date) + let items = cvAgendaItems(date) VStack(alignment: .leading, spacing: 5) { HStack(spacing: 4) { Text(cvDayFmt.string(from: date)) @@ -815,12 +809,20 @@ struct CalendarView: View { .font(AppFonts.sans(12, weight: isTod ? .bold : .medium)) .foregroundColor(isTod ? AppColors.accent : AppColors.text(cs)) } - if dots.isEmpty { + if items.isEmpty { Text("Free").font(AppFonts.sans(9)).foregroundColor(AppColors.text3(cs)) } else { - ForEach(dots.indices, id: \.self) { i in - RoundedRectangle(cornerRadius: 2).fill(dots[i].opacity(0.5)) - .frame(maxWidth: .infinity).frame(height: 7) + ForEach(Array(items.prefix(4).enumerated()), id: \.offset) { _, item in + HStack(spacing: 5) { + RoundedRectangle(cornerRadius: 1.5).fill(item.color) + .frame(width: 2.5, height: 11) + Text(item.title).font(AppFonts.sans(9)) + .foregroundColor(AppColors.text2(cs)).lineLimit(1) + } + } + if items.count > 4 { + Text("+\(items.count - 4) more").font(AppFonts.sans(8)) + .foregroundColor(AppColors.text3(cs)) } } } @@ -890,7 +892,66 @@ struct CalendarView: View { private func cvTimeGrid(days: [Date]) -> some View { let totalH = CGFloat(cvEndH - cvStartH) * cvHourH - return ZStack(alignment: .topLeading) { + return VStack(spacing: 0) { + cvAllDayBand(days: days) + cvHourGrid(days: days, totalH: totalH) + } + } + + /// Untimed tasks, all-day events and the day's workout — these have no slot on + /// the hour grid, so they sit in a compact band above it (one column per day). + private func cvAllDayItems(_ date: Date) -> [CVTimeItem] { + var items: [CVTimeItem] = [] + if let w = workoutVM.program(for: date) { + items.append(CVTimeItem(title: w.name, color: AppColors.blue, startMin: -1, endMin: -1)) + } + for t in taskVM.occurrences(on: date) where !t.isComplete && !t.hasTime { + items.append(CVTimeItem(title: t.title, color: t.quadrant.color, startMin: -1, endMin: -1)) + } + for ev in (calStore.isAuthorized ? calStore.events(for: date) : []) where ev.isAllDay { + items.append(CVTimeItem(title: ev.title ?? "Event", color: Color(cgColor: ev.calendar.cgColor), startMin: -1, endMin: -1)) + } + return items + } + + /// Everything happening on a date — all-day items first, then timed — for compact + /// agenda listings (e.g. the Week column). Recurrence-aware. + private func cvAgendaItems(_ date: Date) -> [CVTimeItem] { + (cvAllDayItems(date) + cvTimeItems(date)).sorted { $0.startMin < $1.startMin } + } + + @ViewBuilder + private func cvAllDayBand(days: [Date]) -> some View { + let perDay = days.map { cvAllDayItems($0) } + if perDay.contains(where: { !$0.isEmpty }) { + HStack(alignment: .top, spacing: 2) { + Text("all-day") + .font(AppFonts.mono(7, weight: .semibold)) + .foregroundColor(AppColors.text3(cs)) + .frame(width: 36, alignment: .trailing) + ForEach(Array(days.enumerated()), id: \.offset) { idx, _ in + VStack(spacing: 2) { + ForEach(Array(perDay[idx].enumerated()), id: \.offset) { _, item in + Text(item.title) + .font(AppFonts.sans(8.5, weight: .medium)) + .foregroundColor(item.color) + .lineLimit(1) + .padding(.horizontal, 5).padding(.vertical, 3) + .frame(maxWidth: .infinity, alignment: .leading) + .background(item.color.opacity(0.18)) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } + } + .frame(maxWidth: .infinity) + } + } + .padding(.horizontal, 8).padding(.vertical, 6) + Divider().background(AppColors.border(cs)) + } + } + + private func cvHourGrid(days: [Date], totalH: CGFloat) -> some View { + ZStack(alignment: .topLeading) { VStack(spacing: 0) { ForEach(cvStartH...cvEndH, id: \.self) { h in HStack(spacing: 0) { @@ -996,10 +1057,11 @@ struct CalendarView: View { private func cvTimeItems(_ date: Date) -> [CVTimeItem] { var items: [CVTimeItem] = [] - for task in taskVM.tasks where !task.isComplete { - guard let d = task.dueDate, cal.isDate(d, inSameDayAs: date) else { continue } + // Recurrence-aware, and only *timed* tasks belong on the hour grid — + // untimed tasks are surfaced separately in the all-day band. + for task in taskVM.occurrences(on: date) where !task.isComplete && task.hasTime { + guard let d = task.dueDate else { continue } let h = cal.component(.hour, from: d), m = cal.component(.minute, from: d) - guard h != 0 || m != 0 else { continue } items.append(CVTimeItem(title: task.title, color: task.quadrant.color, startMin: h * 60 + m, endMin: h * 60 + m + 30)) } for ev in calStore.isAuthorized ? calStore.events(for: date) : [] { diff --git a/KisaniCal/Views/TodayView.swift b/KisaniCal/Views/TodayView.swift index 23890e6..c4918da 100644 --- a/KisaniCal/Views/TodayView.swift +++ b/KisaniCal/Views/TodayView.swift @@ -1277,24 +1277,35 @@ struct TaskRowView: View { let task: TaskItem let onToggle: () -> Void + @State private var showCompleteConfirm = false + private let dueFmt: DateFormatter = { let f = DateFormatter(); f.dateFormat = "MMM d"; return f }() + /// A task scheduled for a day after today — completing one of these is easy to do + /// by accident from the Next 7 Days / Later sections, so we confirm first. + private var isFutureTask: Bool { + guard let d = task.dueDate else { return false } + let cal = Calendar.current + return cal.startOfDay(for: d) > cal.startOfDay(for: Date()) + } + var body: some View { HStack(spacing: 10) { - Button(action: onToggle) { - ZStack { - Circle() - .fill(task.quadrant.softColor) - .frame(width: 22, height: 22) - Circle() - .stroke(task.quadrant.color, lineWidth: 2) - .frame(width: 22, height: 22) + Button { + if !task.isComplete && isFutureTask { + showCompleteConfirm = true + } else { + onToggle() } - .frame(width: 36, height: 44) - .padding(.leading, 4) - .contentShape(Circle()) + } label: { + Circle() + .strokeBorder(task.quadrant.color, lineWidth: 1.5) + .frame(width: 18, height: 18) + .frame(width: 36, height: 44) + .padding(.leading, 4) + .contentShape(Circle()) } .buttonStyle(PressButtonStyle(scale: 0.82)) @@ -1350,6 +1361,18 @@ struct TaskRowView: View { .clipShape(RoundedRectangle(cornerRadius: AppRadius.small)) .overlay(RoundedRectangle(cornerRadius: AppRadius.small) .stroke(AppColors.border(cs), lineWidth: 1)) + .confirmationDialog( + "Complete this task?", + isPresented: $showCompleteConfirm, + titleVisibility: .visible + ) { + Button("Mark Complete") { onToggle() } + Button("Cancel", role: .cancel) {} + } message: { + if let d = task.dueDate { + Text("“\(task.title)” is scheduled for \(dueFmt.string(from: d)).") + } + } } private func countdownLabel(to date: Date) -> String { diff --git a/project.yml b/project.yml index 91446f4..8af96b3 100644 --- a/project.yml +++ b/project.yml @@ -10,7 +10,7 @@ settings: base: DEVELOPMENT_TEAM: K8BLMMR883 MARKETING_VERSION: "2.0" - CURRENT_PROJECT_VERSION: "5" + CURRENT_PROJECT_VERSION: "9" schemes: KisaniCal: diff --git a/support-site/index.html b/support-site/index.html new file mode 100644 index 0000000..c7402e5 --- /dev/null +++ b/support-site/index.html @@ -0,0 +1,98 @@ + + + + + + Wenza — Support + + + + +
+
W
+

Wenza Support

+

A quiet, personal companion for your tasks, your training, and your calendar.

+ +
+

Contact

+
+ Questions, bugs, or feedback? Email + support@example.com. + I read every message and usually reply within a couple of days. +
+
+ +
+

Common questions

+
+
+

Does Wenza collect my data?

+

No. Your tasks, workouts, and health data stay on your device and in your own iCloud. Nothing is sent to a server or any third party.

+
+
+

Will my data sync across devices?

+

Settings and data sync through your private iCloud account, so they survive reinstalls and new devices signed in to the same Apple ID.

+
+
+

Why does Wenza ask for Health and Calendar access?

+

Health access shows your steps, energy, heart rate, and workouts on your dashboard. Calendar access lets your events appear alongside your tasks. Both are optional.

+
+
+
+ + +
+ +