From 02a89a08c39c91b0b97c875ae3a2179ed82f94f0 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 20 Jun 2024 16:39:49 +0200 Subject: [PATCH] Add Account creation feature (Register) Fix unreadMessagesCount when displayedConversation is null Change user agent --- Linphone.xcodeproj/project.pbxproj | 24 +- Linphone/Core/CoreContext.swift | 7 +- Linphone/Localizable.xcstrings | 32 ++ Linphone/Ressources/linphonerc-factory | 3 +- .../RegisterCodeConfirmationFragment.swift | 260 ++++++------ .../Fragments/RegisterFragment.swift | 17 +- .../Viewmodel/RegisterViewModel.swift | 377 +++++++++++++++++- .../ViewModel/ConversationViewModel.swift | 2 +- Linphone/UI/Main/Fragments/ToastView.swift | 21 + 9 files changed, 569 insertions(+), 174 deletions(-) diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 2fc9d6d40..59c53d4f0 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -1179,7 +1179,6 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "DEBUG=1", - "USE_CRASHLYTICS=1", ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = msgNotificationService/Info.plist; @@ -1193,7 +1192,7 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; - OTHER_SWIFT_FLAGS = "$(inherited) -DUSE_CRASHLYTICS"; + OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone.msgNotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1219,10 +1218,7 @@ DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_PREPROCESSOR_DEFINITIONS = ( - "$(inherited)", - "USE_CRASHLYTICS=1", - ); + GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = msgNotificationService/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = msgNotificationService; @@ -1235,7 +1231,7 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; - OTHER_SWIFT_FLAGS = "$(inherited) -DUSE_CRASHLYTICS"; + OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone.msgNotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1382,7 +1378,6 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "DEBUG=1", - "USE_CRASHLYTICS=1", ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Linphone/Info.plist; @@ -1408,8 +1403,8 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.3; - MARKETING_VERSION = 6.0; - OTHER_SWIFT_FLAGS = "$(inherited) -DUSE_CRASHLYTICS"; + MARKETING_VERSION = 6.0.0; + OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1435,10 +1430,7 @@ DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; - GCC_PREPROCESSOR_DEFINITIONS = ( - "$(inherited)", - "USE_CRASHLYTICS=1", - ); + GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Linphone/Info.plist; INFOPLIST_KEY_NSCameraUsageDescription = "Camera usage is required for video VOIP calls"; @@ -1463,8 +1455,8 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.3; - MARKETING_VERSION = 6.0; - OTHER_SWIFT_FLAGS = "$(inherited) -DUSE_CRASHLYTICS"; + MARKETING_VERSION = 6.0.0; + OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index c413881fb..7ae9798a6 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -114,8 +114,11 @@ final class CoreContext: ObservableObject { self.mCore.autoIterateEnabled = false self.mCore.callkitEnabled = true self.mCore.pushNotificationEnabled = true - - self.mCore.setUserAgent(name: "Linphone iOS 6.0 Beta (\(UIDevice.current.localizedModel)) - Linphone SDK : \(self.coreVersion)", version: "6.0") + + let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String + let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + + self.mCore.setUserAgent(name: "\(appName ?? "Linphone")iOS/\(version ?? "6.0.0") Beta (\(UIDevice.current.localizedModel)) LinphoneSDK", version: self.coreVersion) self.mCore.videoCaptureEnabled = true self.mCore.videoDisplayEnabled = true diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 827ba45ed..90003759d 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -290,6 +290,38 @@ } } }, + "assistant_account_register_push_notification_not_received_error" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Push notification with auth token not received in 5 seconds, please try again later" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "La notification poussée avec le jeton d\\'authentification n'a pas été reçue dans les 5 secondes, merci de réessayer plus tard" + } + } + } + }, + "assistant_account_register_unexpected_error" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unexpected error occurred, please try again later" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Un erreur inattendue est survenue, merci de réessayer plus tard" + } + } + } + }, "assistant_already_have_an_account" : { "localizations" : { "en" : { diff --git a/Linphone/Ressources/linphonerc-factory b/Linphone/Ressources/linphonerc-factory index eb87047ef..cc44698e3 100644 --- a/Linphone/Ressources/linphonerc-factory +++ b/Linphone/Ressources/linphonerc-factory @@ -45,8 +45,7 @@ store_friends=0 record_aware=1 [account_creator] -url=https://flexisip-staging-master.linphone.org/login # For testing -# url=https://subscribe.linphone.org/api/ +url=https://subscribe.linphone.org/api/ [lime] lime_update_threshold=86400 diff --git a/Linphone/UI/Assistant/Fragments/RegisterCodeConfirmationFragment.swift b/Linphone/UI/Assistant/Fragments/RegisterCodeConfirmationFragment.swift index 2a0107edc..ff128d410 100644 --- a/Linphone/UI/Assistant/Fragments/RegisterCodeConfirmationFragment.swift +++ b/Linphone/UI/Assistant/Fragments/RegisterCodeConfirmationFragment.swift @@ -25,7 +25,6 @@ struct RegisterCodeConfirmationFragment: View { @Environment(\.dismiss) var dismiss - @StateObject var viewModel = ViewModel() @FocusState var isFocused: Bool let textLimit = 4 @@ -40,116 +39,129 @@ struct RegisterCodeConfirmationFragment: View { var body: some View { NavigationView { GeometryReader { geometry in - ScrollView(.vertical) { - VStack { - ZStack { - Image("mountain") - .resizable() - .scaledToFill() - .frame(width: geometry.size.width, height: 100) - .clipped() - - VStack(alignment: .leading) { - HStack { - Image("caret-left") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c500) - .frame(width: 25, height: 25, alignment: .leading) - .padding(.all, 10) - .padding(.top, -75) - .padding(.leading, -10) - .onTapGesture { - withAnimation { - dismiss() + ZStack { + ScrollView(.vertical) { + VStack { + ZStack { + Image("mountain") + .resizable() + .scaledToFill() + .frame(width: geometry.size.width, height: 100) + .clipped() + + VStack(alignment: .leading) { + HStack { + Image("caret-left") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + .padding(.top, -75) + .padding(.leading, -10) + .onTapGesture { + withAnimation { + dismiss() + } } - } - - Spacer() + + Spacer() + } + .padding(.leading) } - .padding(.leading) + .frame(width: geometry.size.width) + + Text("assistant_account_register") + .default_text_style_white_800(styleSize: 20) + .padding(.top, 20) } - .frame(width: geometry.size.width) + .padding(.top, 35) + .padding(.bottom, 10) - Text("assistant_account_register") - .default_text_style_white_800(styleSize: 20) - .padding(.top, 20) - } - .padding(.top, 35) - .padding(.bottom, 10) - - ZStack { - VStack { - Spacer() - HStack { - Spacer() - Image("confirm_sms_code_illu") - .padding(.bottom, -geometry.safeAreaInsets.bottom) - } - } - VStack(alignment: .center) { - Spacer() - - Text(String(format: NSLocalizedString("assistant_account_creation_sms_confirmation_explanation", comment: ""), registerViewModel.phoneNumber)) - .default_text_style(styleSize: 15) - .foregroundStyle(Color.grayMain2c700) - .padding(.horizontal, 10) - .frame(maxWidth: .infinity, alignment: .center) - .multilineTextAlignment(.center) - + ZStack { VStack { - ZStack { - - HStack(spacing: spaceBetweenBoxes) { - otpText(text: viewModel.otp1, focused: viewModel.otpField.isEmpty) - otpText(text: viewModel.otp2, focused: viewModel.otpField.count == 1) - otpText(text: viewModel.otp3, focused: viewModel.otpField.count == 2) - otpText(text: viewModel.otp4, focused: viewModel.otpField.count == 3) - } - - TextField("", text: $viewModel.otpField) - .default_text_style_600(styleSize: 80) - .frame(width: isFocused ? 0 : textFieldOriginalWidth, height: textBoxHeight) - .textContentType(.oneTimeCode) - .foregroundColor(.clear) - .accentColor(.clear) - .background(.clear) - .keyboardType(.numberPad) - .focused($isFocused) - .onChange(of: viewModel.otpField) { _ in - limitText(textLimit) - } + Spacer() + HStack { + Spacer() + Image("confirm_sms_code_illu") + .padding(.bottom, -geometry.safeAreaInsets.bottom) } } - .frame(maxWidth: .infinity, alignment: .center) - .padding(.vertical, 20) - - Button(action: { - dismiss() - }, label: { - Text("assistant_account_creation_wrong_phone_number") - .default_text_style_orange_600(styleSize: 15) - .frame(height: 35) - }) - .padding(.horizontal, 15) - .padding(.vertical, 5) - .cornerRadius(60) - .overlay( - RoundedRectangle(cornerRadius: 60) - .inset(by: 0.5) - .stroke(Color.orangeMain500, lineWidth: 1) - ) - .padding(.bottom) - .frame(maxWidth: .infinity) - - Spacer() - Spacer() + VStack(alignment: .center) { + Spacer() + + Text(String(format: NSLocalizedString("assistant_account_creation_sms_confirmation_explanation", comment: ""), registerViewModel.phoneNumber)) + .default_text_style(styleSize: 15) + .foregroundStyle(Color.grayMain2c700) + .padding(.horizontal, 10) + .frame(maxWidth: .infinity, alignment: .center) + .multilineTextAlignment(.center) + + VStack { + ZStack { + + HStack(spacing: spaceBetweenBoxes) { + otpText(text: registerViewModel.otp1, focused: registerViewModel.otpField.isEmpty) + otpText(text: registerViewModel.otp2, focused: registerViewModel.otpField.count == 1) + otpText(text: registerViewModel.otp3, focused: registerViewModel.otpField.count == 2) + otpText(text: registerViewModel.otp4, focused: registerViewModel.otpField.count == 3) + } + + TextField("", text: $registerViewModel.otpField) + .default_text_style_600(styleSize: 80) + .frame(width: isFocused ? 0 : textFieldOriginalWidth, height: textBoxHeight) + .textContentType(.oneTimeCode) + .foregroundColor(.clear) + .accentColor(.clear) + .background(.clear) + .keyboardType(.numberPad) + .focused($isFocused) + .onChange(of: registerViewModel.otpField) { _ in + limitText(textLimit) + if registerViewModel.otpField.count > 3 { + registerViewModel.validateCode() + } + } + } + } + .frame(maxWidth: .infinity, alignment: .center) + .padding(.vertical, 20) + + Button(action: { + dismiss() + }, label: { + Text("assistant_account_creation_wrong_phone_number") + .default_text_style_orange_600(styleSize: 15) + .frame(height: 35) + }) + .padding(.horizontal, 15) + .padding(.vertical, 5) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(Color.orangeMain500, lineWidth: 1) + ) + .padding(.bottom) + .frame(maxWidth: .infinity) + + Spacer() + Spacer() + } + .frame(maxWidth: sharedMainViewModel.maxWidth) + .padding(.horizontal, 20) } - .frame(maxWidth: sharedMainViewModel.maxWidth) - .padding(.horizontal, 20) + } + .frame(minHeight: geometry.size.height) + .onAppear { + registerViewModel.otpField = "" } } - .frame(minHeight: geometry.size.height) + + if registerViewModel.createInProgress { + PopupLoadingView() + .background(.black.opacity(0.65)) + } } } .navigationTitle("") @@ -175,54 +187,12 @@ struct RegisterCodeConfirmationFragment: View { } func limitText(_ upper: Int) { - if viewModel.otpField.count > upper { - viewModel.otpField = String(viewModel.otpField.prefix(upper)) + if registerViewModel.otpField.count > upper { + registerViewModel.otpField = String(registerViewModel.otpField.prefix(upper)) } } } -class ViewModel: ObservableObject { - - @Published var otpField = "" { - didSet { - guard otpField.count <= 5, - otpField.last?.isNumber ?? true else { - otpField = oldValue - return - } - } - } - var otp1: String { - guard otpField.count >= 1 else { - return "" - } - return String(Array(otpField)[0]) - } - var otp2: String { - guard otpField.count >= 2 else { - return "" - } - return String(Array(otpField)[1]) - } - var otp3: String { - guard otpField.count >= 3 else { - return "" - } - return String(Array(otpField)[2]) - } - var otp4: String { - guard otpField.count >= 4 else { - return "" - } - return String(Array(otpField)[3]) - } - - @Published var borderColor: Color = .black - var successCompletionHandler: (() -> Void)? - @Published var showResendText = false - -} - #Preview { RegisterCodeConfirmationFragment(registerViewModel: RegisterViewModel()) } diff --git a/Linphone/UI/Assistant/Fragments/RegisterFragment.swift b/Linphone/UI/Assistant/Fragments/RegisterFragment.swift index 2a9510835..45307c16b 100644 --- a/Linphone/UI/Assistant/Fragments/RegisterFragment.swift +++ b/Linphone/UI/Assistant/Fragments/RegisterFragment.swift @@ -31,7 +31,6 @@ struct RegisterFragment: View { @FocusState var isPhoneNumberFocused: Bool @FocusState var isPasswordFocused: Bool - @State private var isLinkActive = false @State private var isShowPopup = false var body: some View { @@ -103,14 +102,14 @@ struct RegisterFragment: View { HStack { Menu { - Picker("", selection: $registerViewModel.dialPlanSelected) { + Picker("", selection: $registerViewModel.dialPlanValueSelected) { ForEach(Array(registerViewModel.dialPlansLabelList.enumerated()), id: \.offset) { index, dialPlan in Text(dialPlan).tag(registerViewModel.dialPlansShortLabelList[index]) } } } label: { HStack { - Text(registerViewModel.dialPlanSelected) + Text(registerViewModel.dialPlanValueSelected) Image("caret-down") .renderingMode(.template) @@ -183,7 +182,7 @@ struct RegisterFragment: View { ) .padding(.bottom) - NavigationLink(isActive: $isLinkActive, destination: { + NavigationLink(isActive: $registerViewModel.isLinkActive, destination: { RegisterCodeConfirmationFragment(registerViewModel: registerViewModel) }, label: { Text("assistant_account_create") @@ -196,7 +195,7 @@ struct RegisterFragment: View { .padding(.vertical, 10) .background((registerViewModel.username.isEmpty || registerViewModel.phoneNumber.isEmpty || registerViewModel.passwd.isEmpty) ? Color.orangeMain100 : Color.orangeMain500) .cornerRadius(60) - .disabled(!isLinkActive) + .disabled(!registerViewModel.isLinkActive) .padding(.bottom) .simultaneousGesture( TapGesture().onEnded { @@ -283,7 +282,9 @@ struct RegisterFragment: View { titleSecondButton: Text("Continue"), actionSecondButton: { self.isShowPopup = false - self.isLinkActive = true + registerViewModel.createInProgress = true + registerViewModel.startAccountCreation() + registerViewModel.phoneNumberConfirmedByUser() } ) .background(.black.opacity(0.65)) @@ -292,6 +293,10 @@ struct RegisterFragment: View { } } + if registerViewModel.createInProgress { + PopupLoadingView() + .background(.black.opacity(0.65)) + } } } .navigationTitle("") diff --git a/Linphone/UI/Assistant/Viewmodel/RegisterViewModel.swift b/Linphone/UI/Assistant/Viewmodel/RegisterViewModel.swift index bebc94fa4..45e387c3b 100644 --- a/Linphone/UI/Assistant/Viewmodel/RegisterViewModel.swift +++ b/Linphone/UI/Assistant/Viewmodel/RegisterViewModel.swift @@ -19,29 +19,174 @@ import Foundation import linphonesw +import Combine class RegisterViewModel: ObservableObject { + static let TAG = "[RegisterViewModel]" + let accountTokenNotification = Notification.Name("AccountCreationTokenReceived") + private var coreContext = CoreContext.shared @Published var username: String = "" + @Published var usernameError: String = "" @Published var phoneNumber: String = "" + @Published var phoneNumberError: String = "" @Published var passwd: String = "" + @Published var passwordError: String = "" @Published var domain: String = "sip.linphone.org" @Published var displayName: String = "" @Published var transportType: String = "TLS" - @Published var dialPlanSelected: String = "🇫🇷 +33" + @Published var dialPlanValueSelected: String = "🇫🇷 +33" @Published var dialPlansList: [DialPlan] = [] @Published var dialPlansLabelList: [String] = [] @Published var dialPlansShortLabelList: [String] = [] + private let HASHALGORITHM = "SHA-256" + + private var accountManagerServices: AccountManagerServices? + private var accountCreationToken: String? + private var accountCreatedAuthInfo: AuthInfo? + private var accountCreated: Account? + private var normalizedPhoneNumber: String? + + private var accountManagerServicesSuscriptions = Set() + private var mCoreSuscriptions = Set() + + @Published var isLinkActive: Bool = false + @Published var createInProgress: Bool = false + + @Published var otpField = "" { + didSet { + guard otpField.count <= 5, + otpField.last?.isNumber ?? true else { + otpField = oldValue + return + } + } + } + var otp1: String { + guard otpField.count >= 1 else { + return "" + } + return String(Array(otpField)[0]) + } + var otp2: String { + guard otpField.count >= 2 else { + return "" + } + return String(Array(otpField)[1]) + } + var otp3: String { + guard otpField.count >= 3 else { + return "" + } + return String(Array(otpField)[2]) + } + var otp4: String { + guard otpField.count >= 4 else { + return "" + } + return String(Array(otpField)[3]) + } + init() { getDialPlansList() + getAccountCreationToken() + } + + func addDelegate(core: Core) { + self.accountManagerServicesSuscriptions.insert(self.accountManagerServices!.publisher?.onRequestSuccessful?.postOnCoreQueue { + (ams: AccountManagerServices, request: AccountManagerServices.Request, data: String) in + Log.info("\(RegisterViewModel.TAG) Request \(request) was successful, data is \(data)") + switch request { + case AccountManagerServices.Request.CreateAccountUsingToken: + if !data.isEmpty { + self.storeAccountInCore(core: core, identity: data) + self.sendCodeBySms() + } else { + Log.error( + "\(RegisterViewModel.TAG) No data found for createAccountUsingToken request, can't continue!" + ) + } + + case AccountManagerServices.Request.SendPhoneNumberLinkingCodeBySms: + DispatchQueue.main.async { + self.createInProgress = false + self.isLinkActive = true + } + + case AccountManagerServices.Request.LinkPhoneNumberUsingCode: + let account = self.accountCreated + if account != nil { + Log.info( + "\(RegisterViewModel.TAG) Account \(account?.params?.identityAddress?.asStringUriOnly()) has been created & activated, setting it as default" + ) + + if let assistantLinphone = Bundle.main.path(forResource: "assistant_linphone_default_values", ofType: nil) { + core.loadConfigFromXml(xmlUri: assistantLinphone) + } + + DispatchQueue.main.async { + self.createInProgress = false + } + + do { + try core.addAccount(account: account!) + core.defaultAccount = account + } catch { + } + } + + default: break + } + }) + + self.accountManagerServicesSuscriptions.insert(self.accountManagerServices!.publisher?.onRequestError?.postOnCoreQueue { + (ams: AccountManagerServices, request: AccountManagerServices.Request, statusCode: Int, errorMessage: String, parameterErrors: Dictionary?) in + Log.error( + "\(RegisterViewModel.TAG) Request \(request) returned an error with status code \(statusCode) and message \(errorMessage)" + ) + + if !errorMessage.isEmpty { + DispatchQueue.main.async { + ToastViewModel.shared.toastMessage = "Error: \(errorMessage)" + ToastViewModel.shared.displayToast = true + } + } + + switch request { + case AccountManagerServices.Request.SendAccountCreationTokenByPush: + Log.warn("\(RegisterViewModel.TAG) Cancelling job waiting for push notification") + default: break + } + + DispatchQueue.main.async { + self.createInProgress = false + } + }) + + NotificationCenter.default.addObserver(forName: accountTokenNotification, object: nil, queue: nil) { notification in + if !(self.username.isEmpty || self.passwd.isEmpty) { + if let token = notification.userInfo?["token"] as? String { + if !token.isEmpty { + self.accountCreationToken = token + Log.info( + "\(RegisterViewModel.TAG) Extracted token \(self.accountCreationToken ?? "Error token") from push payload, creating account" + ) + self.createAccount() + } else { + Log.error("\(RegisterViewModel.TAG) Push payload JSON object has an empty 'token'!") + self.onFlexiApiTokenRequestError() + } + } + } + } } func getDialPlansList() { - coreContext.doOnCoreQueue { core in + coreContext.doOnCoreQueue { _ in let dialPlans = Factory.Instance.dialPlans dialPlans.forEach { dialPlan in self.dialPlansList.append(dialPlan) @@ -54,4 +199,232 @@ class RegisterViewModel: ObservableObject { } } } + + func getAccountCreationToken() { + coreContext.doOnCoreQueue { core in + do { + self.accountManagerServices = try core.createAccountManagerServices() + if self.accountManagerServices != nil { + self.accountManagerServices!.language = Locale.current.identifier + self.addDelegate(core: core) + } + } catch { + + } + } + } + + func startAccountCreation() { + coreContext.doOnCoreQueue { core in + if self.accountCreationToken == nil { + Log.info("\(RegisterViewModel.TAG) We don't have a creation token, let's request one") + self.requestFlexiApiToken(core: core) + } else { + let authInfo = self.accountCreatedAuthInfo + if authInfo != nil { + Log.info("\(RegisterViewModel.TAG) Account has already been created, requesting SMS to be sent") + self.sendCodeBySms() + } else { + Log.info("\(RegisterViewModel.TAG) We've already have a token \(self.accountCreationToken ?? ""), continuing") + self.createAccount() + } + } + } + } + + func storeAccountInCore(core: Core, identity: String) { + do { + let passwordValue = passwd + let sipIdentity = try Factory.Instance.createAddress(addr: identity) + + // We need to have an AuthInfo for newly created account to authorize phone number linking request + let authInfo = try Factory.Instance.createAuthInfo( + username: sipIdentity.username ?? "Error username", + userid: nil, + passwd: passwordValue, + ha1: nil, + realm: nil, + domain: sipIdentity.domain + ) + + core.addAuthInfo(info: authInfo) + Log.info("\(RegisterViewModel.TAG) Auth info for SIP identity \(sipIdentity.asStringUriOnly()) created & added") + + var dialPlan: DialPlan? + + dialPlansList.forEach { dial in + let countryCode = dialPlanValueSelected.components(separatedBy: "+") + if dial.countryCallingCode == countryCode[1] { + dialPlan = dial + } + } + + let accountParams = try core.createAccountParams() + try accountParams.setIdentityaddress(newValue: sipIdentity) + if dialPlan != nil { + let dialPlanTmp = dialPlan?.internationalCallPrefix ?? "Error international call prefix" + let isoCountryCodeTmp = dialPlan?.isoCountryCode ?? "Error iso country code" + Log.info( + "\(RegisterViewModel.TAG) Setting international prefix \(dialPlanTmp) and country \(isoCountryCodeTmp) to account params" + ) + accountParams.internationalPrefix = dialPlan!.internationalCallPrefix + accountParams.internationalPrefixIsoCountryCode = dialPlan!.isoCountryCode + } + let account = try core.createAccount(params: accountParams) + + Log.info("\(RegisterViewModel.TAG) Account for SIP identity \(sipIdentity.asStringUriOnly()) created & added") + + accountCreatedAuthInfo = authInfo + accountCreated = account + } catch let error { + Log.error("\(RegisterViewModel.TAG) Failed to create address from SIP Identity \(identity)!") + Log.error("\(RegisterViewModel.TAG) Error is \(error)") + } + } + + func requestFlexiApiToken(core: Core) { + if !core.isPushNotificationAvailable { + Log.error( + "\(RegisterViewModel.TAG) Core says push notification aren't available, can't request a token from FlexiAPI" + ) + self.onFlexiApiTokenRequestError() + return + } + + let pushConfig = core.pushNotificationConfig + if pushConfig != nil && self.accountManagerServices != nil { + pushConfig!.provider = "apns.dev" + var formatedPnParam = pushConfig!.param + formatedPnParam = formatedPnParam?.replacingOccurrences(of: "voip&remote", with: "remote") + pushConfig!.param = formatedPnParam + + let coreRemoteToken = pushConfig!.remoteToken + var formatedRemoteToken = "" + if coreRemoteToken != nil { + formatedRemoteToken = String(coreRemoteToken!.prefix(64)) + pushConfig!.prid = formatedRemoteToken.uppercased() + self.accountManagerServices!.requestAccountCreationTokenByPush(pnProvider: pushConfig?.provider ?? "", pnParam: pushConfig?.param ?? "", pnPrid: pushConfig?.prid ?? "") + } else { + Log.warn("\(RegisterViewModel.TAG) No remote push token available in core for account creator configuration") + } + + Log.info("\(RegisterViewModel.TAG) Found push notification info: provider \("apns.dev"), param \(formatedPnParam ?? "error") and prid \(formatedRemoteToken)") + } else { + Log.error("\(RegisterViewModel.TAG) No push configuration object in Core, shouldn't happen!") + self.onFlexiApiTokenRequestError() + } + } + + func onFlexiApiTokenRequestError() { + Log.error("\(RegisterViewModel.TAG) Flexi API token request by push error!") + + DispatchQueue.main.async { + ToastViewModel.shared.toastMessage = "Failed_push_notification_not_received_error" + ToastViewModel.shared.displayToast = true + } + } + + func sendCodeBySms() { + let account = accountCreated + if accountManagerServices != nil && account != nil { + let phoneNumberValue = normalizedPhoneNumber + if phoneNumberValue == nil || phoneNumberValue!.isEmpty { + Log.error("\(RegisterViewModel.TAG) Phone number is null or empty, this shouldn't happen at this step!") + return + } + + let identity = account!.params!.identityAddress + if identity != nil { + Log.info( + "\(RegisterViewModel.TAG) Account \(identity!.asStringUriOnly()) should now be created, asking account manager to send a confirmation code by SMS to \(phoneNumberValue ?? "")" + ) + + accountManagerServices!.requestPhoneNumberLinkingCodeBySms( + sipIdentity: identity!, + phoneNumber: phoneNumberValue! + ) + } + } + } + + func createAccount() { + if accountManagerServices != nil { + let token = accountCreationToken + if token == nil || (token != nil && token!.isEmpty) { + Log.error("\(RegisterViewModel.TAG) No account creation token, can't create account!") + return + } + + if username.isEmpty || passwd.isEmpty { + Log.error("\(RegisterViewModel.TAG) Either username \(username) or password is null or empty!") + return + } + + Log.info( + "\(RegisterViewModel.TAG) Account creation token is \(token ?? "Error token"), creating account with username \(username) and algorithm \(HASHALGORITHM)" + ) + + do { + try accountManagerServices!.createAccountUsingToken( + username: username, + password: passwd, + algorithm: HASHALGORITHM, + token: token! + ) + } catch { + Log.info( + "\(RegisterViewModel.TAG) Can't create account using token" + ) + } + } + } + + func phoneNumberConfirmedByUser() { + coreContext.doOnCoreQueue { core in + if self.accountManagerServices != nil { + var dialPlan: DialPlan? + + self.dialPlansList.forEach { dial in + let countryCode = self.dialPlanValueSelected.components(separatedBy: "+") + Log.info("dialPlansListdialPlansList \(dial.countryCallingCode) \(countryCode[1])") + if dial.countryCallingCode == countryCode[1] { + dialPlan = dial + } + } + if (dialPlan == nil) { + Log.error("\(RegisterViewModel.TAG) No dial plan (country) selected!") + } + + let number = self.phoneNumber + let formattedPhoneNumber = dialPlan?.formatPhoneNumber(phoneNumber: number, escapePlus: false) + Log.info( + "\(RegisterViewModel.TAG) Formatted phone number \(number) using dial plan \(dialPlan?.country ?? "Error country") is \(formattedPhoneNumber ?? "Error phone number")" + ) + + self.normalizedPhoneNumber = formattedPhoneNumber + } else { + Log.error("\(RegisterViewModel.TAG) Account manager services hasn't been initialized!") + + DispatchQueue.main.async { + ToastViewModel.shared.toastMessage = "Failed_account_register_unexpected_error" + ToastViewModel.shared.displayToast = true + } + } + } + } + + func validateCode() { + createInProgress = true + let account = accountCreated + if accountManagerServices != nil && account != nil { + let code = otpField + let identity = account!.params?.identityAddress + if identity != nil { + Log.info( + "\(RegisterViewModel.TAG) Activating account using code \(code) for account \(identity!.asStringUriOnly())" + ) + accountManagerServices!.linkPhoneNumberToAccountUsingCode(sipIdentity: identity!, code: code) + } + } + } } diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index 3cc07eec6..8e416ccbe 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -400,7 +400,7 @@ class ConversationViewModel: ObservableObject { let isFirstMessageTmp = (eventLog.chatMessage?.isOutgoing ?? false) ? isFirstMessageOutgoingTmp : isFirstMessageIncomingTmp - let unreadMessagesCount = self.displayedConversation!.chatRoom.unreadMessagesCount + let unreadMessagesCount = self.displayedConversation != nil ? self.displayedConversation!.chatRoom.unreadMessagesCount : 0 var statusTmp: Message.Status? = .sending switch eventLog.chatMessage?.state { diff --git a/Linphone/UI/Main/Fragments/ToastView.swift b/Linphone/UI/Main/Fragments/ToastView.swift index b68c15604..6babc0fe0 100644 --- a/Linphone/UI/Main/Fragments/ToastView.swift +++ b/Linphone/UI/Main/Fragments/ToastView.swift @@ -171,6 +171,27 @@ struct ToastView: View { .foregroundStyle(Color.redDanger500) .default_text_style(styleSize: 15) .padding(8) + + case "Failed_push_notification_not_received_error": + Text("assistant_account_register_push_notification_not_received_error") + .multilineTextAlignment(.center) + .foregroundStyle(Color.redDanger500) + .default_text_style(styleSize: 15) + .padding(8) + + case "Failed_account_register_unexpected_error": + Text("assistant_account_register_unexpected_error") + .multilineTextAlignment(.center) + .foregroundStyle(Color.redDanger500) + .default_text_style(styleSize: 15) + .padding(8) + + case let str where str.contains("Error: "): + Text(toastViewModel.toastMessage) + .multilineTextAlignment(.center) + .foregroundStyle(Color.redDanger500) + .default_text_style(styleSize: 15) + .padding(8) default: Text("Error")