verification notice sheet
This commit is contained in:
parent
ddb0f79fc1
commit
7635902bb8
@ -63,6 +63,7 @@
|
||||
045B408E2EC6897B002D3F9A /* PinnedMessageManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045B408D2EC68974002D3F9A /* PinnedMessageManagerTest.swift */; };
|
||||
045B40922ECE406A002D3F9A /* ConversationViewController+PinnedMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045B40912ECE4060002D3F9A /* ConversationViewController+PinnedMessages.swift */; };
|
||||
045B40952ECF98C1002D3F9A /* PinnedMessagesDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045B40942ECF98BB002D3F9A /* PinnedMessagesDetailsViewController.swift */; };
|
||||
046092262FBCD2DA00A8765F /* SafetyTipsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046092232FBCC7E700A8765F /* SafetyTipsManager.swift */; };
|
||||
046926092E8EBAAE00B1FC74 /* TSInfoMessage+Polls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046926082E8EBAA800B1FC74 /* TSInfoMessage+Polls.swift */; };
|
||||
0477BE322FA4FC41002F9B47 /* TSReleaseNotesThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0477BE312FA4FC38002F9B47 /* TSReleaseNotesThread.swift */; };
|
||||
047A6DD02E00B5720048EDF4 /* BackupKeyReminderMegaphoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047A6DCF2E00B5640048EDF4 /* BackupKeyReminderMegaphoneTests.swift */; };
|
||||
@ -4208,6 +4209,7 @@
|
||||
045B408D2EC68974002D3F9A /* PinnedMessageManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessageManagerTest.swift; sourceTree = "<group>"; };
|
||||
045B40912ECE4060002D3F9A /* ConversationViewController+PinnedMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationViewController+PinnedMessages.swift"; sourceTree = "<group>"; };
|
||||
045B40942ECF98BB002D3F9A /* PinnedMessagesDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessagesDetailsViewController.swift; sourceTree = "<group>"; };
|
||||
046092232FBCC7E700A8765F /* SafetyTipsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafetyTipsManager.swift; sourceTree = "<group>"; };
|
||||
046926082E8EBAA800B1FC74 /* TSInfoMessage+Polls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSInfoMessage+Polls.swift"; sourceTree = "<group>"; };
|
||||
0477BE312FA4FC38002F9B47 /* TSReleaseNotesThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSReleaseNotesThread.swift; sourceTree = "<group>"; };
|
||||
047A6DCF2E00B5640048EDF4 /* BackupKeyReminderMegaphoneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupKeyReminderMegaphoneTests.swift; sourceTree = "<group>"; };
|
||||
@ -8486,6 +8488,14 @@
|
||||
path = PinnedMessages;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
046092252FBCD28300A8765F /* SafetyTips */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
046092232FBCC7E700A8765F /* SafetyTipsManager.swift */,
|
||||
);
|
||||
path = SafetyTips;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0484CECC2F44B7B4009AB2CB /* AdminDelete */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -14640,6 +14650,7 @@
|
||||
E19C3D922CF6B34200F2C496 /* QRCodes */,
|
||||
6600F352298C8FBB00B1EDB7 /* Registration */,
|
||||
F9B0DC3B28948656004E07B7 /* Resources */,
|
||||
046092252FBCD28300A8765F /* SafetyTips */,
|
||||
50B791552E8B39230063E71E /* Search */,
|
||||
6673FF6A2978B5B900F96CFD /* SecureValueRecovery */,
|
||||
F9C5CB98289453B200548EEE /* Security */,
|
||||
@ -19775,6 +19786,7 @@
|
||||
F9C5CDF8289453B400548EEE /* ReverseDispatchQueue.swift in Sources */,
|
||||
F945FE4A2984796D00C835C7 /* RingrtcFieldTrials.swift in Sources */,
|
||||
557238D32F2D53FD0033BC9A /* RingrtcVp9Config.swift in Sources */,
|
||||
046092262FBCD2DA00A8765F /* SafetyTipsManager.swift in Sources */,
|
||||
668A01332C2B6088007B8808 /* Scheduler.swift in Sources */,
|
||||
72C905912B9ACA3D00E586B8 /* ScreenLock.swift in Sources */,
|
||||
7255A4D22B98E2B700E95368 /* ScrubbingLogFormatter.swift in Sources */,
|
||||
|
||||
@ -1408,17 +1408,15 @@ extension ConversationViewController: CVComponentDelegate {
|
||||
}
|
||||
|
||||
public func didTapSafetyTips() {
|
||||
let viewController = SafetyTipsViewController()
|
||||
viewController.delegate = self
|
||||
present(viewController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SafetyTipsViewControllerDelegate
|
||||
|
||||
extension ConversationViewController: SafetyTipsViewControllerDelegate {
|
||||
public func didTapViewMoreSafetyTips() {
|
||||
let viewController = MoreSafetyTipsViewController()
|
||||
let viewController = SafetyTipsViewController(
|
||||
primaryButton: SafetyTipsViewController.Button(
|
||||
title: CommonStrings.viewMoreButton,
|
||||
action: { [weak self] in
|
||||
let viewController = MoreSafetyTipsViewController()
|
||||
self?.present(viewController, animated: true)
|
||||
},
|
||||
),
|
||||
)
|
||||
present(viewController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
12
Signal/Images.xcassets/safety-tips/verificationcode_alert_96.imageset/Contents.json
vendored
Normal file
12
Signal/Images.xcassets/safety-tips/verificationcode_alert_96.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "verificationcode_alert_96.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Signal/Images.xcassets/safety-tips/verificationcode_alert_96.imageset/verificationcode_alert_96.pdf
vendored
Normal file
BIN
Signal/Images.xcassets/safety-tips/verificationcode_alert_96.imageset/verificationcode_alert_96.pdf
vendored
Normal file
Binary file not shown.
@ -73,10 +73,10 @@ class OutgoingDeviceRestorePresenter: OutgoingDeviceRestoreInitialPresenter {
|
||||
"OUTGOING_DEVICE_RESTORE_CONTINUE_ON_OTHER_DEVICE_TITLE",
|
||||
comment: "Title of prompt notifying that action is necessary on the other device.",
|
||||
),
|
||||
body: HeroSheetViewController.Body(text: OWSLocalizedString(
|
||||
body: HeroSheetViewController.Body(textContent: .plain(OWSLocalizedString(
|
||||
"OUTGOING_DEVICE_RESTORE_CONTINUE_ON_OTHER_DEVICE_BODY",
|
||||
comment: "Body of prompt notifying that action is necessary on the other device.",
|
||||
)),
|
||||
))),
|
||||
primary: .hero(.animation(named: "circular_indeterminate", height: 60)),
|
||||
secondary: nil,
|
||||
)
|
||||
|
||||
@ -12,10 +12,10 @@ final class DonationReadMoreSheetViewController: HeroSheetViewController {
|
||||
hero: .image(.sustainerHeart),
|
||||
title: nil,
|
||||
body: HeroSheetViewController.Body(
|
||||
text: OWSLocalizedString(
|
||||
textContent: .plain(OWSLocalizedString(
|
||||
"DONATION_READ_MORE_SHEET_BODY",
|
||||
comment: "Body text for a sheet discussing donating to Signal.",
|
||||
),
|
||||
)),
|
||||
textAlignment: .left,
|
||||
textColor: .Signal.label,
|
||||
bulletPoints: [
|
||||
|
||||
@ -40,12 +40,17 @@ class ChatListFYISheetCoordinator {
|
||||
|
||||
struct KeyTransparencySelfCheckFailed {}
|
||||
|
||||
struct SMSVerificationCodeSent {
|
||||
let timestampMs: UInt64
|
||||
}
|
||||
|
||||
case badgeThanks(BadgeThanks)
|
||||
case badgeIssue(BadgeIssue)
|
||||
case badgeExpiration(BadgeExpiration)
|
||||
case backupSubscriptionExpired(BackupSubscriptionExpired)
|
||||
case backupSubscriptionFailedToRenew(BackupSubscriptionFailedToRenew)
|
||||
case keyTransparencySelfCheckFailed(KeyTransparencySelfCheckFailed)
|
||||
case smsVerificationCodeSent(SMSVerificationCodeSent)
|
||||
}
|
||||
|
||||
private let backupExportJobRunner: BackupExportJobRunner
|
||||
@ -93,7 +98,9 @@ class ChatListFYISheetCoordinator {
|
||||
// MARK: -
|
||||
|
||||
private func nextSheetToPresent(tx: DBReadTransaction) -> FYISheet? {
|
||||
if let sheet = shouldShowBadgeThanksSheet(successMode: .oneTimeBoost, tx: tx) {
|
||||
if let sheet = shouldShowSMSVerificationCodeSentSheet(tx: tx) {
|
||||
return sheet
|
||||
} else if let sheet = shouldShowBadgeThanksSheet(successMode: .oneTimeBoost, tx: tx) {
|
||||
return sheet
|
||||
} else if let sheet = shouldShowBadgeThanksSheet(successMode: .recurringSubscriptionInitiation, tx: tx) {
|
||||
return sheet
|
||||
@ -126,6 +133,23 @@ class ChatListFYISheetCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for `.smsVerificationCodeSent` FYI sheets.
|
||||
///
|
||||
/// When another device tries to register and receives an SMS code, notify
|
||||
/// the primary device by showing an FYI sheet.
|
||||
///
|
||||
private func shouldShowSMSVerificationCodeSentSheet(
|
||||
tx: DBReadTransaction,
|
||||
) -> FYISheet? {
|
||||
|
||||
let safetyTipsKVStore = SafetyTipsManager()
|
||||
guard let timestamp = safetyTipsKVStore.lastVerificationCodeTimestampMsWithinExpiryTime(transaction: tx) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return .smsVerificationCodeSent(FYISheet.SMSVerificationCodeSent(timestampMs: timestamp))
|
||||
}
|
||||
|
||||
/// Checks for `.badgeThanks` FYI sheets.
|
||||
///
|
||||
/// When creating a new donation we show a `BadgeThankSheet` inline in the
|
||||
@ -219,6 +243,8 @@ class ChatListFYISheetCoordinator {
|
||||
from chatListViewController: ChatListViewController,
|
||||
) async {
|
||||
switch fyiSheet {
|
||||
case .smsVerificationCodeSent(let smsVerificationCodeSent):
|
||||
await _present(smsVerificationCodeSent: smsVerificationCodeSent, from: chatListViewController)
|
||||
case .badgeThanks(let badgeThanks):
|
||||
await _present(badgeThanks: badgeThanks, from: chatListViewController)
|
||||
case .badgeIssue(let badgeIssue):
|
||||
@ -460,6 +486,22 @@ class ChatListFYISheetCoordinator {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func _present(
|
||||
smsVerificationCodeSent: FYISheet.SMSVerificationCodeSent,
|
||||
from chatListViewController: ChatListViewController,
|
||||
) async {
|
||||
let sheet = SMSVerificationCodeSentHeroSheet(
|
||||
timestamp: smsVerificationCodeSent.timestampMs,
|
||||
presentingFrom: chatListViewController,
|
||||
)
|
||||
chatListViewController.present(sheet, animated: true, completion: { [self] in
|
||||
let kvStore = SafetyTipsManager()
|
||||
db.write { tx in
|
||||
kvStore.removeVerificationCodeRequestedTimestampMs(transaction: tx)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ChatListViewController: BadgeIssueSheetDelegate
|
||||
@ -595,3 +637,70 @@ private final class KeyTransparencySelfCheckFailedHeroSheet: HeroSheetViewContro
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private final class SMSVerificationCodeSentHeroSheet: HeroSheetViewController {
|
||||
init(timestamp: UInt64, presentingFrom fromViewController: UIViewController) {
|
||||
let timestampString = DateUtil.formatMessageTimestampForCVC(timestamp, 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),
|
||||
.paragraphSpacingAfter(4.0),
|
||||
),
|
||||
"\n",
|
||||
timestampString.styled(with: .font(.dynamicTypeBody)),
|
||||
"\n",
|
||||
bodyPartTwo.styled(
|
||||
with: .font(.dynamicTypeBody),
|
||||
.paragraphSpacingBefore(12.0),
|
||||
),
|
||||
])
|
||||
|
||||
super.init(
|
||||
hero: .image(.verificationcodeAlert96),
|
||||
title: nil,
|
||||
body: HeroSheetViewController.Body(
|
||||
textContent: .attributed(body),
|
||||
textAlignment: .natural,
|
||||
textColor: UIColor.Signal.label,
|
||||
),
|
||||
primary: .button(HeroSheetViewController.Button(
|
||||
title: OWSLocalizedString(
|
||||
"SAFETY_TIPS_BUTTON_ACTION_TITLE",
|
||||
comment: "Title for Safety Tips button in thread details.",
|
||||
),
|
||||
style: .secondary,
|
||||
action: .custom({ [fromViewController] sheet in
|
||||
sheet.dismiss(animated: true)
|
||||
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: {
|
||||
(fromViewController as? ChatListViewController)?.showAppSettings(mode: .accountSettings)
|
||||
},
|
||||
),
|
||||
)
|
||||
fromViewController.present(safetyTipsVC, animated: true)
|
||||
}),
|
||||
)),
|
||||
secondary: .button(.dismissing(
|
||||
title: CommonStrings.okButton,
|
||||
style: .secondary,
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,6 +151,13 @@ extension ChatListViewController {
|
||||
name: DonationReceiptCredentialRedemptionJob.didFailNotification,
|
||||
object: nil,
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(smsVerificationCodeRequested),
|
||||
name: .smsVerificationCodeRequested,
|
||||
object: nil,
|
||||
)
|
||||
SafetyTipsManager.startObservingDarwinNotifications()
|
||||
|
||||
SUIEnvironment.shared.contactsViewHelperRef.addObserver(self)
|
||||
|
||||
@ -221,6 +228,13 @@ extension ChatListViewController {
|
||||
showFYISheetIfNecessary()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func smsVerificationCodeRequested(_ notification: NSNotification) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
showFYISheetIfNecessary()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func appExpiryDidChange(_ notification: NSNotification) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
@ -1378,6 +1378,7 @@ extension ChatListViewController {
|
||||
case donate(donateMode: DonateViewController.DonateMode)
|
||||
case linkedDevices
|
||||
case proxy
|
||||
case accountSettings
|
||||
}
|
||||
|
||||
func showAppSettings(mode: ShowAppSettingsMode? = nil, completion: (() -> Void)? = nil) {
|
||||
@ -1491,6 +1492,9 @@ extension ChatListViewController {
|
||||
|
||||
case .proxy:
|
||||
viewControllers += [PrivacySettingsViewController(), AdvancedPrivacySettingsViewController(), ProxySettingsViewController()]
|
||||
|
||||
case .accountSettings:
|
||||
viewControllers += [AccountSettingsViewController(appReadiness: appReadiness)]
|
||||
}
|
||||
|
||||
navigationController.setViewControllers(viewControllers, animated: false)
|
||||
|
||||
@ -12,12 +12,18 @@ public enum SafetyTipsType {
|
||||
case group
|
||||
}
|
||||
|
||||
public protocol SafetyTipsViewControllerDelegate: AnyObject {
|
||||
func didTapViewMoreSafetyTips()
|
||||
}
|
||||
|
||||
public class SafetyTipsViewController: InteractiveSheetViewController, UIScrollViewDelegate {
|
||||
public struct Button {
|
||||
let title: String
|
||||
let action: () -> Void
|
||||
}
|
||||
|
||||
override public var placeOnGlassIfAvailable: Bool { true }
|
||||
let primaryButton: Button
|
||||
|
||||
init(primaryButton: Button) {
|
||||
self.primaryButton = primaryButton
|
||||
}
|
||||
|
||||
private enum SafetyTips: CaseIterable {
|
||||
case chatsFromSignal
|
||||
@ -79,8 +85,6 @@ public class SafetyTipsViewController: InteractiveSheetViewController, UIScrollV
|
||||
let contentScrollView = UIScrollView()
|
||||
let stackView = UIStackView()
|
||||
|
||||
public weak var delegate: SafetyTipsViewControllerDelegate?
|
||||
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
@ -133,26 +137,26 @@ public class SafetyTipsViewController: InteractiveSheetViewController, UIScrollV
|
||||
var config = UIButton.Configuration.filled()
|
||||
config.baseBackgroundColor = UIColor.Signal.secondaryFill
|
||||
config.cornerStyle = .capsule
|
||||
var attrString = AttributedString(CommonStrings.viewMoreButton)
|
||||
var attrString = AttributedString(primaryButton.title)
|
||||
attrString.font = .dynamicTypeBodyClamped.medium()
|
||||
config.attributedTitle = attrString
|
||||
config.baseForegroundColor = UIColor.Signal.label
|
||||
config.contentInsets = .init(margin: 14)
|
||||
let viewMoreButton = UIButton(
|
||||
let button = UIButton(
|
||||
configuration: config,
|
||||
primaryAction: .init(handler: { [weak self] _ in
|
||||
self?.dismiss(animated: true)
|
||||
self?.delegate?.didTapViewMoreSafetyTips()
|
||||
self?.primaryButton.action()
|
||||
}),
|
||||
)
|
||||
|
||||
viewMoreButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(viewMoreButton)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(button)
|
||||
NSLayoutConstraint.activate([
|
||||
viewMoreButton.bottomAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.bottomAnchor),
|
||||
viewMoreButton.leadingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.leadingAnchor, constant: 20),
|
||||
viewMoreButton.trailingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.trailingAnchor, constant: -20),
|
||||
viewMoreButton.heightAnchor.constraint(equalToConstant: 52),
|
||||
button.bottomAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.bottomAnchor),
|
||||
button.leadingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.leadingAnchor, constant: 20),
|
||||
button.trailingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.trailingAnchor, constant: -20),
|
||||
button.heightAnchor.constraint(equalToConstant: 52),
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@ -8257,6 +8257,9 @@
|
||||
/* Title for the 'account' link in settings. */
|
||||
"SETTINGS_ACCOUNT" = "Account";
|
||||
|
||||
/* Label for button in Safety Tips to go to 'account' page in settings. */
|
||||
"SETTINGS_ACCOUNT_BUTTON" = "Open Account Settings";
|
||||
|
||||
/* Label for button in settings to get your account data report */
|
||||
"SETTINGS_ACCOUNT_DATA_REPORT_BUTTON" = "Your Account Data";
|
||||
|
||||
@ -10144,6 +10147,12 @@
|
||||
/* An error message indicating that a usernames-related requeset failed because of a network error. */
|
||||
"USERNAMES_REMOTE_MUTATION_ERROR_DESCRIPTION" = "Usernames can only be updated when connected to the internet.";
|
||||
|
||||
/* First part of body for a hero sheet informing the user a verification code was requested. {{ Embeds time the code was requested }} */
|
||||
"VERIFICATION_CODE_REQUESTED_HERO_BODY_FIRST" = "A Verification Code Was Requested";
|
||||
|
||||
/* Second part of body for a hero sheet informing the user a verification code was requested. */
|
||||
"VERIFICATION_CODE_REQUESTED_HERO_BODY_SECOND" = "Do not give your verification code to anyone. Signal will never message you for it. If you received a message from someone pretending to be Signal, it is a scam.\nYou can safely ignore this message if you requested the code yourself.";
|
||||
|
||||
/* Format for info message indicating that the verification state was unverified on this device. Embeds {{user's name or phone number}}. */
|
||||
"VERIFICATION_STATE_CHANGE_FORMAT_NOT_VERIFIED_LOCAL" = "You marked %@ as not verified.";
|
||||
|
||||
|
||||
@ -95,6 +95,10 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
}
|
||||
|
||||
private func verificationCodeRequestTimestampMs(userInfo: [AnyHashable: Any]) -> UInt64? {
|
||||
return (userInfo["verificationCodeRequested"] as? [String: Any])?["timestamp"] as? UInt64
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func _didReceive(_ request: UNNotificationRequest, logger: NSELogger) async -> UNNotificationContent {
|
||||
globalEnvironment.setUp(logger: logger)
|
||||
@ -140,6 +144,15 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
globalEnvironment.setAppIsReady()
|
||||
}
|
||||
|
||||
if let timestamp = verificationCodeRequestTimestampMs(userInfo: request.content.userInfo) {
|
||||
await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { transaction in
|
||||
let kvStore = SafetyTipsManager()
|
||||
kvStore.setLastVerificationCodeRequestedTimestampMs(value: timestamp, transaction: transaction)
|
||||
}
|
||||
Logger.info("Skipping fetch for verification code requested push")
|
||||
return UNNotificationContent()
|
||||
}
|
||||
|
||||
// Mark down that the APNS token is working since we got a push.
|
||||
// Do this as early as possible but after the app is ready and has run
|
||||
// GRDB migrations and such.
|
||||
|
||||
75
SignalServiceKit/SafetyTips/SafetyTipsManager.swift
Normal file
75
SignalServiceKit/SafetyTips/SafetyTipsManager.swift
Normal file
@ -0,0 +1,75 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
extension NSNotification.Name {
|
||||
public static let smsVerificationCodeRequested = Notification.Name("SafetyTipsKeyValueStore.smsVerificationCodeRequested")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public class SafetyTipsManager {
|
||||
private static var hasStartedObserving = false
|
||||
static let expiryTimeSeconds = (TimeInterval.minute * 10)
|
||||
|
||||
private enum StoreKeys {
|
||||
static let smsCodeRequestedTimestampMs: String = "smsCodeRequestedTimestampMsKey"
|
||||
}
|
||||
|
||||
private let kvStore: NewKeyValueStore
|
||||
|
||||
public init() {
|
||||
self.kvStore = NewKeyValueStore(collection: "SafetyTips")
|
||||
}
|
||||
|
||||
public func setLastVerificationCodeRequestedTimestampMs(
|
||||
value: UInt64,
|
||||
transaction: DBWriteTransaction,
|
||||
) {
|
||||
kvStore.writeValue(
|
||||
value,
|
||||
forKey: StoreKeys.smsCodeRequestedTimestampMs,
|
||||
tx: transaction,
|
||||
)
|
||||
|
||||
transaction.addSyncCompletion {
|
||||
// Wake up observer in the main app to check KV store
|
||||
DarwinNotificationCenter.postNotification(name: .smsVerificationCodeRequested)
|
||||
}
|
||||
}
|
||||
|
||||
public func lastVerificationCodeTimestampMsWithinExpiryTime(
|
||||
transaction: DBReadTransaction,
|
||||
) -> UInt64? {
|
||||
guard
|
||||
let timestamp = kvStore.fetchValue(
|
||||
UInt64.self,
|
||||
forKey: StoreKeys.smsCodeRequestedTimestampMs,
|
||||
tx: transaction,
|
||||
)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let timestampDate = Date(timeIntervalSince1970: TimeInterval(timestamp) / 1000.0)
|
||||
let seconds = Date().timeIntervalSince(timestampDate)
|
||||
guard seconds <= Self.expiryTimeSeconds else {
|
||||
return nil
|
||||
}
|
||||
return timestamp
|
||||
}
|
||||
|
||||
public func removeVerificationCodeRequestedTimestampMs(
|
||||
transaction: DBWriteTransaction,
|
||||
) {
|
||||
kvStore.removeValue(forKey: StoreKeys.smsCodeRequestedTimestampMs, tx: transaction)
|
||||
}
|
||||
|
||||
public static func startObservingDarwinNotifications() {
|
||||
guard !hasStartedObserving else { return }
|
||||
hasStartedObserving = true
|
||||
_ = DarwinNotificationCenter.addObserver(name: .smsVerificationCodeRequested, queue: .main) { _ in
|
||||
NotificationCenter.default.post(name: .smsVerificationCodeRequested, object: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,7 @@ import Foundation
|
||||
|
||||
public struct DarwinNotificationName: ExpressibleByStringLiteral {
|
||||
static let primaryDBFolderNameDidChange: DarwinNotificationName = "org.signal.primaryDBFolderNameDidChange"
|
||||
static let smsVerificationCodeRequested: DarwinNotificationName = "org.signal.safetyTips.smsVerificationCodeRequested"
|
||||
|
||||
static func sdsCrossProcess(for type: AppContextType) -> DarwinNotificationName {
|
||||
DarwinNotificationName("org.signal.sdscrossprocess.\(type)")
|
||||
|
||||
@ -22,6 +22,11 @@ open class HeroSheetViewController: StackSheetViewController {
|
||||
}
|
||||
|
||||
public struct Body {
|
||||
public enum TextContent {
|
||||
case plain(String)
|
||||
case attributed(NSAttributedString)
|
||||
}
|
||||
|
||||
public struct BulletPoint {
|
||||
public let icon: UIImage
|
||||
public let text: String
|
||||
@ -48,20 +53,20 @@ open class HeroSheetViewController: StackSheetViewController {
|
||||
}
|
||||
}
|
||||
|
||||
public let text: String
|
||||
public let textContent: TextContent
|
||||
public let textAlignment: NSTextAlignment
|
||||
public let textColor: UIColor
|
||||
public let bulletPoints: [BulletPoint]
|
||||
public let toggle: Toggle?
|
||||
|
||||
public init(
|
||||
text: String,
|
||||
textContent: TextContent,
|
||||
textAlignment: NSTextAlignment = .center,
|
||||
textColor: UIColor = .Signal.secondaryLabel,
|
||||
bulletPoints: [BulletPoint] = [],
|
||||
toggle: Toggle? = nil,
|
||||
) {
|
||||
self.text = text
|
||||
self.textContent = textContent
|
||||
self.textAlignment = textAlignment
|
||||
self.textColor = textColor
|
||||
self.bulletPoints = bulletPoints
|
||||
@ -137,7 +142,7 @@ open class HeroSheetViewController: StackSheetViewController {
|
||||
) {
|
||||
self.hero = hero
|
||||
self.titleText = title
|
||||
self.body = Body(text: body)
|
||||
self.body = Body(textContent: .plain(body))
|
||||
self.primary = primaryButton.map { .button($0) }
|
||||
self.secondary = secondaryButton.map { .button($0) }
|
||||
super.init()
|
||||
@ -193,10 +198,16 @@ open class HeroSheetViewController: StackSheetViewController {
|
||||
let bodyLabel = UILabel()
|
||||
self.stackView.addArrangedSubview(bodyLabel)
|
||||
self.stackView.setCustomSpacing(32, after: bodyLabel)
|
||||
bodyLabel.text = body.text
|
||||
switch body.textContent {
|
||||
case .plain(let text):
|
||||
bodyLabel.text = text
|
||||
bodyLabel.font = .dynamicTypeSubheadline
|
||||
case .attributed(let attributedText):
|
||||
// attributed strings should set their own font.
|
||||
bodyLabel.attributedText = attributedText
|
||||
}
|
||||
bodyLabel.textColor = body.textColor
|
||||
bodyLabel.textAlignment = body.textAlignment
|
||||
bodyLabel.font = .dynamicTypeSubheadline
|
||||
bodyLabel.numberOfLines = 0
|
||||
|
||||
for bodyBullet in body.bulletPoints {
|
||||
@ -378,7 +389,7 @@ open class HeroSheetViewController: StackSheetViewController {
|
||||
hero: .image(UIImage(named: "sustainer-heart")!),
|
||||
title: nil,
|
||||
body: HeroSheetViewController.Body(
|
||||
text: "As an independent nonprofit, Signal is committed to private messaging and calls. No ads, no trackers, no surveillance. Donate today to support Signal.",
|
||||
textContent: .plain("As an independent nonprofit, Signal is committed to private messaging and calls. No ads, no trackers, no surveillance. Donate today to support Signal."),
|
||||
textAlignment: .left,
|
||||
textColor: .Signal.label,
|
||||
bulletPoints: [
|
||||
@ -407,7 +418,7 @@ open class HeroSheetViewController: StackSheetViewController {
|
||||
hero: .image(UIImage(named: "toggle-32")!),
|
||||
title: nil,
|
||||
body: HeroSheetViewController.Body(
|
||||
text: #"Give Boots extra dinner? He'd like you to know he's "extra hungry" tonight."#,
|
||||
textContent: .plain(#"Give Boots extra dinner? He'd like you to know he's "extra hungry" tonight."#),
|
||||
toggle: HeroSheetViewController.Body.Toggle(
|
||||
text: "Extra Food?",
|
||||
isOn: true,
|
||||
@ -452,7 +463,7 @@ open class HeroSheetViewController: StackSheetViewController {
|
||||
SheetPreviewViewController(sheet: HeroSheetViewController(
|
||||
hero: .image(UIImage(named: "transfer_complete")!),
|
||||
title: LocalizationNotNeeded("Continue on your other device"),
|
||||
body: HeroSheetViewController.Body(text: LocalizationNotNeeded("Continue transferring your account on your other device.")),
|
||||
body: HeroSheetViewController.Body(textContent: .plain(LocalizationNotNeeded("Continue transferring your account on your other device."))),
|
||||
primary: .hero(.animation(named: "circular_indeterminate", height: 60)),
|
||||
secondary: nil,
|
||||
))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user