From 4cf1dbd8b56082fa41f8bb9bf37dc6ced59259fc Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Wed, 24 Sep 2025 19:17:29 +0200 Subject: [PATCH] Add advanced settings to third-party SIP account login view --- .../Localizable/en.lproj/Localizable.strings | 1 + .../Localizable/fr.lproj/Localizable.strings | 1 + .../Assistant/Fragments/LoginFragment.swift | 3 + .../Fragments/RegisterFragment.swift | 15 +-- .../ThirdPartySipAccountLoginFragment.swift | 121 ++++++++++++++++-- .../Viewmodel/AccountLoginViewModel.swift | 16 ++- Linphone/Utils/KeyboardResponder.swift | 24 ++++ LinphoneApp.xcodeproj/project.pbxproj | 4 + 8 files changed, 163 insertions(+), 22 deletions(-) create mode 100644 Linphone/Utils/KeyboardResponder.swift diff --git a/Linphone/Localizable/en.lproj/Localizable.strings b/Linphone/Localizable/en.lproj/Localizable.strings index 427023c6c..30849b247 100644 --- a/Linphone/Localizable/en.lproj/Localizable.strings +++ b/Linphone/Localizable/en.lproj/Localizable.strings @@ -93,6 +93,7 @@ "assistant_third_party_sip_account_warning_explanation" = "Some features require a %@ account, such as group messaging, video conferences…\n\nThese features are hidden when you register with a third party SIP account.\n\nTo enable it in a commercial project, please contact us."; "assistant_third_party_sip_account_warning_ok" = "I understand"; "assistant_web_platform_link" = "subscribe.linphone.org"; +"authentication_id" = "Authentication ID (if different)"; "bottom_navigation_calls_label" = "Calls"; "bottom_navigation_contacts_label" = "Contacts"; "bottom_navigation_conversations_label" = "Conversations"; diff --git a/Linphone/Localizable/fr.lproj/Localizable.strings b/Linphone/Localizable/fr.lproj/Localizable.strings index 593ccef3a..363d9c56b 100644 --- a/Linphone/Localizable/fr.lproj/Localizable.strings +++ b/Linphone/Localizable/fr.lproj/Localizable.strings @@ -93,6 +93,7 @@ "assistant_third_party_sip_account_warning_explanation" = "Certaines fonctionnalités telles que les conversations de groupe, les vidéo-conférences, etc… nécessitent un compte %@.\n\nCes fonctionnalités seront masquées si vous utilisez un compte SIP tiers.\n\nPour les activer dans un projet commercial, merci de nous contacter."; "assistant_third_party_sip_account_warning_ok" = "J’ai compris"; "assistant_web_platform_link" = "subscribe.linphone.org"; +"authentication_id" = "Identifiant de connexion (si différent)"; "bottom_navigation_calls_label" = "Appels"; "bottom_navigation_contacts_label" = "Contacts"; "bottom_navigation_conversations_label" = "Conversations"; diff --git a/Linphone/UI/Assistant/Fragments/LoginFragment.swift b/Linphone/UI/Assistant/Fragments/LoginFragment.swift index b8e063153..32e9f710c 100644 --- a/Linphone/UI/Assistant/Fragments/LoginFragment.swift +++ b/Linphone/UI/Assistant/Fragments/LoginFragment.swift @@ -18,12 +18,14 @@ */ import SwiftUI +import Combine struct LoginFragment: View { @ObservedObject private var coreContext = CoreContext.shared @StateObject private var accountLoginViewModel = AccountLoginViewModel() + @StateObject private var keyboard = KeyboardResponder() @State private var isSecured: Bool = true @@ -377,6 +379,7 @@ struct LoginFragment: View { .clipped() } .frame(minHeight: geometry.size.height) + .padding(.bottom, keyboard.currentHeight) } func acceptGeneralTerms() { diff --git a/Linphone/UI/Assistant/Fragments/RegisterFragment.swift b/Linphone/UI/Assistant/Fragments/RegisterFragment.swift index b302c9400..037a8df74 100644 --- a/Linphone/UI/Assistant/Fragments/RegisterFragment.swift +++ b/Linphone/UI/Assistant/Fragments/RegisterFragment.swift @@ -26,6 +26,8 @@ struct RegisterFragment: View { @ObservedObject var registerViewModel: RegisterViewModel @ObservedObject var sharedMainViewModel = SharedMainViewModel.shared + @StateObject private var keyboard = KeyboardResponder() + @Environment(\.dismiss) var dismiss @State private var isSecured: Bool = true @@ -181,14 +183,6 @@ struct RegisterFragment: View { .autocapitalization(.none) .padding(.leading, 5) .keyboardType(.numberPad) - .toolbar { - ToolbarItemGroup(placement: .keyboard) { - Spacer() - Button("Done") { - UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) - } - } - } .onChange(of: registerViewModel.phoneNumber) { _ in if !registerViewModel.phoneNumberError.isEmpty { registerViewModel.phoneNumberError = "" @@ -355,11 +349,8 @@ struct RegisterFragment: View { .clipped() } .frame(minHeight: geometry.size.height) + .padding(.bottom, keyboard.currentHeight) } } -#Preview { - RegisterFragment(registerViewModel: RegisterViewModel()) -} - // swiftlint:enable line_length diff --git a/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountLoginFragment.swift b/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountLoginFragment.swift index 1ced5b2a7..bc49fc824 100644 --- a/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountLoginFragment.swift +++ b/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountLoginFragment.swift @@ -24,25 +24,61 @@ struct ThirdPartySipAccountLoginFragment: View { @ObservedObject private var coreContext = CoreContext.shared @ObservedObject var accountLoginViewModel: AccountLoginViewModel + @StateObject private var keyboard = KeyboardResponder() + @Environment(\.dismiss) var dismiss @State private var isSecured: Bool = true + @State private var advancedSettingsIsOpen: Bool = false @FocusState var isNameFocused: Bool @FocusState var isPasswordFocused: Bool @FocusState var isDomainFocused: Bool @FocusState var isDisplayNameFocused: Bool + @FocusState var isSipProxyUrlFocused: Bool + @FocusState var isAuthIdFocused: Bool + @FocusState var isOutboundProxyFocused: Bool var body: some View { GeometryReader { geometry in - if #available(iOS 16.4, *) { - ScrollView(.vertical) { - innerScrollView(geometry: geometry) - } - .scrollBounceBehavior(.basedOnSize) - } else { - ScrollView(.vertical) { - innerScrollView(geometry: geometry) + ScrollViewReader { proxy in + if #available(iOS 16.4, *) { + ScrollView(.vertical) { + innerScrollView(geometry: geometry) + } + .scrollBounceBehavior(.basedOnSize) + .onChange(of: isAuthIdFocused) { field in + if field { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + proxy.scrollTo(2, anchor: .top) + } + } + } + .onChange(of: isOutboundProxyFocused) { field in + if field { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + proxy.scrollTo(2, anchor: .top) + } + } + } + } else { + ScrollView(.vertical) { + innerScrollView(geometry: geometry) + } + .onChange(of: isAuthIdFocused) { field in + if field { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + proxy.scrollTo(2, anchor: .top) + } + } + } + .onChange(of: isOutboundProxyFocused) { field in + if field { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + proxy.scrollTo(2, anchor: .top) + } + } + } } } } @@ -208,6 +244,74 @@ struct ThirdPartySipAccountLoginFragment: View { .stroke(Color.gray200, lineWidth: 1) ) .padding(.bottom) + + HStack(alignment: .center) { + Text("settings_advanced_title") + .default_text_style_800(styleSize: 18) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + + Image(advancedSettingsIsOpen ? "caret-up" : "caret-down") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + } + .padding(.top, 10) + .padding(.bottom, 10) + .background(.white) + .onTapGesture { + withAnimation { + advancedSettingsIsOpen.toggle() + } + } + + if advancedSettingsIsOpen { + VStack(alignment: .leading) { + Text("authentication_id") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("authentication_id", text: $accountLoginViewModel.authId) + .id(1) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .background(.white) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isAuthIdFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isAuthIdFocused) + } + + VStack(alignment: .leading) { + Text("account_settings_sip_proxy_url_title") + .default_text_style_700(styleSize: 15) + .padding(.bottom, -5) + + TextField("account_settings_sip_proxy_url_title", text: $accountLoginViewModel.outboundProxy) + .id(2) + .default_text_style(styleSize: 15) + .frame(height: 25) + .padding(.horizontal, 20) + .padding(.vertical, 15) + .background(.white) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isOutboundProxyFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .focused($isOutboundProxyFocused) + } + .padding(.bottom) + } } .frame(maxWidth: SharedMainViewModel.shared.maxWidth) .padding(.horizontal, 20) @@ -241,6 +345,7 @@ struct ThirdPartySipAccountLoginFragment: View { .clipped() } .frame(minHeight: geometry.size.height) + .padding(.bottom, keyboard.currentHeight) } } diff --git a/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift b/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift index a6a5d2194..d785cf189 100644 --- a/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift +++ b/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift @@ -29,6 +29,8 @@ class AccountLoginViewModel: ObservableObject { @Published var domain: String = "sip.linphone.org" @Published var displayName: String = "" @Published var transportType: String = "TLS" + @Published var authId: String = "" + @Published var outboundProxy: String = "" private var mCoreDelegate: CoreDelegate! @@ -83,7 +85,7 @@ class AccountLoginViewModel: ObservableObject { // The realm will be determined automatically from the first register, as well as the algorithm let authInfo = try Factory.Instance.createAuthInfo( username: self.username, - userid: "", + userid: self.authId, passwd: self.passwd, ha1: "", realm: "", @@ -100,7 +102,15 @@ class AccountLoginViewModel: ObservableObject { try accountParams.setIdentityaddress(newValue: identity) // We also need to configure where the proxy server is located - let address = try Factory.Instance.createAddress(addr: String("sip:" + self.domain)) + var serverAddress: Address + if (!self.outboundProxy.isEmpty) { + let server = self.outboundProxy.starts(with: "sip:") ? self.outboundProxy : String("sip:" + self.outboundProxy) + serverAddress = try Factory.Instance.createAddress(addr: server) + } else { + serverAddress = try Factory.Instance.createAddress(addr: String("sip:" + self.domain)) + } + + let address = serverAddress // We use the Address object to easily set the transport protocol try address.setTransport(newValue: transport) @@ -156,6 +166,8 @@ class AccountLoginViewModel: ObservableObject { DispatchQueue.main.async { self.domain = "sip.linphone.org" self.transportType = "TLS" + self.authId = "" + self.outboundProxy = "" } } catch { NSLog(error.localizedDescription) } diff --git a/Linphone/Utils/KeyboardResponder.swift b/Linphone/Utils/KeyboardResponder.swift new file mode 100644 index 000000000..ad7b8625c --- /dev/null +++ b/Linphone/Utils/KeyboardResponder.swift @@ -0,0 +1,24 @@ +import Foundation +import UIKit +import Combine + +final class KeyboardResponder: ObservableObject { + @Published var currentHeight: CGFloat = 0 + + private var cancellables: Set = [] + + init() { + let willShow = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification) + .map { notification -> CGFloat in + (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0 + } + + let willHide = NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification) + .map { _ in CGFloat(0) } + + Publishers.Merge(willShow, willHide) + .receive(on: RunLoop.main) + .assign(to: \.currentHeight, on: self) + .store(in: &cancellables) + } +} diff --git a/LinphoneApp.xcodeproj/project.pbxproj b/LinphoneApp.xcodeproj/project.pbxproj index 940737d92..28424090b 100644 --- a/LinphoneApp.xcodeproj/project.pbxproj +++ b/LinphoneApp.xcodeproj/project.pbxproj @@ -108,6 +108,7 @@ D73449992BC6932A00778C56 /* MeetingWaitingRoomFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73449982BC6932A00778C56 /* MeetingWaitingRoomFragment.swift */; }; D734499B2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D734499A2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift */; }; D737AEEF2DA011F2005C1280 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D737AEED2DA011F2005C1280 /* Localizable.strings */; }; + D738ACEE2E857BF10039F7D1 /* KeyboardResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D738ACED2E857BEF0039F7D1 /* KeyboardResponder.swift */; }; D7458F392E0BDCF4000C957A /* linphoneExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D7458F2F2E0BDCF4000C957A /* linphoneExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; D748BF2C2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */; }; D748BF2E2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */; }; @@ -334,6 +335,7 @@ D734499A2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingWaitingRoomViewModel.swift; sourceTree = ""; }; D737AEEE2DA011F2005C1280 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Localizable/en.lproj/Localizable.strings; sourceTree = ""; }; D737AEF02DA01203005C1280 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = Localizable/fr.lproj/Localizable.strings; sourceTree = ""; }; + D738ACED2E857BEF0039F7D1 /* KeyboardResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardResponder.swift; sourceTree = ""; }; D7458F2F2E0BDCF4000C957A /* linphoneExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = linphoneExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountLoginFragment.swift; sourceTree = ""; }; D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountWarningFragment.swift; sourceTree = ""; }; @@ -583,6 +585,7 @@ D717071C2AC591EF0037746F /* Utils */ = { isa = PBXGroup; children = ( + D738ACED2E857BEF0039F7D1 /* KeyboardResponder.swift */, D7DF8BE82E2104E5003A3BC7 /* EmojiPickerView.swift */, D703F7072DC8C5FF005B8F75 /* FilePicker.swift */, D717A10D2CEB770D00849D92 /* ShareSheetController.swift */, @@ -1279,6 +1282,7 @@ D7DC096F2CFA1D7600A6D47C /* AccountProfileFragment.swift in Sources */, D717A10E2CEB772300849D92 /* ShareSheetController.swift in Sources */, 66C491FD2B24D36500CEA16D /* AudioRouteUtils.swift in Sources */, + D738ACEE2E857BF10039F7D1 /* KeyboardResponder.swift in Sources */, D70959F12B8DF3EC0014AC0B /* ConversationModel.swift in Sources */, C6DC4E3D2C199C4E009096FD /* BundleExtenion.swift in Sources */, D7343FEF2D3FE16C0059D784 /* HelpViewModel.swift in Sources */,