Build history & release notes
summariseText() method using Haiku + callClaudeRaw() helper for plain-text responsesDocumentSummaryCard overlay after success animation for paid users with text-based importsworkersInvocationsAdaptive doesn't expose a country dimension — removed broken CF byCountry query aliasbyCountry alias returning per-country request totalsdays cap raised 30 → 90SKU column; iapByProduct keyed by product bundle IDfetchSalesReports now parses IAP rows (IA1, IAY, etc.) into iapByProduct keyed by product titlefetchSalesReports added to worker — fetches ASC SALES/SUMMARY daily reports (gzip TSV), parses new installs by date and country code/admin/metrics now returns appstore (version status, rating, recent reviews via ASC JWT) and importCounts (Haiku/Sonnet KV counters per day)handleProxy tracks model usage fire-and-forget in KV (usage:{date}:{model}) with 90-day TTL — enables real-time text vs vision splitgenerateAscJWT + fetchAscData added to worker — ES256 JWT via Web Crypto, fetches app attributes, versions, and reviews in parallelfullScreenCover on next launch — "Got it" dismisses it, no full onboarding repeathasSeenWelcomeUpdate set alongside hasCompletedOnboarding — they never see the modal separatelyhasSeenWelcomeUpdate Bool added to AppState (UserDefaults); WelcomeUpdateView added to OnboardingView.swiftAssets.xcassets, rendered as rotated cards with gradient scrim and label overlayCyclingMessageViewTask, shows "Try Again" on the import button, retains loaded file, keeps import method card openCancellationError no longer swallowed by inner PDF/OCR catch blocks — Task.checkCancellation() re-throws before error-handling runsImportView body split into importNavigationStack + body to resolve SwiftUI type-checker timeoutemptyResponse to trigger the OCR → Haiku fallback — previously this silent empty result bypassed OCR entirely and could push the import to the slow Sonnet vision pathshowExtractOverlay flag rather than collapsing the card; the overlay reappears naturally when a new file is picked, the card is collapsed and re-expanded, or extraction is started againselectedMethod = nil) so canImport becomes false and the overlay hides entirely — the PDF/photo is kept in memory so re-expanding the card shows the file still selected and ready to retrycatch where pdfData != nil and the OCR-fallback catch were swallowing CancellationError before it reached the outer handler — added try Task.checkCancellation() at the entry of each inner catch block so cancellation re-throws immediately and is silently dismissedOCRService() init marked nonisolated — was inheriting @MainActor isolation from ImportView, making it uncallable from background TaskGroup tasks.fast recognition — clean digital fonts don't need the ML correction pass; photos continue using .accurate.accurate level takes 20–30 s per dense calendar page; 5 sequential = 2+ min; parallel cuts total time to the slowest single pageextractTextFromHTML marked nonisolated — pure string function was inheriting main-actor isolation from its class, causing a Swift 6 error when called from a background TaskisBirthdayInvite could be evaluatedICSService.swift — standalone zero-dependency iCalendar parsercache_control: ephemeral; cached input tokens 90% cheaper after first call; anthropic-beta: prompt-caching-2024-07-31 header added to Cloudflare WorkerFlowRow layout from CareServiceStatsView into FlowRow.swiftshowGooglePage, shouldShowGooglePage, googleCalendarPage, googleCalStep, and related debug flag; onboarding is now always 6 pages and the notification page advances straight to completionPurchaseService.presentOfferCodeSheet() calls Apple's native AppStore.presentOfferCodeRedeemSheet(in:) — users can redeem offer codes (e.g. MMPS3441 for 6 months free) directly in the appTransaction.updates listener now publishes on entitlementVerified (PassthroughSubject) whenever a new subscription or lifetime entitlement is verified — covers offer codes, renewals, and Ask to Buy approvalsSortedApp subscribes to entitlementVerified as a backstop — codes redeemed directly in the App Store unlock the account immediately without needing the paywall opencallClaude now accepts an optional maxTokens override; extractEvents(from:) passes 8192 when a timetable grid is detected — a 35-class schedule with recurrence on every event generates ~3 000 tokens of JSON, which was truncating mid-array at the old 4096 cap and causing "We had trouble reading the results"filterToDateRelevantLines now skips filtering entirely when a timetable grid is detected — previously stripped class-name lines with no dates, causing most classes to be missedmax_tokens: 8192 — a gym timetable with 30+ recurring events was truncating mid-JSON at 4096, causing "We had trouble reading the results".onAppear× dismiss buttonlooksLikeCalendarGrid() detector — finds ≥4 unique day-of-week names with word-boundary matching (won't fire on "month"/"monitor") in the first 600 chars; routes grid documents straight to visionErrorOccurrenceTimeSpanTooBig when Claude places the series end date in event.endDateRecurrenceRule on the extracted eventRecurrenceRule.swift data model: RecurrenceFrequency (daily/weekly/fortnightly/monthly), RecurrenceDay (Sun–Sat) — converts to EKRecurrenceRule (Apple), RFC 5545 RRULE (Google), and Graph API recurrencePattern+recurrenceRange (Outlook)🔁 Repeating badge — tapping opens RecurrenceEditorSheet (frequency, days of week, end date or count)RecurrenceEditorSheet — live summary preview, frequency list picker, day-of-week grid chips, end condition (on date or after N occurrences), remove option📎 on its own line so users know there's something worth expandingopenURL always routes through Safari and bypasses universal links — switched to UIApplication.shared.open with .universalLinksOnly: true so iOS opens the Outlook app directly when installed, then falls back to SafariwebLink on event creation — now captured in SchoolEvent.outlookWebLink and used for "Open in Outlook Calendar"webLink is a universal link — iOS opens it directly in the Outlook app if installed, Safari otherwise; navigates to the specific event in both casesms-outlook://events/open?restid= approach used Graph REST IDs where EWS IDs were expected — that's why it only opened the app home.regularMaterial background with an orange border and triangle icon — prominent but not alarming; replaces the tiny red caption text that was easy to missms-outlook://events/open?restid={id} to jump directly to the specific event in the Outlook app — previously just opened the app home?date=YYYY-MM-DD parameter so Outlook web opens on the correct dayoutlookCalendars argument build error resolvedms-outlook://) then falls back to outlook.live.com/calendar/view/day — previously opened Apple Calendarsorted://attachment/… deep link inline — Graph API has no separate URL field, so "tap the link above" was showing with nothing tappableOutlookAuthService — PKCE sign-in, token refresh, Keychain storage; public client (no secret), direct token exchange with Microsoft identity endpointOutlookCalendarService — create/update/delete via Microsoft Graph API; all-day & timed events, reminders, location, notes, attachment deep links$batch endpoint (20 IDs/request) — externally deleted events removed on next app foregroundlineLimit(2) + fixedSize to match settings chipsSpacer() instead of Spacer(minLength: 4)GoogleAuthService — sign-in, token refresh, secure Keychain storageGoogleCalendarService — create, update, delete events via REST API with reminders, location, notes, and attachment deep linkssorted://attachment/UUID// suffix to prevent Google URL parser truncationalreadyDeleted errorREFRESH-INTERVAL;VALUE=DURATION:PT30M and X-PUBLISHED-TTL:PT30M to ICS feed (Swift generator + worker empty skeleton) — Apple Calendar and Outlook respect this and re-fetch every 30 minutes. Google ignores it. Worker redeployed.TabView for Google/Outlook/Other users — previously always present, so navigating from page 3 to page 5 whooshed visibly through it, looking broken.webcal:// reverted from webcals:// — webcals:// is not registered on iOS and caused Safari to show "invalid address"; webcal:// is the correct scheme now that the worker returns a valid ICS on first request.sortedapp.io not yet onboarded to Cloudflare; worker stays on sorted-proxy.xmuhlebach.workers.dev.webcalURL changed from webcal:// to webcals:// — webcal:// converts to http:// internally, causing iOS Calendar to warn "Insecure Connection" on Cloudflare's HTTP→HTTPS redirect. webcals:// hits HTTPS directly.appTheme.primary (Aperol Orange) instead of system blue.accentColor is not a valid ShapeStyle for .fill() — replaced with brand orange in Build 190CalendarType promoted to top-level enum with rawValue; user's choice persisted to AppState.chosenCalendarType on selection#6A99CF)@2x/@3x imagesets (total ~49 KB)MARKETING_VERSION updated from 1.6 → 1.7calendar.google.com, Outlook: outlook.live.com, Other: webcal://). Skippable.webcal:// URL. Tap "Subscribe in Calendar" to open the native subscribe dialog in Apple Calendar, Google Calendar, Outlook, etc.ICSFeedService generates RFC 5545-compliant ICS (all-day VALUE=DATE, timed UTC, 75-octet line folding, escaped text) and pushes it to Cloudflare KV on every save — fire-and-forget.feedId UUID persisted + CloudKit-synced). Resetting all data deletes the KV entry and rotates the ID, invalidating old links.GET/PUT/DELETE /feed/:feedId handlers. Auth helper refactored out of handleProxy into shared authenticate() function. ICS_FEEDS KV namespace added to wrangler.toml.#if DEBUG so it compiles away entirely in release builds.ScrollView so all steps fit on small iPhones. Title sharpened to "Connect Google Calendar".Spacer().frame(minHeight:maxHeight:) on 5 pages — content distributes vertically on large screens, compresses on small ones. "How it Works" page retains ScrollView.MARKETING_VERSION updated from 1.5 → 1.6VStack with no scroll container — content was squished/clipped on iPhone SE and 13 mini. Wrapped each page in ScrollView with .scrollBounceBehavior(.basedOnSize); Continue button stays fixed at the bottomnudgeShownCount) — stops pestering users who've seen it and haven't convertedAppState now run on a dedicated serial background queue (com.sorted.persist) — eliminates main-thread hang risk from every save operation (events, profiles, schools, care services, skipped events, reminder days, calendar identifier, newsletter weekday)checkShareExtensionHandoff() file reads (image/PDF from App Group container) moved to Task.detached so the main thread is never blocked on disk I/O during foreground transitionsUserDefaults.synchronize() calls in share handoff path75c4fb8
VStack used .frame(maxHeight: .infinity) which caused LazyVGrid to not propagate the taller row height to the outer VStack when 2+ place pills exceeded the minimum height — "Tap to edit…" text clipped over the cards. Replaced with minHeight: 58 only.c8bf13a
.widgetAccentable() on a template image in vibrant lock screen mode renders the image transparent on many wallpapers — replaced with .renderingMode(.template) + .foregroundStyle(.white) for guaranteed visibility against AccessoryWidgetBackground().white / .white.opacity(0.7) explicitly362ca60
MARKETING_VERSION 1.4 → 1.5 — reflects attachment pipeline fix, QuickLook fixes, onboarding and import screen redesigns, and UI polish since 1.4edf13a4
.contentShape(Rectangle()) so the entire row is tappable, not just the thumbnail and text4d12547
ShareViewController now saves raw PDF bytes to App Group (pendingSharePDF.pdf) alongside extracted text — previously only text was stored, so AttachmentStore never received the file and attachmentId was never setAppState.checkShareExtensionHandoff() new "pdf" branch reads raw bytes + text from App Group into pendingImportPDFData + pendingImportTextSortedApp "Open With" handler now also populates pendingImportPDFData from the raw file URLImportView.loadPendingImport() picks up pendingImportPDFData into local pdfData, triggering AttachmentStore.savePDF and setting attachmentId on all extracted eventsCalendarService.buildNotes() no longer says "tap the link above" when no deep link exists — uses plain attribution label instead4c03ff4
ContentView was wrapping EventDetailView in an extra NavigationStack — nested NavigationStacks are unsupported and broke all sheet presentations (QuickLook) inside the view.quickLookPreview and auto-open .task from the Form chain to the NavigationStack level — QuickLook must be at the navigation root to present reliablyb861dcc
2c66d9d
UIWindowScene and apply as explicit padding — page-style TabView always fills full screen, UIKit is the only reliable source.body→.subheadline, detail .subheadline→.footnote, vertical padding 14→11pt34e9552
.background() — ZStack now lays out within the safe area, heading no longer hidden behind Dynamic Island.title2→.title, subtitle .subheadline→.body, step title→.body, step detail .caption→.subheadlineacc7adc
dc9f14a
LazyVGrid with two labeled groups ("Screenshot anything" / "Snap a photo of anything"), each containing an HStack of 2 cards28cff81
UseCaseMarquee for a static LazyVGrid — same 4 use case cards, no movementUseCaseMarquee structba8554d
EducationCarePickerView already shows the "Already added" section — fix was applied when the unified picker was built. No code changes needed.6b94619
7e619a4
PurchaseCelebrationOverlay was private to PaywallView — made it internal so ImportView can use itshowBoostCelebration state to ImportView; triggered after successful boost purchase; now matches the paywall celebration path exactlyab6d7ba
EmojiService.categories — above the generic assembly/performance 🎭 catch-all827b25a
SchoolCalApp.swift → SortedApp.swift; struct SchoolCalApp → SortedAppproductName = SchoolCal → Sorted in project.pbxprojSchoolCal.xcodeprojUseCaseMarquee struct: 4 cards duplicated (8 total), offset animated from 0 → -setWidth with .linear.repeatForever(autoreverses: false)clipShape clips overflow on both edgesfreeLimit 5 → 10; all UI uses UsageTracker.freeLimit dynamically so updates everywhere automaticallysystemGray3 tint on .borderedProminent looked like a disabled control.bordered (outlined); .borderedProminent (filled) for primary CTA; green for ownedperson.fill at caption2 size sits left of name in profile chips; "All" chip unchangedSpacer(); standard iOS pattern (mirrors Calendar's new event form)strokeBorder(separator, lineWidth: 1.5) draws inside the shape boundary, making the visible fill edge at 34.5pt vs solid circles' full 36pt — a known perceptual effect where borders make shapes appear smallersystemBackground fill + separator border with tertiarySystemFill (solid, reaches full boundary, no border needed); strokeBorder(primary) now only drawn when the custom colour is selected· separator: Profiles: "Tap to edit · Long-press for more options." / Places: "Tap to see details · Swipe to remove."CalendarService.applyLocation now always sets EKStructuredLocation for any non-empty location string — previously only applied for detected street addresses; removed isAddress guard and looksLikeStreetAddress regexEventDetailView custom init(event:) didn't include autoOpenPreview parameter, causing compiler to reject ContentView's call with "Extra argument"; parameter now threaded throughsorted://attachment/… in iOS Calendar now opens Sorted to the event detail sheet AND immediately launches QuickLook — no extra tap requiredEventDetailView gains autoOpenPreview: Bool; a .task fires 400 ms after appearance then sets previewURL; ContentView passes autoOpenPreview: true only for deep-link-initiated sheetsCGImage(jpegDataProviderSource:) preserves JPEG encoding inside the PDF stream — ~60–80 KB/page; 14-page newsletter ≈ 900 KBisExcludedFromBackup = true — files now included in iCloud/iTunes backups and survive reinstall + restoreDocuments/Attachments/ via new AttachmentStore singleton; every extracted event carries attachmentId: UUID?CalendarService sets ekEvent.url = sorted://attachment/{uuid} — tapping in iOS Calendar opens Sorted to the source fileEventDetailView shows photo thumbnail or PDF icon; tapping opens QuickLook previewSchoolCalApp routes sorted://attachment/… to the correct event sheetUIPasteboard.general.hasStrings as pre-check before accessing .string — Apple's designed API for clipboard availability without triggering the permission alert.string returned nil synchronously before the "Allow Paste?" dialog was answered, causing a false "clipboard is empty" errorcamera.fill SF Symbol.accessoryCircular now uses AccessoryWidgetBackground() for standard frosted-glass backing.accessoryRectangular title marked .widgetAccentable()systemSmall switched to spinner_white asset (properly 1x/2x/3x)scripts/pre-commit.sh — sets CURRENT_PROJECT_VERSION to git rev-list --count HEAD + 1 on every commit; covers all 3 targetsscripts/install-hooks.sh — one-liner to wire up hook on fresh clonesv{version} ({build}) from bundle info, debug builds only.confirmationDialog anchored to context-menu origin with a downward triangle — replaced with DeleteProfileSheet sliding up from bottomAngularGradient colour picker replaced with plain circle + paintpalette.fill icon — matches AddProfileViewEducationCarePickerViewshowingPlacePicker replaces bothLazyVGrid as ghost card — dashed border, centred icon, always last itemmin(profiles.count + 1, 3)List as last row — always visibleonConfirm() was called immediately after save, collapsing the sheet before confetti/big number could render — deferred to overlay dismiss callback via pendingConfirmedsystemPromptBase and imageSystemPrompt — eliminates second normaliseTitles Claude API callANTHROPIC_API_KEY re-set with valid keyEKEvent.url (tappable in Calendar.app); photo/PDF append source note — single-event onlySchoolEvent.sourceReference: String? addedattest(keyId:clientDataHash:) → attestKey(_:clientDataHash:) to match ObjC bridgingDCAppAttestServiceEKStructuredLocation (tappable Maps link) using Claude's isAddress flag.lazy + .prefix(25) — no main-thread blockingwillAutoRenew — hides immediately after cancellingLazyVGrid (1=full, 2–3=side by side, 4+=max 3/row)PlaceReference enum; migrateLegacyIds() one-time migration#E8622A as secondary throughout| no longer misread as digit 1