diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index cb334a8a8..7c6dc5bae 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -251,15 +251,39 @@ class CoreContext: ObservableObject { } } } - }, onAuthenticationRequested: { (_: Core, authInfo: AuthInfo, method: AuthMethod) in - guard let username = authInfo.username, let server = authInfo.authorizationServer, !server.isEmpty else { + }, onAuthenticationRequested: { (core: Core, authInfo: AuthInfo, method: AuthMethod) in + guard let username = authInfo.username, let domain = authInfo.domain, let realm = authInfo.realm else { Log.error("Authentication requested but either username [\(String(describing: authInfo.username))], domain [\(String(describing: authInfo.domain))] or server [\(String(describing: authInfo.authorizationServer))] is nil or empty!") return } + if method == .Bearer { - Log.info("Authentication requested method is Bearer, starting Single Sign On activity with server URL \(server) and username \(username)") - self.bearerAuthInfoPendingPasswordUpdate = authInfo - SingleSignOnManager.shared.setUp(ssoUrl: server, user: username) + if let server = authInfo.authorizationServer, !server.isEmpty { + Log.info("Authentication requested method is Bearer, starting Single Sign On activity with server URL \(server) and username \(username)") + self.bearerAuthInfoPendingPasswordUpdate = authInfo + SingleSignOnManager.shared.setUp(ssoUrl: server, user: username) + } + } + + if method == .HttpDigest { + guard let accountFound = core.accountList.first(where: { + $0.params?.identityAddress?.username == authInfo.username && + $0.params?.identityAddress?.domain == authInfo.domain + }) else { + Log.info("[CoreContext] Failed to find account matching auth info, aborting auth dialog") + return + } + + let identity = "\(authInfo.username ?? "username")@\(authInfo.domain ?? "domain")" + Log.info("[CoreContext] Authentication requested method is HttpDigest, showing dialog asking user for password for identity [\(identity)]") + + DispatchQueue.main.async { + NotificationCenter.default.post( + name: NSNotification.Name("PasswordUpdate"), + object: nil, + userInfo: ["address": "sip:" + identity] + ) + } } }, onTransferStateChanged: { (_: Core, transferred: Call, callState: Call.State) in Log.info("[CoreContext] Transferred call \(transferred.remoteAddress!.asStringUriOnly()) state changed \(callState)") diff --git a/Linphone/Localizable/cs.lproj/Localizable.strings b/Linphone/Localizable/cs.lproj/Localizable.strings index 32fe2cf8f..b273f077e 100644 --- a/Linphone/Localizable/cs.lproj/Localizable.strings +++ b/Linphone/Localizable/cs.lproj/Localizable.strings @@ -22,6 +22,9 @@ "account_settings_avpf_title" = "AVPF"; "account_settings_conference_factory_uri_title" = "URI konfiguračního serveru konference"; "account_settings_cpim_in_basic_conversations_title" = "Použít CPIM v \"základních\" konverzacích"; +"account_settings_dialog_invalid_password_title" = "Vyžadováno ověření"; +"account_settings_dialog_invalid_password_message" = "Připojení se nezdařilo, protože chybí nebo je neplatné ověření účtu\n%@.\n\nMůžete znovu zadat heslo nebo zkontrolovat nastavení účtu v konfiguraci."; +"account_settings_dialog_invalid_password_hint" = "Nové heslo"; "account_settings_enable_ice_title" = "Povolit ICE"; "account_settings_enable_turn_title" = "Povolit TURN"; "account_settings_expire_title" = "Platnost (v sekundách)"; diff --git a/Linphone/Localizable/en.lproj/Localizable.strings b/Linphone/Localizable/en.lproj/Localizable.strings index 8b3824f94..84af55f12 100644 --- a/Linphone/Localizable/en.lproj/Localizable.strings +++ b/Linphone/Localizable/en.lproj/Localizable.strings @@ -38,6 +38,9 @@ "account_settings_ccmp_server_url_title" = "CCMP server URL"; "account_settings_conference_factory_uri_title" = "Conference factory URI"; "account_settings_cpim_in_basic_conversations_title" = "Use CPIM in \"basic\" conversations"; +"account_settings_dialog_invalid_password_title" = "Authentication needed"; +"account_settings_dialog_invalid_password_message" = "Connection failed because authentication is missing or invalid for account \n%@.\n\nYou can provide password again, or check your account configuration in the settings."; +"account_settings_dialog_invalid_password_hint" = "Password"; "account_settings_enable_ice_title" = "Enable ICE"; "account_settings_enable_turn_title" = "Enable TURN"; "account_settings_expire_title" = "Expire (in seconds)"; diff --git a/Linphone/Localizable/fr.lproj/Localizable.strings b/Linphone/Localizable/fr.lproj/Localizable.strings index 907950da3..8e7891cea 100644 --- a/Linphone/Localizable/fr.lproj/Localizable.strings +++ b/Linphone/Localizable/fr.lproj/Localizable.strings @@ -38,6 +38,9 @@ "account_settings_ccmp_server_url_title" = "URL du serveur CCMP"; "account_settings_conference_factory_uri_title" = "URI du serveur de conversations"; "account_settings_cpim_in_basic_conversations_title" = "Utiliser CPIM dans les conversations \"basiques\""; +"account_settings_dialog_invalid_password_title" = "Autentification requise"; +"account_settings_dialog_invalid_password_message" = "La connexion a échoué pour le compte \n%@.\n\nVous pouvez renseigner votre mot de passe à nouveau ou bien vérifier les options de configuration de votre compte."; +"account_settings_dialog_invalid_password_hint" = "Mot de passe"; "account_settings_enable_ice_title" = "Activer ICE"; "account_settings_enable_turn_title" = "Activer TURN"; "account_settings_expire_title" = "Expiration (en secondes)"; diff --git a/Linphone/Localizable/uk.lproj/Localizable.strings b/Linphone/Localizable/uk.lproj/Localizable.strings index b7bc5531f..d892e94fb 100644 --- a/Linphone/Localizable/uk.lproj/Localizable.strings +++ b/Linphone/Localizable/uk.lproj/Localizable.strings @@ -298,6 +298,9 @@ "account_settings_bundle_mode_title" = "Режим об'єднання"; "account_settings_ccmp_server_url_title" = "URL-адреса сервера CCMP"; "account_settings_conference_factory_uri_title" = "URI ресурсу конференцій"; +"account_settings_dialog_invalid_password_title" = "Потрібна автентифікація"; +"account_settings_dialog_invalid_password_message" = "З’єднання не вдалося, оскільки автентифікація відсутня або недійсна для облікового запису\n%@.\n\nВи можете ввести пароль ще раз або перевірити конфігурацію облікового запису в налаштуваннях."; +"account_settings_dialog_invalid_password_hint" = "Новий пароль"; "account_settings_enable_ice_title" = "Увімкнути ICE"; "account_settings_enable_turn_title" = "Увімкнути TURN"; "account_settings_expire_title" = "Спливає (в секундах)"; diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 37dde86b2..14898305d 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -82,6 +82,9 @@ struct ContentView: View { @State var isShowConversationInfoPopup: Bool = false @State var conversationInfoPopupText: String = "" + @State var isShowUpdatePasswordPopup: Bool = false + @State var passwordUpdateAddress: String = "" + var body: some View { GeometryReader { geometry in VStack(spacing: 0) { @@ -1293,6 +1296,18 @@ struct ContentView: View { } } + if isShowUpdatePasswordPopup { + PopupUpdatePassword( + isShowUpdatePasswordPopup: $isShowUpdatePasswordPopup, + passwordUpdateAddress: $passwordUpdateAddress + ) + .background(.black.opacity(0.65)) + .zIndex(3) + .onTapGesture { + self.isShowUpdatePasswordPopup.toggle() + } + } + if telecomManager.meetingWaitingRoomDisplayed { MeetingWaitingRoomFragment() .zIndex(3) @@ -1353,6 +1368,10 @@ struct ContentView: View { .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("CoreStarted"))) { _ in accountProfileViewModel.setAvatarModel() } + .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("PasswordUpdate")).compactMap { $0.userInfo?["address"] as? String }) { address in + passwordUpdateAddress = address + isShowUpdatePasswordPopup = true + } } .overlay { if isMenuOpen { diff --git a/Linphone/UI/Main/Fragments/PopupUpdatePassword.swift b/Linphone/UI/Main/Fragments/PopupUpdatePassword.swift new file mode 100644 index 000000000..5efbfadf8 --- /dev/null +++ b/Linphone/UI/Main/Fragments/PopupUpdatePassword.swift @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import SwiftUI + +struct PopupUpdatePassword: View { + + @ObservedObject var sharedMainViewModel = SharedMainViewModel.shared + + @Binding var isShowUpdatePasswordPopup: Bool + @Binding var passwordUpdateAddress: String + + @State private var passwordPopupText: String = "" + @State private var isSecured: Bool = true + + @FocusState var isPasswordFocused: Bool + + var body: some View { + GeometryReader { geometry in + VStack(alignment: .leading) { + Text("account_settings_dialog_invalid_password_title") + .default_text_style_800(styleSize: 16) + .frame(alignment: .leading) + .padding(.bottom, 2) + + Text(String(format: String(localized: "account_settings_dialog_invalid_password_message"), passwordUpdateAddress)) + .default_text_style(styleSize: 15) + .padding(.bottom, 20) + + ZStack(alignment: .trailing) { + Group { + if isSecured { + SecureField("account_settings_dialog_invalid_password_hint", text: $passwordPopupText) + .default_text_style(styleSize: 15) + .frame(height: 25) + .focused($isPasswordFocused) + } else { + TextField("account_settings_dialog_invalid_password_hint", text: $passwordPopupText) + .default_text_style(styleSize: 15) + .disableAutocorrection(true) + .autocapitalization(.none) + .frame(height: 25) + .focused($isPasswordFocused) + } + } + + Button(action: { + isSecured.toggle() + }, label: { + Image(self.isSecured ? "eye-slash" : "eye") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 20, height: 20) + }) + } + .padding(.horizontal, 20) + .padding(.vertical, 15) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isPasswordFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .padding(.bottom) + + Button(action: { + isShowUpdatePasswordPopup = false + }, label: { + Text("dialog_cancel") + .default_text_style_orange_600(styleSize: 20) + .frame(height: 35) + .frame(maxWidth: .infinity) + }) + .padding(.horizontal, 20) + .padding(.vertical, 10) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(Color.orangeMain500, lineWidth: 1) + ) + .padding(.bottom, 10) + + Button(action: { + setNewPassword() + isShowUpdatePasswordPopup = false + }, label: { + Text("dialog_ok") + .default_text_style_white_600(styleSize: 20) + .frame(height: 35) + .frame(maxWidth: .infinity) + }) + .padding(.horizontal, 20) + .padding(.vertical, 10) + .background(passwordPopupText.isEmpty ? Color.orangeMain100 : Color.orangeMain500) + .cornerRadius(60) + .disabled(passwordPopupText.isEmpty) + } + .padding(.horizontal, 20) + .padding(.vertical, 20) + .background(.white) + .cornerRadius(20) + .padding(.horizontal) + .frame(maxHeight: .infinity) + .shadow(color: Color.orangeMain500, radius: 0, x: 0, y: 2) + .frame(maxWidth: SharedMainViewModel.shared.maxWidth) + .position(x: geometry.size.width / 2, y: geometry.size.height / 2) + } + } + + func setNewPassword() { + CoreContext.shared.doOnCoreQueue { core in + Log.info("[SetNewPassword] ---- \(core.defaultAccount?.params?.identityAddress?.asStringUriOnly() ?? "No account found") \(passwordUpdateAddress)") + + if let account = core.accountList.first { $0.params?.identityAddress?.asStringUriOnly() == passwordUpdateAddress } { + let authInfo = account.findAuthInfo() + if (authInfo != nil) { + Log.info( + "[SetNewPassword] Updating password for username \(authInfo!.username) using auth info \(authInfo!)" + ) + authInfo!.password = passwordPopupText + core.addAuthInfo(info: authInfo!) + core.refreshRegisters() + } else { + Log.warn( + "[SetNewPassword] Failed to find auth info for account \(account.params?.identityAddress?.asStringUriOnly())" + ) + } + } + } + } +} + +#Preview { + PopupUpdatePassword(isShowUpdatePasswordPopup: .constant(true), passwordUpdateAddress: .constant("example@sip.linphone.org")) +} diff --git a/Linphone/UI/Main/Fragments/PopupViewWithTextField.swift b/Linphone/UI/Main/Fragments/PopupViewWithTextField.swift index 6311c3b05..58d22c217 100644 --- a/Linphone/UI/Main/Fragments/PopupViewWithTextField.swift +++ b/Linphone/UI/Main/Fragments/PopupViewWithTextField.swift @@ -1,9 +1,21 @@ -// -// PopupViewWithTextField.swift -// Linphone -// -// Created by Benoît Martins on 12/11/2024. -// +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import SwiftUI diff --git a/LinphoneApp.xcodeproj/project.pbxproj b/LinphoneApp.xcodeproj/project.pbxproj index 55a268cfd..804acf4d3 100644 --- a/LinphoneApp.xcodeproj/project.pbxproj +++ b/LinphoneApp.xcodeproj/project.pbxproj @@ -192,6 +192,7 @@ D7E6D0552AEBFCCE00A57AAF /* ContactsInnerFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6D0542AEBFCCE00A57AAF /* ContactsInnerFragment.swift */; }; D7EAACCF2AD6ED8000AA6A8A /* PermissionsFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EAACCE2AD6ED8000AA6A8A /* PermissionsFragment.swift */; }; D7EFD1E42CD11F70005E67CD /* EphemeralFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EFD1E32CD11F53005E67CD /* EphemeralFragment.swift */; }; + D7F01ADE2E1D0DA3006942C0 /* PopupUpdatePassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7F01ADD2E1D0D92006942C0 /* PopupUpdatePassword.swift */; }; D7F4D9CB2B5FD27200CDCD76 /* CallsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7F4D9CA2B5FD27200CDCD76 /* CallsListFragment.swift */; }; D7F5F6412C359F3B007FCF2F /* SipAddressesPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7F5F6402C359F3B007FCF2F /* SipAddressesPopup.swift */; }; D7FB55112AD447FD00A5AB15 /* RegisterFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FB55102AD447FD00A5AB15 /* RegisterFragment.swift */; }; @@ -408,6 +409,7 @@ D7E6D0542AEBFCCE00A57AAF /* ContactsInnerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsInnerFragment.swift; sourceTree = ""; }; D7EAACCE2AD6ED8000AA6A8A /* PermissionsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsFragment.swift; sourceTree = ""; }; D7EFD1E32CD11F53005E67CD /* EphemeralFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EphemeralFragment.swift; sourceTree = ""; }; + D7F01ADD2E1D0D92006942C0 /* PopupUpdatePassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupUpdatePassword.swift; sourceTree = ""; }; D7F4D9CA2B5FD27200CDCD76 /* CallsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallsListFragment.swift; sourceTree = ""; }; D7F5F6402C359F3B007FCF2F /* SipAddressesPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SipAddressesPopup.swift; sourceTree = ""; }; D7FB55102AD447FD00A5AB15 /* RegisterFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterFragment.swift; sourceTree = ""; }; @@ -782,6 +784,7 @@ D74C9CFD2ACAEC150021626A /* Fragments */ = { isa = PBXGroup; children = ( + D7F01ADD2E1D0D92006942C0 /* PopupUpdatePassword.swift */, D74C9CFE2ACAEC5E0021626A /* PopupView.swift */, D72343352AD037AF009AA24E /* ToastView.swift */, D750D3382AD3E6EE00EC99C5 /* PopupLoadingView.swift */, @@ -1342,6 +1345,7 @@ D7C48DF62AFCDF4700D938CB /* ContactInnerActionsFragment.swift in Sources */, D72343322ACEFF58009AA24E /* QRScannerController.swift in Sources */, 662B69D92B25DE18007118BF /* TelecomManager.swift in Sources */, + D7F01ADE2E1D0DA3006942C0 /* PopupUpdatePassword.swift in Sources */, D72343342ACEFFC3009AA24E /* QRScanner.swift in Sources */, 66246C6A2C622AE900973E97 /* TimeZoneExtension.swift in Sources */, 66C491FB2B24D32600CEA16D /* CoreExtension.swift in Sources */,