From 2419e4d7535fedd89cdbb7f44be9cc231a288afa Mon Sep 17 00:00:00 2001 From: Sasha Weiss Date: Wed, 4 Feb 2026 14:13:37 -0800 Subject: [PATCH] Add first-time education sheet for Key Transparency --- .../safety-numbers/Contents.json | 6 ++ .../Contents.json | 0 .../safety-number-change.pdf | 0 .../Contents.json | 12 ++++ .../safety-number-verification.pdf | Bin 0 -> 21972 bytes .../DebugUI/DebugUIPrompts.swift | 5 ++ .../translations/en.lproj/Localizable.strings | 6 ++ .../KeyTransparencyManager.swift | 56 ++++++++++++------ .../FingerprintViewController.swift | 48 ++++++++++++++- 9 files changed, 112 insertions(+), 21 deletions(-) create mode 100644 Signal/Images.xcassets/safety-numbers/Contents.json rename Signal/Images.xcassets/{ => safety-numbers}/safety-number-change.imageset/Contents.json (100%) rename Signal/Images.xcassets/{ => safety-numbers}/safety-number-change.imageset/safety-number-change.pdf (100%) create mode 100644 Signal/Images.xcassets/safety-numbers/safety-number-verification.imageset/Contents.json create mode 100644 Signal/Images.xcassets/safety-numbers/safety-number-verification.imageset/safety-number-verification.pdf diff --git a/Signal/Images.xcassets/safety-numbers/Contents.json b/Signal/Images.xcassets/safety-numbers/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Signal/Images.xcassets/safety-numbers/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Images.xcassets/safety-number-change.imageset/Contents.json b/Signal/Images.xcassets/safety-numbers/safety-number-change.imageset/Contents.json similarity index 100% rename from Signal/Images.xcassets/safety-number-change.imageset/Contents.json rename to Signal/Images.xcassets/safety-numbers/safety-number-change.imageset/Contents.json diff --git a/Signal/Images.xcassets/safety-number-change.imageset/safety-number-change.pdf b/Signal/Images.xcassets/safety-numbers/safety-number-change.imageset/safety-number-change.pdf similarity index 100% rename from Signal/Images.xcassets/safety-number-change.imageset/safety-number-change.pdf rename to Signal/Images.xcassets/safety-numbers/safety-number-change.imageset/safety-number-change.pdf diff --git a/Signal/Images.xcassets/safety-numbers/safety-number-verification.imageset/Contents.json b/Signal/Images.xcassets/safety-numbers/safety-number-verification.imageset/Contents.json new file mode 100644 index 0000000000..0dd833d47e --- /dev/null +++ b/Signal/Images.xcassets/safety-numbers/safety-number-verification.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "safety-number-verification.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Images.xcassets/safety-numbers/safety-number-verification.imageset/safety-number-verification.pdf b/Signal/Images.xcassets/safety-numbers/safety-number-verification.imageset/safety-number-verification.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ac65e9d3498d1229ff180e0ac4937e70393e2050 GIT binary patch literal 21972 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 SelfCheckState? { - return selfCheckKVStore.fetchValue( + return kvStore.fetchValue( Int64.self, - forKey: selfCheckKVStoreKey, + forKey: KVStoreKeys.selfCheckState, tx: tx, ) .map { SelfCheckState(rawValue: $0)! } } private static func setSelfCheckState(_ state: SelfCheckState, tx: DBWriteTransaction) { - selfCheckKVStore.writeValue(state.rawValue, forKey: selfCheckKVStoreKey, tx: tx) + kvStore.writeValue(state.rawValue, forKey: KVStoreKeys.selfCheckState, tx: tx) } public static func shouldWarnSelfCheckFailed(tx: DBReadTransaction) -> Bool { @@ -504,43 +520,47 @@ public final class KeyTransparencyManager { // MARK: - Opt-out - private static let isEnabledKVStore = NewKeyValueStore(collection: "KT.IsEnabled") - private static let isEnabledKVStoreKey = "isEnabled" - public static func isEnabled(tx: DBReadTransaction) -> Bool { guard BuildFlags.KeyTransparency.enabled else { return false } - return isEnabledKVStore.fetchValue(Bool.self, forKey: isEnabledKVStoreKey, tx: tx) ?? true + return kvStore.fetchValue(Bool.self, forKey: KVStoreKeys.isEnabled, tx: tx) ?? true } public static func setIsEnabled(_ isEnabled: Bool, tx: DBWriteTransaction) { logger.info("\(isEnabled)") - isEnabledKVStore.writeValue(isEnabled, forKey: isEnabledKVStoreKey, tx: tx) + kvStore.writeValue(isEnabled, forKey: KVStoreKeys.isEnabled, tx: tx) if !isEnabled { - selfCheckKVStore.removeAll(tx: tx) + kvStore.removeValue(forKey: KVStoreKeys.distinguishedTreeHead, tx: tx) + kvStore.removeValue(forKey: KVStoreKeys.selfCheckState, tx: tx) selfCheckCronStore.setMostRecentDate(.distantPast, jitter: 0, tx: tx) - distinguishedTreeKVStore.removeAll(tx: tx) failIfThrows { try KeyTransparencyRecord.deleteAll(tx.database) } } } + // MARK: - First-time education + + public static func shouldShowFirstTimeEducation(tx: DBReadTransaction) -> Bool { + return kvStore.fetchValue(Bool.self, forKey: KVStoreKeys.shouldShowFirstTimeEducation, tx: tx) ?? true + } + + public static func setHasShownFirstTimeEducation(_ value: Bool, tx: DBWriteTransaction) { + kvStore.writeValue(value, forKey: KVStoreKeys.shouldShowFirstTimeEducation, tx: tx) + } + // MARK: - - private static let distinguishedTreeKVStore = NewKeyValueStore(collection: "KT.DistinguishedTree") - private static let distinguishedTreeKVStoreKey = "head" - fileprivate static func getLastDistinguishedTreeHead(tx: DBReadTransaction) -> Data? { - return distinguishedTreeKVStore.fetchValue(Data.self, forKey: distinguishedTreeKVStoreKey, tx: tx) + return kvStore.fetchValue(Data.self, forKey: KVStoreKeys.distinguishedTreeHead, tx: tx) } fileprivate static func setLastDistinguishedTreeHead(_ blob: Data, tx: DBWriteTransaction) { - distinguishedTreeKVStore.writeValue(blob, forKey: distinguishedTreeKVStoreKey, tx: tx) + kvStore.writeValue(blob, forKey: KVStoreKeys.distinguishedTreeHead, tx: tx) } fileprivate static func getKeyTransparencyRecord( diff --git a/SignalUI/SafetyNumbers/FingerprintViewController.swift b/SignalUI/SafetyNumbers/FingerprintViewController.swift index 6a2bc7107c..39eecec8ed 100644 --- a/SignalUI/SafetyNumbers/FingerprintViewController.swift +++ b/SignalUI/SafetyNumbers/FingerprintViewController.swift @@ -32,12 +32,14 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon let fingerprintResult: FingerprintResult? let keyTransparencyState: KeyTransparencyState? + let keyTransparencyShouldShowEducation: Bool ( fingerprintResult, keyTransparencyState, + keyTransparencyShouldShowEducation, ) = db.read { tx in guard let theirAci else { - return (nil, nil) + return (nil, nil, false) } let theirAddress = SignalServiceAddress(theirAci) @@ -50,7 +52,7 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx), let myAciIdentityKey = identityManager.identityKeyPair(for: .aci, tx: tx)?.keyPair.identityKey else { - return (nil, nil) + return (nil, nil, false) } let keyTransparencyIsEnabled = KeyTransparencyManager.isEnabled(tx: tx) @@ -59,6 +61,7 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon localIdentifiers: localIdentifiers, tx: tx, ) + let keyTransparencyShouldShowEducation = KeyTransparencyManager.shouldShowFirstTimeEducation(tx: tx) return ( FingerprintResult( @@ -78,6 +81,7 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon checkParams: keyTransparencyCheckParams, viewInitialState: keyTransparencyCheckParams == nil ? .unableToVerify : .readyToVerify, ), + keyTransparencyShouldShowEducation, ) } @@ -112,7 +116,19 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon ), ) let navigationController = OWSNavigationController(rootViewController: fingerprintViewController) - viewController.present(navigationController, animated: true) + + if keyTransparencyShouldShowEducation { + let educationSheet = KeyTransparencyFirstTimeEducationHeroSheet { + db.write { tx in + KeyTransparencyManager.setHasShownFirstTimeEducation(true, tx: tx) + } + + viewController.present(navigationController, animated: true) + } + viewController.present(educationSheet, animated: true) + } else { + viewController.present(navigationController, animated: true) + } } // MARK: - @@ -846,6 +862,32 @@ private final class KeyTransparencyFailureHeroSheet: HeroSheetViewController { // MARK: - +private final class KeyTransparencyFirstTimeEducationHeroSheet: HeroSheetViewController { + init(onContinue: @MainActor @escaping () -> Void) { + super.init( + hero: .image(UIImage(named: "safety-number-verification")!), + title: OWSLocalizedString( + "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_EDUCATION_SHEET_TITLE", + comment: "Title for a sheet introducing Key Transparency.", + ), + body: OWSLocalizedString( + "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_EDUCATION_SHEET_BODY", + comment: "Body for a sheet introducing Key Transparency.", + ), + primaryButton: HeroSheetViewController.Button( + title: CommonStrings.continueButton, + action: { sheet in + sheet.dismiss(animated: true) { + onContinue() + } + }, + ), + ) + } +} + +// MARK: - + #if DEBUG private extension IdentityKey {