Only show ACI Safety Numbers

This commit is contained in:
Max Radermacher 2023-11-01 21:35:05 -05:00 committed by GitHub
parent 0e5424f090
commit e179fac4e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 120 additions and 1469 deletions

View File

@ -807,7 +807,6 @@
662C440B2A156DF7001F83E2 /* SecureValueRecovery2Impl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662C440A2A156DF7001F83E2 /* SecureValueRecovery2Impl.swift */; };
662C44172A1D21D7001F83E2 /* SecureValueRecovery2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662C44152A1D2101001F83E2 /* SecureValueRecovery2Tests.swift */; };
663BA3182A4B8595004B9A43 /* SpoilerRenderState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663BA3172A4B8595004B9A43 /* SpoilerRenderState.swift */; };
663BA31C2A4C9997004B9A43 /* safety-numbers.json in Resources */ = {isa = PBXBuildFile; fileRef = 663BA31B2A4C9997004B9A43 /* safety-numbers.json */; };
663BA3202A4CF96B004B9A43 /* MessageBodyDisplayConfigurations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663BA31F2A4CF96B004B9A43 /* MessageBodyDisplayConfigurations.swift */; };
663D6A7C292319BC00CABC49 /* ConversationPickerFailedRecipientsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663D6A7B292319BC00CABC49 /* ConversationPickerFailedRecipientsSheet.swift */; };
6640639E294D20A900997E0B /* OutgoingCallEventSyncMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6640639D294D20A900997E0B /* OutgoingCallEventSyncMessage.swift */; };
@ -3398,7 +3397,6 @@
662C440A2A156DF7001F83E2 /* SecureValueRecovery2Impl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureValueRecovery2Impl.swift; sourceTree = "<group>"; };
662C44152A1D2101001F83E2 /* SecureValueRecovery2Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureValueRecovery2Tests.swift; sourceTree = "<group>"; };
663BA3172A4B8595004B9A43 /* SpoilerRenderState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpoilerRenderState.swift; sourceTree = "<group>"; };
663BA31B2A4C9997004B9A43 /* safety-numbers.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "safety-numbers.json"; sourceTree = "<group>"; };
663BA31F2A4CF96B004B9A43 /* MessageBodyDisplayConfigurations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBodyDisplayConfigurations.swift; sourceTree = "<group>"; };
663D6A7B292319BC00CABC49 /* ConversationPickerFailedRecipientsSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationPickerFailedRecipientsSheet.swift; sourceTree = "<group>"; };
6640639D294D20A900997E0B /* OutgoingCallEventSyncMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingCallEventSyncMessage.swift; sourceTree = "<group>"; };
@ -7720,7 +7718,6 @@
880C0FF6233D3F7C00386FB8 /* playPauseButton.json */,
346EFC3825FFDC6900F493C7 /* restore-dark.json */,
346EFC3725FFDC6900F493C7 /* restore.json */,
663BA31B2A4C9997004B9A43 /* safety-numbers.json */,
66586D3529005A1B00DDA9B9 /* story_viewer_onboarding_1.json */,
66586D3429005A1B00DDA9B9 /* story_viewer_onboarding_2.json */,
66586D3629005A1B00DDA9B9 /* story_viewer_onboarding_3.json */,
@ -10802,7 +10799,6 @@
346EFC3C25FFDC6A00F493C7 /* restore-dark.json in Resources */,
346EFC3B25FFDC6A00F493C7 /* restore.json in Resources */,
34CF0788203E6B78005C4D61 /* ringback_tone_ansi.caf in Resources */,
663BA31C2A4C9997004B9A43 /* safety-numbers.json in Resources */,
882F8DE6251AB23600AA4359 /* Settings.bundle in Resources */,
4C63CC00210A620B003AE45C /* SignalTSan.supp in Resources */,
4C6F527C20FFE8400097DEEE /* SignalUBSan.supp in Resources */,

View File

@ -1,12 +0,0 @@
{
"images" : [
{
"filename" : "safety-numbers.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,42 +0,0 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="29.3486" y="6" width="20" height="32" rx="4" transform="rotate(25 29.3486 6)" fill="#86A1E3" stroke="#3D62BC" stroke-width="1.75"/>
<g clip-path="url(#clip0_8_3611)">
<path d="M36.7111 19.4259C36.9746 19.2614 37.0549 18.9144 36.8904 18.6509C36.7259 18.3873 36.3789 18.3071 36.1153 18.4716L32.9916 20.4217L32.6913 19.1078C32.622 18.805 32.3204 18.6156 32.0176 18.6848C31.7147 18.754 31.5253 19.0557 31.5946 19.3585L32.0709 21.4426C32.1115 21.62 32.2353 21.7668 32.4033 21.8367C32.5714 21.9065 32.7628 21.8908 32.9172 21.7944L36.7111 19.4259Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.6389 24.3772C30.9879 25.0957 31.7929 25.4711 32.5677 25.2766L34.8402 24.706C36.1636 24.3738 37.2969 23.5214 37.9831 22.3421L39.3586 19.9783C39.7524 19.3014 39.6289 18.4426 39.0602 17.9042L36.9889 15.943C36.4292 15.4131 35.7127 15.0789 34.9469 14.9908L32.1132 14.6647C31.3353 14.5752 30.5979 15.0326 30.3326 15.7694L29.4059 18.3425C28.9436 19.6262 29.0191 21.0422 29.6152 22.2696L30.6389 24.3772ZM32.2938 24.1855C32.0355 24.2503 31.7672 24.1252 31.6508 23.8857L30.6272 21.7781C30.1635 20.8235 30.1048 19.7221 30.4644 18.7236L31.3911 16.1505C31.4795 15.905 31.7253 15.7525 31.9846 15.7823L34.8183 16.1084C35.3422 16.1687 35.8325 16.3973 36.2155 16.76L38.2868 18.7211C38.4763 18.9005 38.5175 19.1868 38.3862 19.4124L37.0107 21.7763C36.477 22.6935 35.5956 23.3565 34.5663 23.6149L32.2938 24.1855Z" fill="white"/>
<path d="M36.7111 19.4259C36.9746 19.2614 37.0549 18.9144 36.8904 18.6509C36.7259 18.3873 36.3789 18.3071 36.1153 18.4716L32.9916 20.4217L32.6913 19.1078C32.622 18.805 32.3204 18.6156 32.0176 18.6848C31.7147 18.754 31.5253 19.0557 31.5946 19.3585L32.0709 21.4426C32.1115 21.62 32.2353 21.7668 32.4033 21.8367C32.5714 21.9065 32.7628 21.8908 32.9172 21.7944L36.7111 19.4259Z" stroke="white" stroke-width="0.25"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.6389 24.3772C30.9879 25.0957 31.7929 25.4711 32.5677 25.2766L34.8402 24.706C36.1636 24.3738 37.2969 23.5214 37.9831 22.3421L39.3586 19.9783C39.7524 19.3014 39.6289 18.4426 39.0602 17.9042L36.9889 15.943C36.4292 15.4131 35.7127 15.0789 34.9469 14.9908L32.1132 14.6647C31.3353 14.5752 30.5979 15.0326 30.3326 15.7694L29.4059 18.3425C28.9436 19.6262 29.0191 21.0422 29.6152 22.2696L30.6389 24.3772ZM32.2938 24.1855C32.0355 24.2503 31.7672 24.1252 31.6508 23.8857L30.6272 21.7781C30.1635 20.8235 30.1048 19.7221 30.4644 18.7236L31.3911 16.1505C31.4795 15.905 31.7253 15.7525 31.9846 15.7823L34.8183 16.1084C35.3422 16.1687 35.8325 16.3973 36.2155 16.76L38.2868 18.7211C38.4763 18.9005 38.5175 19.1868 38.3862 19.4124L37.0107 21.7763C36.477 22.6935 35.5956 23.3565 34.5663 23.6149L32.2938 24.1855Z" stroke="white" stroke-width="0.25"/>
</g>
<path d="M28.6304 28.8359L30.443 29.6812" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path d="M24.0991 26.7229L25.9117 27.5681" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path d="M33.1621 30.9492L34.9747 31.7945" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path d="M27.3628 31.5549L29.1754 32.4002" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path d="M22.8311 29.4419L24.6437 30.2871" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path d="M31.894 33.668L33.7067 34.5132" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path d="M26.0947 34.2739L27.9073 35.1192" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path d="M21.563 32.1609L23.3756 33.0061" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path d="M30.6265 36.387L32.4391 37.2322" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<rect y="14.4524" width="20" height="32" rx="4" transform="rotate(-25 0 14.4524)" fill="#EBEAE8" stroke="#837D72" stroke-width="1.75"/>
<g clip-path="url(#clip1_8_3611)">
<path d="M15.0172 17.4425C15.0605 17.1348 14.8463 16.8503 14.5387 16.807C14.2311 16.7636 13.9465 16.9778 13.9032 17.2854L13.3892 20.9319L12.1896 20.3174C11.9131 20.1758 11.5742 20.2851 11.4325 20.5616C11.2909 20.8381 11.4002 21.177 11.6767 21.3187L13.5794 22.2934C13.7414 22.3763 13.9334 22.3758 14.095 22.292C14.2565 22.2082 14.3675 22.0514 14.3929 21.8712L15.0172 17.4425Z" fill="#837D72"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.9069 25.2767C15.6817 25.4712 16.4868 25.0958 16.8358 24.3772L17.8594 22.2696C18.4556 21.0423 18.531 19.6263 18.0687 18.3425L17.1421 15.7694C16.8767 15.0327 16.1394 14.5752 15.3615 14.6648L12.5277 14.9909C11.762 15.079 11.0455 15.4131 10.4857 15.9431L8.41443 17.9042C7.84579 18.4426 7.72225 19.3015 8.11609 19.9783L9.49156 22.3421C10.1778 23.5215 11.311 24.3739 12.6344 24.7061L14.9069 25.2767ZM15.8238 23.8857C15.7075 24.1252 15.4391 24.2504 15.1809 24.1855L12.9084 23.615C11.8791 23.3566 10.9977 22.6936 10.4639 21.7763L9.08846 19.4125C8.95717 19.1869 8.99835 18.9006 9.1879 18.7211L11.2592 16.76C11.6422 16.3974 12.1324 16.1688 12.6564 16.1085L15.4901 15.7824C15.7494 15.7525 15.9952 15.905 16.0836 16.1506L17.0103 18.7237C17.3698 19.7222 17.3111 20.8235 16.8475 21.7781L15.8238 23.8857Z" fill="#837D72"/>
<path d="M15.0172 17.4425C15.0605 17.1348 14.8463 16.8503 14.5387 16.807C14.2311 16.7636 13.9465 16.9778 13.9032 17.2854L13.3892 20.9319L12.1896 20.3174C11.9131 20.1758 11.5742 20.2851 11.4325 20.5616C11.2909 20.8381 11.4002 21.177 11.6767 21.3187L13.5794 22.2934C13.7414 22.3763 13.9334 22.3758 14.095 22.292C14.2565 22.2082 14.3675 22.0514 14.3929 21.8712L15.0172 17.4425Z" stroke="#837D72" stroke-width="0.25"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.9069 25.2767C15.6817 25.4712 16.4868 25.0958 16.8358 24.3772L17.8594 22.2696C18.4556 21.0423 18.531 19.6263 18.0687 18.3425L17.1421 15.7694C16.8767 15.0327 16.1394 14.5752 15.3615 14.6648L12.5277 14.9909C11.762 15.079 11.0455 15.4131 10.4857 15.9431L8.41443 17.9042C7.84579 18.4426 7.72225 19.3015 8.11609 19.9783L9.49156 22.3421C10.1778 23.5215 11.311 24.3739 12.6344 24.7061L14.9069 25.2767ZM15.8238 23.8857C15.7075 24.1252 15.4391 24.2504 15.1809 24.1855L12.9084 23.615C11.8791 23.3566 10.9977 22.6936 10.4639 21.7763L9.08846 19.4125C8.95717 19.1869 8.99835 18.9006 9.1879 18.7211L11.2592 16.76C11.6422 16.3974 12.1324 16.1688 12.6564 16.1085L15.4901 15.7824C15.7494 15.7525 15.9952 15.905 16.0836 16.1506L17.0103 18.7237C17.3698 19.7222 17.3111 20.8235 16.8475 21.7781L15.8238 23.8857Z" stroke="#837D72" stroke-width="0.25"/>
</g>
<path d="M17.0317 29.6814L18.8444 28.8362" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
<path d="M12.5 31.7944L14.3126 30.9492" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
<path d="M21.5635 27.5681L23.3761 26.7229" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
<path d="M18.2998 32.4001L20.1124 31.5549" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
<path d="M13.7681 34.5134L15.5807 33.6682" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
<path d="M22.8311 30.2871L24.6437 29.4419" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
<path d="M19.5674 35.1191L21.38 34.2739" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
<path d="M15.0361 37.2322L16.8487 36.3869" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
<path d="M24.0991 33.0061L25.9117 32.1609" stroke="#837D72" stroke-width="1.5" stroke-linecap="round"/>
<defs>
<clipPath id="clip0_8_3611">
<rect width="12" height="12" fill="white" transform="translate(30.8608 12.2219) rotate(25)"/>
</clipPath>
<clipPath id="clip1_8_3611">
<rect width="12" height="12" fill="white" transform="translate(5.73828 17.2935) rotate(-25)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 7.5 KiB

File diff suppressed because one or more lines are too long

View File

@ -488,7 +488,7 @@ extension ConversationViewController: CVComponentDelegate {
}
}
FingerprintViewController.present(from: self, address: address)
FingerprintViewController.present(for: address.aci, from: self)
}
public func didTapUnverifiedIdentityChange(_ address: SignalServiceAddress) {

View File

@ -405,8 +405,7 @@ class ConversationSettingsViewController: OWSTableViewController2, BadgeCollecti
return
}
let contactAddress = contactThread.contactAddress
assert(contactAddress.isValid)
FingerprintViewController.present(from: self, address: contactAddress)
FingerprintViewController.present(for: contactAddress.aci, from: self)
}
func showColorAndWallpaperSettingsView() {

View File

@ -241,7 +241,7 @@ class MemberActionSheet: OWSTableSheetViewController {
actionBlock: { [weak self] in
guard let self = self, let fromViewController = self.fromViewController else { return }
self.dismiss(animated: true) {
FingerprintViewController.present(from: fromViewController, address: self.address)
FingerprintViewController.present(for: self.address.aci, from: fromViewController)
}
}
))

