Skip to content

Commit 33a8457

Browse files
authored
Megaphone for inactive primary devices
1 parent f2ede4d commit 33a8457

File tree

14 files changed

+269
-18
lines changed

14 files changed

+269
-18
lines changed

Signal.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
043CC30E2E1EF79C00D9002E /* ThreadFinderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043CC30D2E1EF79300D9002E /* ThreadFinderTests.swift */; };
1313
047A6DD02E00B5720048EDF4 /* BackupKeyReminderMegaphoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047A6DCF2E00B5640048EDF4 /* BackupKeyReminderMegaphoneTests.swift */; };
1414
04AA85BC2E0EFC5C0051C0A7 /* BackupEnablementReminderMegaphoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041A5F082E05D3AC00FAED05 /* BackupEnablementReminderMegaphoneTests.swift */; };
15+
04BBBE902E259A6900E914B1 /* InactivePrimaryDeviceReminderMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04BBBE8F2E259A5F00E914B1 /* InactivePrimaryDeviceReminderMegaphone.swift */; };
16+
04BBBE922E26C92D00E914B1 /* InactivePrimaryDeviceStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04BBBE912E26C92300E914B1 /* InactivePrimaryDeviceStore.swift */; };
17+
04BBBE942E26F00900E914B1 /* InactivePrimaryDeviceStoreTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04BBBE932E26F00000E914B1 /* InactivePrimaryDeviceStoreTest.swift */; };
1518
04BC94D22E061D8300446C52 /* BackupArchiveAttachmentByteCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04BC94D12E061D7500446C52 /* BackupArchiveAttachmentByteCounter.swift */; };
1619
04E66D402DFC825B0059DBAC /* BackupKeyReminderMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E66D3E2DFC81BC0059DBAC /* BackupKeyReminderMegaphone.swift */; };
1720
04E66D422DFF3A4B0059DBAC /* BackupsReminderCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E66D412DFF3A3E0059DBAC /* BackupsReminderCoordinator.swift */; };
@@ -3839,6 +3842,9 @@
38393842
041A5F082E05D3AC00FAED05 /* BackupEnablementReminderMegaphoneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupEnablementReminderMegaphoneTests.swift; sourceTree = "<group>"; };
38403843
043CC30D2E1EF79300D9002E /* ThreadFinderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadFinderTests.swift; sourceTree = "<group>"; };
38413844
047A6DCF2E00B5640048EDF4 /* BackupKeyReminderMegaphoneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupKeyReminderMegaphoneTests.swift; sourceTree = "<group>"; };
3845+
04BBBE8F2E259A5F00E914B1 /* InactivePrimaryDeviceReminderMegaphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InactivePrimaryDeviceReminderMegaphone.swift; sourceTree = "<group>"; };
3846+
04BBBE912E26C92300E914B1 /* InactivePrimaryDeviceStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InactivePrimaryDeviceStore.swift; sourceTree = "<group>"; };
3847+
04BBBE932E26F00000E914B1 /* InactivePrimaryDeviceStoreTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InactivePrimaryDeviceStoreTest.swift; sourceTree = "<group>"; };
38423848
04BC94D12E061D7500446C52 /* BackupArchiveAttachmentByteCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchiveAttachmentByteCounter.swift; sourceTree = "<group>"; };
38433849
04E66D3E2DFC81BC0059DBAC /* BackupKeyReminderMegaphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupKeyReminderMegaphone.swift; sourceTree = "<group>"; };
38443850
04E66D412DFF3A3E0059DBAC /* BackupsReminderCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupsReminderCoordinator.swift; sourceTree = "<group>"; };
@@ -10891,6 +10897,7 @@
1089110897
8806EF1A248DBFC100E764C7 /* ContactPermissionReminderMegaphone.swift */,
1089210898
D9C2D77F299EC11400D79715 /* CreateUsernameMegaphone.swift */,
1089310899
D9C0AE682BD82DBC00FCB05E /* InactiveLinkedDeviceReminderMegaphone.swift */,
10900+
04BBBE8F2E259A5F00E914B1 /* InactivePrimaryDeviceReminderMegaphone.swift */,
1089410901
88A505F923DBA1360005C012 /* IntroducingPINs.swift */,
1089510902
8837F74023DA0B0F00772A32 /* MegaphoneView.swift */,
1089610903
B9B7BC642D41C61500C26E42 /* NewLinkedDeviceNotificationMegaphone.swift */,
@@ -13186,6 +13193,7 @@
1318613193
isa = PBXGroup;
1318713194
children = (
1318813195
D9C0AE612BD7102500FCB05E /* InactiveLinkedDeviceFinderTest.swift */,
13196+
04BBBE932E26F00000E914B1 /* InactivePrimaryDeviceStoreTest.swift */,
1318913197
D9708B5B29E4CCCB004306FA /* OWSDeviceManagerTest.swift */,
1319013198
D9B95A9729E8906200D7CB95 /* OWSDeviceTest.swift */,
1319113199
F9FA363529F335E500C13830 /* ProvisioningCipherTests.swift */,
@@ -13609,6 +13617,7 @@
1360913617
F9C5CAF8289453B200548EEE /* Util */,
1361013618
503AECCC29B2B86200642F66 /* VoiceMessage */,
1361113619
5013365D2B2BC2CD004119F1 /* ZkParams */,
13620+
04BBBE912E26C92300E914B1 /* InactivePrimaryDeviceStore.swift */,
1361213621
F9C985D2289459860029F9AD /* SignalServiceKit-Prefix.pch */,
1361313622
F9C5C899289451B900548EEE /* SignalServiceKit.h */,
1361413623
50E642C829E4E9CD00566D5D /* SSKEnvironment.swift */,
@@ -17183,6 +17192,7 @@
1718317192
8852572C27DD40870032073C /* HomeTabBarController.swift in Sources */,
1718417193
B9A0807A2B07D76A000FDB5B /* HomeTabViewController.swift in Sources */,
1718517194
D9C0AE692BD82DBC00FCB05E /* InactiveLinkedDeviceReminderMegaphone.swift in Sources */,
17195+
04BBBE902E259A6900E914B1 /* InactivePrimaryDeviceReminderMegaphone.swift in Sources */,
1718617196
D9E43C052CC194140001536E /* IncomingCallControls.swift in Sources */,
1718717197
D9E43C062CC194140001536E /* IncomingReactionsView.swift in Sources */,
1718817198
66C1BF552D0CC88A002296F7 /* IncrementalMessageTSAttachmentMigrationRunner.swift in Sources */,
@@ -18045,6 +18055,7 @@
1804518055
661BFE0A2C07FB950065435B /* ImageMetadata.swift in Sources */,
1804618056
F9C5CDDF289453B400548EEE /* ImageQuality.swift in Sources */,
1804718057
D9C0AE662BD7103100FCB05E /* InactiveLinkedDeviceFinder.swift in Sources */,
18058+
04BBBE922E26C92D00E914B1 /* InactivePrimaryDeviceStore.swift in Sources */,
1804818059
05412B3C2C22219E007AC9C7 /* InboxFilter.swift in Sources */,
1804918060
505166D72BB37DAE00FF6B4A /* IncomingCallEventSyncMessageManager.swift in Sources */,
1805018061
505166D62BB37DA700FF6B4A /* IncomingCallEventSyncMessageParams.swift in Sources */,
@@ -18891,6 +18902,7 @@
1889118902
D9F399B02A967664001599EC /* IdentityKeyCheckerTest.swift in Sources */,
1889218903
D9F399B42A96E54C001599EC /* IdentityKeyMismatchManagerTest.swift in Sources */,
1889318904
D9C0AE672BD7162300FCB05E /* InactiveLinkedDeviceFinderTest.swift in Sources */,
18905+
04BBBE942E26F00900E914B1 /* InactivePrimaryDeviceStoreTest.swift in Sources */,
1889418906
D979CC4F2AD4DECB006AAC49 /* IncomingCallEventSyncMessageManagerTest.swift in Sources */,
1889518907
D958C67D2BA0F3B2002F6888 /* IncomingCallLogEventSyncMessageManagerTest.swift in Sources */,
1889618908
D979CC4C2AD4DECB006AAC49 /* IndividualCallRecordManagerTest.swift in Sources */,

Signal/Megaphones/ExperienceUpgradeManager.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ class ExperienceUpgradeManager {
6868
case .inactiveLinkedDeviceReminder:
6969
return ExperienceUpgradeManifest
7070
.checkPreconditionsForInactiveLinkedDeviceReminder(tx: transaction)
71+
case .inactivePrimaryDeviceReminder:
72+
return ExperienceUpgradeManifest
73+
.checkPreconditionsForInactivePrimaryDeviceReminder(tx: transaction)
7174
case .pinReminder:
7275
return ExperienceUpgradeManifest
7376
.checkPreconditionsForPinReminder(transaction: transaction)
@@ -210,6 +213,7 @@ class ExperienceUpgradeManager {
210213
.newLinkedDeviceNotification,
211214
.createUsernameReminder,
212215
.inactiveLinkedDeviceReminder,
216+
.inactivePrimaryDeviceReminder,
213217
.contactPermissionReminder,
214218
.backupKeyReminder,
215219
.enableBackupsReminder:
@@ -293,6 +297,18 @@ class ExperienceUpgradeManager {
293297
fromViewController: fromViewController,
294298
experienceUpgrade: experienceUpgrade
295299
)
300+
case .inactivePrimaryDeviceReminder:
301+
let isPrimaryDevice = db.read { tx in
302+
// If isPrimaryDevice is nil, it means we aren't registered yet, and shouldn't show the megaphone.
303+
return DependenciesBridge.shared.tsAccountManager.registrationState(tx: tx).isPrimaryDevice ?? true
304+
}
305+
306+
guard !isPrimaryDevice else {
307+
owsFailDebug("Trying to show inactive primary device megaphone, but this is the primary device or an unregistered device")
308+
return nil
309+
}
310+
311+
return InactivePrimaryDeviceReminderMegaphone(fromViewController: fromViewController, experienceUpgrade: experienceUpgrade)
296312
case .contactPermissionReminder:
297313
return ContactPermissionReminderMegaphone(experienceUpgrade: experienceUpgrade, fromViewController: fromViewController)
298314
case .remoteMegaphone(let megaphone):
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//
2+
// Copyright 2025 Signal Messenger, LLC
3+
// SPDX-License-Identifier: AGPL-3.0-only
4+
//
5+
6+
import SignalServiceKit
7+
import SafariServices
8+
9+
final class InactivePrimaryDeviceReminderMegaphone: MegaphoneView {
10+
private var learnMoreURL: URL { URL(string: "https://support.signal.org/hc/articles/9021007554074")! }
11+
12+
init(
13+
fromViewController: UIViewController,
14+
experienceUpgrade: ExperienceUpgrade
15+
) {
16+
super.init(experienceUpgrade: experienceUpgrade)
17+
18+
titleText = OWSLocalizedString(
19+
"INACTIVE_PRIMARY_DEVICE_REMINDER_MEGAPHONE_TITLE",
20+
comment: "Title for an in-app megaphone about a user's inactive primary device."
21+
)
22+
23+
bodyText = OWSLocalizedString(
24+
"INACTIVE_PRIMARY_DEVICE_REMINDER_MEGAPHONE_BODY",
25+
comment: "Body for an in-app megaphone about a user's inactive primary device."
26+
)
27+
28+
imageName = "inactive-linked-device-reminder-megaphone"
29+
imageContentMode = .center
30+
31+
let viewControllerRef = fromViewController
32+
let learnMoreButton = Button(title: OWSLocalizedString(
33+
"INACTIVE_PRIMARY_DEVICE_REMINDER_MEGAPHONE_LEARN_MORE_BUTTON",
34+
comment: "Title for a button in an in-app megaphone about a user's inactive linked device, indicating the user wants to learn more."
35+
)) { [weak viewControllerRef] in
36+
viewControllerRef?.present(SFSafariViewController(url: self.learnMoreURL), animated: true)
37+
}
38+
39+
let gotItButton = snoozeButton(
40+
fromViewController: fromViewController,
41+
snoozeTitle: OWSLocalizedString(
42+
"INACTIVE_PRIMARY_DEVICE_REMINDER_MEGAPHONE_GOT_IT_BUTTON",
43+
comment: "Title for a button in an in-app megaphone about a user's inactive primary device, temporarily dismissing the megaphone."
44+
)
45+
)
46+
setButtons(primary: gotItButton, secondary: learnMoreButton)
47+
}
48+
49+
@available(*, unavailable, message: "Use other constructor!")
50+
required init(coder: NSCoder) {
51+
owsFail("Use other constructor!")
52+
}
53+
}

Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController+Notifications.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ extension ChatListViewController {
9191
name: .backupPlanChanged,
9292
object: nil
9393
)
94+
NotificationCenter.default.addObserver(
95+
self,
96+
selector: #selector(reloadExperienceUpgrades),
97+
name: .inactivePrimaryDeviceChanged,
98+
object: nil
99+
)
94100

95101
viewState.backupDownloadProgressViewState.downloadQueueStatus =
96102
DependenciesBridge.shared.backupAttachmentDownloadQueueStatusReporter.currentStatus()
@@ -285,6 +291,13 @@ extension ChatListViewController {
285291
db.read { viewState.backupDownloadProgressViewState.refetchDBState(tx: $0) }
286292
viewState.backupDownloadProgressView.update(viewState: viewState.backupDownloadProgressViewState)
287293
}
294+
295+
@objc
296+
private func reloadExperienceUpgrades() {
297+
AssertIsOnMainThread()
298+
299+
_ = ExperienceUpgradeManager.presentNext(fromViewController: self)
300+
}
288301
}
289302

290303
// MARK: - Notifications

Signal/translations/en.lproj/Localizable.strings

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4207,6 +4207,18 @@
42074207
/* Title for an in-app megaphone about a user's inactive linked device. */
42084208
"INACTIVE_LINKED_DEVICE_REMINDER_MEGAPHONE_TITLE" = "Inactive Linked Device";
42094209

4210+
/* Body for an in-app megaphone about a user's inactive primary device. */
4211+
"INACTIVE_PRIMARY_DEVICE_REMINDER_MEGAPHONE_BODY" = "Open Signal on your phone to keep your account active";
4212+
4213+
/* Title for a button in an in-app megaphone about a user's inactive primary device, temporarily dismissing the megaphone. */
4214+
"INACTIVE_PRIMARY_DEVICE_REMINDER_MEGAPHONE_GOT_IT_BUTTON" = "Got It";
4215+
4216+
/* Title for a button in an in-app megaphone about a user's inactive linked device, indicating the user wants to learn more. */
4217+
"INACTIVE_PRIMARY_DEVICE_REMINDER_MEGAPHONE_LEARN_MORE_BUTTON" = "Learn more";
4218+
4219+
/* Title for an in-app megaphone about a user's inactive primary device. */
4220+
"INACTIVE_PRIMARY_DEVICE_REMINDER_MEGAPHONE_TITLE" = "Inactive Primary Device";
4221+
42104222
/* Label reminding the user that they are in archive mode, and that muted chats remain archived when they receive a new message. */
42114223
"INBOX_VIEW_ARCHIVE_MODE_MUTED_CHATS_REMINDER" = "Muted chats that are archived will remain archived when a new message arrives.";
42124224

SignalServiceKit/Dependencies/DependenciesBridge.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ public class DependenciesBridge {
113113
public let identityKeyMismatchManager: IdentityKeyMismatchManager
114114
public let identityManager: OWSIdentityManager
115115
public let inactiveLinkedDeviceFinder: InactiveLinkedDeviceFinder
116+
public let inactivePrimaryDeviceStore: InactivePrimaryDeviceStore
116117
let incomingCallEventSyncMessageManager: IncomingCallEventSyncMessageManager
117118
let incomingCallLogEventSyncMessageManager: IncomingCallLogEventSyncMessageManager
118119
public let incomingPniChangeNumberProcessor: IncomingPniChangeNumberProcessor
@@ -244,6 +245,7 @@ public class DependenciesBridge {
244245
identityKeyMismatchManager: IdentityKeyMismatchManager,
245246
identityManager: OWSIdentityManager,
246247
inactiveLinkedDeviceFinder: InactiveLinkedDeviceFinder,
248+
inactivePrimaryDeviceStore: InactivePrimaryDeviceStore,
247249
incomingCallEventSyncMessageManager: IncomingCallEventSyncMessageManager,
248250
incomingCallLogEventSyncMessageManager: IncomingCallLogEventSyncMessageManager,
249251
incomingPniChangeNumberProcessor: IncomingPniChangeNumberProcessor,
@@ -374,6 +376,7 @@ public class DependenciesBridge {
374376
self.identityKeyMismatchManager = identityKeyMismatchManager
375377
self.identityManager = identityManager
376378
self.inactiveLinkedDeviceFinder = inactiveLinkedDeviceFinder
379+
self.inactivePrimaryDeviceStore = inactivePrimaryDeviceStore
377380
self.incomingCallEventSyncMessageManager = incomingCallEventSyncMessageManager
378381
self.incomingCallLogEventSyncMessageManager = incomingCallLogEventSyncMessageManager
379382
self.incomingPniChangeNumberProcessor = incomingPniChangeNumberProcessor

SignalServiceKit/Environment/AppSetup.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,13 +977,16 @@ public class AppSetup {
977977
whoAmIManager: whoAmIManager,
978978
)
979979

980+
let inactivePrimaryDeviceStore = InactivePrimaryDeviceStore()
981+
980982
let chatConnectionManager = ChatConnectionManagerImpl(
981983
accountManager: tsAccountManager,
982984
appExpiry: appExpiry,
983985
appReadiness: appReadiness,
984986
db: db,
985987
libsignalNet: libsignalNet,
986988
registrationStateChangeManager: registrationStateChangeManager,
989+
inactivePrimaryDeviceStore: inactivePrimaryDeviceStore,
987990
userDefaults: appContext.appUserDefaults()
988991
)
989992

@@ -1486,6 +1489,7 @@ public class AppSetup {
14861489
identityKeyMismatchManager: identityKeyMismatchManager,
14871490
identityManager: identityManager,
14881491
inactiveLinkedDeviceFinder: inactiveLinkedDeviceFinder,
1492+
inactivePrimaryDeviceStore: inactivePrimaryDeviceStore,
14891493
incomingCallEventSyncMessageManager: incomingCallEventSyncMessageManager,
14901494
incomingCallLogEventSyncMessageManager: incomingCallLogEventSyncMessageManager,
14911495
incomingPniChangeNumberProcessor: incomingPniChangeNumberProcessor,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//
2+
// Copyright 2025 Signal Messenger, LLC
3+
// SPDX-License-Identifier: AGPL-3.0-only
4+
//
5+
6+
extension Notification.Name {
7+
public static let inactivePrimaryDeviceChanged = Notification.Name("inactivePrimaryDeviceChanged")
8+
}
9+
10+
public class InactivePrimaryDeviceStore: NSObject {
11+
private enum StoreKeys {
12+
static let hasInactivePrimaryDeviceAlert: String = "hasInactivePrimaryDevice"
13+
}
14+
15+
private let kvStore: KeyValueStore
16+
17+
public override init() {
18+
self.kvStore = KeyValueStore(collection: "InactivePrimaryDeviceStore")
19+
}
20+
21+
public func setValueForInactivePrimaryDeviceAlert(
22+
value: Bool,
23+
transaction: DBWriteTransaction
24+
) {
25+
kvStore.setBool(
26+
value,
27+
key: StoreKeys.hasInactivePrimaryDeviceAlert,
28+
transaction: transaction
29+
)
30+
}
31+
32+
public func valueForInactivePrimaryDeviceAlert(transaction: DBReadTransaction) -> Bool {
33+
return kvStore.getBool(
34+
StoreKeys.hasInactivePrimaryDeviceAlert,
35+
transaction: transaction
36+
) ?? false
37+
}
38+
}

SignalServiceKit/Megaphones/ExperienceUpgrade.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ extension ExperienceUpgrade {
116116
.newLinkedDeviceNotification,
117117
.createUsernameReminder,
118118
.inactiveLinkedDeviceReminder,
119+
.inactivePrimaryDeviceReminder,
119120
.pinReminder,
120121
.contactPermissionReminder,
121122
.backupKeyReminder,

0 commit comments

Comments
 (0)