From e1cbcb5d7a4124bc4840daf1674fb0a854d50174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Fri, 1 Oct 2021 19:32:14 +0200 Subject: [PATCH] Add notification action to copy OTP or just inform about the copied OTP (#513) * Add notification action to copy OTP or just inform about the copied OTP The notification either shows the current OTP which can be copied by a notification action or it shows just a hint to inform about the copied OTP. This depends on the new option "autoCopyOTP". * Extract method * Set type and style one-time --- pass.xcodeproj/project.pbxproj | 6 +- .../GeneralSettingsTableViewController.swift | 102 ++++++++++-------- .../PasswordNavigationViewController.swift | 20 +++- pass/de.lproj/Localizable.strings | 14 ++- pass/en.lproj/Localizable.strings | 14 ++- .../CredentialProviderViewController.swift | 2 +- .../Controllers/ExtensionViewController.swift | 2 +- passKit/Helpers/DefaultsKeys.swift | 2 + passKit/Helpers/Globals.swift | 4 + .../NotificationCenterDispatcher.swift | 47 ++++++++ passKit/Helpers/Utils.swift | 18 ---- 11 files changed, 155 insertions(+), 76 deletions(-) create mode 100644 passKit/Helpers/NotificationCenterDispatcher.swift diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index 2bf067c..ad14dc6 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -78,6 +78,7 @@ 30B00F5526D59562004DAC61 /* PasscodeLockViewControllerForExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B00F5426D59562004DAC61 /* PasscodeLockViewControllerForExtension.swift */; }; 30B00F5626D597A8004DAC61 /* PasscodeLockViewControllerForExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B00F5426D59562004DAC61 /* PasscodeLockViewControllerForExtension.swift */; }; 30B04860209A5141001013CA /* PasswordTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B0485F209A5141001013CA /* PasswordTest.swift */; }; + 30B331772704DBEE00D64A99 /* NotificationCenterDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B331762704DBEE00D64A99 /* NotificationCenterDispatcher.swift */; }; 30B4C7BA24084AAA008B86F7 /* PasswordGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B4C7B924084AAA008B86F7 /* PasswordGenerator.swift */; }; 30BAC8C622E3BAAF00438475 /* TestBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAC8C422E3BAAF00438475 /* TestBase.swift */; }; 30BAC8C722E3BAAF00438475 /* TestPGPKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAC8C522E3BAAF00438475 /* TestPGPKeys.swift */; }; @@ -372,6 +373,7 @@ 30A86F94230F237000F821A4 /* CryptoFrameworkTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFrameworkTest.swift; sourceTree = ""; }; 30B00F5426D59562004DAC61 /* PasscodeLockViewControllerForExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasscodeLockViewControllerForExtension.swift; sourceTree = ""; }; 30B0485F209A5141001013CA /* PasswordTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordTest.swift; sourceTree = ""; }; + 30B331762704DBEE00D64A99 /* NotificationCenterDispatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenterDispatcher.swift; sourceTree = ""; }; 30B4C7B924084AAA008B86F7 /* PasswordGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordGenerator.swift; sourceTree = ""; }; 30BAC8C422E3BAAF00438475 /* TestBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestBase.swift; sourceTree = ""; }; 30BAC8C522E3BAAF00438475 /* TestPGPKeys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestPGPKeys.swift; sourceTree = ""; }; @@ -742,10 +744,10 @@ 9AFC87E025B3B556008D6060 /* Services */ = { isa = PBXGroup; children = ( - 9AFC87E125B3B5C6008D6060 /* PasswordNavigationDataSource.swift */, 9AFC87EF25B514AD008D6060 /* PasswordDecryptor.swift */, 9AFC87FF25B51EC3008D6060 /* PasswordEncryptor.swift */, 9AFC87F725B51742008D6060 /* PasswordManager.swift */, + 9AFC87E125B3B5C6008D6060 /* PasswordNavigationDataSource.swift */, ); path = Services; sourceTree = ""; @@ -875,6 +877,7 @@ 30697C2421F63C590064FCAC /* Globals.swift */, 3032327322C7F710009EBD9C /* KeyFileManager.swift */, 30BAC8CC22E3BB9700438475 /* KeyStore.swift */, + 30B331762704DBEE00D64A99 /* NotificationCenterDispatcher.swift */, 30697C2321F63C580064FCAC /* NotificationNames.swift */, 302202EE222F14E400555236 /* SearchBarScope.swift */, 30697C2721F63C590064FCAC /* Utils.swift */, @@ -1515,6 +1518,7 @@ A2AA934422DE30DD00D79A00 /* PGPAgent.swift in Sources */, 30697C3B21F63C990064FCAC /* String+Localization.swift in Sources */, 302E85612125ECC70031BA64 /* Parser.swift in Sources */, + 30B331772704DBEE00D64A99 /* NotificationCenterDispatcher.swift in Sources */, 30CCA91A232591320048CA51 /* ObjectivePGPInterface.swift in Sources */, 30697C4621F63CAB0064FCAC /* GitCredential.swift in Sources */, 30B4C7BA24084AAA008B86F7 /* PasswordGenerator.swift in Sources */, diff --git a/pass/Controllers/GeneralSettingsTableViewController.swift b/pass/Controllers/GeneralSettingsTableViewController.swift index ef57e29..4a944b2 100644 --- a/pass/Controllers/GeneralSettingsTableViewController.swift +++ b/pass/Controllers/GeneralSettingsTableViewController.swift @@ -28,6 +28,14 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController { return uiSwitch }() + let autoCopyOTPSwitch: UISwitch = { + let uiSwitch = UISwitch() + uiSwitch.onTintColor = Colors.systemBlue + uiSwitch.sizeToFit() + uiSwitch.addTarget(self, action: #selector(autoCopyOTPSwitchAction(_:)), for: UIControl.Event.valueChanged) + return uiSwitch + }() + let rememberPGPPassphraseSwitch: UISwitch = { let uiSwitch = UISwitch() uiSwitch.onTintColor = Colors.systemBlue @@ -91,6 +99,7 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController { [.title: "HidePasswordImages".localize(), .action: "none"], [.title: "HideUnknownFields".localize(), .action: "none"], [.title: "HideOtpFields".localize(), .action: "none"], + [.title: "AutoCopyOTP".localize(), .action: "none"], ], ] super.viewDidLoad() @@ -98,71 +107,64 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = super.tableView(tableView, cellForRowAt: indexPath) + cell.accessoryType = .none + cell.selectionStyle = .none switch cell.textLabel!.text! { case "HideUnknownFields".localize(): - cell.accessoryType = .none - let detailButton = UIButton(type: .detailDisclosure) - hideUnknownSwitch.frame = CGRect(x: detailButton.bounds.width + 10, y: 0, width: hideUnknownSwitch.bounds.width, height: hideUnknownSwitch.bounds.height) - detailButton.frame = CGRect(x: 0, y: 5, width: detailButton.bounds.width, height: detailButton.bounds.height) - detailButton.addTarget(self, action: #selector(GeneralSettingsTableViewController.tapHideUnknownSwitchDetailButton(_:)), for: UIControl.Event.touchDown) - let accessoryView = UIView(frame: CGRect(x: 0, y: 0, width: detailButton.bounds.width + hideUnknownSwitch.bounds.width + 10, height: cell.contentView.bounds.height)) - accessoryView.addSubview(detailButton) - accessoryView.addSubview(hideUnknownSwitch) - hideUnknownSwitch.center.y = accessoryView.center.y - detailButton.center.y = accessoryView.center.y - cell.accessoryView = accessoryView - cell.selectionStyle = .none hideUnknownSwitch.isOn = Defaults.isHideUnknownOn + addDetailButton(to: cell, for: hideUnknownSwitch, with: #selector(tapHideUnknownSwitchDetailButton)) case "HideOtpFields".localize(): - cell.accessoryType = .none - let detailButton = UIButton(type: .detailDisclosure) - hideOTPSwitch.frame = CGRect(x: detailButton.bounds.width + 10, y: 0, width: hideOTPSwitch.bounds.width, height: hideOTPSwitch.bounds.height) - detailButton.frame = CGRect(x: 0, y: 5, width: detailButton.bounds.width, height: detailButton.bounds.height) - detailButton.addTarget(self, action: #selector(GeneralSettingsTableViewController.tapHideOTPSwitchDetailButton(_:)), for: UIControl.Event.touchDown) - let accessoryView = UIView(frame: CGRect(x: 0, y: 0, width: detailButton.bounds.width + hideOTPSwitch.bounds.width + 10, height: cell.contentView.bounds.height)) - accessoryView.addSubview(detailButton) - accessoryView.addSubview(hideOTPSwitch) - hideOTPSwitch.center.y = accessoryView.center.y - detailButton.center.y = accessoryView.center.y - cell.accessoryView = accessoryView - cell.selectionStyle = .none hideOTPSwitch.isOn = Defaults.isHideOTPOn + addDetailButton(to: cell, for: hideOTPSwitch, with: #selector(tapHideOTPSwitchDetailButton)) case "RememberPgpKeyPassphrase".localize(): - cell.accessoryType = .none - cell.selectionStyle = .none cell.accessoryView = rememberPGPPassphraseSwitch case "RememberGitCredentialPassphrase".localize(): - cell.accessoryType = .none - cell.selectionStyle = .none cell.accessoryView = rememberGitCredentialPassphraseSwitch case "ShowFolders".localize(): - cell.accessoryType = .none - cell.selectionStyle = .none cell.accessoryView = showFolderSwitch case "EnableGPGID".localize(): - cell.accessoryType = .none - cell.selectionStyle = .none cell.accessoryView = enableGPGIDSwitch case "HidePasswordImages".localize(): - cell.accessoryType = .none - let detailButton = UIButton(type: .detailDisclosure) - hidePasswordImagesSwitch.frame = CGRect(x: detailButton.bounds.width + 10, y: 0, width: hidePasswordImagesSwitch.bounds.width, height: hidePasswordImagesSwitch.bounds.height) - detailButton.frame = CGRect(x: 0, y: 5, width: detailButton.bounds.width, height: detailButton.bounds.height) - detailButton.addTarget(self, action: #selector(GeneralSettingsTableViewController.tapHidePasswordImagesSwitchDetailButton(_:)), for: UIControl.Event.touchDown) - let accessoryView = UIView(frame: CGRect(x: 0, y: 0, width: detailButton.bounds.width + hidePasswordImagesSwitch.bounds.width + 10, height: cell.contentView.bounds.height)) - accessoryView.addSubview(detailButton) - accessoryView.addSubview(hidePasswordImagesSwitch) - hidePasswordImagesSwitch.center.y = accessoryView.center.y - detailButton.center.y = accessoryView.center.y - cell.accessoryView = accessoryView - cell.selectionStyle = .none hidePasswordImagesSwitch.isOn = Defaults.isHidePasswordImagesOn + addDetailButton(to: cell, for: hidePasswordImagesSwitch, with: #selector(tapHidePasswordImagesSwitchDetailButton)) + case "AutoCopyOTP".localize(): + autoCopyOTPSwitch.isOn = Defaults.autoCopyOTP + addDetailButton(to: cell, for: autoCopyOTPSwitch, with: #selector(tapAutoCopyOTPSwitchDetailButton)) default: break } return cell } + private func addDetailButton(to cell: UITableViewCell, for uiSwitch: UISwitch, with action: Selector) { + let detailButton = UIButton(type: .detailDisclosure) + uiSwitch.frame = CGRect( + x: detailButton.bounds.width + 10, + y: 0, + width: uiSwitch.bounds.width, + height: uiSwitch.bounds.height + ) + detailButton.frame = CGRect( + x: 0, + y: 5, + width: detailButton.bounds.width, + height: detailButton.bounds.height + ) + detailButton.addTarget(self, action: action, for: UIControl.Event.touchDown) + let accessoryViewFrame = CGRect( + x: 0, + y: 0, + width: detailButton.bounds.width + uiSwitch.bounds.width + 10, + height: cell.contentView.bounds.height + ) + let accessoryView = UIView(frame: accessoryViewFrame) + accessoryView.addSubview(detailButton) + accessoryView.addSubview(uiSwitch) + uiSwitch.center.y = accessoryView.center.y + detailButton.center.y = accessoryView.center.y + cell.accessoryView = accessoryView + } + @objc func tapHideUnknownSwitchDetailButton(_: Any?) { let alertMessage = "HideUnknownFieldsExplanation.".localize() @@ -178,6 +180,13 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController { Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil) } + @objc + func tapAutoCopyOTPSwitchDetailButton(_: Any?) { + let alertMessage = "AutoCopyOTPExplanation.".localize() + let alertTitle = "AutoCopyOTP".localize() + Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil) + } + @objc func tapHidePasswordImagesSwitchDetailButton(_: Any?) { let alertMessage = "HidePasswordImagesExplanation.".localize() @@ -197,6 +206,11 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController { NotificationCenter.default.post(name: .passwordDetailDisplaySettingChanged, object: nil) } + @objc + func autoCopyOTPSwitchAction(_: Any?) { + Defaults.autoCopyOTP = autoCopyOTPSwitch.isOn + } + @objc func rememberPGPPassphraseSwitchAction(_: Any?) { Defaults.isRememberPGPPassphraseOn = rememberPGPPassphraseSwitch.isOn diff --git a/pass/Controllers/PasswordNavigationViewController.swift b/pass/Controllers/PasswordNavigationViewController.swift index a46ff1f..36e43c0 100644 --- a/pass/Controllers/PasswordNavigationViewController.swift +++ b/pass/Controllers/PasswordNavigationViewController.swift @@ -80,8 +80,26 @@ class PasswordNavigationViewController: UIViewController { } private func requestNotificationPermission() { + // Ask for permission to receive notifications + let notificationCenter = UNUserNotificationCenter.current() let permissionOptions = UNAuthorizationOptions(arrayLiteral: .alert) - UNUserNotificationCenter.current().requestAuthorization(options: permissionOptions) { _, _ in } + notificationCenter.requestAuthorization(options: permissionOptions) { _, _ in } + + // Register notification action + let copyAction = UNNotificationAction( + identifier: Globals.otpNotificationCopyAction, + title: "CopyToPasteboard".localize(), + options: UNNotificationActionOptions(rawValue: 0) + ) + let otpCategory = UNNotificationCategory( + identifier: Globals.otpNotificationCategory, + actions: [copyAction], + intentIdentifiers: [], + hiddenPreviewsBodyPlaceholder: "", + options: [] + ) + notificationCenter.setNotificationCategories([otpCategory]) + notificationCenter.delegate = NotificationCenterDispatcher.shared } override func viewWillAppear(_ animated: Bool) { diff --git a/pass/de.lproj/Localizable.strings b/pass/de.lproj/Localizable.strings index ac9f741..9cd5cc3 100644 --- a/pass/de.lproj/Localizable.strings +++ b/pass/de.lproj/Localizable.strings @@ -18,11 +18,15 @@ "Contributors" = "Mitwirkende"; // OTP related -"TimeBased" = "Zeitbasiert"; -"HmacBased" = "HMAC-basiert"; -"None" = "Kein valides Token"; -"ExpiresIn" = "(läuft in %ds ab)"; -"OTPFor" = "Einmalpasswort für %@"; +"TimeBased" = "Zeitbasiert"; +"HmacBased" = "HMAC-basiert"; +"None" = "Kein valides Token"; +"ExpiresIn" = "(läuft in %ds ab)"; +"OTPForPassword" = "Einmalpasswort für %@"; +"OTPForPasswordCopied" = "Einmalpasswort für %@ kopiert"; +"CopyToPasteboard" = "In Zwischenablage kopieren"; +"AutoCopyOTP" = "OTPs automatisch kopieren"; +"AutoCopyOTPExplanation." = "Nachdem Login und Passwort automatisch in die vorgesehenen Felder gefüllt wurden, wird eine Benachrichtigung mit dem aktuellen Einmalpasswort angezeigt. Dieses kann in die Zwischenablage kopiert werden. Ist diese Option aktiviert, geschieht das Kopieren automatisch."; // General (error) messages "Error" = "Fehler"; diff --git a/pass/en.lproj/Localizable.strings b/pass/en.lproj/Localizable.strings index 79a89fa..40f3d27 100644 --- a/pass/en.lproj/Localizable.strings +++ b/pass/en.lproj/Localizable.strings @@ -18,11 +18,15 @@ "Contributors" = "Contributors"; // OTP related -"TimeBased" = "time-based"; -"HmacBased" = "HMAC-based"; -"None" = "None"; -"ExpiresIn" = "(expires in %ds)"; -"OTPFor" = "One-time Password for %@"; +"TimeBased" = "time-based"; +"HmacBased" = "HMAC-based"; +"None" = "None"; +"ExpiresIn" = "(expires in %ds)"; +"OTPForPassword" = "One-time password for %@"; +"OTPForPasswordCopied" = "One-time password for %@ copied"; +"CopyToPasteboard" = "Copy to pasteboard"; +"AutoCopyOTP" = "Automatically Copy OTPs"; +"AutoCopyOTPExplanation." = "After username and password have been auto-filled into a form by Pass, a notification is shown with the current one-time password which can be copied to the pasteboard. Enabling this option automatically copies the current one-time password."; // General (error) messages "Error" = "Error"; diff --git a/passAutoFillExtension/Controllers/CredentialProviderViewController.swift b/passAutoFillExtension/Controllers/CredentialProviderViewController.swift index 8a26416..7ad6be1 100644 --- a/passAutoFillExtension/Controllers/CredentialProviderViewController.swift +++ b/passAutoFillExtension/Controllers/CredentialProviderViewController.swift @@ -19,7 +19,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController { }() private lazy var credentialProvider: CredentialProvider = { [unowned self] in - CredentialProvider(viewController: self, extensionContext: extensionContext, afterDecryption: Utils.showNotificationWithOTP) + CredentialProvider(viewController: self, extensionContext: extensionContext, afterDecryption: NotificationCenterDispatcher.showOTPNotification) }() private lazy var passwordsTableEntries = PasswordStore.shared.fetchPasswordEntityCoreData(withDir: false) diff --git a/passExtension/Controllers/ExtensionViewController.swift b/passExtension/Controllers/ExtensionViewController.swift index f2f6cb7..92e3f07 100644 --- a/passExtension/Controllers/ExtensionViewController.swift +++ b/passExtension/Controllers/ExtensionViewController.swift @@ -20,7 +20,7 @@ class ExtensionViewController: UIViewController { }() private lazy var credentialProvider: CredentialProvider = { [unowned self] in - CredentialProvider(viewController: self, extensionContext: extensionContext!, afterDecryption: Utils.showNotificationWithOTP) + CredentialProvider(viewController: self, extensionContext: extensionContext!, afterDecryption: NotificationCenterDispatcher.showOTPNotification) }() private lazy var passwordsTableEntries = PasswordStore.shared.fetchPasswordEntityCoreData(withDir: false) diff --git a/passKit/Helpers/DefaultsKeys.swift b/passKit/Helpers/DefaultsKeys.swift index d1fe882..afba469 100644 --- a/passKit/Helpers/DefaultsKeys.swift +++ b/passKit/Helpers/DefaultsKeys.swift @@ -57,4 +57,6 @@ public extension DefaultsKeys { var passwordGenerator: DefaultsKey { .init("passwordGenerator", defaultValue: PasswordGenerator()) } var encryptInArmored: DefaultsKey { .init("encryptInArmored", defaultValue: false) } + + var autoCopyOTP: DefaultsKey { .init("autoCopyOTP", defaultValue: false) } } diff --git a/passKit/Helpers/Globals.swift b/passKit/Helpers/Globals.swift index b5b48f6..9950a06 100644 --- a/passKit/Helpers/Globals.swift +++ b/passKit/Helpers/Globals.swift @@ -47,6 +47,10 @@ public final class Globals { public static let oneTimePasswordDots = "••••••" public static let passwordFont = UIFont(name: "Courier-Bold", size: UIFont.labelFontSize - 1) + public static let otpNotification = bundleIdentifier + ".notification.otp" + public static let otpNotificationCategory = bundleIdentifier + ".notification.otp.category" + public static let otpNotificationCopyAction = bundleIdentifier + ".notification.otp.action.copy" + // UI related public static let tableCellButtonSize = CGFloat(20.0) diff --git a/passKit/Helpers/NotificationCenterDispatcher.swift b/passKit/Helpers/NotificationCenterDispatcher.swift new file mode 100644 index 0000000..2c411e4 --- /dev/null +++ b/passKit/Helpers/NotificationCenterDispatcher.swift @@ -0,0 +1,47 @@ +// +// NotificationCenterDispatcher.swift +// passKit +// +// Created by Danny Moesch on 29.09.21. +// Copyright © 2021 Bob Sun. All rights reserved. +// + +public class NotificationCenterDispatcher: NSObject, UNUserNotificationCenterDelegate { + public static let shared = NotificationCenterDispatcher() + + public func userNotificationCenter(_: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + if response.actionIdentifier == Globals.otpNotificationCopyAction { + if let otp = response.notification.request.content.userInfo["otp"] as? String { + UIPasteboard.general.string = otp + } + } + completionHandler() + } + + public static func showOTPNotification(password: Password) { + guard let otp = password.currentOtp else { + return + } + let notificationCenter = UNUserNotificationCenter.current() + notificationCenter.getNotificationSettings { state in + guard state.authorizationStatus == .authorized else { + return + } + let content = UNMutableNotificationContent() + if Defaults.autoCopyOTP { + content.title = "OTPForPasswordCopied".localize(password.name) + } else { + content.title = "OTPForPassword".localize(password.name) + content.body = otp + content.categoryIdentifier = Globals.otpNotificationCategory + content.userInfo = [ + "path": password.namePath, + "otp": otp, + ] + } + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) + let request = UNNotificationRequest(identifier: Globals.otpNotification, content: content, trigger: trigger) + notificationCenter.add(request) + } + } +} diff --git a/passKit/Helpers/Utils.swift b/passKit/Helpers/Utils.swift index 16e854a..f078590 100644 --- a/passKit/Helpers/Utils.swift +++ b/passKit/Helpers/Utils.swift @@ -70,22 +70,4 @@ public enum Utils { return passphrase } } - - public static func showNotificationWithOTP(password: Password) { - guard let otp = password.currentOtp else { - return - } - let notificationCenter = UNUserNotificationCenter.current() - notificationCenter.getNotificationSettings { state in - guard state.authorizationStatus == .authorized else { - return - } - let content = UNMutableNotificationContent() - content.title = "OTPFor".localize(password.name) - content.body = otp - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) - let request = UNNotificationRequest(identifier: "otpNotification", content: content, trigger: trigger) - notificationCenter.add(request) - } - } }