View File

@ -850,12 +850,6 @@
/* VoiceOver description of current camera zoom level. */
"CAMERA_VO_ZOOM_LEVEL" = "%@times";
/* Message for alert explaining that a user cannot be verified. */
"CANT_VERIFY_IDENTITY_ALERT_MESSAGE" = "This user can't be verified until you've exchanged messages with them.";
/* Title for alert explaining that a user cannot be verified. */
"CANT_VERIFY_IDENTITY_ALERT_TITLE" = "Error";
/* Alert shown when the user needs to exchange messages to see the safety number. */
"CANT_VERIFY_IDENTITY_EXCHANGE_MESSAGES" = "A safety number will be created with this person after you exchange messages with them.";
@ -5572,21 +5566,6 @@
/* Snippet to share {{safety number}} with a friend. sent e.g. via SMS */
"SAFETY_NUMBER_SHARE_FORMAT" = "Our Signal Safety Number:\n%@";
/* Header informing the user about the transition from phone number to user identifier based. */
"SAFETY_NUMBER_TRANSITION_HEADER_ALERT" = "Safety numbers are being updated.";
/* Button text for a sheet informing the user about the transition from phone number to user identifier based. */
"SAFETY_NUMBER_TRANSITION_SHEET_HELP_TEXT" = "Need help?";
/* Informs the user about the transition from phone number to user identifier based. */
"SAFETY_NUMBER_TRANSITION_SHEET_PARAGRAPH_1" = "Safety numbers are being updated over a transition period to enable upcoming privacy features in Signal.";
/* Informs the user about the transition from phone number to user identifier based. */
"SAFETY_NUMBER_TRANSITION_SHEET_PARAGRAPH_2" = "To verify safety numbers, match the color card with your contacts device. If these dont match, swipe and try the other pair of safety numbers. Only one pair needs to match.";
/* Title for a sheet informing the user about the transition from phone number to user identifier based. */
"SAFETY_NUMBER_TRANSITION_SHEET_TITLE" = "Changes to Safety Numbers";
/* Action sheet heading */
"SAFETY_NUMBERS_ACTIONSHEET_TITLE" = "Your safety number with %@ has changed. You may wish to verify it.";
@ -7589,7 +7568,7 @@
"VERIFY_PRIVACY_MULTIPLE" = "Review Safety Numbers";
/* Instructions for verifying your safety number. Embeds {{contact's name}} */
"VERIFY_SAFETY_NUMBER_INSTRUCTIONS" = "To verify end-to-end encryption with %@, match the color card above with their device and compare the numbers. If these dont match, swipe and try the other pair of safety numbers. Only one pair needs to match.";
"VERIFY_SAFETY_NUMBER_INSTRUCTIONS" = "To verify end-to-end encryption with %1$@, compare the numbers above with their device. You can also scan the code on their device.";
/* Toast alert text shown when tapping on a video that cannot be played. */
"VIDEO_BROKEN" = "This video cant be played";

View File

