Conversation
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…s→clipShape project-wide
…() modifier Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
SecureGalleryView called both onDismiss?() and the environment dismiss() at every dismissal site. When the gallery is a pushed nav destination (Camera -> Gallery -> Select for Decoys), onDismiss is nav.navigateBack(), so Save popped the stack twice -- removing .gallery then .camera -- landing on the empty Color.clear root (black screen). Call exactly one mechanism: the injected onDismiss when present, otherwise the environment dismiss(). Fixes empty-gallery, decoy Back, and decoy Save. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Videos are stored in a separate "videos" directory, but activatePoisonPill() only wiped the photo gallery and thumbnails via deleteNonDecoyImages(). All videos therefore survived the poison pill -- a serious data-leak that defeats the feature's purpose. Add deleteNonDecoyVideos(), invoked before deleteNonDecoyImages() (which removes the decoy directory used for the decoy check). A video is preserved only if a file with the same name exists in the decoy directory; since decoy selection is photo-only today, every video is destroyed -- while remaining forward-compatible if video decoys are added later. Tests: PoisonPillVideoDeletionTests covers both the destroy and decoy-preserve paths. To make them run, the previously orphaned FakeEncryptionScheme / FakeThumbnailCache test helpers were added to the SnapSafeTests target (and FakeEncryptionScheme updated to match the current EncryptionScheme protocol). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A number of test files existed on disk but were never members of the SnapSafeTests target, so they silently never compiled or ran. Removed (obsolete/superseded - tested types that no longer exist): - SecureFileManagerTests, EditedPhotoTrackingTests (SecureFileManager removed) - LocationManagerTests (replaced by LocationRepository) - PINManagerTests (replaced by PinRepository/PinCrypto) - SecurePhotoTests (SecurePhoto model removed) - CameraModelTests (CameraModel renamed to CameraViewModel) - CameraLifecycleTests (CameraViewModel.isSessionActive removed) - FaceDetectorTests (MaskMode reduced to .pixelate; blurFaces removed) - PhotoDetailViewModelTests (built on removed SecurePhoto + showFaceDetection) - SnapSafeTests (empty Xcode template stub) Folded into the target (valid tests of current code, with minor fixes): - VerifyPinUseCaseTests - updated to current AuthorizePinUseCase / VerifyPinUseCase initializers; import FactoryKit; @mainactor. - SECVFileFormatTests - UInt32 conversions; fileLength: label. - SecureImageRepositoryTests - drop removed getPhotoByName tests; saveImage now takes CLLocation; add getVideosDirectory override for isolation. - Added the previously-orphaned FakeEncryptionScheme / FakeThumbnailCache helpers to the target. Full unit suite now compiles and runs: 92 passed, 0 failed (was 58). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously the poison pill destroyed ALL videos because videos could never be marked as decoys: saveDecoySelections() skipped video items and there was no video-decoy storage. Even preserving a video as-is would be broken, since activatePoisonPill deletes the real key's DEK, making real-key content undecryptable. Mirror the photo decoy model for videos: - VideoEncryptionService: add encryptVideoForDecoy(...) async (awaitable encrypt). - SecureImageRepository: inject VideoEncryptionService; add isDecoyVideo, addDecoyVideoWithKey (decrypt with current key -> re-encrypt with poison-pill key into the decoy dir), removeDecoyVideo; count videos toward the shared decoy limit. deleteNonDecoyVideos now destroys non-decoy videos AND moves each decoy's poison-pill-key copy into the videos dir, replacing the real-key original (runs before deleteNonDecoyImages, which removes the decoy dir). - AddDecoyVideoUseCase + DI wiring (and pass VideoEncryptionService into the SecureImageRepository factory). - MixedMediaGalleryViewModel: saveDecoySelections add/remove decoy videos; decoy count, pre-selection, and strings now include videos. Result: decoy videos are re-encrypted with the poison-pill key and remain playable under the duress PIN; all non-decoy videos are destroyed. Tests (PoisonPillVideoDeletionTests, + FakeVideoEncryptionService): - non-decoy videos destroyed (decoy photo preserved) - addDecoyVideoWithKey re-encrypts and marks the video a decoy - end-to-end: mark decoy video -> poison pill -> decoy file replaced by the poison-pill-key copy, non-decoy video destroyed Full unit suite: 93 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Marking a video as a decoy re-encrypts it with the poison-pill key, which can
take a while for large videos. Previously the decoy gallery dismissed
immediately and the work ran detached, giving the user no feedback.
- saveDecoySelections() is now async and drives isSavingDecoys + a
completed/total counter (only items whose decoy state changes are processed).
- SecureGalleryView awaits the save and dismisses only when it completes,
showing a dimmed spinner overlay ("Saving decoy media…" with an N-of-M count
for multiple items). Save/Back are disabled while saving.
Note: the SECV re-encryption still runs on the main actor (the
VideoEncryptionService is @mainactor). The indeterminate ProgressView animates
on the render server so the spinner stays alive, but the rest of the UI is
blocked during the crypto. Moving the crypto off the main actor is a follow-up;
the recording path already consumes its progress via receive(on: .main), so it
should be feasible.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Video cells showed a generic film-icon placeholder. Now each video has a real thumbnail. Because videos are encrypted SECV (unreadable by AVAssetImageGenerator), the thumbnail is generated once at record time from the plaintext .mov, before it is deleted. - SecureImageRepository: durable, encrypted video-thumbnail storage in Application Support (videoThumbnails/, excluded from backup) — generate from the plaintext .mov via AVAssetImageGenerator.image(at:), store encrypted with the current key, read+decrypt with an in-memory cache, delete one / delete all. - Security: thumbnails are derived from real frames, so deleteAllVideoThumbnails runs on poison-pill activation and security reset; per-video thumbnail (and any decoy copy) is removed when a video is deleted. - ThumbnailCache: video-name-keyed get/put/evict (prefixed). - CameraViewModel.encryptRecordedVideo generates+stores the thumbnail before the .mov is deleted. - VideoCellView loads the decrypted thumbnail via .task (mirrors PhotoCell), with a play badge and decoy shield; falls back to the icon placeholder. Scope: record-time only — videos recorded before this change and decoy videos (after the pill) show the placeholder. Tests (VideoThumbnailTests): store writes an encrypted file; read returns the image; delete removes it; poison pill wipes the whole thumbnails dir. Full unit suite: 97 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SecureGalleryViewModel was unreferenced (the live gallery uses MixedMediaGalleryViewModel). Removed the 22KB class and file. The file also declared the shared SelectionMode enum, which the live view model uses, so that enum was moved into MixedMediaGalleryViewModel.swift. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
~13 test files had silently never been added to the SnapSafeTests target, so they never compiled and their tests never ran (the bundle reported success while executing nothing). Add scripts/check_test_target_membership.rb, which fails if any .swift under SnapSafeTests/ is not compiled by the target, and run it first in the fastlane build/test lanes (so CI's `fastlane test` and local runs both enforce it). Verified it fails on an orphan file and passes when clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rship guard Add a "Building, Testing & Releasing" section covering the fastlane lanes, the test-target membership guard (scripts/check_test_target_membership.rb) and how it's enforced, the CI workflows, and the tag-driven release process. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l deleted ALL videos addDecoyVideoWithKey decrypted the original to a temp file and re-encrypted it with the poison-pill key, but never created those output files. The video encryption service opens its output with FileHandle(forWritingTo:), which requires the file to already exist (the camera and the sharing path both pre-create it). So every decrypt/encrypt threw, addDecoyVideoWithKey caught it and returned false, and NO video was ever marked as a decoy. Consequences, both reported by the user: - Entering the poison PIN destroyed ALL videos, including ones the user tried to mark as decoys (deleteNonDecoyVideos found zero decoy files). - The decoy shield badge never appeared on video cells (isDecoyVideo was always false). Fix: pre-create the temp plaintext file and the decoy file before calling the encryption service, matching the camera/sharing pattern. Why tests missed it: FakeVideoEncryptionService wrote output via Data.write(to:), which creates the file, masking the missing precondition. The fake now models the real precondition (throws if the output file does not exist), so this class of bug is caught. With the faithful fake the two decoy tests went RED, then GREEN after the fix. Full suite: 0 failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…haring + decoy marking) decryptVideoFile read `upToCount: trailer.chunkSize` (the full 1 MB) for EVERY chunk, but the encoder writes a partial final chunk (min(chunkSize, remaining)). On the last chunk that over-read swallowed the auth tag (and index/trailer), so the subsequent tag read returned nothing and decryption threw SECVError.fileIOError -- for essentially any video (almost all have a partial final chunk). This silently broke every bulk-decrypt caller, decryptVideoForSharing: - Marking a video as a decoy (addDecoyVideoWithKey decrypts then re-encrypts) always failed, so no video ever became a decoy. Hence the poison pill deleted ALL videos and the decoy shield badge never appeared. - Video sharing (also decryptVideoForSharing) was broken the same way. Playback was unaffected -- it streams via a custom AVAssetResourceLoader (makeEncryptedVideoAsset), a different path. Fix: read each chunk's actual size, min(chunkSize, originalSize - chunkIndex*chunkSize); AES-GCM ciphertext length equals the plaintext length, so this reads exactly the ciphertext and leaves the tag intact. This was the real root cause behind the decoy-video badge / "all videos deleted" reports (the earlier saveDecoySelections and pre-create fixes were necessary but not sufficient). Tests (DecoyVideoIntegrationTests): real-service encrypt/decrypt round-trips for single-chunk and multi-chunk-with-partial-last inputs (assert exact bytes), plus an end-to-end "mark video as decoy -> isDecoyVideo true" using the real VideoEncryptionService. Full suite: 0 failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On poison-pill activation, deleteAllVideoThumbnails() destroys every video thumbnail (they are derived from real frames and were encrypted with the now-deleted real key). But decoy videos survive the pill, so they were left with no thumbnail -> the gallery showed the placeholder icon. Mirror the decoy-video mechanism for thumbnails: when a video is marked as a decoy, re-encrypt its thumbnail with the poison-pill key into a separate decoyVideoThumbnails/ directory. On the pill, after wiping the real-key thumbnails, restore the decoy thumbnails into videoThumbnails/. They are encrypted with the poison key (the active key after the pill), so readVideoThumbnail decrypts them normally; clearAllThumbnails() flushes the in-memory cache so no real thumbnail leaks. Cleanup: removeDecoyVideo drops the decoy thumbnail copy; securityFailureReset wipes the decoy thumbnail directory too. Test (VideoThumbnailTests): marking a video as a decoy stores a poison-key thumbnail; after the poison pill the decoy video's thumbnail is restored while a non-decoy video's thumbnail is destroyed. Full suite: 0 failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Import picker only accepted images. Allow videos too: a picked video is copied to a temp file (ImportedMovie transferable), encrypted to SECV with the current key in the videos directory, and given a thumbnail -- mirroring the camera record path. Imported videos use the camera's "video_yyyyMMdd_HHmmss" naming (bumping the second on collision) so they stay unique and sort correctly. Tests (VideoImportTests + DecoyVideoIntegrationTests): import creates an encrypted .secv with the video_ prefix, the name is date-parseable/sortable, repeated imports get distinct names, a non-decodable input still encrypts but skips the thumbnail, and a real-service round-trip recovers the original bytes. Full suite: 0 failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the flat black translucent backgrounds on the camera controls (switch/flash/gallery/settings buttons, recording indicator, zoom capsule) with a glassControlBackground modifier: Apple Liquid Glass (glassEffect) on iOS 26+, with an .ultraThinMaterial fallback for the iOS 18.5 deployment floor. The shutter keeps its dedicated design. The glass is intentionally non-interactive: these backgrounds sit inside Buttons and tap gestures, and interactive glass installs its own touch handling that swallowed the button taps (regression: the flash toggle could not be changed). The enclosing control owns the interaction; the modifier is purely visual. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace SwiftUI's SecureField + @focusstate with a UIViewRepresentable (PINEntryField) backed by a UITextField subclass that owns its own focus lifecycle. First-responder acquisition is driven by the field's didMoveToWindow plus a bounded spaced retry, instead of a single fire-once attempt that raced the security overlay / scene-activation transition and silently failed ~half the time (no cursor, no keyboard). PINVerificationView drops the obsolete @focusstate (and the lock-icon slide animation that depended on it), computes shouldFocus from scenePhase + isLoading, and unifies the scenePhase handler around != .active so all non-active transitions clear the PIN. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
removeAllDecoyPhotos was wired into RemovePoisonPillUseCase, but the matching video cleanup didn't exist — so after deleting the pill, the decoy .secv file and its decoy thumbnail still sat in their shadow directories. isDecoyVideo is a file-existence check, so the gallery kept rendering the shield badge on every previously-marked decoy video. Adds SecureImageRepository.removeAllDecoyVideos (mirrors the photo path: delete each file in getDecoyVideoFiles, plus deleteAllDecoyVideoThumbnails) and calls it from RemovePoisonPillUseCase alongside removeAllDecoyPhotos. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spec for adding an Info sheet to the video detail view that mirrors ImageInfoView. Captures location + creation date in the .mov header at record-time via AVCaptureMovieFileOutput.metadata (single ISO 6709 coordinate, photo-style); read back via AVAsset.commonMetadata over EncryptedVideoDataSource. Backwards-compatible — pre-feature videos show the technical section plus filename-derived date with "(from filename)" hint, and location "—". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New shared utility enum providing makeCaptureItems (location/date/software), ISO 6709 encode/parse, ISO 8601 date round-trip, codec FourCC → string, and preferredTransform → TiffOrientation helpers. All 7 unit tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds VideoMetaData struct with DateSource enum and display-string computed properties (resolution, duration, codec, bitrate, frame rate, location, orientation). All 8 formatting tests pass under SnapSafeTests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements getVideoMetaData on SecureImageRepository (actor) which reads duration, resolution, codec, frame rate, bitrate, GPS, and capture date from encrypted .secv files via EncryptedVideoDataSource, with filename fallback for pre-existing videos that carry no embedded metadata. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On cold launch the NavigationStack pushed .pinVerification while the security overlay also showed its own PINVerificationView, mounting two competing first-responder text fields. The coordinator's textFieldShouldEndEditing returned false while enabled, so the winning (hidden) field never resigned and permanently blocked the visible field against auto-focus and taps. - Make the security overlay the sole PIN surface; ContentView no longer pushes .pinVerification (root stays Color.clear while the overlay gates). - Remove the refuse-to-resign textFieldShouldEndEditing override. - Drive first responder from the field's window lifecycle (didMoveToWindow + bounded retry) instead of a single fire-once attempt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Older versions of these actions were pinning to an ancient version of node.
cleanup work
This is to bring ios inline with android.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… filter Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This is three big changes in one. Wheee
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is just about done, checking it out here.