Add UX for rotating the AEP, from Backup Settings
This commit is contained in:
parent
60a97f04ec
commit
3aebe57906
@ -2515,6 +2515,7 @@
|
||||
D9388C912DA475230048D4F9 /* BackupSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9388C902DA4751F0048D4F9 /* BackupSettingsStore.swift */; };
|
||||
D9392DE02D88AA5800728C01 /* ZoomableMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9392DDF2D88AA5000728C01 /* ZoomableMediaView.swift */; };
|
||||
D93964B62E038C7B00094117 /* BackupSettingsAttachmentUploadTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93964B52E038C7B00094117 /* BackupSettingsAttachmentUploadTracker.swift */; };
|
||||
D93BDD942E43064500779BD8 /* BackupKeepKeySafeSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93BDD932E43063B00779BD8 /* BackupKeepKeySafeSheet.swift */; };
|
||||
D93CE1242A5C84F600D916B7 /* OWSSyncRequestMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93CE1232A5C84F600D916B7 /* OWSSyncRequestMessage.swift */; };
|
||||
D93EDC042AE9E3CD0004BDD9 /* DonationSettingsViewController+MySupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93EDC032AE9E3CD0004BDD9 /* DonationSettingsViewController+MySupport.swift */; };
|
||||
D93F4D5A2D800DD20042926C /* AvatarDefaultColorManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93F4D552D7FAC3C0042926C /* AvatarDefaultColorManager.swift */; };
|
||||
@ -2526,7 +2527,7 @@
|
||||
D945319E2CE53CEB004DAB30 /* SubscriptionRedemptionNecessityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D945319D2CE53CC8004DAB30 /* SubscriptionRedemptionNecessityChecker.swift */; };
|
||||
D9495A6D2C7683D100843BC1 /* TSOutgoingMessageRecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9495A6C2C7683D100843BC1 /* TSOutgoingMessageRecipientState.swift */; };
|
||||
D9495A702C76965600843BC1 /* TSOutgoingMessageRecipientStateTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9495A6E2C76963F00843BC1 /* TSOutgoingMessageRecipientStateTest.swift */; };
|
||||
D949C4052DF3A597007E095C /* BackupOnboardingConfirmKeyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D949C4042DF3A588007E095C /* BackupOnboardingConfirmKeyViewController.swift */; };
|
||||
D949C4052DF3A597007E095C /* BackupConfirmKeyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D949C4042DF3A588007E095C /* BackupConfirmKeyViewController.swift */; };
|
||||
D94AEB3A2D28837F00B03D7A /* MasterKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D94AEB392D28837A00B03D7A /* MasterKey.swift */; };
|
||||
D94AEB3C2D28940A00B03D7A /* PreKeyTaskAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D94AEB3B2D28940500B03D7A /* PreKeyTaskAPIClient.swift */; };
|
||||
D94D67CD2C9DEF870091B485 /* BackupArchivePostFrameRestoreActionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D94D67CC2C9DEF6E0091B485 /* BackupArchivePostFrameRestoreActionManager.swift */; };
|
||||
@ -6466,6 +6467,7 @@
|
||||
D938CD5F29283402006FB16A /* Paypal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Paypal.swift; sourceTree = "<group>"; };
|
||||
D9392DDF2D88AA5000728C01 /* ZoomableMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableMediaView.swift; sourceTree = "<group>"; };
|
||||
D93964B52E038C7B00094117 /* BackupSettingsAttachmentUploadTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupSettingsAttachmentUploadTracker.swift; sourceTree = "<group>"; };
|
||||
D93BDD932E43063B00779BD8 /* BackupKeepKeySafeSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupKeepKeySafeSheet.swift; sourceTree = "<group>"; };
|
||||
D93CE1232A5C84F600D916B7 /* OWSSyncRequestMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSSyncRequestMessage.swift; sourceTree = "<group>"; };
|
||||
D93EDC032AE9E3CD0004BDD9 /* DonationSettingsViewController+MySupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DonationSettingsViewController+MySupport.swift"; sourceTree = "<group>"; };
|
||||
D93F4D552D7FAC3C0042926C /* AvatarDefaultColorManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarDefaultColorManager.swift; sourceTree = "<group>"; };
|
||||
@ -6478,7 +6480,7 @@
|
||||
D945319D2CE53CC8004DAB30 /* SubscriptionRedemptionNecessityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRedemptionNecessityChecker.swift; sourceTree = "<group>"; };
|
||||
D9495A6C2C7683D100843BC1 /* TSOutgoingMessageRecipientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSOutgoingMessageRecipientState.swift; sourceTree = "<group>"; };
|
||||
D9495A6E2C76963F00843BC1 /* TSOutgoingMessageRecipientStateTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSOutgoingMessageRecipientStateTest.swift; sourceTree = "<group>"; };
|
||||
D949C4042DF3A588007E095C /* BackupOnboardingConfirmKeyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupOnboardingConfirmKeyViewController.swift; sourceTree = "<group>"; };
|
||||
D949C4042DF3A588007E095C /* BackupConfirmKeyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupConfirmKeyViewController.swift; sourceTree = "<group>"; };
|
||||
D94AEB392D28837A00B03D7A /* MasterKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterKey.swift; sourceTree = "<group>"; };
|
||||
D94AEB3B2D28940500B03D7A /* PreKeyTaskAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreKeyTaskAPIClient.swift; sourceTree = "<group>"; };
|
||||
D94D67CC2C9DEF6E0091B485 /* BackupArchivePostFrameRestoreActionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchivePostFrameRestoreActionManager.swift; sourceTree = "<group>"; };
|
||||
@ -12500,7 +12502,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D9DE34FB2DEE76E6005099D7 /* Onboarding */,
|
||||
D949C4042DF3A588007E095C /* BackupConfirmKeyViewController.swift */,
|
||||
D93FA5BE2DE77E440013879E /* BackupEnablingManager.swift */,
|
||||
D93BDD932E43063B00779BD8 /* BackupKeepKeySafeSheet.swift */,
|
||||
D98CA2B22DF2450E0060370E /* BackupRecordKeyViewController.swift */,
|
||||
D932C0EA2E13AD3600FEF9C3 /* BackupSettingsAttachmentDownloadTracker.swift */,
|
||||
D93964B52E038C7B00094117 /* BackupSettingsAttachmentUploadTracker.swift */,
|
||||
@ -12905,7 +12909,6 @@
|
||||
D9DE34FB2DEE76E6005099D7 /* Onboarding */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D949C4042DF3A588007E095C /* BackupOnboardingConfirmKeyViewController.swift */,
|
||||
D99934592DE97BB6002C9196 /* BackupOnboardingCoordinator.swift */,
|
||||
D9DE34FC2DEE775D005099D7 /* BackupOnboardingIntroViewController.swift */,
|
||||
D98CA2AC2DF14A830060370E /* BackupOnboardingKeyIntroViewController.swift */,
|
||||
@ -16783,10 +16786,11 @@
|
||||
B95A765C2B76C5BB00AA7E97 /* AvatarViewPresentationContextProvider.swift in Sources */,
|
||||
66485EB02CCC515A00B8613F /* BackupArchiveInternalErrorViewController.swift in Sources */,
|
||||
66A1F4E62E03641D0095DE4B /* BackupBGProcessingTaskRunner.swift in Sources */,
|
||||
D949C4052DF3A597007E095C /* BackupConfirmKeyViewController.swift in Sources */,
|
||||
041A5F072E05B3F900FAED05 /* BackupEnablementMegaphone.swift in Sources */,
|
||||
D9DF21EC2E21BD6600A962B2 /* BackupEnablingManager.swift in Sources */,
|
||||
D93BDD942E43064500779BD8 /* BackupKeepKeySafeSheet.swift in Sources */,
|
||||
04E66D402DFC825B0059DBAC /* BackupKeyReminderMegaphone.swift in Sources */,
|
||||
D949C4052DF3A597007E095C /* BackupOnboardingConfirmKeyViewController.swift in Sources */,
|
||||
D999345A2DE97BBC002C9196 /* BackupOnboardingCoordinator.swift in Sources */,
|
||||
D9DE34FD2DEE7765005099D7 /* BackupOnboardingIntroViewController.swift in Sources */,
|
||||
D98CA2AD2DF14A890060370E /* BackupOnboardingKeyIntroViewController.swift in Sources */,
|
||||
|
||||
78
Signal/Backups/BackupConfirmKeyViewController.swift
Normal file
78
Signal/Backups/BackupConfirmKeyViewController.swift
Normal file
@ -0,0 +1,78 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import SignalServiceKit
|
||||
import SignalUI
|
||||
|
||||
class BackupConfirmKeyViewController: EnterAccountEntropyPoolViewController {
|
||||
private let aep: AccountEntropyPool
|
||||
|
||||
init(
|
||||
aep: AccountEntropyPool,
|
||||
onContinue: @escaping () -> Void,
|
||||
onSeeKeyAgain: @escaping () -> Void,
|
||||
) {
|
||||
self.aep = aep
|
||||
|
||||
super.init()
|
||||
|
||||
configure(
|
||||
aepValidationPolicy: .acceptOnly(aep),
|
||||
colorConfig: ColorConfig(
|
||||
background: UIColor.Signal.groupedBackground,
|
||||
aepEntryBackground: UIColor.Signal.secondaryGroupedBackground,
|
||||
),
|
||||
headerStrings: HeaderStrings(
|
||||
title: OWSLocalizedString(
|
||||
"BACKUP_ONBOARDING_CONFIRM_KEY_TITLE",
|
||||
comment: "Title for a view asking users to confirm their 'Backup Key'."
|
||||
),
|
||||
subtitle: OWSLocalizedString(
|
||||
"BACKUP_ONBOARDING_CONFIRM_KEY_SUBTITLE",
|
||||
comment: "Subtitle for a view asking users to confirm their 'Backup Key'."
|
||||
)
|
||||
),
|
||||
footerButtonConfig: FooterButtonConfig(
|
||||
title: BackupKeepKeySafeSheet.seeKeyAgainButtonTitle,
|
||||
action: {
|
||||
onSeeKeyAgain()
|
||||
}
|
||||
),
|
||||
onEntryConfirmed: { [weak self] aep in
|
||||
guard let self else { return }
|
||||
|
||||
present(
|
||||
BackupKeepKeySafeSheet(
|
||||
onContinue: onContinue,
|
||||
onSeeKeyAgain: onSeeKeyAgain
|
||||
),
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
#if DEBUG
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview {
|
||||
let aep = try! AccountEntropyPool(key: String(
|
||||
repeating: "a",
|
||||
count: AccountEntropyPool.Constants.byteLength
|
||||
))
|
||||
|
||||
return UINavigationController(
|
||||
rootViewController: BackupConfirmKeyViewController(
|
||||
aep: aep,
|
||||
onContinue: { print("Continuing...!") },
|
||||
onSeeKeyAgain: { print("Seeing key again...!") }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#endif
|
||||
56
Signal/Backups/BackupKeepKeySafeSheet.swift
Normal file
56
Signal/Backups/BackupKeepKeySafeSheet.swift
Normal file
@ -0,0 +1,56 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import SignalUI
|
||||
import SignalServiceKit
|
||||
|
||||
class BackupKeepKeySafeSheet: HeroSheetViewController {
|
||||
static var seeKeyAgainButtonTitle: String {
|
||||
return OWSLocalizedString(
|
||||
"BACKUP_ONBOARDING_CONFIRM_KEY_SEE_KEY_AGAIN_BUTTON_TITLE",
|
||||
comment: "Title for a button offering to let users see their 'Backup Key'."
|
||||
)
|
||||
}
|
||||
|
||||
/// - Parameter onContinue
|
||||
/// Called after dismissing this sheet when the user taps "Continue",
|
||||
/// indicating acknowledgement of the "keep key safe" warning.
|
||||
/// - Parameter onSeeKeyAgain
|
||||
/// Called after dismissing this sheet when the user taps "See Key Again",
|
||||
/// indicating they want another opportunity to record their key.
|
||||
init(
|
||||
onContinue: @escaping () -> Void,
|
||||
onSeeKeyAgain: @escaping () -> Void,
|
||||
) {
|
||||
super.init(
|
||||
hero: .image(.backupsKey),
|
||||
title: OWSLocalizedString(
|
||||
"BACKUP_ONBOARDING_CONFIRM_KEY_KEEP_KEY_SAFE_SHEET_TITLE",
|
||||
comment: "Title for a sheet warning users to their 'Backup Key' safe."
|
||||
),
|
||||
body: OWSLocalizedString(
|
||||
"BACKUP_ONBOARDING_CONFIRM_KEY_KEEP_KEY_SAFE_SHEET_BODY",
|
||||
comment: "Body for a sheet warning users to their 'Backup Key' safe."
|
||||
),
|
||||
primary: .button(HeroSheetViewController.Button(
|
||||
title: CommonStrings.continueButton,
|
||||
action: { sheet in
|
||||
sheet.dismiss(animated: true) {
|
||||
onContinue()
|
||||
}
|
||||
}
|
||||
)),
|
||||
secondary: .button(HeroSheetViewController.Button(
|
||||
title: Self.seeKeyAgainButtonTitle,
|
||||
style: .secondary,
|
||||
action: .custom({ sheet in
|
||||
sheet.dismiss(animated: true) {
|
||||
onSeeKeyAgain()
|
||||
}
|
||||
}),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -8,27 +8,76 @@ import SignalUI
|
||||
import SwiftUI
|
||||
|
||||
class BackupRecordKeyViewController: HostingController<BackupRecordKeyView> {
|
||||
private let onCompletion: (BackupRecordKeyViewController) -> Void
|
||||
private let viewModel: BackupRecordKeyViewModel
|
||||
private let isOnboardingFlow: Bool
|
||||
struct Option: OptionSet {
|
||||
let rawValue: Int
|
||||
|
||||
/// Show a "done" button instead of the nav bar "back" button. Note that
|
||||
/// this prevents dismissal using standard navigation.
|
||||
static let replaceNavBarBackWithDoneButton = Option(rawValue: 1 << 0)
|
||||
/// Show a "continue" button in the view footer. Not compatible with
|
||||
/// `.showCreateNewKeyButton`.
|
||||
static let showContinueButton = Option(rawValue: 1 << 1)
|
||||
/// Show a "create new key" button in the view footer. Not compatible
|
||||
/// with `.showContinueButton`.
|
||||
static let showCreateNewKeyButton = Option(rawValue: 1 << 2)
|
||||
}
|
||||
|
||||
enum AEPMode {
|
||||
/// The user's current AEP, which must only be viewed after device auth.
|
||||
case current(AccountEntropyPool, LocalDeviceAuthentication.AuthSuccess)
|
||||
/// A new candidate AEP.
|
||||
case newCandidate(AccountEntropyPool)
|
||||
|
||||
fileprivate var aep: AccountEntropyPool {
|
||||
switch self {
|
||||
case .current(let aep, _): return aep
|
||||
case .newCandidate(let aep): return aep
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let onCompletion: (BackupRecordKeyViewController) -> Void
|
||||
private let onCreateNewKeyPressed: (BackupRecordKeyViewController) -> Void
|
||||
private let options: [Option]
|
||||
private let viewModel: BackupRecordKeyViewModel
|
||||
|
||||
/// - Parameter onCreateNewKeyPressed
|
||||
/// Called when the user taps the "create new key" button. Only relevant if
|
||||
/// the `.showCreateNewKey` option is passed.
|
||||
/// - Parameter onCompletion
|
||||
/// Called when the user "completes" recording their Backup Key, such as by
|
||||
/// tapping a "continue" or "done" button (depending on the passed options).
|
||||
init(
|
||||
aep: AccountEntropyPool,
|
||||
isOnboardingFlow: Bool,
|
||||
onCompletion: @escaping (BackupRecordKeyViewController) -> Void,
|
||||
aepMode: AEPMode,
|
||||
options: [Option],
|
||||
onCreateNewKeyPressed: @escaping (BackupRecordKeyViewController) -> Void = { _ in },
|
||||
onCompletion: @escaping (BackupRecordKeyViewController) -> Void
|
||||
) {
|
||||
self.onCompletion = onCompletion
|
||||
self.isOnboardingFlow = isOnboardingFlow
|
||||
self.viewModel = BackupRecordKeyViewModel(aep: aep, isOnboardingFlow: isOnboardingFlow)
|
||||
self.onCreateNewKeyPressed = onCreateNewKeyPressed
|
||||
self.options = options
|
||||
self.viewModel = BackupRecordKeyViewModel(aep: aepMode.aep, options: options)
|
||||
|
||||
super.init(wrappedView: BackupRecordKeyView(viewModel: viewModel))
|
||||
|
||||
viewModel.actionsDelegate = self
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
if options.contains(.replaceNavBarBackWithDoneButton) {
|
||||
navigationController?.interactivePopGestureRecognizer?.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
if options.contains(.replaceNavBarBackWithDoneButton) {
|
||||
navigationController?.interactivePopGestureRecognizer?.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension BackupRecordKeyViewController: BackupRecordKeyViewModel.ActionsDelegate {
|
||||
func copyToClipboard(_ aep: AccountEntropyPool) {
|
||||
fileprivate func copyToClipboard(_ aep: AccountEntropyPool) {
|
||||
UIPasteboard.general.setItems(
|
||||
[[UIPasteboard.typeAutomatic: aep.rawData]],
|
||||
options: [.expirationDate: Date().addingTimeInterval(60)]
|
||||
@ -41,9 +90,13 @@ extension BackupRecordKeyViewController: BackupRecordKeyViewModel.ActionsDelegat
|
||||
toast.presentToastView(from: .bottom, of: view, inset: view.safeAreaInsets.bottom + 8)
|
||||
}
|
||||
|
||||
func complete() {
|
||||
fileprivate func complete() {
|
||||
onCompletion(self)
|
||||
}
|
||||
|
||||
fileprivate func createNewKey() {
|
||||
onCreateNewKeyPressed(self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
@ -52,17 +105,21 @@ private class BackupRecordKeyViewModel: ObservableObject {
|
||||
protocol ActionsDelegate: AnyObject {
|
||||
func copyToClipboard(_ aep: AccountEntropyPool)
|
||||
func complete()
|
||||
func createNewKey()
|
||||
}
|
||||
|
||||
let aep: AccountEntropyPool
|
||||
let options: [BackupRecordKeyViewController.Option]
|
||||
|
||||
weak var actionsDelegate: ActionsDelegate?
|
||||
let aep: AccountEntropyPool
|
||||
let isOnboardingFlow: Bool
|
||||
|
||||
init(aep: AccountEntropyPool, isOnboardingFlow: Bool) {
|
||||
init(aep: AccountEntropyPool, options: [BackupRecordKeyViewController.Option]) {
|
||||
self.aep = aep
|
||||
self.isOnboardingFlow = isOnboardingFlow
|
||||
self.options = options
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
func copyToClipboard() {
|
||||
actionsDelegate?.copyToClipboard(aep)
|
||||
}
|
||||
@ -70,8 +127,14 @@ private class BackupRecordKeyViewModel: ObservableObject {
|
||||
func complete() {
|
||||
actionsDelegate?.complete()
|
||||
}
|
||||
|
||||
func createNewKey() {
|
||||
actionsDelegate?.createNewKey()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
struct BackupRecordKeyView: View {
|
||||
fileprivate let viewModel: BackupRecordKeyViewModel
|
||||
|
||||
@ -119,7 +182,7 @@ struct BackupRecordKeyView: View {
|
||||
))
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(Color.Signal.secondaryLabel)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.background {
|
||||
@ -130,8 +193,19 @@ struct BackupRecordKeyView: View {
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
} pinnedFooter: {
|
||||
// Only add "continue" button if we're in the onboarding flow.
|
||||
if viewModel.isOnboardingFlow {
|
||||
if viewModel.options.contains(.showCreateNewKeyButton) {
|
||||
Button {
|
||||
viewModel.createNewKey()
|
||||
} label: {
|
||||
Text(OWSLocalizedString(
|
||||
"BACKUP_RECORD_KEY_CREATE_NEW_KEY_BUTTON_TITLE",
|
||||
comment: "Title for a button allowing users to create a new 'Backup Key'."
|
||||
))
|
||||
.foregroundStyle(Color.Signal.ultramarine)
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.options.contains(.showContinueButton) {
|
||||
Button {
|
||||
viewModel.complete()
|
||||
} label: {
|
||||
@ -149,15 +223,15 @@ struct BackupRecordKeyView: View {
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
.background(Color.Signal.groupedBackground)
|
||||
.navigationBarBackButtonHidden(!viewModel.isOnboardingFlow)
|
||||
.navigationBarItems(leading: viewModel.isOnboardingFlow ? nil : doneButton)
|
||||
.navigationBarBackButtonHidden(viewModel.options.contains(.replaceNavBarBackWithDoneButton))
|
||||
.navigationBarItems(leading: viewModel.options.contains(.replaceNavBarBackWithDoneButton) ? doneButton : nil)
|
||||
}
|
||||
|
||||
private var doneButton: some View {
|
||||
Button(action: {
|
||||
Button {
|
||||
viewModel.complete()
|
||||
}) {
|
||||
Text(OWSLocalizedString("BUTTON_DONE", comment: "Label for generic done button."))
|
||||
} label: {
|
||||
Text(CommonStrings.doneButton)
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(Color.Signal.label)
|
||||
}
|
||||
@ -227,26 +301,44 @@ private struct DisplayAccountEntropyPoolView: View {
|
||||
#if DEBUG
|
||||
|
||||
private extension BackupRecordKeyViewModel {
|
||||
static func forPreview() -> BackupRecordKeyViewModel {
|
||||
static func forPreview(
|
||||
options: [BackupRecordKeyViewController.Option],
|
||||
) -> BackupRecordKeyViewModel {
|
||||
class PreviewActionsDelegate: ActionsDelegate {
|
||||
func copyToClipboard(_ aep: AccountEntropyPool) {
|
||||
print("Copying \(aep.rawData) to clipboard...!")
|
||||
}
|
||||
|
||||
func complete() {
|
||||
print("Continuing...!")
|
||||
}
|
||||
func copyToClipboard(_ aep: AccountEntropyPool) { print("Copying \(aep.rawData) to clipboard...!") }
|
||||
func complete() { print("Continuing...!") }
|
||||
func createNewKey() { print("Creating new key...!") }
|
||||
}
|
||||
|
||||
let viewModel = BackupRecordKeyViewModel(aep: AccountEntropyPool(), isOnboardingFlow: true)
|
||||
let viewModel = BackupRecordKeyViewModel(
|
||||
aep: AccountEntropyPool(),
|
||||
options: options,
|
||||
)
|
||||
let actionsDelegate = PreviewActionsDelegate()
|
||||
ObjectRetainer.retainObject(actionsDelegate, forLifetimeOf: viewModel)
|
||||
return viewModel
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
BackupRecordKeyView(viewModel: .forPreview())
|
||||
#Preview("NavBarDone") {
|
||||
NavigationView {
|
||||
BackupRecordKeyView(viewModel: .forPreview(options: [.replaceNavBarBackWithDoneButton]))
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("CreateNewKey") {
|
||||
NavigationView {
|
||||
BackupRecordKeyView(viewModel: .forPreview(options: [
|
||||
.replaceNavBarBackWithDoneButton,
|
||||
.showCreateNewKeyButton,
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("ContinueButton") {
|
||||
NavigationView {
|
||||
BackupRecordKeyView(viewModel: .forPreview(options: [.showContinueButton]))
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@ -28,6 +28,7 @@ class BackupSettingsViewController:
|
||||
private let backupSubscriptionManager: BackupSubscriptionManager
|
||||
private let db: DB
|
||||
private let deviceSleepManager: DeviceSleepManager
|
||||
private let svr: SecureValueRecovery
|
||||
private let tsAccountManager: TSAccountManager
|
||||
|
||||
private let onLoadAction: OnLoadAction?
|
||||
@ -57,6 +58,7 @@ class BackupSettingsViewController:
|
||||
backupSubscriptionManager: DependenciesBridge.shared.backupSubscriptionManager,
|
||||
db: DependenciesBridge.shared.db,
|
||||
deviceSleepManager: deviceSleepManager,
|
||||
svr: DependenciesBridge.shared.svr,
|
||||
tsAccountManager: DependenciesBridge.shared.tsAccountManager,
|
||||
)
|
||||
}
|
||||
@ -76,6 +78,7 @@ class BackupSettingsViewController:
|
||||
backupSubscriptionManager: BackupSubscriptionManager,
|
||||
db: DB,
|
||||
deviceSleepManager: DeviceSleepManager,
|
||||
svr: SecureValueRecovery,
|
||||
tsAccountManager: TSAccountManager
|
||||
) {
|
||||
owsPrecondition(
|
||||
@ -100,6 +103,7 @@ class BackupSettingsViewController:
|
||||
self.backupSubscriptionManager = backupSubscriptionManager
|
||||
self.db = db
|
||||
self.deviceSleepManager = deviceSleepManager
|
||||
self.svr = svr
|
||||
self.tsAccountManager = tsAccountManager
|
||||
|
||||
self.onLoadAction = onLoadAction
|
||||
@ -916,61 +920,141 @@ class BackupSettingsViewController:
|
||||
return
|
||||
}
|
||||
|
||||
guard await LocalDeviceAuthentication().performBiometricAuth() else {
|
||||
guard let authSuccess = await LocalDeviceAuthentication().performBiometricAuth() else {
|
||||
return
|
||||
}
|
||||
|
||||
navigationController?.pushViewController(
|
||||
BackupRecordKeyViewController(
|
||||
aep: aep,
|
||||
isOnboardingFlow: false,
|
||||
onCompletion: { [weak self] recordKeyViewController in
|
||||
self?.showKeyRecordedConfirmationSheet(
|
||||
fromViewController: recordKeyViewController
|
||||
)
|
||||
}
|
||||
),
|
||||
animated: true
|
||||
let recordKeyViewController = BackupRecordKeyViewController(
|
||||
aepMode: .current(aep, authSuccess),
|
||||
options: [.replaceNavBarBackWithDoneButton, .showCreateNewKeyButton],
|
||||
onCreateNewKeyPressed: { [weak self] recordKeyViewController in
|
||||
guard let self else { return }
|
||||
|
||||
// If appropriate, the warning sheet will let the user continue
|
||||
// in a "create new AEP" flow.
|
||||
showCreateNewBackupKeyWarningSheet(fromViewController: recordKeyViewController)
|
||||
},
|
||||
onCompletion: { [weak self] recordKeyViewController in
|
||||
guard let self else { return }
|
||||
|
||||
let keepKeySafeSheet = BackupKeepKeySafeSheet(
|
||||
onContinue: { [weak self] in
|
||||
guard let self else { return }
|
||||
navigationController?.popToViewController(self, animated: true)
|
||||
},
|
||||
onSeeKeyAgain: {
|
||||
// The sheet is already dismissed, and the topmost
|
||||
// view is a BackupRecordKeyViewController.
|
||||
}
|
||||
)
|
||||
recordKeyViewController.present(
|
||||
keepKeySafeSheet,
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
)
|
||||
navigationController?.interactivePopGestureRecognizer?.isEnabled = false
|
||||
|
||||
navigationController?.pushViewController(recordKeyViewController, animated: true)
|
||||
}
|
||||
|
||||
private func showKeyRecordedConfirmationSheet(fromViewController: BackupRecordKeyViewController) {
|
||||
let sheet = HeroSheetViewController(
|
||||
private func showCreateNewBackupKeyWarningSheet(
|
||||
fromViewController: BackupRecordKeyViewController,
|
||||
) {
|
||||
let currentBackupPlan = db.read { tx in
|
||||
backupSettingsStore.backupPlan(tx: tx)
|
||||
}
|
||||
|
||||
// Only allow creating a new Backup Key if Backups are already disabled.
|
||||
let primary: HeroSheetViewController.Button
|
||||
let secondary: HeroSheetViewController.Button?
|
||||
switch currentBackupPlan {
|
||||
case .disabled:
|
||||
primary = HeroSheetViewController.Button(
|
||||
title: CommonStrings.continueButton,
|
||||
action: { sheet in
|
||||
sheet.dismiss(animated: true) { [weak self] in
|
||||
guard let self else { return }
|
||||
showRecordNewBackupKey()
|
||||
}
|
||||
}
|
||||
)
|
||||
secondary = .dismissing(
|
||||
title: CommonStrings.cancelButton,
|
||||
style: .secondary,
|
||||
)
|
||||
case .disabling, .free, .paid, .paidExpiringSoon, .paidAsTester:
|
||||
primary = .dismissing(title: CommonStrings.cancelButton)
|
||||
secondary = nil
|
||||
}
|
||||
|
||||
let warningSheet = HeroSheetViewController(
|
||||
hero: .image(.backupsKey),
|
||||
title: OWSLocalizedString(
|
||||
"BACKUP_ONBOARDING_CONFIRM_KEY_KEEP_KEY_SAFE_SHEET_TITLE",
|
||||
comment: "Title for a sheet warning users to their 'Backup Key' safe."
|
||||
"BACKUP_SETTINGS_CREATE_NEW_KEY_WARNING_SHEET_TITLE",
|
||||
comment: "Title for a sheet warning users about creating a new Backup Key."
|
||||
),
|
||||
body: OWSLocalizedString(
|
||||
"BACKUP_ONBOARDING_CONFIRM_KEY_KEEP_KEY_SAFE_SHEET_BODY",
|
||||
comment: "Body for a sheet warning users to their 'Backup Key' safe."
|
||||
"BACKUP_SETTINGS_CREATE_NEW_KEY_WARNING_SHEET_BODY",
|
||||
comment: "Body for a sheet warning users about creating a new Backup Key."
|
||||
),
|
||||
primary: .button(HeroSheetViewController.Button(
|
||||
title: OWSLocalizedString(
|
||||
"BUTTON_CONTINUE",
|
||||
comment: "Label for 'continue' button."
|
||||
),
|
||||
action: { [weak self] _ in
|
||||
self?.dismiss(animated: true)
|
||||
self?.navigationController?.interactivePopGestureRecognizer?.isEnabled = true
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
)),
|
||||
secondary: .button(HeroSheetViewController.Button(
|
||||
title: OWSLocalizedString(
|
||||
"BACKUP_ONBOARDING_CONFIRM_KEY_SEE_KEY_AGAIN_BUTTON_TITLE",
|
||||
comment: "Title for a button offering to let users see their 'Backup Key'."
|
||||
),
|
||||
style: .secondary,
|
||||
action: .custom({ [weak self] _ in
|
||||
self?.dismiss(animated: true)
|
||||
self?.navigationController?.interactivePopGestureRecognizer?.isEnabled = true
|
||||
})
|
||||
))
|
||||
primary: .button(primary),
|
||||
secondary: secondary.map { .button($0) },
|
||||
)
|
||||
fromViewController.present(sheet, animated: true)
|
||||
}}
|
||||
fromViewController.present(warningSheet, animated: true)
|
||||
}
|
||||
|
||||
private func showRecordNewBackupKey() {
|
||||
let newCandidateAEP = AccountEntropyPool()
|
||||
let recordKeyViewController = BackupRecordKeyViewController(
|
||||
aepMode: .newCandidate(newCandidateAEP),
|
||||
options: [.showContinueButton],
|
||||
onCompletion: { [weak self] _ in
|
||||
guard let self else { return }
|
||||
showConfirmNewBackupKey(newCandidateAEP: newCandidateAEP)
|
||||
}
|
||||
)
|
||||
|
||||
navigationController?.pushViewController(recordKeyViewController, animated: true)
|
||||
}
|
||||
|
||||
private func showConfirmNewBackupKey(newCandidateAEP: AccountEntropyPool) {
|
||||
let confirmKeyViewController = BackupConfirmKeyViewController(
|
||||
aep: newCandidateAEP,
|
||||
onContinue: { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
Logger.warn("Setting new AEP!")
|
||||
|
||||
// Persists the new AEP, and schedules all the relevant side
|
||||
// effects. A big deal!
|
||||
db.write { tx in
|
||||
self.svr.setNewAccountEntropyPoolWithSideEffects(
|
||||
newCandidateAEP,
|
||||
disablePIN: false,
|
||||
authedAccount: .implicit(),
|
||||
transaction: tx
|
||||
)
|
||||
}
|
||||
|
||||
// Pop all the way back to Backup Settings.
|
||||
navigationController?.popToViewController(self, animated: true) {
|
||||
self.presentToast(text: OWSLocalizedString(
|
||||
"BACKUP_SETTINGS_CREATE_NEW_KEY_SUCCESS_TOAST",
|
||||
comment: "Toast shown when a new Backup Key has been created successfully."
|
||||
))
|
||||
}
|
||||
},
|
||||
onSeeKeyAgain: { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
// Popping drops us back on the BackupRecordKeyViewController.
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
)
|
||||
|
||||
navigationController?.pushViewController(confirmKeyViewController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
@ -1384,6 +1468,10 @@ struct BackupSettingsView: View {
|
||||
.foregroundStyle(Color.Signal.secondaryLabel)
|
||||
}
|
||||
|
||||
SignalSection {
|
||||
BackupViewKeyView(viewModel: viewModel)
|
||||
}
|
||||
|
||||
case .disabledFailedToDisableRemotely:
|
||||
SignalSection {
|
||||
VStack(alignment: .center) {
|
||||
@ -1416,6 +1504,10 @@ struct BackupSettingsView: View {
|
||||
SignalSection {
|
||||
reenableBackupsButton
|
||||
}
|
||||
|
||||
SignalSection {
|
||||
BackupViewKeyView(viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1993,6 +2085,16 @@ private struct BackupDetailsView: View {
|
||||
)
|
||||
)
|
||||
|
||||
BackupViewKeyView(viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private struct BackupViewKeyView: View {
|
||||
let viewModel: BackupSettingsViewModel
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
viewModel.showViewBackupKey()
|
||||
} label: {
|
||||
@ -2007,7 +2109,6 @@ private struct BackupDetailsView: View {
|
||||
}
|
||||
}
|
||||
.foregroundStyle(Color.Signal.label)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -2044,6 +2145,7 @@ private extension BackupSettingsViewModel {
|
||||
func setShouldAllowBackupDownloadsOnCellular() { print("Downloads on cellular: true") }
|
||||
|
||||
func showViewBackupKey() { print("Showing View Backup Key!") }
|
||||
func showCreateNewBackupKey() { print("Showing Create New Backup Key!") }
|
||||
}
|
||||
|
||||
let viewModel = BackupSettingsViewModel(
|
||||
|
||||
@ -54,11 +54,15 @@ class BackupsReminderCoordinator {
|
||||
fromViewController.present(navController, animated: true)
|
||||
}
|
||||
|
||||
private func showRecordBackupKey(backupKeyReminderNavController: UINavigationController, aep: AccountEntropyPool) {
|
||||
private func showRecordBackupKey(
|
||||
backupKeyReminderNavController: UINavigationController,
|
||||
localDeviceAuthSuccess: LocalDeviceAuthentication.AuthSuccess,
|
||||
aep: AccountEntropyPool
|
||||
) {
|
||||
backupKeyReminderNavController.pushViewController(
|
||||
BackupRecordKeyViewController(
|
||||
aep: aep,
|
||||
isOnboardingFlow: true,
|
||||
aepMode: .current(aep, localDeviceAuthSuccess),
|
||||
options: [.showContinueButton],
|
||||
onCompletion: { [weak self] _ in
|
||||
self?.showConfirmBackupKey(backupKeyReminderNavController: backupKeyReminderNavController, aep: aep)
|
||||
},
|
||||
@ -69,7 +73,7 @@ class BackupsReminderCoordinator {
|
||||
|
||||
private func showConfirmBackupKey(backupKeyReminderNavController: UINavigationController, aep: AccountEntropyPool) {
|
||||
backupKeyReminderNavController.pushViewController(
|
||||
BackupOnboardingConfirmKeyViewController(
|
||||
BackupConfirmKeyViewController(
|
||||
aep: aep,
|
||||
onContinue: { [weak self] in
|
||||
self?.dismissHandler(false)
|
||||
@ -96,15 +100,17 @@ extension BackupsReminderCoordinator: RegistrationEnterAccountEntropyPoolPresent
|
||||
|
||||
func forgotKeyAction() {
|
||||
Task { @MainActor in
|
||||
guard await LocalDeviceAuthentication().performBiometricAuth() else {
|
||||
return
|
||||
}
|
||||
guard
|
||||
let authSuccess = await LocalDeviceAuthentication().performBiometricAuth(),
|
||||
let backupKeyReminderNavController = backupKeyReminderNavController,
|
||||
let aep = db.read(block: { accountKeyStore.getAccountEntropyPool(tx: $0) })
|
||||
else { return }
|
||||
|
||||
showRecordBackupKey(backupKeyReminderNavController: backupKeyReminderNavController, aep: aep)
|
||||
showRecordBackupKey(
|
||||
backupKeyReminderNavController: backupKeyReminderNavController,
|
||||
localDeviceAuthSuccess: authSuccess,
|
||||
aep: aep
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,116 +0,0 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import SignalServiceKit
|
||||
import SignalUI
|
||||
|
||||
class BackupOnboardingConfirmKeyViewController: EnterAccountEntropyPoolViewController {
|
||||
private let aep: AccountEntropyPool
|
||||
|
||||
init(
|
||||
aep: AccountEntropyPool,
|
||||
onContinue: @escaping () -> Void,
|
||||
onSeeKeyAgain: @escaping () -> Void,
|
||||
) {
|
||||
self.aep = aep
|
||||
|
||||
super.init()
|
||||
|
||||
configure(
|
||||
aepValidationPolicy: .acceptOnly(aep),
|
||||
colorConfig: ColorConfig(
|
||||
background: UIColor.Signal.groupedBackground,
|
||||
aepEntryBackground: UIColor.Signal.secondaryGroupedBackground,
|
||||
),
|
||||
headerStrings: HeaderStrings(
|
||||
title: OWSLocalizedString(
|
||||
"BACKUP_ONBOARDING_CONFIRM_KEY_TITLE",
|
||||
comment: "Title for a view asking users to confirm their 'Backup Key'."
|
||||
),
|
||||
subtitle: OWSLocalizedString(
|
||||
"BACKUP_ONBOARDING_CONFIRM_KEY_SUBTITLE",
|
||||
comment: "Subtitle for a view asking users to confirm their 'Backup Key'."
|
||||
)
|
||||
),
|
||||
footerButtonConfig: FooterButtonConfig(
|
||||
title: seeKeyAgainButtonTitle,
|
||||
action: {
|
||||
onSeeKeyAgain()
|
||||
}
|
||||
),
|
||||
onEntryConfirmed: { [weak self] aep in
|
||||
self?.showKeepKeySafeSheet(
|
||||
onContinue: onContinue,
|
||||
onSeeKeyAgain: onSeeKeyAgain
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var seeKeyAgainButtonTitle: String {
|
||||
return OWSLocalizedString(
|
||||
"BACKUP_ONBOARDING_CONFIRM_KEY_SEE_KEY_AGAIN_BUTTON_TITLE",
|
||||
comment: "Title for a button offering to let users see their 'Backup Key'."
|
||||
)
|
||||
}
|
||||
|
||||
private func showKeepKeySafeSheet(
|
||||
onContinue: @escaping () -> Void,
|
||||
onSeeKeyAgain: @escaping () -> Void,
|
||||
) {
|
||||
let sheet = HeroSheetViewController(
|
||||
hero: .image(.backupsKey),
|
||||
title: OWSLocalizedString(
|
||||
"BACKUP_ONBOARDING_CONFIRM_KEY_KEEP_KEY_SAFE_SHEET_TITLE",
|
||||
comment: "Title for a sheet warning users to their 'Backup Key' safe."
|
||||
),
|
||||
body: OWSLocalizedString(
|
||||
"BACKUP_ONBOARDING_CONFIRM_KEY_KEEP_KEY_SAFE_SHEET_BODY",
|
||||
comment: "Body for a sheet warning users to their 'Backup Key' safe."
|
||||
),
|
||||
primaryButton: HeroSheetViewController.Button(
|
||||
title: CommonStrings.continueButton,
|
||||
action: { sheet in
|
||||
sheet.dismiss(animated: true) {
|
||||
onContinue()
|
||||
}
|
||||
}
|
||||
),
|
||||
secondaryButton: HeroSheetViewController.Button(
|
||||
title: seeKeyAgainButtonTitle,
|
||||
style: .secondary,
|
||||
action: .custom({ sheet in
|
||||
sheet.dismiss(animated: true) {
|
||||
onSeeKeyAgain()
|
||||
}
|
||||
}),
|
||||
)
|
||||
)
|
||||
|
||||
present(sheet, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
#if DEBUG
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview {
|
||||
let aep = try! AccountEntropyPool(key: String(
|
||||
repeating: "a",
|
||||
count: AccountEntropyPool.Constants.byteLength
|
||||
))
|
||||
|
||||
return UINavigationController(
|
||||
rootViewController: BackupOnboardingConfirmKeyViewController(
|
||||
aep: aep,
|
||||
onContinue: { print("Continuing...!") },
|
||||
onSeeKeyAgain: { print("Seeing key again...!") }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#endif
|
||||
@ -90,8 +90,8 @@ class BackupOnboardingCoordinator {
|
||||
|
||||
onboardingNavController.pushViewController(
|
||||
BackupOnboardingKeyIntroViewController(
|
||||
onDeviceAuthSucceeded: { [self] in
|
||||
showRecordBackupKey()
|
||||
onDeviceAuthSucceeded: { [self] authSuccess in
|
||||
showRecordBackupKey(localDeviceAuthSuccess: authSuccess)
|
||||
}
|
||||
),
|
||||
animated: true
|
||||
@ -100,7 +100,9 @@ class BackupOnboardingCoordinator {
|
||||
|
||||
// MARK: -
|
||||
|
||||
private func showRecordBackupKey() {
|
||||
private func showRecordBackupKey(
|
||||
localDeviceAuthSuccess: LocalDeviceAuthentication.AuthSuccess
|
||||
) {
|
||||
guard
|
||||
let onboardingNavController,
|
||||
let aep = db.read(block: { accountKeyStore.getAccountEntropyPool(tx: $0) })
|
||||
@ -108,8 +110,8 @@ class BackupOnboardingCoordinator {
|
||||
|
||||
onboardingNavController.pushViewController(
|
||||
BackupRecordKeyViewController(
|
||||
aep: aep,
|
||||
isOnboardingFlow: true,
|
||||
aepMode: .current(aep, localDeviceAuthSuccess),
|
||||
options: [.showContinueButton],
|
||||
onCompletion: { [self] _ in
|
||||
showConfirmBackupKey(aep: aep)
|
||||
},
|
||||
@ -124,7 +126,7 @@ class BackupOnboardingCoordinator {
|
||||
guard let onboardingNavController else { return }
|
||||
|
||||
onboardingNavController.pushViewController(
|
||||
BackupOnboardingConfirmKeyViewController(
|
||||
BackupConfirmKeyViewController(
|
||||
aep: aep,
|
||||
onContinue: { [self] in
|
||||
Task {
|
||||
|
||||
@ -8,10 +8,10 @@ import SignalUI
|
||||
import SwiftUI
|
||||
|
||||
class BackupOnboardingKeyIntroViewController: HostingController<BackupOnboardingKeyIntroView> {
|
||||
private let onDeviceAuthSucceeded: () -> Void
|
||||
private let onDeviceAuthSucceeded: (LocalDeviceAuthentication.AuthSuccess) -> Void
|
||||
private let viewModel: BackupsOnboardingKeyIntroViewModel
|
||||
|
||||
init(onDeviceAuthSucceeded: @escaping () -> Void) {
|
||||
init(onDeviceAuthSucceeded: @escaping (LocalDeviceAuthentication.AuthSuccess) -> Void) {
|
||||
self.onDeviceAuthSucceeded = onDeviceAuthSucceeded
|
||||
self.viewModel = BackupsOnboardingKeyIntroViewModel()
|
||||
|
||||
@ -26,8 +26,8 @@ class BackupOnboardingKeyIntroViewController: HostingController<BackupOnboarding
|
||||
extension BackupOnboardingKeyIntroViewController: BackupsOnboardingKeyIntroViewModel.ActionsDelegate {
|
||||
fileprivate func onContinue() {
|
||||
Task {
|
||||
if await LocalDeviceAuthentication().performBiometricAuth() {
|
||||
onDeviceAuthSucceeded()
|
||||
if let authSuccess = await LocalDeviceAuthentication().performBiometricAuth() {
|
||||
onDeviceAuthSucceeded(authSuccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,7 +59,7 @@ class OutgoingDeviceRestoreViewModel: ObservableObject, DeviceTransferServiceObs
|
||||
}
|
||||
|
||||
func confirmTransfer() async -> Bool {
|
||||
return await LocalDeviceAuthentication().performBiometricAuth()
|
||||
return await LocalDeviceAuthentication().performBiometricAuth() != nil
|
||||
}
|
||||
|
||||
/// This uses the QuickRestore path behind the scenes to bootstrap a device transfer between two devices.
|
||||
|
||||
@ -586,6 +586,9 @@
|
||||
/* Title for a button allowing users to copy their 'Backup Key' to the clipboard. */
|
||||
"BACKUP_RECORD_KEY_COPY_TO_CLIPBOARD_BUTTON_TITLE" = "Copy to Clipboard";
|
||||
|
||||
/* Title for a button allowing users to create a new 'Backup Key'. */
|
||||
"BACKUP_RECORD_KEY_CREATE_NEW_KEY_BUTTON_TITLE" = "Create New Key";
|
||||
|
||||
/* Subtitle for a view allowing users to record their 'Backup Key'. */
|
||||
"BACKUP_RECORD_KEY_SUBTITLE" = "This key is required to recover your account and data. Store this key somewhere safe. If you lose it, you won’t be able to recover your account.";
|
||||
|
||||
@ -709,6 +712,15 @@
|
||||
/* Notice that backups is a beta feature */
|
||||
"BACKUP_SETTINGS_BETA_NOTICE_HEADER" = "This is a Beta feature that we will be adding functionality to on an ongoing basis";
|
||||
|
||||
/* Toast shown when a new Backup Key has been created successfully. */
|
||||
"BACKUP_SETTINGS_CREATE_NEW_KEY_SUCCESS_TOAST" = "New key created!";
|
||||
|
||||
/* Body for a sheet warning users about creating a new Backup Key. */
|
||||
"BACKUP_SETTINGS_CREATE_NEW_KEY_WARNING_SHEET_BODY" = "Creating a new key is only necessary if someone else knows your key. Backups must first be disabled to create a new key. You will need to re-enable and re-upload your Backup, including media, after creating a new key.";
|
||||
|
||||
/* Title for a sheet warning users about creating a new Backup Key. */
|
||||
"BACKUP_SETTINGS_CREATE_NEW_KEY_WARNING_SHEET_TITLE" = "Create A New Backup Key";
|
||||
|
||||
/* Footer for a menu section allowing users to turn off Backups. */
|
||||
"BACKUP_SETTINGS_DISABLE_BACKUPS_BUTTON_FOOTER" = "This will delete all your backup data. No new backups will be created.";
|
||||
|
||||
|
||||
@ -12,6 +12,9 @@ public struct LocalDeviceAuthentication {
|
||||
case genericError(localizedErrorMessage: String)
|
||||
}
|
||||
|
||||
/// An opaque object representing successful authentication.
|
||||
public struct AuthSuccess {}
|
||||
|
||||
public struct AttemptToken {}
|
||||
|
||||
private let context: LAContext
|
||||
@ -22,21 +25,23 @@ public struct LocalDeviceAuthentication {
|
||||
|
||||
// MARK: -
|
||||
|
||||
public func performBiometricAuth() async -> Bool {
|
||||
public func performBiometricAuth() async -> AuthSuccess? {
|
||||
let localDeviceAuthAttemptToken: AttemptToken
|
||||
|
||||
switch self.checkCanAttempt() {
|
||||
case .success(let attemptToken): localDeviceAuthAttemptToken = attemptToken
|
||||
case .failure(.notRequired): return true
|
||||
case .failure(.canceled), .failure(.genericError): return false
|
||||
case .failure(.notRequired): return AuthSuccess()
|
||||
case .failure(.canceled), .failure(.genericError): return nil
|
||||
}
|
||||
|
||||
switch await self.attempt(token: localDeviceAuthAttemptToken) {
|
||||
case .success, .failure(.notRequired): return true
|
||||
case .failure(.canceled), .failure(.genericError): return false
|
||||
case .success, .failure(.notRequired): return AuthSuccess()
|
||||
case .failure(.canceled), .failure(.genericError): return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Returns whether checking local device auth is possible. Must be called
|
||||
/// prior to calling ``attempt(token:)``.
|
||||
public func checkCanAttempt() -> Result<AttemptToken, AuthError> {
|
||||
@ -52,7 +57,7 @@ public struct LocalDeviceAuthentication {
|
||||
|
||||
/// Returns the result of performing local device authentication. Must be
|
||||
/// called after calling ``checkCanAttempt()``.
|
||||
public func attempt(token: AttemptToken) async -> Result<Void, AuthError> {
|
||||
public func attempt(token: AttemptToken) async -> Result<AuthSuccess, AuthError> {
|
||||
do {
|
||||
try await context.evaluatePolicy(
|
||||
.deviceOwnerAuthentication,
|
||||
@ -62,7 +67,7 @@ public struct LocalDeviceAuthentication {
|
||||
)
|
||||
)
|
||||
|
||||
return .success(())
|
||||
return .success(AuthSuccess())
|
||||
} catch {
|
||||
return .failure(parseAuthError(error))
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import Foundation
|
||||
import SignalServiceKit
|
||||
import Lottie
|
||||
|
||||
public class HeroSheetViewController: StackSheetViewController {
|
||||
open class HeroSheetViewController: StackSheetViewController {
|
||||
public enum Hero {
|
||||
/// Scaled image to display at the top of the sheet
|
||||
case image(UIImage)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user