← Back to warehouse

LeaveHack Review

SwiftUI iOS app — Vacation optimizer with holiday OCR, CSV import, and leave opportunity detection.
Review date: May 2026

UI / UX Screenshots

Key insight: Onboarding is cheeky and bold — "Your boss doesn't need to know about this..." sets a playful, subversive tone. The import flow offers 3 paths (template, screenshot, CSV). The template picker shows country holiday counts. The dashboard reveals leave optimization with calendar strips and "Take X leave" calculations.
5
Critical Issues
8
Warnings
6
Improvements
4
Architecture Wins

Critical Issues

Critical No Receipt Validation — Purchases Accepted Blindly PurchaseManager.swift:112

You set isProUser = true immediately after .purchase() succeeds without validating the App Store receipt. A jailbroken device can fake purchase success. You also have a TODO comment at line 194 admitting this.

isProUser = true // After purchase completes // Line 194: // TODO: Add receipt validation for production

Fix: Implement AppTransaction (StoreKit 2) or server-side receipt validation before unlocking Pro. At minimum, check transaction.revocationDate == nil and transaction.environment == .production (with override for TestFlight).

Critical Trial Logic Is Client-Side Only — Easily Bypassed PurchaseManager.swift:85-89

3-day trial is computed via Date().addingTimeInterval(3 * 24 * 60 * 60) and stored in UserDefaults. A user can delete and reinstall the app to reset the trial indefinitely. No server-side or Keychain persistence.

trialEndsAt = Date().addingTimeInterval(3 * 24 * 60 * 60) UserDefaults.standard.set(trialEndsAt, forKey: trialEndsAtKey)

Fix: Store trial state in Keychain with kSecAttrAccessibleAfterFirstUnlock. Or better: use StoreKit 2 trial offers (free trial IAP product) so Apple handles eligibility.

Critical AppState Is a God Class — 200+ Lines of Mixed Concerns LeavePlannerApp.swift:50-200

AppState handles onboarding state, holiday storage, leave opportunities, theme, purchase state, and navigation flags. This violates Single Responsibility Principle and makes unit testing impossible. Any change to one feature risks breaking another.

Fix: Split into HolidayStore, OnboardingState, ThemeManager, and NavigationState. AppState becomes a lightweight facade that composes these.

Critical OCR Processing Blocks Main Thread OnboardingView.swift:201-227

The Vision framework callback returns on a background queue, but you immediately dispatch to main for UI state. However, the image preprocessing (if any) and the isProcessing = true spinner setup happen synchronously. More importantly: if the user picks a large image, the UIImage decoding happens on main before Vision even starts.

Fix: Downscale images to ~2MP before passing to Vision. Run UIImage.jpegData or resizing on a background queue. Vision's perform() already runs async — protect the entry point.

Critical Force Unwrap in Date Computation HomeView.swift:10-11

Calendar.current.date(byAdding: .month, value: 6, to: now)! — this will crash if the calendar fails to compute 6 months from now (rare but possible with some calendars or invalid dates).

let sixMonthsLater = Calendar.current.date(byAdding: .month, value: 6, to: now)!

Fix: Use guard let and fall back to now.addingTimeInterval(180 * 86400) if the calendar computation fails.

Warnings

Warning PurchaseManager Singleton + @MainActor = Race Condition Risk PurchaseManager.swift:28

Singleton pattern with @MainActor means all purchases happen on main. StoreKit 2's Product.products() and purchase() are already async — forcing main actor serialization is unnecessary and blocks UI during product loading.

Fix: Remove @MainActor from the class. Use @MainActor only on published property setters, or publish state changes via await MainActor.run.

Warning Silent Failure on Purchase Errors PurchaseManager.swift:141-143

Purchase errors are only print()'d. The user sees an infinite spinner or a disabled button with no explanation.

} catch { print("Purchase failed: \(error)") }

Fix: Publish an @Published var purchaseError: Error? and surface it in PaywallView as an alert or inline error.

Warning UIKit Image Picker in SwiftUI App OnboardingView.swift:282-318

You're hand-rolling UIViewControllerRepresentable for UIImagePickerController when PhotosPicker (PhotosUI) has been available since iOS 16. More code, more bugs, no native SwiftUI benefits like automatic lifecycle.