@ -7,20 +7,17 @@ import CommonCrypto
import LibSignalClient
public class OWSFingerprint {
public let myAci: Aci
public let theirAci: Aci
public enum Source {
case aci(myAci: Aci, theirAci: Aci)
case e164(myE164: E164, theirE164: E164)
}
public let source: Source
public let myAciIdentityKey: Data
public let theirAciIdentityKey: Data
private let hashIterations: UInt32
private let myFingerprintData: Data
private let theirFingerprintData: Data
private let theirName: String
private let hashIterations: UInt32
public let theirName: String
/**
* Formats numeric fingerprint, 3 lines in groups of 5 digits.
@ -34,13 +31,15 @@ public class OWSFingerprint {
}
public init(
source: Source,
myAci: Aci,
theirAci: Aci,
myAciIdentityKey: Data,
theirAciIdentityKey: Data,
theirName: String,
hashIterations: UInt32 = Constants.defaultHashIterations
) {
self.source = source
self.myAci = myAci
self.theirAci = theirAci
let myAciIdentityKey = myAciIdentityKey.prependKeyType()
self.myAciIdentityKey = myAciIdentityKey
let theirAciIdentityKey = theirAciIdentityKey.prependKeyType()
@ -48,7 +47,7 @@ public class OWSFingerprint {
self.hashIterations = hashIterations
self.theirName = theirName
let (myStableSourceData, theirStableSourceData) = Self.stableData(for: source)
let (myStableSourceData, theirStableSourceData) = Self.stableData(myAci: myAci, theirAci: theirAci)
self.myFingerprintData = Self.dataForStableAddress(
myStableSourceData,
publicKey: myAciIdentityKey,
@ -208,19 +207,8 @@ public class OWSFingerprint {
// MARK: - Private helpers
private static func stableData(for source: Source) -> (my: Data, their: Data) {
switch source {
case let .aci(myAci, theirAci):
return (my: myAci.rawUUID.data, their: theirAci.rawUUID.data)
case let .e164(myE164, theirE164):
guard
let myData = myE164.stringValue.data(using: .utf8),
let theirData = theirE164.stringValue.data(using: .utf8)
else {
owsFail("Unable to serialize e164")
}
return (my: myData, their: theirData)
}
private static func stableData(myAci: Aci, theirAci: Aci) -> (my: Data, their: Data) {
return (my: myAci.rawUUID.data, their: theirAci.rawUUID.data)
}
/**
@ -296,17 +284,11 @@ public class OWSFingerprint {
}
private var scannableFingerprintVersion: UInt32 {
switch source {
case .e164:
return Constants.e164ScannableFormatVersion
case .aci:
return Constants.aciScannableFormatVersion
}
return Constants.aciScannableFormatVersion
}
public enum Constants {
static let hashingVersion: UInt32 = 0
static let e164ScannableFormatVersion: UInt32 = 1
static let aciScannableFormatVersion: UInt32 = 2
public static let defaultHashIterations: UInt32 = 5200
}

View File

@ -8,8 +8,8 @@ import LibSignalClient
public class OWSFingerprintBuilder {
public struct FingerprintResult {
public let theirAci: Aci
public let fingerprints: [OWSFingerprint]
public let initialDisplayIndex: Int
public let theirRecipientIdentity: OWSRecipientIdentity
public let fingerprint: OWSFingerprint
}
private let contactsManager: ContactsManagerProtocol
@ -30,13 +30,12 @@ public class OWSFingerprintBuilder {
/// identity key. You can use these to present a new identity key for
/// verification.
public func fingerprints(
theirAddress: SignalServiceAddress,
theirAci: Aci,
theirRecipientIdentity: OWSRecipientIdentity,
tx: SDSAnyReadTransaction
) -> FingerprintResult? {
guard
let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx.asV2Read),
let myE164 = E164(localIdentifiers.phoneNumber),
let myAciIdentityKey = identityManager.identityKeyPair(for: .aci, tx: tx.asV2Read)?.publicKey
else {
owsFailDebug("Missing local properties!")
@ -44,54 +43,21 @@ public class OWSFingerprintBuilder {
}
let myAci = localIdentifiers.aci
guard let theirAci = theirAddress.aci else {
Logger.warn("Missing their ACI!")
return nil
}
let theirAciIdentityKey = theirRecipientIdentity.identityKey
let theirE164 = theirAddress.e164
let theirName = contactsManager.displayName(
for: SignalServiceAddress(serviceId: theirAci, e164: theirE164),
transaction: tx
)
let theirName = contactsManager.displayName(for: SignalServiceAddress(theirAci), transaction: tx)
let aciFingerprint = OWSFingerprint(
source: .aci(myAci: myAci, theirAci: theirAci),
myAci: myAci,
theirAci: theirAci,
myAciIdentityKey: myAciIdentityKey,
theirAciIdentityKey: theirAciIdentityKey,
theirName: theirName
)
let e164Fingerprint: OWSFingerprint? = theirE164.map { theirE164 in
return OWSFingerprint(
source: .e164(myE164: myE164, theirE164: theirE164),
myAciIdentityKey: myAciIdentityKey,
theirAciIdentityKey: theirAciIdentityKey,
theirName: theirName
)
}
if FeatureFlags.onlyAciSafetyNumbers {
return FingerprintResult(
theirAci: theirAci,
fingerprints: [aciFingerprint],
initialDisplayIndex: 0
)
} else if let e164Fingerprint {
// We have both, but prefer the ACI.
return FingerprintResult(
theirAci: theirAci,
fingerprints: [aciFingerprint, e164Fingerprint],
initialDisplayIndex: 0
)
} else {
// If we default to ACI safety number and don't have the e164,
// that's fine. Just show the ACI one.
return FingerprintResult(
theirAci: theirAci,
fingerprints: [aciFingerprint],
initialDisplayIndex: 0
)
}
return FingerprintResult(
theirAci: theirAci,
theirRecipientIdentity: theirRecipientIdentity,
fingerprint: aciFingerprint
)
}
}

View File

@ -114,10 +114,6 @@ public class FeatureFlags: BaseFlags {
}
}
/// If true, _only_ aci safety numbers will be displayed, and e164 safety numbers will not
/// be displayed.
public static let onlyAciSafetyNumbers = false
public static let editMessageSend = true
public static let doNotSendGroupChangeMessagesOnProfileKeyRotation = false

View File

@ -221,10 +221,6 @@ public class RemoteConfig: BaseFlags {
return isEnabled(.enableAutoAPNSRotation, defaultValue: false)
}
public static var defaultToAciSafetyNumber: Bool {
return isEnabled(.safetyNumberAci)
}
/// The minimum length for a valid nickname, in Unicode codepoints.
public static var minNicknameLength: UInt32 {
getUInt32Value(forFlag: .minNicknameLength, defaultValue: 3)
@ -582,9 +578,16 @@ private struct Flags {
// Even if we don't fetch a fresh remote config, we may cross the time threshold
// while the app is in memory, updating the value from false to true.
// As such we also take fresh remote config values and swap them in at runtime.
#if false
// If there are time-gated flags, use this definition.
enum SupportedTimeGatedFlags: String, FlagType {
case safetyNumberAci
}
#else
// If there aren't any time-gated flags, use this definition.
enum SupportedTimeGatedFlags: FlagType {
var rawValue: String { fatalError() }
}
#endif
}
// MARK: -
@ -610,7 +613,6 @@ private extension FlagType {
case "maxGroupCallRingSize": return "global.calling.maxGroupCallRingSize"
case "minNicknameLength": return "global.nicknames.min"
case "maxNicknameLength": return "global.nicknames.max"
case "safetyNumberAci": return "global.safetyNumberAci"
case "cdsDisableCompatibilityMode": return "cds.disableCompatibilityMode"
case "maxAttachmentDownloadSizeBytes": return "global.attachments.maxBytes"
default: return Flags.prefix + rawValue

View File

@ -10,14 +10,15 @@ import XCTest
final class OWSFingerprintTest: XCTestCase {
func testDisplayableTextInsertsSpaces() {
let aliceE164 = E164("+19995550101")!
let bobE164 = E164("+18885550102")!
let aliceAci = Aci.constantForTesting("00000000-0000-4000-8000-0000000000a1")
let bobAci = Aci.constantForTesting("00000000-0000-4000-8000-0000000000b1")
let aliceIdentityKey = ECKeyPair.generateKeyPair().publicKey
let bobIdentityKey = ECKeyPair.generateKeyPair().publicKey
let aliceToBobFingerprint = OWSFingerprint(
source: .e164(myE164: aliceE164, theirE164: bobE164),
myAci: aliceAci,
theirAci: bobAci,
myAciIdentityKey: aliceIdentityKey,
theirAciIdentityKey: bobIdentityKey,
theirName: "Bob",
@ -41,70 +42,46 @@ final class OWSFingerprintTest: XCTestCase {
}
func testTextMatchesReciprocally() {
let sourceSets: [(
aliceToBob: OWSFingerprint.Source,
bobToAlice: OWSFingerprint.Source,
charlieToAlice: OWSFingerprint.Source
)] = [
{
let aliceE164 = E164("+19995550101")!
let bobE164 = E164("+18885550102")!
let charlieE164 = E164("+17775550103")!
return (
.e164(myE164: aliceE164, theirE164: bobE164),
.e164(myE164: bobE164, theirE164: aliceE164),
.e164(myE164: charlieE164, theirE164: aliceE164)
)
}(),
{
let aliceAci = Aci.randomForTesting()
let bobAci = Aci.randomForTesting()
let charlieAci = Aci.randomForTesting()
return (
.aci(myAci: aliceAci, theirAci: bobAci),
.aci(myAci: bobAci, theirAci: aliceAci),
.aci(myAci: charlieAci, theirAci: aliceAci)
)
}()
]
let aliceAci = Aci.randomForTesting()
let bobAci = Aci.randomForTesting()
let charlieAci = Aci.randomForTesting()
let aliceIdentityKey = ECKeyPair.generateKeyPair().publicKey
let bobIdentityKey = ECKeyPair.generateKeyPair().publicKey
let charlieIdentityKey = ECKeyPair.generateKeyPair().publicKey
for (aliceToBob, bobToAlice, charlieToAlice) in sourceSets {
let aliceToBobFingerprint = OWSFingerprint(
source: aliceToBob,
myAciIdentityKey: aliceIdentityKey,
theirAciIdentityKey: bobIdentityKey,
theirName: "Bob",
hashIterations: 2
)
let bobToAliceFingerprint = OWSFingerprint(
source: bobToAlice,
myAciIdentityKey: bobIdentityKey,
theirAciIdentityKey: aliceIdentityKey,
theirName: "Alice",
hashIterations: 2
)
XCTAssertEqual(
aliceToBobFingerprint.displayableText,
bobToAliceFingerprint.displayableText
)
let aliceToBobFingerprint = OWSFingerprint(
myAci: aliceAci,
theirAci: bobAci,
myAciIdentityKey: aliceIdentityKey,
theirAciIdentityKey: bobIdentityKey,
theirName: "Bob",
hashIterations: 2
)
let bobToAliceFingerprint = OWSFingerprint(
myAci: bobAci,
theirAci: aliceAci,
myAciIdentityKey: bobIdentityKey,
theirAciIdentityKey: aliceIdentityKey,
theirName: "Alice",
hashIterations: 2
)
XCTAssertEqual(
aliceToBobFingerprint.displayableText,
bobToAliceFingerprint.displayableText
)
let charlieToAliceFingerprint = OWSFingerprint(
source: charlieToAlice,
myAciIdentityKey: charlieIdentityKey,
theirAciIdentityKey: aliceIdentityKey,
theirName: "Alice",
hashIterations: 2
)
XCTAssertNotEqual(
aliceToBobFingerprint.displayableText,
charlieToAliceFingerprint.displayableText
)
}
let charlieToAliceFingerprint = OWSFingerprint(
myAci: charlieAci,
theirAci: aliceAci,
myAciIdentityKey: charlieIdentityKey,
theirAciIdentityKey: aliceIdentityKey,
theirName: "Alice",
hashIterations: 2
)
XCTAssertNotEqual(
aliceToBobFingerprint.displayableText,
charlieToAliceFingerprint.displayableText
)
}
}

View File

@ -14,20 +14,20 @@ class FingerprintScanViewController: OWSViewController, OWSNavigationChildContro
private let recipientIdentity: OWSRecipientIdentity
private let contactName: String
private let identityKey: Data
private let fingerprints: [OWSFingerprint]
private let fingerprint: OWSFingerprint
private lazy var qrCodeScanViewController = QRCodeScanViewController(appearance: .masked())
init(
recipientAci: Aci,
recipientIdentity: OWSRecipientIdentity,
fingerprints: [OWSFingerprint]
fingerprint: OWSFingerprint
) {
self.recipientAci = recipientAci
self.recipientIdentity = recipientIdentity
self.identityKey = recipientIdentity.identityKey
self.fingerprints = fingerprints
self.fingerprint = fingerprint
self.contactName = Self.contactsManager.displayName(for: SignalServiceAddress(recipientAci))
super.init()
@ -117,26 +117,12 @@ class FingerprintScanViewController: OWSViewController, OWSNavigationChildContro
)
}
// Check all of them, if any succeed its success.
var localizedErrorDescriptionToShow: String?
for (i, fingerprint) in fingerprints.enumerated() {
switch fingerprint.matchesLogicalFingerprintsData(combinedFingerprintData) {
case .match:
showSuccess()
return
case .noMatch(let localizedErrorDescription):
// Prefer no match errors to version erorrs, if we end
// up displaying an error.
localizedErrorDescriptionToShow = localizedErrorDescription
fallthrough
case
.weHaveOldVersion(let localizedErrorDescription),
.theyHaveOldVersion(let localizedErrorDescription):
if i == fingerprints.count - 1 {
// We reached the end, show the error for the last one.
showFailure(localizedErrorDescription: localizedErrorDescriptionToShow ?? localizedErrorDescription)
}
}
switch fingerprint.matchesLogicalFingerprintsData(combinedFingerprintData) {
case .match:
showSuccess()
case .noMatch(let localizedErrorDescription), .weHaveOldVersion(let localizedErrorDescription), .theyHaveOldVersion(let localizedErrorDescription):
// We reached the end, show the error for the last one.
showFailure(localizedErrorDescription: localizedErrorDescription)
}
}

View File

@ -15,39 +15,30 @@ import UIKit
public class FingerprintViewController: OWSViewController, OWSNavigationChildController {
public class func present(
from viewController: UIViewController,
address theirAddress: SignalServiceAddress
for theirAci: Aci?,
from viewController: UIViewController
) {
owsAssertBeta(theirAddress.isValid)
let identityManager = DependenciesBridge.shared.identityManager
guard let theirRecipientIdentity = databaseStorage.read(block: { tx in
identityManager.recipientIdentity(for: theirAddress, tx: tx.asV2Read)
}) else {
OWSActionSheets.showActionSheet(
title: OWSLocalizedString(
"CANT_VERIFY_IDENTITY_ALERT_TITLE",
comment: "Title for alert explaining that a user cannot be verified."
),
message: OWSLocalizedString(
"CANT_VERIFY_IDENTITY_ALERT_MESSAGE",
comment: "Message for alert explaining that a user cannot be verified."
)
)
return
}
guard let fingerprintResult = databaseStorage.read(block: { tx in
let fingerprintResult = databaseStorage.read { (tx) -> OWSFingerprintBuilder.FingerprintResult? in
guard let theirAci else {
return nil
}
let identityManager = DependenciesBridge.shared.identityManager
let theirAddress = SignalServiceAddress(theirAci)
guard let theirRecipientIdentity = identityManager.recipientIdentity(for: theirAddress, tx: tx.asV2Read) else {
return nil
}
return OWSFingerprintBuilder(
contactsManager: contactsManager,
identityManager: identityManager,
tsAccountManager: DependenciesBridge.shared.tsAccountManager
).fingerprints(
theirAddress: theirAddress,
theirAci: theirAci,
theirRecipientIdentity: theirRecipientIdentity,
tx: tx
)
}) else {
}
guard let fingerprintResult else {
let actionSheet = ActionSheetController(message: OWSLocalizedString(
"CANT_VERIFY_IDENTITY_EXCHANGE_MESSAGES",
comment: "Alert shown when the user needs to exchange messages to see the safety number."
@ -66,10 +57,9 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
}
let fingerprintViewController = FingerprintViewController(
fingerprints: fingerprintResult.fingerprints,
initialDisplayIndex: fingerprintResult.initialDisplayIndex,
fingerprint: fingerprintResult.fingerprint,
recipientAci: fingerprintResult.theirAci,
recipientIdentity: theirRecipientIdentity
recipientIdentity: fingerprintResult.theirRecipientIdentity
)
let navigationController = OWSNavigationController(rootViewController: fingerprintViewController)
viewController.present(navigationController, animated: true)
@ -89,26 +79,21 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
private let recipientAci: Aci
private let recipientIdentity: OWSRecipientIdentity
private let contactName: String
private let identityKey: Data
private let fingerprints: [OWSFingerprint]
private let fingerprint: OWSFingerprint
private var isVerified = false
private var selectedIndex: Int
public init(
fingerprints: [OWSFingerprint],
initialDisplayIndex: Int,
fingerprint: OWSFingerprint,
recipientAci: Aci,
recipientIdentity: OWSRecipientIdentity
) {
self.recipientAci = recipientAci
self.contactName = Self.contactsManager.displayName(for: SignalServiceAddress(recipientAci))
// By capturing the identity key when we enter these views, we prevent the edge case
// where the user verifies a key that we learned about while this view was open.
self.recipientIdentity = recipientIdentity
self.identityKey = recipientIdentity.identityKey
self.fingerprints = fingerprints
self.selectedIndex = initialDisplayIndex
self.fingerprint = fingerprint
super.init()
@ -145,125 +130,18 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
configureUI()
}
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// TODO: This doesn't seem to work, maybe because the views haven't been sized yet. When we build with Xcode 15, we can use `viewIsAppearing()`.
if #available(iOS 17, *) { owsFailDebug("Canary to fix this when we're building with Xcode 15!") }
fingerprintCarouselPageControl.currentPage = selectedIndex
scrollToSelectedIndex(animated: false)
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !DependenciesBridge.shared.db.read(block: self.hasShownTransitionSheet) {
// Its fine to not re-read the value in the write tx; stakes are low.
DependenciesBridge.shared.db.write(block: self.showTransitionSheet)
}
}
public override func themeDidChange() {
super.themeDidChange()
view.backgroundColor = Self.backgroundColor
updateVerificationStateLabel()
setSafetyNumbersUpdateTextViewText()
setCarouselPageControlColors()
setInstructionsText()
setVerifyUnverifyButtonColors()
}
// MARK: UI
private lazy var safetyNumbersUpdateTextView: LinkingTextView = {
let textView = LinkingTextView()
textView.delegate = self
return textView
}()
private func setSafetyNumbersUpdateTextViewText() {
// Link doesn't matter, we will override tap behavior.
let learnMoreString = CommonStrings.learnMore.styled(with: .link(URL(string: Constants.transitionLearnMoreUrl)!))
safetyNumbersUpdateTextView.attributedText = NSAttributedString.composed(of: [
OWSLocalizedString(
"SAFETY_NUMBER_TRANSITION_HEADER_ALERT",
comment: "Header informing the user about the transition from phone number to user identifier based."
),
"\n",
learnMoreString
]).styled(
with: .font(.dynamicTypeFootnote),
.color(Theme.secondaryTextAndIconColor)
)
safetyNumbersUpdateTextView.linkTextAttributes = [
.foregroundColor: Theme.primaryTextColor,
.underlineColor: UIColor.clear,
.underlineStyle: NSUnderlineStyle.single.rawValue
]
}
private lazy var safetyNumbersUpdateView: UIView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .fill
stackView.alignment = .center
stackView.spacing = 16
let imageView = UIImageView(image: UIImage(named: "safety_number_transition"))
imageView.autoSetDimensions(to: .square(48))
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(safetyNumbersUpdateTextView)
return stackView
}()
private lazy var fingerprintCards: [FingerprintCard] = {
return fingerprints.map { fingerprint in
return FingerprintCard(fingerprint: fingerprint, controller: self)
}
}()
private lazy var fingerprintCarousel: UIScrollView = {
let scrollView = UIScrollView()
scrollView.isPagingEnabled = true
scrollView.isDirectionalLockEnabled = true
scrollView.alwaysBounceVertical = false
scrollView.alwaysBounceHorizontal = true
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
var xOffset: CGFloat = Constants.cardHInset
var previousView: UIView = scrollView
var nextEdge: ALEdge = .leading
for fingerprintCard in fingerprintCards {
scrollView.addSubview(fingerprintCard)
fingerprintCard.autoPinVerticalEdges(toEdgesOf: scrollView)
scrollView.autoPinHeight(toHeightOf: fingerprintCard, relation: .greaterThanOrEqual)
fingerprintCard.autoPinEdge(.leading, to: nextEdge, of: previousView, withOffset: xOffset)
previousView = fingerprintCard
xOffset = Constants.interCardSpacing
nextEdge = .trailing
}
previousView.autoPinEdge(.trailing, to: .trailing, of: scrollView, withOffset: -Constants.cardHInset)
scrollView.delegate = self
return scrollView
}()
private lazy var fingerprintCarouselPageControl: UIPageControl = {
let control = UIPageControl()
control.numberOfPages = fingerprints.count
control.addTarget(self, action: #selector(didUpdatePageControl), for: .valueChanged)
return control
}()
private func setCarouselPageControlColors() {
fingerprintCarouselPageControl.pageIndicatorTintColor = Theme.isDarkThemeEnabled ? .ows_gray65 : .ows_gray25
fingerprintCarouselPageControl.currentPageIndicatorTintColor = Theme.primaryTextColor
}
private lazy var fingerprintCard = FingerprintCard(fingerprint: fingerprint, controller: self)
private lazy var instructionsTextView: UITextView = {
let textView = LinkingTextView()
@ -279,7 +157,7 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
// Link doesn't matter, we will override tap behavior.
let learnMoreString = CommonStrings.learnMore.styled(with: .link(URL(string: Constants.learnMoreUrl)!))
instructionsTextView.attributedText = NSAttributedString.composed(of: [
String(format: instructionsFormat, contactName),
String(format: instructionsFormat, fingerprint.theirName),
" ",
learnMoreString
]).styled(
@ -318,60 +196,34 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
}
private func configureUI() {
let scrollView = UIScrollView()
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
let containerView = UIView()
view.addSubview(scrollView)
let containerView = UIView()
scrollView.addSubview(containerView)
scrollView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom)
containerView.autoPinEdges(toEdgesOf: scrollView)
containerView.autoPinWidth(toWidthOf: view)
containerView.addSubview(safetyNumbersUpdateView)
containerView.addSubview(fingerprintCarousel)
containerView.addSubview(fingerprintCarouselPageControl)
containerView.addSubview(fingerprintCard)
containerView.addSubview(instructionsTextView)
view.addSubview(verifyUnverifyButton)
safetyNumbersUpdateView.autoPinEdge(.leading, to: .leading, of: containerView, withOffset: .scaleFromIPhone5To7Plus(18, 24))
safetyNumbersUpdateView.autoPinEdge(.trailing, to: .trailing, of: containerView, withOffset: -.scaleFromIPhone5To7Plus(18, 24))
safetyNumbersUpdateView.autoPinEdge(toSuperviewSafeArea: .top, withInset: 12)
fingerprintCarousel.autoPinHorizontalEdges(toEdgesOf: containerView)
fingerprintCards.forEach {
$0.autoPinWidth(toWidthOf: containerView, offset: -.scaleFromIPhone5To7Plus(60, 105))
}
fingerprintCarouselPageControl.autoHCenterInSuperview()
fingerprintCarouselPageControl.autoPinEdge(.top, to: .bottom, of: fingerprintCarousel, withOffset: 8)
fingerprintCard.autoPinEdge(toSuperviewSafeArea: .top, withInset: 56)
fingerprintCard.autoPinWidth(toWidthOf: containerView, offset: -.scaleFromIPhone5To7Plus(60, 105))
fingerprintCard.autoHCenterInSuperview()
instructionsTextView.autoPinEdge(.leading, to: .leading, of: containerView, withOffset: .scaleFromIPhone5To7Plus(18, 28))
instructionsTextView.autoPinEdge(.trailing, to: .trailing, of: containerView, withOffset: -.scaleFromIPhone5To7Plus(18, 28))
instructionsTextView.autoPinEdge(.bottom, to: .bottom, of: scrollView)
instructionsTextView.autoPinEdge(.bottom, to: .bottom, of: scrollView, withOffset: -8)
verifyUnverifyButton.autoHCenterInSuperview()
verifyUnverifyButton.autoPinEdge(.top, to: .bottom, of: scrollView, withOffset: .scaleFromIPhone5To7Plus(12, 24))
verifyUnverifyButton.autoPinEdge(toSuperviewSafeArea: .bottom, withInset: .scaleFromIPhone5To7Plus(16, 40))
if fingerprints.count <= 1 {
safetyNumbersUpdateView.isHidden = true
fingerprintCarouselPageControl.isHidden = true
scrollView.isScrollEnabled = false
fingerprintCarousel.autoPinEdge(toSuperviewSafeArea: .top, withInset: 56)
instructionsTextView.autoPinEdge(.top, to: .bottom, of: fingerprintCarousel, withOffset: 24)
} else {
fingerprintCarousel.autoPinEdge(.top, to: .bottom, of: safetyNumbersUpdateView, withOffset: 24)
instructionsTextView.autoPinEdge(.top, to: .bottom, of: fingerprintCarouselPageControl, withOffset: 16)
}
instructionsTextView.autoPinEdge(.top, to: .bottom, of: fingerprintCard, withOffset: 24)
updateVerificationStateLabel()
setSafetyNumbersUpdateTextViewText()
setCarouselPageControlColors()
setInstructionsText()
setVerifyUnverifyButtonColors()
}
@ -410,12 +262,7 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
layer.cornerRadius = Constants.cornerRadius
self.backgroundColor = {
switch fingerprint.source {
case .aci: return UIColor(rgbHex: 0x506ecd)
case .e164: return UIColor(rgbHex: 0xdeddda)
}
}()
self.backgroundColor = UIColor(rgbHex: 0x506ecd)
addSubview(shareButton)
addSubview(qrCodeView)
@ -450,16 +297,9 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
private lazy var shareButton: UIButton = {
let button = UIButton()
let tintColor: UIColor
switch fingerprint.source {
case .aci:
tintColor = .white
case .e164:
tintColor = .black
}
button.setTemplateImage(
Theme.iconImage(.buttonShare).withRenderingMode(.alwaysTemplate),
tintColor: tintColor
tintColor: .white
)
button.addTarget(self, action: #selector(didTapShare), for: .touchUpInside)
return button
@ -500,12 +340,7 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
label.text = fingerprint.displayableText
label.font = UIFont(name: "Menlo-Regular", size: 23)
label.textAlignment = .center
switch fingerprint.source {
case .aci:
label.textColor = .white
case .e164:
label.textColor = Theme.lightThemeSecondaryTextAndIconColor
}
label.textColor = .white
label.numberOfLines = 3
label.lineBreakMode = .byTruncatingTail
label.adjustsFontSizeToFitWidth = true
@ -538,7 +373,6 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
// MARK: PillBoxView
class PillBoxView: UIView {
override var bounds: CGRect {
didSet {
self.layer.cornerRadius = bounds.height / 2
@ -546,180 +380,6 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
}
}
// MARK: - Transition Sheet
private lazy var kvStore: KeyValueStore = {
return DependenciesBridge.shared.keyValueStoreFactory.keyValueStore(collection: "MultiFingerprintVC")
}()
private static let hasShownTransitionSheetKey = "hasShownTransitionSheetKey"
private func hasShownTransitionSheet(_ tx: DBReadTransaction) -> Bool {
return self.kvStore.getBool(Self.hasShownTransitionSheetKey, defaultValue: false, transaction: tx)
}
private func setHasShownTransitionSheet(_ tx: DBWriteTransaction) {
self.kvStore.setBool(true, key: Self.hasShownTransitionSheetKey, transaction: tx)
}
private func showTransitionSheet(_ tx: DBWriteTransaction) {
self.setHasShownTransitionSheet(tx)
tx.addAsyncCompletion(on: DispatchQueue.main) {
let sheet = TransitionSheetViewController(parent: self)
self.present(sheet, animated: true)
}
}
class TransitionSheetViewController: InteractiveSheetViewController {
let contentScrollView = UIScrollView()
let stackView = UIStackView()
public override var interactiveScrollViews: [UIScrollView] { [contentScrollView] }
public override var sheetBackgroundColor: UIColor { Theme.tableView2PresentedBackgroundColor }
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
private weak var parentVc: FingerprintViewController?
init(parent: FingerprintViewController) {
self.parentVc = parent
super.init()
}
override public func viewDidLoad() {
super.viewDidLoad()
minimizedHeight = 600
super.allowsExpansion = true
contentView.addSubview(contentScrollView)
stackView.axis = .vertical
stackView.layoutMargins = UIEdgeInsets(hMargin: 24, vMargin: 24)
stackView.spacing = 16
stackView.isLayoutMarginsRelativeArrangement = true
contentScrollView.addSubview(stackView)
stackView.autoPinHeightToSuperview()
// Pin to the scroll view's viewport, not to its scrollable area
stackView.autoPinWidth(toWidthOf: contentScrollView)
contentScrollView.autoPinEdgesToSuperviewEdges()
contentScrollView.alwaysBounceVertical = true
buildContents()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !animationView.isAnimationQueued && !animationView.isAnimationPlaying {
animationView.play { [weak self] success in
guard success else { return }
self?.loopAnimation()
}
}
}
override public func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if animationView.isAnimationQueued || animationView.isAnimationPlaying {
animationView.stop()
}
}
private func loopAnimation() {
animationView.play(fromFrame: 60, toFrame: 360, completion: { [weak self] success in
guard success else { return }
self?.loopAnimation()
})
}
private lazy var animationView: AnimationView = {
let animationView = AnimationView(name: "safety-numbers")
animationView.contentMode = .scaleAspectFit
animationView.isUserInteractionEnabled = false
animationView.backgroundColor = .white
animationView.layer.cornerRadius = 12
animationView.layer.masksToBounds = true
return animationView
}()
private func buildContents() {
let titleLabel = UILabel()
titleLabel.textAlignment = .center
titleLabel.font = UIFont.dynamicTypeTitle2.semibold()
titleLabel.text = OWSLocalizedString(
"SAFETY_NUMBER_TRANSITION_SHEET_TITLE",
comment: "Title for a sheet informing the user about the transition from phone number to user identifier based."
)
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
stackView.addArrangedSubview(titleLabel)
let paragraphs: [String] = [
OWSLocalizedString(
"SAFETY_NUMBER_TRANSITION_SHEET_PARAGRAPH_1",
comment: "Informs the user about the transition from phone number to user identifier based."
),
OWSLocalizedString(
"SAFETY_NUMBER_TRANSITION_SHEET_PARAGRAPH_2",
comment: "Informs the user about the transition from phone number to user identifier based."
)
]
var lastParagraphLabel: UILabel!
for paragraph in paragraphs {
let paragraphLabel = UILabel()
paragraphLabel.text = paragraph
paragraphLabel.textAlignment = .natural
paragraphLabel.font = .dynamicTypeSubheadlineClamped
paragraphLabel.numberOfLines = 0
paragraphLabel.lineBreakMode = .byWordWrapping
paragraphLabel.textColor = Theme.secondaryTextAndIconColor
stackView.addArrangedSubview(paragraphLabel)
lastParagraphLabel = paragraphLabel
}
stackView.setCustomSpacing(20, after: lastParagraphLabel)
stackView.addArrangedSubview(animationView)
stackView.setCustomSpacing(18, after: animationView)
animationView.autoMatch(.height, to: .width, of: animationView, withMultiplier: 172/346)
let learnMoreTitle = OWSLocalizedString(
"SAFETY_NUMBER_TRANSITION_SHEET_HELP_TEXT",
comment: "Button text for a sheet informing the user about the transition from phone number to user identifier based."
)
let learnMoreButton = UIButton(type: .system)
learnMoreButton.setTitle(learnMoreTitle, for: .normal)
learnMoreButton.titleLabel?.font = .dynamicTypeBody
learnMoreButton.setTitleColor(Theme.isDarkThemeEnabled ? .ows_accentBlueDark : .link, for: .normal)
learnMoreButton.addTarget(self, action: #selector(didTapLearnMore), for: .touchUpInside)
stackView.addArrangedSubview(learnMoreButton)
stackView.setCustomSpacing(24, after: learnMoreButton)
let continueButton = OWSButton(
title: OWSLocalizedString(
"ALERT_ACTION_ACKNOWLEDGE",
comment: "generic button text to acknowledge that the corresponding text was read."
)
) { [weak self] in
self?.dismiss(animated: true)
}
continueButton.layer.cornerRadius = 16
continueButton.backgroundColor = .ows_accentBlue
continueButton.dimsWhenHighlighted = true
continueButton.titleLabel?.font = UIFont.dynamicTypeBody.semibold()
continueButton.autoSetDimension(.height, toSize: 50, relation: .greaterThanOrEqual)
stackView.addArrangedSubview(continueButton)
}
@objc
func didTapLearnMore() {
FingerprintViewController.showLearnMoreUrl(from: self)
}
}
// MARK: Actions
@objc
@ -737,12 +397,6 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
viewController.present(safariVC, animated: true)
}
@objc
private func didUpdatePageControl() {
self.selectedIndex = fingerprintCarouselPageControl.currentPage
scrollToSelectedIndex()
}
@objc
private func didTapVerifyUnverify(_ gestureRecognizer: UITapGestureRecognizer) {
guard gestureRecognizer.state == .recognized else { return }
@ -764,10 +418,6 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
}
private func shareFingerprint(from fromView: UIView) {
let fingerprint = fingerprints[selectedIndex]
Logger.debug("Sharing safety numbers")
let compareActivity = CompareSafetyNumbersActivity(delegate: self)
let shareFormat = NSLocalizedString(
@ -802,21 +452,11 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
let viewController = FingerprintScanViewController(
recipientAci: recipientAci,
recipientIdentity: recipientIdentity,
fingerprints: self.fingerprints
fingerprint: self.fingerprint
)
navigationController?.pushViewController(viewController, animated: true)
}
private func scrollToSelectedIndex(animated: Bool = true) {
let xOffset: CGFloat
if selectedIndex == 0 {
xOffset = 0
} else {
xOffset = (CGFloat(selectedIndex) * UIScreen.main.bounds.width) - (Constants.interCardSpacing + Constants.cardHInset)
}
fingerprintCarousel.setContentOffset(.init(x: xOffset, y: 0), animated: animated)
}
// MARK: Notifications
private var identityStateChangeObserver: Any?
@ -830,10 +470,6 @@ public class FingerprintViewController: OWSViewController, OWSNavigationChildCon
enum Constants {
static let cardHInset: CGFloat = .scaleFromIPhone5To7Plus(30, 53)
static var interCardSpacing: CGFloat = cardHInset / 2
// Link doesn't matter, we will override tap behavior.
static let transitionLearnMoreUrl = "https://support.signal.org/"
static let learnMoreUrl = "https://support.signal.org/learnMore"
}
}
@ -845,7 +481,7 @@ extension FingerprintViewController: CompareSafetyNumbersActivityDelegate {
from: self,
identityKey: identityKey,
recipientAci: recipientAci,
contactName: contactName,
contactName: fingerprint.theirName,
tag: logTag
)
}
@ -864,24 +500,10 @@ extension FingerprintViewController: CompareSafetyNumbersActivityDelegate {
}
extension FingerprintViewController: UITextViewDelegate {
public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
if URL.absoluteString == Constants.transitionLearnMoreUrl {
DependenciesBridge.shared.db.write {
self.showTransitionSheet($0)
}
} else if URL.absoluteString == Constants.learnMoreUrl {
if URL.absoluteString == Constants.learnMoreUrl {
self.didTapLearnMore()
}
return false
}
}
extension FingerprintViewController: UIScrollViewDelegate {
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let selectedIndex = Int(scrollView.contentOffset.x / (scrollView.frame.width - (Constants.cardHInset * 2)))
self.selectedIndex = selectedIndex
self.fingerprintCarouselPageControl.currentPage = selectedIndex
}
}

View File

@ -1,799 +0,0 @@
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import Lottie
import PureLayout
import SafariServices
import SignalMessaging
import SignalServiceKit
import UIKit
public class MultiFingerprintViewController: OWSViewController, OWSNavigationChildController {
public var preferredNavigationBarStyle: OWSNavigationBarStyle {
return .solid
}
public var navbarBackgroundColorOverride: UIColor? {
return Self.backgroundColor
}
public override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
private let recipientAddress: SignalServiceAddress
private let recipientIdentity: OWSRecipientIdentity
private let contactName: String
private let identityKey: IdentityKey
private let fingerprints: [OWSFingerprint]
private var selectedIndex: Int
public init(
fingerprints: [OWSFingerprint],
defaultIndex: Int,
recipientAddress: SignalServiceAddress,
recipientIdentity: OWSRecipientIdentity
) {
self.recipientAddress = recipientAddress
self.contactName = SSKEnvironment.shared.contactsManagerRef.displayName(for: recipientAddress)
// By capturing the identity key when we enter these views, we prevent the edge case
// where the user verifies a key that we learned about while this view was open.
self.recipientIdentity = recipientIdentity
self.identityKey = recipientIdentity.identityKey
self.fingerprints = fingerprints
self.selectedIndex = defaultIndex
super.init()
title = NSLocalizedString("PRIVACY_VERIFICATION_TITLE", comment: "Navbar title")
navigationItem.leftBarButtonItem = .init(
barButtonSystemItem: .done,
target: self, action: #selector(didTapDone),
accessibilityIdentifier: "FingerprintViewController.done"
)
identityStateChangeObserver = NotificationCenter.default.addObserver(
forName: .identityStateDidChange,
object: nil,
queue: .main) { [weak self] _ in
self?.identityStateDidChange()
}
}
deinit {
if let identityStateChangeObserver {
NotificationCenter.default.removeObserver(identityStateChangeObserver)
}
}
private static var backgroundColor: UIColor {
return Theme.isDarkThemeEnabled ? .ows_gray90 : .ows_gray02
}
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = Self.backgroundColor
configureUI()
}
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
scrollToSelectedIndex(animated: false)
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !DependenciesBridge.shared.db.read(block: self.hasShownTransitionSheet) {
// Its fine to not re-read the value in the write tx; stakes are low.
DependenciesBridge.shared.db.write(block: self.showTransitionSheet)
}
}
public override func themeDidChange() {
super.themeDidChange()
view.backgroundColor = Self.backgroundColor
updateVerificationStateLabel()
setSafetyNumbersUpdateTextViewText()
setCarouselPageControlColors()
setInstructionsText()
setVerifyUnverifyButtonColors()
}
// MARK: UI
private lazy var safetyNumbersUpdateTextView: LinkingTextView = {
let textView = LinkingTextView()
textView.delegate = self
return textView
}()
private func setSafetyNumbersUpdateTextViewText() {
// Link doesn't matter, we will override tap behavior.
let learnMoreString = CommonStrings.learnMore.styled(with: .link(URL(string: Constants.transitionLearnMoreUrl)!))
safetyNumbersUpdateTextView.attributedText = NSAttributedString.composed(of: [
OWSLocalizedString(
"SAFETY_NUMBER_TRANSITION_HEADER_ALERT",
comment: "Header informing the user about the transition from phone number to user identifier based."
),
"\n",
learnMoreString
]).styled(
with: .font(.dynamicTypeFootnote),
.color(Theme.secondaryTextAndIconColor)
)
safetyNumbersUpdateTextView.linkTextAttributes = [
.foregroundColor: Theme.primaryTextColor,
.underlineColor: UIColor.clear,
.underlineStyle: NSUnderlineStyle.single.rawValue
]
}
private lazy var safetyNumbersUpdateView: UIView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .fill
stackView.alignment = .center
stackView.spacing = 16
let imageView = UIImageView(image: UIImage(named: "safety_number_transition"))
imageView.autoSetDimensions(to: .square(48))
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(safetyNumbersUpdateTextView)
return stackView
}()
private lazy var fingerprintCards: [FingerprintCard] = {
return fingerprints.map { fingerprint in
return FingerprintCard(fingerprint: fingerprint, controller: self)
}
}()
private lazy var fingerprintCarousel: UIScrollView = {
let scrollView = UIScrollView()
scrollView.isPagingEnabled = true
scrollView.isDirectionalLockEnabled = true
scrollView.alwaysBounceVertical = false
scrollView.alwaysBounceHorizontal = true
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
var xOffset: CGFloat = Constants.cardHInset
var previousView: UIView = scrollView
var nextEdge: ALEdge = .leading
for fingerprintCard in fingerprintCards {
scrollView.addSubview(fingerprintCard)
fingerprintCard.autoPinVerticalEdges(toEdgesOf: scrollView)
scrollView.autoPinHeight(toHeightOf: fingerprintCard, relation: .greaterThanOrEqual)
fingerprintCard.autoPinEdge(.leading, to: nextEdge, of: previousView, withOffset: xOffset)
previousView = fingerprintCard
xOffset = Constants.interCardSpacing
nextEdge = .trailing
}
previousView.autoPinEdge(.trailing, to: .trailing, of: scrollView, withOffset: -Constants.cardHInset)
scrollView.delegate = self
return scrollView
}()
private lazy var fingerprintCarouselPageControl: UIPageControl = {
let control = UIPageControl()
control.numberOfPages = fingerprints.count
control.addTarget(self, action: #selector(didUpdatePageControl), for: .valueChanged)
return control
}()
private func setCarouselPageControlColors() {
fingerprintCarouselPageControl.pageIndicatorTintColor = Theme.isDarkThemeEnabled ? .ows_gray65 : .ows_gray25
fingerprintCarouselPageControl.currentPageIndicatorTintColor = Theme.primaryTextColor
}
private lazy var instructionsTextView: UITextView = {
let textView = LinkingTextView()
textView.delegate = self
return textView
}()
private func setInstructionsText() {
let instructionsFormat = OWSLocalizedString(
"VERIFY_SAFETY_NUMBER_INSTRUCTIONS",
comment: "Instructions for verifying your safety number. Embeds {{contact's name}}"
)
// Link doesn't matter, we will override tap behavior.
let learnMoreString = CommonStrings.learnMore.styled(with: .link(URL(string: Constants.learnMoreUrl)!))
instructionsTextView.attributedText = NSAttributedString.composed(of: [
String(format: instructionsFormat, contactName),
" ",
learnMoreString
]).styled(
with: .font(.dynamicTypeFootnote),
.color(Theme.secondaryTextAndIconColor),
.alignment(.center)
)
instructionsTextView.linkTextAttributes = [
.foregroundColor: Theme.primaryTextColor,
.underlineColor: UIColor.clear,
.underlineStyle: NSUnderlineStyle.single.rawValue
]
}
private lazy var verifyUnverifyButtonLabel = UILabel()
private lazy var verifyUnverifyPillbox = PillBoxView()
private lazy var verifyUnverifyButton: UIView = {
verifyUnverifyPillbox.layer.masksToBounds = true
verifyUnverifyPillbox.accessibilityIdentifier = "FingerprintViewController.verifyUnverifyButton"
verifyUnverifyPillbox.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapVerifyUnverify)))
verifyUnverifyButtonLabel.font = .systemFont(ofSize: 13, weight: .bold)
verifyUnverifyButtonLabel.textAlignment = .center
verifyUnverifyButtonLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
verifyUnverifyPillbox.addSubview(verifyUnverifyButtonLabel)
verifyUnverifyButtonLabel.autoPinWidthToSuperview(withMargin: 24)
verifyUnverifyButtonLabel.autoPinHeightToSuperview(withMargin: 12)
return verifyUnverifyPillbox
}()
private func setVerifyUnverifyButtonColors() {
verifyUnverifyButtonLabel.textColor = Theme.primaryTextColor
verifyUnverifyPillbox.backgroundColor = Theme.isDarkThemeEnabled ? .ows_gray80 : .white
}
private func configureUI() {
let scrollView = UIScrollView()
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
let containerView = UIView()
view.addSubview(scrollView)
scrollView.addSubview(containerView)
scrollView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom)
containerView.autoPinEdges(toEdgesOf: scrollView)
containerView.autoPinWidth(toWidthOf: view)
containerView.addSubview(safetyNumbersUpdateView)
containerView.addSubview(fingerprintCarousel)
containerView.addSubview(fingerprintCarouselPageControl)
containerView.addSubview(instructionsTextView)
view.addSubview(verifyUnverifyButton)
safetyNumbersUpdateView.autoPinEdge(.leading, to: .leading, of: containerView, withOffset: .scaleFromIPhone5To7Plus(18, 24))
safetyNumbersUpdateView.autoPinEdge(.trailing, to: .trailing, of: containerView, withOffset: -.scaleFromIPhone5To7Plus(18, 24))
safetyNumbersUpdateView.autoPinEdge(toSuperviewSafeArea: .top, withInset: 12)
fingerprintCarousel.autoPinHorizontalEdges(toEdgesOf: containerView)
fingerprintCards.forEach {
$0.autoPinWidth(toWidthOf: containerView, offset: -.scaleFromIPhone5To7Plus(60, 105))
}
fingerprintCarouselPageControl.autoHCenterInSuperview()
fingerprintCarouselPageControl.autoPinEdge(.top, to: .bottom, of: fingerprintCarousel, withOffset: 8)
instructionsTextView.autoPinEdge(.leading, to: .leading, of: containerView, withOffset: .scaleFromIPhone5To7Plus(18, 28))
instructionsTextView.autoPinEdge(.trailing, to: .trailing, of: containerView, withOffset: -.scaleFromIPhone5To7Plus(18, 28))
instructionsTextView.autoPinEdge(.bottom, to: .bottom, of: scrollView)
verifyUnverifyButton.autoHCenterInSuperview()
verifyUnverifyButton.autoPinEdge(.top, to: .bottom, of: scrollView, withOffset: .scaleFromIPhone5To7Plus(12, 24))
verifyUnverifyButton.autoPinEdge(toSuperviewSafeArea: .bottom, withInset: .scaleFromIPhone5To7Plus(16, 40))
if fingerprints.count <= 1 {
safetyNumbersUpdateView.isHidden = true
fingerprintCarouselPageControl.isHidden = true
scrollView.isScrollEnabled = false
fingerprintCarousel.autoPinEdge(toSuperviewSafeArea: .top, withInset: 56)
instructionsTextView.autoPinEdge(.top, to: .bottom, of: fingerprintCarousel, withOffset: 24)
} else {
fingerprintCarousel.autoPinEdge(.top, to: .bottom, of: safetyNumbersUpdateView, withOffset: 24)
instructionsTextView.autoPinEdge(.top, to: .bottom, of: fingerprintCarouselPageControl, withOffset: 16)
}
updateVerificationStateLabel()
setSafetyNumbersUpdateTextViewText()
setCarouselPageControlColors()
setInstructionsText()
setVerifyUnverifyButtonColors()
}
private func updateVerificationStateLabel() {
owsAssertBeta(recipientAddress.isValid)
let isVerified = OWSIdentityManager.shared.verificationState(for: recipientAddress) == .verified
if isVerified {
verifyUnverifyButtonLabel.text = NSLocalizedString(
"PRIVACY_UNVERIFY_BUTTON",
comment: "Button that lets user mark another user's identity as unverified."
)
} else {
verifyUnverifyButtonLabel.text = OWSLocalizedString(
"PRIVACY_VERIFY_BUTTON",
comment: "Button that lets user mark another user's identity as verified."
)
}
view.setNeedsLayout()
}
// MARK: - Fingerprint Card
class FingerprintCard: UIView {
private let fingerprint: OWSFingerprint
private weak var controller: MultiFingerprintViewController?
init(fingerprint: OWSFingerprint, controller: MultiFingerprintViewController) {
self.fingerprint = fingerprint
self.controller = controller
super.init(frame: .zero)
layer.cornerRadius = Constants.cornerRadius
self.backgroundColor = {
switch fingerprint.source {
case .aci: return UIColor(rgbHex: 0x506ecd)
case .e164: return UIColor(rgbHex: 0xdeddda)
}
}()
addSubview(shareButton)
addSubview(qrCodeView)
addSubview(safetyNumberLabel)
shareButton.autoPinEdge(.top, to: .top, of: self, withOffset: 16)
shareButton.autoPinEdge(.trailing, to: .trailing, of: self, withOffset: -16)
qrCodeView.autoPinEdge(.top, to: .bottom, of: shareButton, withOffset: 8)
qrCodeView.autoPinEdge(.leading, to: .leading, of: self, withOffset: .scaleFromIPhone5To7Plus(44, 64))
qrCodeView.autoPinEdge(.trailing, to: .trailing, of: self, withOffset: -.scaleFromIPhone5To7Plus(44, 64))
safetyNumberLabel.autoPinEdge(.top, to: .bottom, of: qrCodeView, withOffset: 30)
safetyNumberLabel.autoPinEdge(.leading, to: .leading, of: self, withOffset: .scaleFromIPhone5To7Plus(20, 35))
safetyNumberLabel.autoPinEdge(.trailing, to: .trailing, of: self, withOffset: -.scaleFromIPhone5To7Plus(20, 35))
safetyNumberLabel.autoPinEdge(.bottom, to: .bottom, of: self, withOffset: -.scaleFromIPhone5To7Plus(27, 47))
}
required init?(coder: NSCoder) {
fatalError()
}
private lazy var shareButton: UIButton = {
let button = UIButton()
let tintColor: UIColor
switch fingerprint.source {
case .aci:
tintColor = .white
case .e164:
tintColor = .black
}
button.setTemplateImage(
Theme.iconImage(.buttonShare).withRenderingMode(.alwaysTemplate),
tintColor: tintColor
)
button.addTarget(self, action: #selector(didTapShare), for: .touchUpInside)
return button
}()
private lazy var qrCodeView: UIView = {
let containerView = UIView()
containerView.backgroundColor = .white
containerView.layer.cornerRadius = Constants.cornerRadius
containerView.layer.masksToBounds = true
let fingerprintImageView = UIImageView()
fingerprintImageView.image = fingerprint.image
// Don't antialias QR Codes.
fingerprintImageView.layer.magnificationFilter = .nearest
fingerprintImageView.layer.minificationFilter = .nearest
fingerprintImageView.setCompressionResistanceLow()
containerView.addSubview(fingerprintImageView)
fingerprintImageView.autoPin(toAspectRatio: 1)
fingerprintImageView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(margin: 20), excludingEdge: .bottom)
let scanLabel = UILabel()
scanLabel.text = NSLocalizedString("PRIVACY_TAP_TO_SCAN", comment: "Button that shows the 'scan with camera' view.")
scanLabel.font = .systemFont(ofSize: .scaleFromIPhone5To7Plus(13, 15))
scanLabel.textColor = Theme.lightThemeSecondaryTextAndIconColor
containerView.addSubview(scanLabel)
scanLabel.autoHCenterInSuperview()
scanLabel.autoPinEdge(.top, to: .bottom, of: fingerprintImageView, withOffset: 12)
scanLabel.autoPinEdge(.bottom, to: .bottom, of: containerView, withOffset: -14)
containerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapToScan)))
return containerView
}()
private lazy var safetyNumberLabel: UILabel = {
let label = UILabel()
label.text = fingerprint.displayableText
label.font = UIFont(name: "Menlo-Regular", size: 23)
label.textAlignment = .center
switch fingerprint.source {
case .aci:
label.textColor = .white
case .e164:
label.textColor = Theme.lightThemeSecondaryTextAndIconColor
}
label.numberOfLines = 3
label.lineBreakMode = .byTruncatingTail
label.adjustsFontSizeToFitWidth = true
label.isUserInteractionEnabled = true
label.accessibilityIdentifier = "FingerprintViewController.fingerprintLabel"
return label
}()
@objc
func didTapToScan() {
controller?.didTapToScan()
}
@objc
func didTapShare() {
controller?.shareFingerprint(from: shareButton)
}
enum Constants {
static let cornerRadius: CGFloat = 18
}
}
// MARK: PillBoxView
class PillBoxView: UIView {
override var bounds: CGRect {
didSet {
self.layer.cornerRadius = bounds.height / 2
}
}
}
// MARK: - Transition Sheet
private lazy var kvStore: KeyValueStore = {
return DependenciesBridge.shared.keyValueStoreFactory.keyValueStore(collection: "MultiFingerprintVC")
}()
private static let hasShownTransitionSheetKey = "hasShownTransitionSheetKey"
private func hasShownTransitionSheet(_ tx: DBReadTransaction) -> Bool {
return self.kvStore.getBool(Self.hasShownTransitionSheetKey, defaultValue: false, transaction: tx)
}
private func setHasShownTransitionSheet(_ tx: DBWriteTransaction) {
self.kvStore.setBool(true, key: Self.hasShownTransitionSheetKey, transaction: tx)
}
private func showTransitionSheet(_ tx: DBWriteTransaction) {
self.setHasShownTransitionSheet(tx)
tx.addAsyncCompletion(on: DispatchQueue.main) {
let sheet = TransitionSheetViewController(parent: self)
self.present(sheet, animated: true)
}
}
class TransitionSheetViewController: InteractiveSheetViewController {
let contentScrollView = UIScrollView()
let stackView = UIStackView()
public override var interactiveScrollViews: [UIScrollView] { [contentScrollView] }
public override var sheetBackgroundColor: UIColor { Theme.tableView2PresentedBackgroundColor }
private weak var parentVc: MultiFingerprintViewController?
init(parent: MultiFingerprintViewController) {
self.parentVc = parent
super.init()
}
override public func viewDidLoad() {
super.viewDidLoad()
minimizedHeight = 600
super.allowsExpansion = true
contentView.addSubview(contentScrollView)
stackView.axis = .vertical
stackView.layoutMargins = UIEdgeInsets(hMargin: 24, vMargin: 24)
stackView.spacing = 16
stackView.isLayoutMarginsRelativeArrangement = true
contentScrollView.addSubview(stackView)
stackView.autoPinHeightToSuperview()
// Pin to the scroll view's viewport, not to its scrollable area
stackView.autoPinWidth(toWidthOf: contentScrollView)
contentScrollView.autoPinEdgesToSuperviewEdges()
contentScrollView.alwaysBounceVertical = true
buildContents()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !animationView.isAnimationQueued && !animationView.isAnimationPlaying {
animationView.play { [weak self] success in
guard success else { return }
self?.loopAnimation()
}
}
}
override public func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if animationView.isAnimationQueued || animationView.isAnimationPlaying {
animationView.stop()
}
}
private func loopAnimation() {
animationView.play(fromFrame: 60, toFrame: 360, completion: { [weak self] success in
guard success else { return }
self?.loopAnimation()
})
}
private lazy var animationView: AnimationView = {
let animationView = AnimationView(name: "safety-numbers")
animationView.contentMode = .scaleAspectFit
animationView.isUserInteractionEnabled = false
animationView.backgroundColor = .white
animationView.layer.cornerRadius = 12
animationView.layer.masksToBounds = true
return animationView
}()
private func buildContents() {
let titleLabel = UILabel()
titleLabel.textAlignment = .center
titleLabel.font = UIFont.dynamicTypeTitle2.semibold()
titleLabel.text = OWSLocalizedString(
"SAFETY_NUMBER_TRANSITION_SHEET_TITLE",
comment: "Title for a sheet informing the user about the transition from phone number to user identifier based."
)
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
stackView.addArrangedSubview(titleLabel)
let paragraphs: [String] = [
OWSLocalizedString(
"SAFETY_NUMBER_TRANSITION_SHEET_PARAGRAPH_1",
comment: "Informs the user about the transition from phone number to user identifier based."
),
OWSLocalizedString(
"SAFETY_NUMBER_TRANSITION_SHEET_PARAGRAPH_2",
comment: "Informs the user about the transition from phone number to user identifier based."
)
]
var lastParagraphLabel: UILabel!
for paragraph in paragraphs {
let paragraphLabel = UILabel()
paragraphLabel.text = paragraph
paragraphLabel.textAlignment = .natural
paragraphLabel.font = .dynamicTypeSubheadlineClamped
paragraphLabel.numberOfLines = 0
paragraphLabel.lineBreakMode = .byWordWrapping
paragraphLabel.textColor = Theme.secondaryTextAndIconColor
stackView.addArrangedSubview(paragraphLabel)
lastParagraphLabel = paragraphLabel
}
stackView.setCustomSpacing(20, after: lastParagraphLabel)
stackView.addArrangedSubview(animationView)
stackView.setCustomSpacing(24, after: animationView)
animationView.autoPinWidth(toWidthOf: self.view, offset: -48)
animationView.autoMatch(.height, to: .width, of: animationView, withMultiplier: 172/346)
let learnMoreLabel = UILabel()
learnMoreLabel.text = OWSLocalizedString(
"SAFETY_NUMBER_TRANSITION_SHEET_HELP_TEXT",
comment: "Button text for a sheet informing the user about the transition from phone number to user identifier based."
)
learnMoreLabel.textAlignment = .center
learnMoreLabel.font = .dynamicTypeBody
learnMoreLabel.textColor = Theme.isDarkThemeEnabled ? .ows_accentBlueDark : .link
learnMoreLabel.isUserInteractionEnabled = true
learnMoreLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapLearnMore)))
stackView.addArrangedSubview(learnMoreLabel)
stackView.setCustomSpacing(30, after: learnMoreLabel)
let continueButton = OWSButton(
title: OWSLocalizedString(
"ALERT_ACTION_ACKNOWLEDGE",
comment: "generic button text to acknowledge that the corresponding text was read."
)
) { [weak self] in
self?.dismiss(animated: true)
}
continueButton.layer.cornerRadius = 16
continueButton.backgroundColor = .ows_accentBlue
continueButton.titleLabel?.font = UIFont.dynamicTypeBody.semibold()
continueButton.autoSetDimension(.height, toSize: 50, relation: .greaterThanOrEqual)
stackView.addArrangedSubview(continueButton)
}
@objc
func didTapLearnMore() {
self.dismiss(animated: true) { [weak self] in
self?.parentVc?.didTapLearnMore()
}
}
}
// MARK: Actions
@objc
private func didTapDone() {
dismiss(animated: true)
}
private func didTapLearnMore() {
let learnMoreUrl = URL(string: "https://support.signal.org/hc/articles/213134107")!
let safariVC = SFSafariViewController(url: learnMoreUrl)
present(safariVC, animated: true)
}
@objc
private func didUpdatePageControl() {
self.selectedIndex = fingerprintCarouselPageControl.currentPage
scrollToSelectedIndex()
}
@objc
private func didTapVerifyUnverify(_ gestureRecognizer: UITapGestureRecognizer) {
guard gestureRecognizer.state == .recognized else { return }
databaseStorage.write { transaction in
let isVerified = OWSIdentityManager.shared.verificationState(for: recipientAddress, transaction: transaction) == .verified
let newVerificationState: OWSVerificationState = isVerified ? .default : .verified
OWSIdentityManager.shared.setVerificationState(
newVerificationState,
identityKey: identityKey,
address: recipientAddress,
isUserInitiatedChange: true,
transaction: transaction
)
}
dismiss(animated: true)
}
private func shareFingerprint(from fromView: UIView) {
let fingerprint = fingerprints[selectedIndex]
Logger.debug("Sharing safety numbers")
let compareActivity = CompareSafetyNumbersActivity(delegate: self)
let shareFormat = NSLocalizedString(
"SAFETY_NUMBER_SHARE_FORMAT",
comment: "Snippet to share {{safety number}} with a friend. sent e.g. via SMS"
)
let shareString = String(format: shareFormat, fingerprint.displayableText)
let activityController = UIActivityViewController(
activityItems: [ shareString ],
applicationActivities: [ compareActivity ]
)
if let popoverPresentationController = activityController.popoverPresentationController {
popoverPresentationController.sourceView = fromView
}
// This value was extracted by inspecting `activityType` in the activityController.completionHandler
let iCloudActivityType = "com.apple.CloudDocsUI.AddToiCloudDrive"
activityController.excludedActivityTypes = [
.postToFacebook,
.postToWeibo,
.airDrop,
.postToTwitter,
.init(rawValue: iCloudActivityType) // This isn't being excluded. RADAR https://openradar.appspot.com/27493621
]
present(activityController, animated: true)
}
fileprivate func didTapToScan() {
let viewController = FingerprintScanViewController(
recipientAddress: recipientAddress,
recipientIdentity: recipientIdentity,
fingerprints: .multiFingerprint(self.fingerprints, defaultIndex: self.selectedIndex)
)
navigationController?.pushViewController(viewController, animated: true)
}
private func scrollToSelectedIndex(animated: Bool = true) {
let xOffset: CGFloat
if selectedIndex == 0 {
xOffset = 0
} else {
xOffset = (CGFloat(selectedIndex) * UIScreen.main.bounds.width) - (Constants.interCardSpacing + Constants.cardHInset)
}
fingerprintCarousel.setContentOffset(.init(x: xOffset, y: 0), animated: animated)
}
// MARK: Notifications
private var identityStateChangeObserver: Any?
private func identityStateDidChange() {
AssertIsOnMainThread()
updateVerificationStateLabel()
}
// MARK: - Constants
enum Constants {
static let cardHInset: CGFloat = .scaleFromIPhone5To7Plus(30, 53)
static var interCardSpacing: CGFloat = cardHInset / 2
// Link doesn't matter, we will override tap behavior.
static let transitionLearnMoreUrl = "https://support.signal.org/"
static let learnMoreUrl = "https://support.signal.org/learnMore"
}
}
extension MultiFingerprintViewController: CompareSafetyNumbersActivityDelegate {
public func compareSafetyNumbersActivitySucceeded(activity: CompareSafetyNumbersActivity) {
FingerprintScanViewController.showVerificationSucceeded(
from: self,
identityKey: identityKey,
recipientAddress: recipientAddress,
contactName: contactName,
tag: logTag
)
}
public func compareSafetyNumbersActivity(_ activity: CompareSafetyNumbersActivity, failedWithError error: Error) {
let isUserError = (error as NSError).code == OWSErrorCode.userError.rawValue
FingerprintScanViewController.showVerificationFailed(
from: self,
isUserError: isUserError,
localizedErrorDescription: error.userErrorDescription,
tag: logTag
)
}
}
extension MultiFingerprintViewController: UITextViewDelegate {
public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
if URL.absoluteString == Constants.transitionLearnMoreUrl {
DependenciesBridge.shared.db.write {
self.showTransitionSheet($0)
}
} else if URL.absoluteString == Constants.learnMoreUrl {
self.didTapLearnMore()
}
return false
}
}
extension MultiFingerprintViewController: UIScrollViewDelegate {
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let selectedIndex = Int(scrollView.contentOffset.x / (scrollView.frame.width - (Constants.cardHInset * 2)))
self.selectedIndex = selectedIndex
self.fingerprintCarouselPageControl.currentPage = selectedIndex
}
}

View File

@ -576,7 +576,7 @@ private class SafetyNumberCell: ContactTableViewCell {
func configure(item: SafetyNumberConfirmationSheet.Item, theme: Theme.ActionSheet, viewController: UIViewController) {
button.setPressedBlock {
FingerprintViewController.present(from: viewController, address: item.address)
FingerprintViewController.present(for: item.address.aci, from: viewController)
}
Self.databaseStorage.read { transaction in