diff --git a/Where/AGENTS.md b/Where/AGENTS.md index 12634a64..81c14445 100644 --- a/Where/AGENTS.md +++ b/Where/AGENTS.md @@ -188,6 +188,7 @@ typed-route list (`SettingsSearch.swift`; every switch is exhaustive), so a new drill-in is a set of compile errors to fill in; About stays the last block and the demo-mode exit the first. +The Data and About screens lead with the shared privacy passport statement. The About screen renders three live sources — the generated attribution report (`WhereCore.AppAttribution`), `RegionDataSource`, and `BuildInfo` — never a list hard-coded in the view. A missing report or unstamped build diff --git a/Where/WhereUI/README.md b/Where/WhereUI/README.md index 3ec7dbb3..34d2b3cb 100644 --- a/Where/WhereUI/README.md +++ b/Where/WhereUI/README.md @@ -25,8 +25,11 @@ the feature [`Where/AGENTS.md`](../AGENTS.md) and this module's Settings. Elsewhere is an entry card on Locations, Resolve a Locations toolbar button, and the data screens (attachments, logged days, regions) sit in the Settings "Data" group. Backup and destructive data management share one Data - drill-in. `AboutSettingsView` is the last Settings block — build - identity, the app's generated attribution report (linked libraries and + drill-in. Both Data and About lead with the same full-width passport-style + privacy statement on a passport-navy, tilt-reflective surface: location + history stays on the user's devices and in their private iCloud account, + never on Where-operated servers. `AboutSettingsView` is the last Settings block — + build identity, the app's generated attribution report (linked libraries and development tools as separate sections), and bundled-data provenance, each vended by whoever owns it rather than listed in the view; it renders an explicit "no report" state, since only the app bundle carries one, and ends diff --git a/Where/WhereUI/SnapshotTests/DataSettingsViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/DataSettingsViewSnapshotTests.swift new file mode 100644 index 00000000..a8b3084d --- /dev/null +++ b/Where/WhereUI/SnapshotTests/DataSettingsViewSnapshotTests.swift @@ -0,0 +1,10 @@ +import SnapshotKitTesting +import Testing +@testable import WhereUI + +@MainActor +struct DataSettingsViewSnapshotTests { + @Test func data() async { + await assertSnapshots(of: DataSettingsView.self) + } +} diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad.png index c983c0a7..7525bec2 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb8a7c160f46977063087ea2a4780897217a6381ebe2ba389527c83b4c9657e1 -size 955834 +oid sha256:d8196f7ec00c516d5a39fde9fbef3628af3cc9209b7accd41d923310046a2c23 +size 1864117 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad_accessibility.png index 7ae807fb..d152642b 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45aaee169a32568a152d89eb3a4f34e9c5327b50a6b68ed52a471dd5ce7ce7cd -size 1205369 +oid sha256:b8fa628dad7a706c87b5572eaedcc291b0e30a9498055591fc616c0794b9d929 +size 1626944 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad_ax5.png index 22ca2a43..191dccf9 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67b086c9e78a44247c5ea51c08518af16c751f51b8c4e8795e48e2c300994724 -size 2401961 +oid sha256:c176007f9e5d0b57c9c651a6abc6770e75991d40ba6cda5a14db6884427619a3 +size 5531642 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad_contrast.png index 1b042b4d..af237cfc 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c64b11a4ad5c000d742913fa014ce28bd074210187fec6d6b97724caa0e71b6f -size 951556 +oid sha256:dc8b8c0306f341e1b6886e1be090049cbbe0c04bf65861374476a5d626a58aee +size 1867488 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad_dark.png index ab2cd603..84b39381 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPad_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf280dabdee082665d729789a839aaebc07f964f252d1769e527ec5ef846cda3 -size 773604 +oid sha256:f96454b716c35d0d4d6ca47bd1bea047d5c5215b3cd78394bcb729542d1cf731 +size 1663390 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone.png index 154927e9..c79352c4 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6955660bdde045d218df5c1875dcf3d1d9455b287af102c84b47278b213c77a5 -size 647309 +oid sha256:abe5b498eeef8f9d96805cd4728abe619721908df8be3282cedef79eb82b621d +size 1322197 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone_accessibility.png index 0b8df0a3..93a5aaa9 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7da9756f248c88d888bcbe57ba6f974113bab971356885519ad29e8ea65f28e5 -size 1006933 +oid sha256:3f8adc4ac9bf45d4b79e794d8cdd64f5d5549e0aca2d114fdda754a0461a2d54 +size 1291389 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone_ax5.png index 05535e7e..d47f764a 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:188aed1fdeb08c4abfc3c366f7ac949e314425da75468f398c764f978debd1d0 -size 2692088 +oid sha256:6e106aafdd2e247304d5571f28e8267ee65f6419725ad8e6f2a075a77ee107d0 +size 6115163 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone_contrast.png index dee8e044..36b8457f 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:158600d82f01b22c6d80c175faf5af6782621c220ac5c5c3126b95f4bbb4dede -size 683421 +oid sha256:b126a44a7b4b825c1d11eb44acf375d35f258f65dfddf035dfce1482448ec5da +size 1339837 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone_dark.png index f8d0d768..52a183b9 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Default_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6214be2024edb4437dc59b1aeca3adaee65b7224a5820c6f28fb87fe29d5aa7 -size 553998 +oid sha256:6bcc91163b432e072890a0e09b2a41ab700406c16b1b81ecab88b5aa38ac483b +size 1176792 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.DirtyTree_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.DirtyTree_iPhone.png index c10af511..3b7a6877 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.DirtyTree_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.DirtyTree_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4dd8fb90e736c8e6c68355315418cff393c3deba2e76112c498cd2380cbae3d -size 650980 +oid sha256:52473ec2b07d0718bce7a4f5cf9d73d275c7d637c66bd52c26c27f8ec596d0fe +size 1325350 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.DirtyTree_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.DirtyTree_iPhone_dark.png index 60c02a57..2b46ab28 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.DirtyTree_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.DirtyTree_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f548328017d832afe70ed1a6d9bcf8a7a23aa2e83897900f431c1b57f88dbf5 -size 557873 +oid sha256:2f4ff49b0f8bd4cf54108d0284e72ffc48929900b31fc8d0540b79d2dff5c37f +size 1179730 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.LibrariesOnly_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.LibrariesOnly_iPhone.png index 73ef8c33..3aa5a7a1 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.LibrariesOnly_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.LibrariesOnly_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98b1186bd54de38df4832b199b182b5f00369400b646bf50870d8a69b0e21e6b -size 644510 +oid sha256:70de0257fcd9ebe21ec1255820b852ca8b8a0892b862fa6f046941ee0fe7c440 +size 1319658 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.LibrariesOnly_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.LibrariesOnly_iPhone_dark.png index a393b146..3faf816f 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.LibrariesOnly_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.LibrariesOnly_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef8f25f0a7f403e1ababdda990451568449b9a7474bd303ed39bf9239f7d093c -size 550854 +oid sha256:62e55dd3be700b006a8b4e8d6188d14e9ab57a4ce72f323e61df1e4873b9ae40 +size 1174104 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Unattributed_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Unattributed_iPhone.png index 1655e28f..f43b7ac7 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Unattributed_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Unattributed_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e0a89b2c88eb0d0a9686bee78a10495a87d97af84f13ac792c04d5adc5102bc -size 655379 +oid sha256:20c23c925aa1f8b38e2f2cdd99b7c751db888c156dd991dc6db305a85241eed9 +size 1323127 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Unattributed_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Unattributed_iPhone_dark.png index 145a7672..9b51e23d 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Unattributed_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AboutSettingsViewSnapshotTests/about.Unattributed_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c33be6a0c3b0d6449733f4309b825a486f58687b3bcfa84a97ce1ea5b962ce90 -size 554945 +oid sha256:45b4b0b618dc9ed6ea135ad161edb3f3a16e7a4378e53db5cb8dde3fe34e3de5 +size 1178000 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPad.png new file mode 100644 index 00000000..ee991738 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPad.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d559c85d2a4a289734e0a61effe37da91a7ed1e07a14398f9b72ac12a301c10 +size 1406460 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPad_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPad_accessibility.png new file mode 100644 index 00000000..34f84df8 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPad_accessibility.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf228910a10ecf529907b717e31df5e67274513567b282ed7fad4a9083d7df04 +size 925953 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPad_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPad_ax5.png new file mode 100644 index 00000000..2e41ea49 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPad_ax5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:592410c7f92ed83d3e46ddd9575b03228faf3a2e5f16e4d7f1aae38ae9e77150 +size 4154460 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPad_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPad_contrast.png new file mode 100644 index 00000000..dce9be11 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPad_contrast.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89541b3e6b764bd63bb55e380928ce51387367a6e96262337831b5134727e0d5 +size 1399613 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPad_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPad_dark.png new file mode 100644 index 00000000..2f9cc591 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPad_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:589b322009ae58db64adfa0af2d00babbc6732c4f848c2fa3da75ff1c88a2bd9 +size 1189688 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPhone.png new file mode 100644 index 00000000..1d5e9d6b --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddb453875207b5a5758d046a5a6a85b802b0e149800492c0d03e823f73f76449 +size 999052 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPhone_accessibility.png new file mode 100644 index 00000000..5e6f7db4 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPhone_accessibility.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d230ec79c3f86f9a65eafcbfe6cb6e15acd370c4b20dcb8bd79e105f47f602e6 +size 746614 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPhone_ax5.png new file mode 100644 index 00000000..ec30f5b8 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPhone_ax5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b183eddd1cc61fe439c3a77c92865cd60a46a8ebe9103eebc8cff14d7f5e6a7 +size 4589088 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPhone_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPhone_contrast.png new file mode 100644 index 00000000..9ea97765 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPhone_contrast.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47786f0b9cfc98c55603494b3a3111e8a3c89d28a5c185c76ef294318ab30b67 +size 1006506 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPhone_dark.png new file mode 100644 index 00000000..9334ccc7 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/DataSettingsViewSnapshotTests/data.Default_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc3fd4ddd22348e26b07599d1ae27c49fe8d9bd831483832da391409f7e1ac43 +size 844112 diff --git a/Where/WhereUI/Sources/Resources/Localizable.xcstrings b/Where/WhereUI/Sources/Resources/Localizable.xcstrings index 8aef4f16..642051e9 100644 --- a/Where/WhereUI/Sources/Resources/Localizable.xcstrings +++ b/Where/WhereUI/Sources/Resources/Localizable.xcstrings @@ -6028,6 +6028,30 @@ } } }, + "settings.privacy.detail" : { + "comment" : "Detail in the privacy passport card shown at the top of Data and About settings.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Your location history stays on your devices and in your private iCloud account. Where sends no data to anyone." + } + } + } + }, + "settings.privacy.title" : { + "comment" : "Headline in the privacy passport card shown at the top of Data and About settings.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Private by design" + } + } + } + }, "settings.regions.empty" : { "extractionState" : "manual", "localizations" : { diff --git a/Where/WhereUI/Sources/Settings/AboutOpenSourceFooter.swift b/Where/WhereUI/Sources/Settings/AboutOpenSourceFooter.swift index 9a36359d..1b25de44 100644 --- a/Where/WhereUI/Sources/Settings/AboutOpenSourceFooter.swift +++ b/Where/WhereUI/Sources/Settings/AboutOpenSourceFooter.swift @@ -5,93 +5,18 @@ import SwiftUI struct AboutOpenSourceFooter: View { static let projectURL = URL(string: "https://github.com/kyleve/Stuff")! - @Environment(\.stylesheet) private var stylesheet - - private var style: WhereStylesheet.AboutOpenSourceStyle { - stylesheet.aboutOpenSource - } - - private var shape: RoundedRectangle { - RoundedRectangle(cornerRadius: style.cornerRadius) - } - var body: some View { Link(destination: Self.projectURL) { - HStack(spacing: style.contentSpacing) { - sourceSeal - - VStack(alignment: .leading, spacing: stylesheet.spacing.xxSmall) { - Text(String(localized: .settingsAboutSourceTitle)) - .font(style.titleFont) - .foregroundStyle(.primary) - Text(String(localized: .settingsAboutSourceAction)) - .font(style.actionFont) - .foregroundStyle(.secondary) - } - - Spacer(minLength: 0) - - Image(systemName: "arrow.up.right") - .foregroundStyle(.tint) - .accessibilityHidden(true) - } - .padding(style.padding) - .frame(maxWidth: .infinity, alignment: .leading) - .background { - let rosette = style.rosette - SecurityPrintRosette( - tint: .accentColor, - wobble: rosette.wobble, - lineWidth: rosette.lineWidth, - primaryRingSpacing: rosette.primaryRingSpacing, - secondaryRingSpacing: rosette.secondaryRingSpacing, - primaryOpacity: rosette.primaryOpacity, - secondaryOpacity: rosette.secondaryOpacity, - ) - } - .glassEffect( - .regular.tint(Color.accentColor.opacity(style.glassTintOpacity)) - .interactive(), - in: shape, - ) - .clipShape(shape) - .contentShape(shape) - .shadow( - color: Color.accentColor.opacity(style.accentGlow.opacity), - radius: style.accentGlow.radius, - y: style.accentGlow.offsetY, - ) - .shadow( - color: Color.black.opacity(style.liftShadow.opacity), - radius: style.liftShadow.radius, - y: style.liftShadow.offsetY, + PassportCard( + title: .settingsAboutSourceTitle, + detail: .settingsAboutSourceAction, + sealSystemImage: "chevron.left.forwardslash.chevron.right", + accessorySystemImage: "arrow.up.right", + isInteractive: true, + surface: .securityPrint, ) } .buttonStyle(.plain) - .accessibilityElement(children: .combine) - } - - private var sourceSeal: some View { - let seal = style.seal - return ZStack { - Circle() - .strokeBorder(.tint, lineWidth: seal.outerLineWidth) - Circle() - .strokeBorder( - .tint.opacity(0.65), - style: StrokeStyle( - lineWidth: seal.innerLineWidth, - dash: [seal.dashLength, seal.dashSpacing], - ), - ) - .padding(seal.innerInset) - Image(systemName: "chevron.left.forwardslash.chevron.right") - .font(seal.symbolFont) - .foregroundStyle(.tint) - } - .frame(width: seal.size, height: seal.size) - .rotationEffect(.degrees(seal.rotationDegrees)) - .accessibilityHidden(true) } } diff --git a/Where/WhereUI/Sources/Settings/AboutSettingsView.swift b/Where/WhereUI/Sources/Settings/AboutSettingsView.swift index 7c87ac08..80440e31 100644 --- a/Where/WhereUI/Sources/Settings/AboutSettingsView.swift +++ b/Where/WhereUI/Sources/Settings/AboutSettingsView.swift @@ -4,9 +4,9 @@ import SnapshotKit import SwiftUI import WhereCore -/// Settings drill-in for what the app *is* rather than what it does: which build -/// is running, the third-party work it is built with, and where its bundled -/// region boundaries came from. +/// Settings drill-in for what the app *is* rather than what it does: its privacy +/// promise, which build is running, the third-party work it is built with, and +/// where its bundled region boundaries came from. /// /// Every fact here is vended by whoever owns it — `BuildInfo` and the generated /// attribution report from `WhereCore`, `RegionDataSource` from `RegionKit` — so @@ -44,6 +44,9 @@ struct AboutSettingsView: View { var body: some View { SettingsFocusScope(focus: focus) { Form { + PrivacyPassportCard() + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) versionSection dependenciesSection developmentToolsSection diff --git a/Where/WhereUI/Sources/Settings/DataSettingsView.swift b/Where/WhereUI/Sources/Settings/DataSettingsView.swift index fc4f1f76..3a2dcc61 100644 --- a/Where/WhereUI/Sources/Settings/DataSettingsView.swift +++ b/Where/WhereUI/Sources/Settings/DataSettingsView.swift @@ -1,9 +1,10 @@ import LifecycleKitUI +import SnapshotKit import SwiftUI import WhereCore -/// Settings drill-in for backup and data management: export or restore the -/// database, erase the selected year's data, or reset the entire app. +/// Settings drill-in for Where's privacy promise and data management: export or +/// restore the database, erase the selected year's data, or reset the entire app. struct DataSettingsView: View { let report: YearReportModel let backup: BackupModel @@ -22,6 +23,9 @@ struct DataSettingsView: View { var body: some View { SettingsFocusScope(focus: focus) { Form { + PrivacyPassportCard() + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) BackupSettingsSection(backup: backup) dataSection resetSection @@ -128,16 +132,21 @@ extension DataSettingsView: SettingsSection { } #if DEBUG - #Preview { - NavigationStack { - DataSettingsView( - report: PreviewSupport.loadedYearReportModel(), - backup: PreviewSupport.backupModel(), - ) - .environment(PreviewSupport.loadedModel()) - .environment(PreviewSupport.loadedSession()) + extension DataSettingsView: SnapshotProviding { + static var snapshots: [SnapshotCase] { + whereSnapshot(name: "Default", configurations: .fullContentScreenDefaults) { + DataSettingsView( + report: PreviewSupport.loadedYearReportModel(), + backup: PreviewSupport.backupModel(), + ) + .environment(PreviewSupport.loadedModel()) + .environment(PreviewSupport.loadedSession()) + } } - .whereBroadwayRoot() + } + + #Preview { + DataSettingsView.snapshotPreviews } #endif diff --git a/Where/WhereUI/Sources/Settings/PrivacyPassportCard.swift b/Where/WhereUI/Sources/Settings/PrivacyPassportCard.swift new file mode 100644 index 00000000..8af4b46d --- /dev/null +++ b/Where/WhereUI/Sources/Settings/PrivacyPassportCard.swift @@ -0,0 +1,31 @@ +import SwiftUI + +/// Reassures the reader that Where's location history stays in their Apple storage. +struct PrivacyPassportCard: View { + @State private var tilt = TiltProvider() + + var body: some View { + PassportCard( + title: .settingsPrivacyTitle, + detail: .settingsPrivacyDetail, + sealSystemImage: "lock.shield.fill", + accessorySystemImage: nil, + isInteractive: false, + surface: .reflective(tilt: tilt), + ) + .onAppear { tilt.start() } + .onDisappear { tilt.stop() } + } +} + +#if DEBUG + #Preview { + Form { + PrivacyPassportCard() + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets()) + } + .whereBroadwayRoot() + } +#endif diff --git a/Where/WhereUI/Sources/Shared/PassportCard.swift b/Where/WhereUI/Sources/Shared/PassportCard.swift new file mode 100644 index 00000000..238e02e7 --- /dev/null +++ b/Where/WhereUI/Sources/Shared/PassportCard.swift @@ -0,0 +1,108 @@ +import SwiftUI + +/// The shared card content and chrome used for Where's passport statements. +struct PassportCard: View { + let title: LocalizedStringResource + let detail: LocalizedStringResource + let sealSystemImage: String + let accessorySystemImage: String? + let isInteractive: Bool + let surface: PassportCardSurfaceKind + + @Environment(\.stylesheet) private var stylesheet + + private var style: WhereStylesheet.PassportCardStyle { + stylesheet.passportCard + } + + private var shape: RoundedRectangle { + RoundedRectangle(cornerRadius: style.cornerRadius) + } + + private var glowColor: Color { + surface.isReflective ? style.reflectiveSurface.backgroundTop : .accentColor + } + + private var glowOpacity: Double { + surface.isReflective + ? style.reflectiveSurface.glowOpacity + : style.accentGlow.opacity + } + + var body: some View { + PassportCardSurface( + surface: surface, + isInteractive: isInteractive, + shape: shape, + ) { + HStack(spacing: style.contentSpacing) { + ZStack { + Circle() + .strokeBorder(.tint, lineWidth: style.seal.outerLineWidth) + Circle() + .strokeBorder( + .tint.opacity(0.65), + style: StrokeStyle( + lineWidth: style.seal.innerLineWidth, + dash: [style.seal.dashLength, style.seal.dashSpacing], + ), + ) + .padding(style.seal.innerInset) + Image(systemName: sealSystemImage) + .font(style.seal.symbolFont) + .foregroundStyle(.tint) + } + .frame(width: style.seal.size, height: style.seal.size) + .rotationEffect(.degrees(style.seal.rotationDegrees)) + .accessibilityHidden(true) + + VStack(alignment: .leading, spacing: stylesheet.spacing.xxSmall) { + Text(title) + .font(style.titleFont) + .foregroundStyle(.primary) + Text(detail) + .font(style.detailFont) + .foregroundStyle(.secondary) + } + + Spacer(minLength: 0) + + if let accessorySystemImage { + Image(systemName: accessorySystemImage) + .foregroundStyle(.tint) + .accessibilityHidden(true) + } + } + .padding(style.padding) + .frame(maxWidth: .infinity, alignment: .leading) + } + .clipShape(shape) + .contentShape(shape) + .shadow( + color: glowColor.opacity(glowOpacity), + radius: style.accentGlow.radius, + y: style.accentGlow.offsetY, + ) + .shadow( + color: Color.black.opacity(style.liftShadow.opacity), + radius: style.liftShadow.radius, + y: style.liftShadow.offsetY, + ) + .accessibilityElement(children: .combine) + } +} + +#if DEBUG + #Preview { + PassportCard( + title: .settingsPrivacyTitle, + detail: .settingsPrivacyDetail, + sealSystemImage: "lock.shield.fill", + accessorySystemImage: nil, + isInteractive: false, + surface: .reflective(tilt: .preview), + ) + .padding() + .whereBroadwayRoot() + } +#endif diff --git a/Where/WhereUI/Sources/Shared/PassportCardSurface.swift b/Where/WhereUI/Sources/Shared/PassportCardSurface.swift new file mode 100644 index 00000000..a5b4f872 --- /dev/null +++ b/Where/WhereUI/Sources/Shared/PassportCardSurface.swift @@ -0,0 +1,82 @@ +import SwiftUI + +/// Applies either the security-print glass or reflective privacy surface to a +/// passport card without making the card's content observe device motion. +struct PassportCardSurface: View { + let surface: PassportCardSurfaceKind + let isInteractive: Bool + let shape: RoundedRectangle + @ViewBuilder let content: Content + + @Environment(\.stylesheet) private var stylesheet + @Environment(\.colorScheme) private var colorScheme + + private var style: WhereStylesheet.PassportCardStyle { + stylesheet.passportCard + } + + private var surfaceTint: Color { + surface.isReflective ? style.reflectiveSurface.accent : .accentColor + } + + var body: some View { + let reflection = style.reflectiveSurface + content + .background { + ZStack { + if surface.isReflective { + LinearGradient( + colors: [reflection.backgroundTop, reflection.backgroundBottom], + startPoint: .topLeading, + endPoint: .bottomTrailing, + ) + } + + SecurityPrintRosette( + tint: surfaceTint, + wobble: style.rosette.wobble, + lineWidth: style.rosette.lineWidth, + primaryRingSpacing: style.rosette.primaryRingSpacing, + secondaryRingSpacing: style.rosette.secondaryRingSpacing, + primaryOpacity: style.rosette.primaryOpacity, + secondaryOpacity: style.rosette.secondaryOpacity, + ) + } + } + .glassEffect( + .regular.tint(surfaceTint.opacity(style.glassTintOpacity)) + .interactive(isInteractive), + in: shape, + ) + .tiltSheen( + tilt: surface.tilt, + staticRoll: reflection.staticPose.roll, + staticPitch: reflection.staticPose.pitch, + in: shape, + intensity: surface.isReflective ? reflection.intensity : 0, + staticGlintIntensity: surface.isReflective + ? reflection.staticGlintIntensity + : 0, + ) + .tint(surfaceTint) + .environment(\.colorScheme, surface.isReflective ? .dark : colorScheme) + } +} + +#if DEBUG + #Preview { + PassportCardSurface( + surface: .reflective(tilt: .preview), + isInteractive: false, + shape: RoundedRectangle( + cornerRadius: WhereStylesheet.default.passportCard.cornerRadius, + ), + ) { + Text("Reflective passport surface") + .padding() + .frame(maxWidth: .infinity) + } + .padding() + .whereBroadwayRoot() + } +#endif diff --git a/Where/WhereUI/Sources/Shared/PassportCardSurfaceKind.swift b/Where/WhereUI/Sources/Shared/PassportCardSurfaceKind.swift new file mode 100644 index 00000000..f6582b7d --- /dev/null +++ b/Where/WhereUI/Sources/Shared/PassportCardSurfaceKind.swift @@ -0,0 +1,20 @@ +/// Selects a passport card's visual surface independently from whether device +/// motion has delivered a sample. +enum PassportCardSurfaceKind { + case securityPrint + case reflective(tilt: TiltProvider) + + var tilt: TiltProvider? { + switch self { + case .securityPrint: nil + case let .reflective(tilt): tilt + } + } + + var isReflective: Bool { + switch self { + case .securityPrint: false + case .reflective: true + } + } +} diff --git a/Where/WhereUI/Sources/Shared/SecurityPrintRosette.swift b/Where/WhereUI/Sources/Shared/SecurityPrintRosette.swift index 9b2ccb03..3edd6cef 100644 --- a/Where/WhereUI/Sources/Shared/SecurityPrintRosette.swift +++ b/Where/WhereUI/Sources/Shared/SecurityPrintRosette.swift @@ -64,7 +64,7 @@ struct SecurityPrintRosette: View { #if DEBUG #Preview { - let rosette = WhereStylesheet.default.aboutOpenSource.rosette + let rosette = WhereStylesheet.default.passportCard.rosette SecurityPrintRosette( tint: .accentColor, wobble: rosette.wobble, diff --git a/Where/WhereUI/Sources/Shared/WhereStylesheet.swift b/Where/WhereUI/Sources/Shared/WhereStylesheet.swift index bfb2f4ff..5490eeba 100644 --- a/Where/WhereUI/Sources/Shared/WhereStylesheet.swift +++ b/Where/WhereUI/Sources/Shared/WhereStylesheet.swift @@ -27,7 +27,7 @@ struct WhereStylesheet: BStylesheet { var launch = LaunchStyle.standard var typography = Typography.standard var settings = SettingsStyle.standard - var aboutOpenSource = AboutOpenSourceStyle.standard + var passportCard = PassportCardStyle.standard var developerOverlay = DeveloperOverlayStyle.standard init() {} @@ -1257,20 +1257,19 @@ extension WhereStylesheet { } } -// MARK: - About open source +// MARK: - Passport card extension WhereStylesheet { - /// Appearance for the compact open-source sign-off at the bottom of About. - /// It echoes the Locations cards' security print without borrowing their - /// region-specific card spec. - struct AboutOpenSourceStyle: Equatable { + /// Appearance for compact passport statements in Settings. + struct PassportCardStyle: Equatable { var cornerRadius: CGFloat var padding: CGFloat var contentSpacing: CGFloat var titleFont: Font - var actionFont: Font + var detailFont: Font var seal: Seal var rosette: Rosette + var reflectiveSurface: ReflectiveSurface var glassTintOpacity: Double var accentGlow: Shadow var liftShadow: Shadow @@ -1295,18 +1294,33 @@ extension WhereStylesheet { var secondaryOpacity: Double } + struct ReflectiveSurface: Equatable { + var backgroundTop: Color + var backgroundBottom: Color + var accent: Color + var glowOpacity: Double + var intensity: Double + var staticGlintIntensity: Double + var staticPose: Pose + + struct Pose: Equatable { + var roll: Double + var pitch: Double + } + } + struct Shadow: Equatable { var opacity: Double var radius: CGFloat var offsetY: CGFloat = 0 } - static let standard = AboutOpenSourceStyle( + static let standard = PassportCardStyle( cornerRadius: 20, padding: 16, contentSpacing: 12, titleFont: .headline, - actionFont: .subheadline, + detailFont: .subheadline, seal: Seal( size: 52, rotationDegrees: -8, @@ -1325,6 +1339,15 @@ extension WhereStylesheet { primaryOpacity: 0.1, secondaryOpacity: 0.06, ), + reflectiveSurface: ReflectiveSurface( + backgroundTop: Color(red: 0.08, green: 0.18, blue: 0.34), + backgroundBottom: Color(red: 0.02, green: 0.07, blue: 0.16), + accent: Color(red: 0.88, green: 0.72, blue: 0.32), + glowOpacity: 0.12, + intensity: 0.28, + staticGlintIntensity: 0.28, + staticPose: .init(roll: 0.3, pitch: -0.15), + ), glassTintOpacity: 0.06, accentGlow: Shadow(opacity: 0.18, radius: 7), liftShadow: Shadow(opacity: 0.08, radius: 5, offsetY: 2), diff --git a/Where/WhereUI/Tests/PassportCardSurfaceKindTests.swift b/Where/WhereUI/Tests/PassportCardSurfaceKindTests.swift new file mode 100644 index 00000000..05c856f2 --- /dev/null +++ b/Where/WhereUI/Tests/PassportCardSurfaceKindTests.swift @@ -0,0 +1,14 @@ +import Testing +@testable import WhereUI + +@MainActor +struct PassportCardSurfaceKindTests { + @Test func surfaceIdentityDoesNotDependOnMotionAvailability() { + let tilt = TiltProvider.preview + + #expect(PassportCardSurfaceKind.securityPrint.tilt == nil) + #expect(PassportCardSurfaceKind.securityPrint.isReflective == false) + #expect(PassportCardSurfaceKind.reflective(tilt: tilt).tilt === tilt) + #expect(PassportCardSurfaceKind.reflective(tilt: tilt).isReflective) + } +} diff --git a/Where/WhereUI/Tests/PassportCardSurfaceTests.swift b/Where/WhereUI/Tests/PassportCardSurfaceTests.swift new file mode 100644 index 00000000..b537bdfb --- /dev/null +++ b/Where/WhereUI/Tests/PassportCardSurfaceTests.swift @@ -0,0 +1,29 @@ +import SwiftUI +import TestHostSupport +import Testing +@testable import WhereUI + +@MainActor +struct PassportCardSurfaceTests { + @Test func hostsSecurityPrintAndReflectiveSurfaces() throws { + let shape = RoundedRectangle( + cornerRadius: WhereStylesheet.default.passportCard.cornerRadius, + ) + let rootView = VStack { + PassportCardSurface(surface: .securityPrint, isInteractive: true, shape: shape) { + Color.clear.frame(height: 80) + } + PassportCardSurface( + surface: .reflective(tilt: .preview), + isInteractive: false, + shape: shape, + ) { + Color.clear.frame(height: 80) + } + } + .whereBroadwayRoot() + try show(UIHostingController(rootView: rootView)) { hosted in + #expect(hosted.view != nil) + } + } +} diff --git a/Where/WhereUI/Tests/PassportCardTests.swift b/Where/WhereUI/Tests/PassportCardTests.swift new file mode 100644 index 00000000..b0b588a3 --- /dev/null +++ b/Where/WhereUI/Tests/PassportCardTests.swift @@ -0,0 +1,22 @@ +import SwiftUI +import TestHostSupport +import Testing +@testable import WhereUI + +@MainActor +struct PassportCardTests { + @Test func hostsInformationalContent() throws { + let rootView = PassportCard( + title: .settingsPrivacyTitle, + detail: .settingsPrivacyDetail, + sealSystemImage: "lock.shield.fill", + accessorySystemImage: nil, + isInteractive: false, + surface: .reflective(tilt: .preview), + ) + .whereBroadwayRoot() + try show(UIHostingController(rootView: rootView)) { hosted in + #expect(hosted.view != nil) + } + } +} diff --git a/Where/WhereUI/Tests/PrivacyPassportCardTests.swift b/Where/WhereUI/Tests/PrivacyPassportCardTests.swift new file mode 100644 index 00000000..8f619f2b --- /dev/null +++ b/Where/WhereUI/Tests/PrivacyPassportCardTests.swift @@ -0,0 +1,15 @@ +import SwiftUI +import TestHostSupport +import Testing +@testable import WhereUI + +@MainActor +struct PrivacyPassportCardTests { + @Test func hosts() throws { + let rootView = PrivacyPassportCard() + .whereBroadwayRoot() + try show(UIHostingController(rootView: rootView)) { hosted in + #expect(hosted.view != nil) + } + } +} diff --git a/Where/WhereUI/Tests/WhereStylesheetTests.swift b/Where/WhereUI/Tests/WhereStylesheetTests.swift index 603e0e0e..6f99483a 100644 --- a/Where/WhereUI/Tests/WhereStylesheetTests.swift +++ b/Where/WhereUI/Tests/WhereStylesheetTests.swift @@ -359,13 +359,13 @@ struct WhereStylesheetTests { #expect(settings.scrollSettleDelay == .milliseconds(350)) } - @Test func aboutOpenSourceStyle() { - let source = style.aboutOpenSource + @Test func passportCardStyle() { + let source = style.passportCard #expect(source.cornerRadius == 20) #expect(source.padding == 16) #expect(source.contentSpacing == 12) #expect(source.titleFont == .headline) - #expect(source.actionFont == .subheadline) + #expect(source.detailFont == .subheadline) #expect(source.seal == .init( size: 52, rotationDegrees: -8, @@ -384,6 +384,15 @@ struct WhereStylesheetTests { primaryOpacity: 0.1, secondaryOpacity: 0.06, )) + #expect(source.reflectiveSurface == .init( + backgroundTop: Color(red: 0.08, green: 0.18, blue: 0.34), + backgroundBottom: Color(red: 0.02, green: 0.07, blue: 0.16), + accent: Color(red: 0.88, green: 0.72, blue: 0.32), + glowOpacity: 0.12, + intensity: 0.28, + staticGlintIntensity: 0.28, + staticPose: .init(roll: 0.3, pitch: -0.15), + )) #expect(source.glassTintOpacity == 0.06) #expect(source.accentGlow == .init(opacity: 0.18, radius: 7)) #expect(source.liftShadow == .init(opacity: 0.08, radius: 5, offsetY: 2))