Fix: Replace with PhotosPicker(selection: $selectedItems, matching: .images). Same for DocumentPicker — use fileImporter modifier.

Warning NavigationView Instead of NavigationStack HomeView.swift:20

iOS 16+ app using deprecated NavigationView. This limits programmatic navigation and deep linking capabilities. No NavigationPath support.

Fix: Migrate to NavigationStack(path: $navigationPath) with a NavigationPath stored in AppState.

Warning Hardcoded Product ID PurchaseManager.swift:39

proUpgrade product ID is hardcoded as an enum case. You can't A/B test pricing or add regional products without a code change and App Store release.

Fix: Load product IDs from a remote config (Firebase / your own) or at minimum a plist. Fall back to hardcoded only if remote fails.

Warning Accessibility Missing on Critical Buttons HomeView.swift:44-55

Settings gear button has no .accessibilityLabel. VoiceOver users hear "Button" with no context.

Image(systemName: "gearshape.fill") .frame(minWidth: 44, minHeight: 44) // No .accessibilityLabel

Fix: Add .accessibilityLabel("Settings") to all icon-only buttons.

Warning CSV Parser Has No Quoting Support CSVParser.swift:29

Simple components(separatedBy: ",") breaks on quoted fields containing commas (e.g., "Independence Day, observed"). Will parse incorrectly or crash on malformed input.

Fix: Use CSV.swift library or implement RFC 4180 compliant parsing with quote handling.

Warning Holiday Storage in UserDefaults, Not Keychain Models.swift:31

Holidays (user's actual leave data) stored in unencrypted UserDefaults. On a shared device or backup, this is readable. Also no migration strategy if model shape changes.

Fix: Use Core Data or SwiftData for structured persistence. Encrypt sensitive data with Keychain if needed.

Architecture & Patterns

Pattern ThemeColors Uses Adaptive Opacity — Good Intent, Brittle ThemeColors.swift (inferred)

You have a theme system but it appears to be scattered across views with ThemeColors.primaryText(for: appState.theme.colorScheme) everywhere. This is repetitive and easy to miss.

Fix: Define a single .environment(\.theme, appState.theme) and use @Environment(\.theme) in views. Or use SwiftUI's native .colorScheme and asset catalog dynamic colors.

Win StoreKit 2 Async/Await Usage PurchaseManager.swift:76-78

Good: Using .task { await purchaseManager.loadProducts() } in PaywallView instead of old SK1 delegate pattern. This is modern and correct.

Win Localized Strings Throughout Multiple files

Good: Consistent use of String(localized: "key") — this is the modern SwiftUI way and shows you're thinking about localization.

Pattern Leave Opportunity Detection Logic Is Solid PlannerLogic.swift:11-80

The algorithm for finding leave opportunities (bridging holidays with minimal leave days) is well-structured and commented. However, it lives in a class with no protocol abstraction — can't swap algorithms for testing.

Fix: Extract protocol OpportunityGenerator { func generate(holidays: [Holiday], leaveDays: Int) -> [LeaveOpportunity] } and make PlannerLogic conform. Test with mocks.

Priority Action List

  1. 1
    Fix receipt validation — This is a reject-risk for App Store review. Use StoreKit 2 AppTransaction or server validation.
  2. 2
    Split AppState — Extract stores. Start with HolidayStore and PurchaseStore.
  3. 3
    Move trial to Keychain — Prevent reinstall abuse. Or switch to StoreKit 2 free trial IAP.
  4. 4
    Replace UIImagePicker with PhotosPicker — Delete 50+ lines of UIKit bridging.
  5. 5
    Add error publishing to PurchaseManager — Surface purchase failures to user.
  6. 6
    Fix force unwraps — Search for ! in view layer, add guards.
  7. 7
    Migrate to NavigationStack — Enables deep linking and programmatic nav.
  8. 8
    Downscale images before OCR — Cap at 2048px longest edge.
  9. 9
    Add accessibility labels — Audit all icon-only buttons.
  10. 10
    Extract protocol for PlannerLogic — Makes the algo testable.