diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 9812dfee78..f4b0643bac 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -68,6 +68,7 @@ 044D77782E5E6D750048C21A /* PollVoteRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D77772E5E6D700048C21A /* PollVoteRecord.swift */; }; 044D84452E9FDDF00090BA64 /* BackupArchivePollTerminateChatUpdateArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D84442E9FDDE30090BA64 /* BackupArchivePollTerminateChatUpdateArchiver.swift */; }; 044D84472E9FEE010090BA64 /* BackupArchivePollArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044D84462E9FEDE20090BA64 /* BackupArchivePollArchiver.swift */; }; + 044EA2232FC0EFAE005B5A3E /* SafetyTipsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044EA2222FC0EFA6005B5A3E /* SafetyTipsSheet.swift */; }; 045B40892EC67510002D3F9A /* PinnedMessageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045B40882EC6750A002D3F9A /* PinnedMessageManager.swift */; }; 045B408C2EC67C03002D3F9A /* PinnedMessageRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045B408B2EC67BFF002D3F9A /* PinnedMessageRecord.swift */; }; 045B408E2EC6897B002D3F9A /* PinnedMessageManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045B408D2EC68974002D3F9A /* PinnedMessageManagerTest.swift */; }; @@ -4219,6 +4220,7 @@ 044D77772E5E6D700048C21A /* PollVoteRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollVoteRecord.swift; sourceTree = ""; }; 044D84442E9FDDE30090BA64 /* BackupArchivePollTerminateChatUpdateArchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchivePollTerminateChatUpdateArchiver.swift; sourceTree = ""; }; 044D84462E9FEDE20090BA64 /* BackupArchivePollArchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchivePollArchiver.swift; sourceTree = ""; }; + 044EA2222FC0EFA6005B5A3E /* SafetyTipsSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafetyTipsSheet.swift; sourceTree = ""; }; 045B40882EC6750A002D3F9A /* PinnedMessageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessageManager.swift; sourceTree = ""; }; 045B408B2EC67BFF002D3F9A /* PinnedMessageRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessageRecord.swift; sourceTree = ""; }; 045B408D2EC68974002D3F9A /* PinnedMessageManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessageManagerTest.swift; sourceTree = ""; }; @@ -11324,6 +11326,7 @@ 0550A5E12C4035170072CC02 /* CLVViewInfo.swift */, 34E95C1F269F4F4F004807EC /* CLVViewState.swift */, 4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */, + 044EA2222FC0EFA6005B5A3E /* SafetyTipsSheet.swift */, 1477630A275E20D700D1067E /* ThreadContextualActionProvider.swift */, ); path = "Chat List"; @@ -18730,6 +18733,7 @@ 348EE28F25B897BF00814FC2 /* ReusableMediaView.swift in Sources */, 66A22C0928A18D49007CD4F5 /* RingerSwitch.swift in Sources */, D9E43C2C2CC194140001536E /* RTCIceServerFetcher.swift in Sources */, + 044EA2232FC0EFAE005B5A3E /* SafetyTipsSheet.swift in Sources */, C1D9B1552B7FA28200D94595 /* SafetyTipsViewController.swift in Sources */, 7677E41329F84C2100AC6A75 /* ScreenLockUI.swift in Sources */, 667EDE6428F8D6B7001FB487 /* SDAnimatedImage+Duration.swift in Sources */, diff --git a/Signal/ConversationView/ConversationViewController+Notifications.swift b/Signal/ConversationView/ConversationViewController+Notifications.swift index 2ed794c120..eaf22c94d0 100644 --- a/Signal/ConversationView/ConversationViewController+Notifications.swift +++ b/Signal/ConversationView/ConversationViewController+Notifications.swift @@ -78,6 +78,14 @@ extension ConversationViewController { object: AVAudioSession.sharedInstance(), ) + NotificationCenter.default.addObserver( + self, + selector: #selector(smsVerificationCodeRequested), + name: .smsVerificationCodeRequested, + object: nil, + ) + SafetyTipsManager.startObservingDarwinNotifications() + AppEnvironment.shared.callService.callServiceState.addObserver(self, syncStateImmediately: false) } @@ -203,6 +211,28 @@ extension ConversationViewController { AssertIsOnMainThread() ensureBottomViewType() } + + @objc + private func smsVerificationCodeRequested(_ notification: NSNotification) { + AssertIsOnMainThread() + + let db = DependenciesBridge.shared.db + let safetyTipsManager = SafetyTipsManager() + let timestamp: UInt64? = db.read { tx in + safetyTipsManager.lastVerificationCodeTimestampMsWithinExpiryTime(transaction: tx) + } + + guard let timestamp else { return } + let actionSheetController = SafetyTipsSheet.makeSmsCodeRequestedSheet( + timestampMs: timestamp, + fromViewController: self, + ) + present(actionSheetController, animated: true, completion: { + db.write { tx in + safetyTipsManager.removeVerificationCodeRequestedTimestampMs(transaction: tx) + } + }) + } } // MARK: - diff --git a/Signal/src/ViewControllers/HomeView/Chat List/ChatListFYISheetCoordinator.swift b/Signal/src/ViewControllers/HomeView/Chat List/ChatListFYISheetCoordinator.swift index d1c5e63100..85db37d5b0 100644 --- a/Signal/src/ViewControllers/HomeView/Chat List/ChatListFYISheetCoordinator.swift +++ b/Signal/src/ViewControllers/HomeView/Chat List/ChatListFYISheetCoordinator.swift @@ -65,6 +65,7 @@ class ChatListFYISheetCoordinator { private let keyTransparencyStore: KeyTransparencyStore private let networkManager: NetworkManager private let profileManager: ProfileManager + private let safetyTipsManager: SafetyTipsManager init( backupArchiveErrorStore: BackupArchiveErrorStore, @@ -86,6 +87,7 @@ class ChatListFYISheetCoordinator { self.keyTransparencyStore = keyTransparencyStore self.networkManager = networkManager self.profileManager = profileManager + self.safetyTipsManager = SafetyTipsManager() } func presentIfNecessary( @@ -150,8 +152,7 @@ class ChatListFYISheetCoordinator { tx: DBReadTransaction, ) -> FYISheet? { - let safetyTipsKVStore = SafetyTipsManager() - guard let timestamp = safetyTipsKVStore.lastVerificationCodeTimestampMsWithinExpiryTime(transaction: tx) else { + guard let timestamp = safetyTipsManager.lastVerificationCodeTimestampMsWithinExpiryTime(transaction: tx) else { return nil } @@ -517,67 +518,13 @@ class ChatListFYISheetCoordinator { smsVerificationCodeSent: FYISheet.SMSVerificationCodeSent, from chatListViewController: ChatListViewController, ) async { - let timestampString = DateUtil.formatMessageTimestampForCVC( - smsVerificationCodeSent.timestampMs, - shouldUseLongFormat: true, + let actionSheetController = SafetyTipsSheet.makeSmsCodeRequestedSheet( + timestampMs: smsVerificationCodeSent.timestampMs, + fromViewController: chatListViewController, ) - let bodyPartOne = OWSLocalizedString( - "VERIFICATION_CODE_REQUESTED_HERO_BODY_FIRST", - comment: "First part of body for a hero sheet informing the user a verification code was requested. {{ Embeds time the code was requested }}", - ) - let bodyPartTwo = OWSLocalizedString( - "VERIFICATION_CODE_REQUESTED_HERO_BODY_SECOND", - comment: "Second part of body for a hero sheet informing the user a verification code was requested.", - ) - let body: NSAttributedString = .composed(of: [ - bodyPartOne.styled( - with: .font(.dynamicTypeHeadline), - .color(UIColor.Signal.label), - .paragraphSpacingAfter(4.0), - ), - "\n", - timestampString.styled( - with: .font(.dynamicTypeBody), - .color(UIColor.Signal.label), - ), - "\n", - bodyPartTwo.styled( - with: .font(.dynamicTypeBody), - .color(UIColor.Signal.label), - .paragraphSpacingBefore(12.0), - ), - ]) - - let actionSheet = ActionSheetController( - message: body, - image: UIImage(resource: .verificationcodeAlert96), - ) - actionSheet.addAction(ActionSheetAction( - title: OWSLocalizedString( - "SAFETY_TIPS_BUTTON_ACTION_TITLE", - comment: "Title for Safety Tips button in thread details.", - ), - handler: { [weak chatListViewController] _ in - guard let chatListViewController else { return } - let safetyTipsVC = SafetyTipsViewController( - primaryButton: SafetyTipsViewController.Button( - title: OWSLocalizedString( - "SETTINGS_ACCOUNT_BUTTON", - comment: "Label for button in Safety Tips to go to 'account' page in settings.", - ), - action: { [weak chatListViewController] in - chatListViewController?.showAppSettings(mode: .accountSettings) - }, - ), - ) - chatListViewController.present(safetyTipsVC, animated: true) - }, - )) - actionSheet.addAction(.ok) - chatListViewController.present(actionSheet, animated: true, completion: { [self] in - let kvStore = SafetyTipsManager() + chatListViewController.present(actionSheetController, animated: true, completion: { [self] in db.write { tx in - kvStore.removeVerificationCodeRequestedTimestampMs(transaction: tx) + safetyTipsManager.removeVerificationCodeRequestedTimestampMs(transaction: tx) } }) } diff --git a/Signal/src/ViewControllers/HomeView/Chat List/SafetyTipsSheet.swift b/Signal/src/ViewControllers/HomeView/Chat List/SafetyTipsSheet.swift new file mode 100644 index 0000000000..1581661a45 --- /dev/null +++ b/Signal/src/ViewControllers/HomeView/Chat List/SafetyTipsSheet.swift @@ -0,0 +1,69 @@ +// +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import SignalServiceKit +import SignalUI + +enum SafetyTipsSheet { + static func makeSmsCodeRequestedSheet(timestampMs: UInt64, fromViewController: UIViewController) -> ActionSheetController { + let timestampString = DateUtil.formatMessageTimestampForCVC( + timestampMs, + shouldUseLongFormat: true, + ) + let bodyPartOne = OWSLocalizedString( + "VERIFICATION_CODE_REQUESTED_HERO_BODY_FIRST", + comment: "First part of body for a hero sheet informing the user a verification code was requested. {{ Embeds time the code was requested }}", + ) + let bodyPartTwo = OWSLocalizedString( + "VERIFICATION_CODE_REQUESTED_HERO_BODY_SECOND", + comment: "Second part of body for a hero sheet informing the user a verification code was requested.", + ) + let body: NSAttributedString = .composed(of: [ + bodyPartOne.styled( + with: .font(.dynamicTypeHeadline), + .color(UIColor.Signal.label), + .paragraphSpacingAfter(4.0), + ), + "\n", + timestampString.styled( + with: .font(.dynamicTypeBody), + .color(UIColor.Signal.label), + ), + "\n", + bodyPartTwo.styled( + with: .font(.dynamicTypeBody), + .color(UIColor.Signal.label), + .paragraphSpacingBefore(12.0), + ), + ]) + + let actionSheet = ActionSheetController( + message: body, + image: UIImage(resource: .verificationcodeAlert96), + ) + actionSheet.addAction(ActionSheetAction( + title: OWSLocalizedString( + "SAFETY_TIPS_BUTTON_ACTION_TITLE", + comment: "Title for Safety Tips button in thread details.", + ), + handler: { [weak fromViewController] _ in + let safetyTipsVC = SafetyTipsViewController( + primaryButton: SafetyTipsViewController.Button( + title: OWSLocalizedString( + "SETTINGS_ACCOUNT_BUTTON", + comment: "Label for button in Safety Tips to go to 'account' page in settings.", + ), + action: { + SignalApp.shared.showAppSettings(mode: .accountSettings) + }, + ), + ) + fromViewController?.present(safetyTipsVC, animated: true) + }, + )) + actionSheet.addAction(.ok) + return actionSheet + } +} diff --git a/SignalNSE/NotificationService.swift b/SignalNSE/NotificationService.swift index b94907b043..61ab79105c 100644 --- a/SignalNSE/NotificationService.swift +++ b/SignalNSE/NotificationService.swift @@ -146,6 +146,11 @@ class NotificationService: UNNotificationServiceExtension { if let timestamp = verificationCodeRequestTimestampMs(userInfo: request.content.userInfo) { await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { transaction in + guard DependenciesBridge.shared.tsAccountManager.registrationState(tx: transaction).isPrimaryDevice == true else { + Logger.info("Received verification code push on non-primary device; ignoring.") + return + } + let kvStore = SafetyTipsManager() kvStore.setLastVerificationCodeRequestedTimestampMs(value: timestamp, transaction: transaction) }