/* * 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 Foundation import linphonesw import Combine // swiftlint:disable line_length // swiftlint:disable type_body_length 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 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 accountManagerServicesRequest: AccountManagerServicesRequest? private var accountCreationToken: String? private var accountCreatedAuthInfo: AuthInfo? private var accountCreated: Account? private var normalizedPhoneNumber: String? private var requestDelegate: AccountManagerServicesRequestDelegate? @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() self.usernameError = "" self.phoneNumberError = "" self.passwordError = "" 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 addDelegate(request: AccountManagerServicesRequest) { coreContext.doOnCoreQueue { core in self.requestDelegate = AccountManagerServicesRequestDelegateStub(onRequestSuccessful: { (request: AccountManagerServicesRequest, data: String) in Log.info("\(RegisterViewModel.TAG) Request \(request) was successful, data is \(data)") switch request.type { case .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 .SendPhoneNumberLinkingCodeBySms: DispatchQueue.main.async { self.createInProgress = false self.isLinkActive = true } case .LinkPhoneNumberUsingCode: let account = self.accountCreated if account != nil { Log.info( "\(RegisterViewModel.TAG) Account \(account?.params?.identityAddress?.asStringUriOnly() ?? "NIL") 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 request.removeDelegate(delegate: self.requestDelegate!) self.requestDelegate = nil } catch { } } default: break } }, onRequestError: { (request: AccountManagerServicesRequest, 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 } } parameterErrors?.keys.forEach({ parameter in let parameterErrorMessage = parameterErrors?.getString(key: parameter) ?? "" switch parameter { case "username": self.usernameError = parameterErrorMessage case "password": self.passwordError = parameterErrorMessage case "phone": self.phoneNumberError = parameterErrorMessage default: break } }) switch request.type { case .SendAccountCreationTokenByPush: Log.warn("\(RegisterViewModel.TAG) Cancelling job waiting for push notification") default: break } DispatchQueue.main.async { self.createInProgress = false } }) request.addDelegate(delegate: self.requestDelegate!) } } func getDialPlansList() { coreContext.doOnCoreQueue { _ in let dialPlans = Factory.Instance.dialPlans dialPlans.forEach { dialPlan in self.dialPlansList.append(dialPlan) self.dialPlansLabelList.append( "\(dialPlan.flag) \(dialPlan.country) | +\(dialPlan.countryCallingCode)" ) self.dialPlansShortLabelList.append( "\(dialPlan.flag) +\(dialPlan.countryCallingCode)" ) } } } func getAccountCreationToken() { coreContext.doOnCoreQueue { core in do { self.accountManagerServices = try core.createAccountManagerServices() if self.accountManagerServices != nil { self.accountManagerServices!.language = Locale.current.identifier } } 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 { #if DEBUG let pushEnvironment = ".dev" #else let pushEnvironment = "" #endif pushConfig!.provider = "apns\(pushEnvironment)" 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() do { let request = try self.accountManagerServices!.createSendAccountCreationTokenByPushRequest( pnProvider: pushConfig?.provider ?? "", pnParam: pushConfig?.param ?? "", pnPrid: pushConfig?.prid ?? "" ) self.addDelegate(request: request) request.submit() } catch { Log.error("\(RegisterViewModel.TAG) Can't create account creation token by push request") } } 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 ?? "")") do { let request = try accountManagerServices?.createSendPhoneNumberLinkingCodeBySmsRequest( sipIdentity: identity!, phoneNumber: phoneNumberValue! ) if request != nil { self.addDelegate(request: request!) request!.submit() } } catch { Log.error("\(RegisterViewModel.TAG) Can't create send phone number linking code by SMS request") } } } } 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 { let request = try accountManagerServices!.createNewAccountUsingTokenRequest( username: username, password: passwd, algorithm: HASHALGORITHM, token: token! ) self.addDelegate(request: request) request.submit() } catch { Log.error("\(RegisterViewModel.TAG) Can't create account using token") } } } func phoneNumberConfirmedByUser() { coreContext.doOnCoreQueue { _ in if self.accountManagerServices != nil { var dialPlan: DialPlan? for dial in self.dialPlansList { let countryCode = self.dialPlanValueSelected.components(separatedBy: "+") if dial.countryCallingCode == countryCode[1] { dialPlan = dial break } } 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())" ) do { let request = try accountManagerServices?.createLinkPhoneNumberToAccountUsingCodeRequest(sipIdentity: identity!, code: code) if request != nil { self.addDelegate(request: request!) request!.submit() } } catch { Log.error("\(RegisterViewModel.TAG) Can't create link phone number to account using code request") } } } } } // swiftlint:enable line_length // swiftlint:enable type_body_length