Use account recovery token FlexiAPI endpoint

This commit is contained in:
Sylvain Berfini 2025-05-06 11:35:21 +02:00
parent 1fdc2bcc58
commit 61517461dd
9 changed files with 421 additions and 46 deletions

View file

@ -0,0 +1,134 @@
/*
* Copyright (c) 2010-2025 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.assistant.fragment
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.core.net.toUri
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.ui.GenericFragment
import org.linphone.databinding.AssistantRecoverAccountFragmentBinding
import org.linphone.ui.assistant.viewmodel.AccountCreationViewModel
import kotlin.getValue
@UiThread
class RecoverAccountFragment : GenericFragment() {
companion object {
private const val TAG = "[Recover Account Fragment]"
}
private lateinit var binding: AssistantRecoverAccountFragmentBinding
private val viewModel: AccountCreationViewModel by navGraphViewModels(
R.id.assistant_nav_graph
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = AssistantRecoverAccountFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
observeToastEvents(viewModel)
viewModel.accountRecoveryTokenReceivedEvent.observe(viewLifecycleOwner) {
it.consume { token ->
Log.i("$TAG Account recovery token received [$token], opening browser")
recoverPhoneNumberAccount(token)
}
}
binding.setBackClickListener {
goBack()
}
binding.setRecoverEmailAccountClickListener {
recoverEmailAccount()
}
binding.setRecoverPhoneNumberAccountClickListener {
viewModel.requestAccountRecoveryToken()
}
}
private fun goBack() {
findNavController().popBackStack()
}
private fun recoverEmailAccount() {
val rootUrl = getString(R.string.web_platform_forgotten_password_url)
val url = "$rootUrl/recovery/email"
try {
Log.i("$TAG Trying to open [$url] URL")
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(browserIntent)
} catch (ise: IllegalStateException) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise"
)
} catch (anfe: ActivityNotFoundException) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe"
)
} catch (e: Exception) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url]: $e"
)
}
}
private fun recoverPhoneNumberAccount(recoveryToken: String) {
val rootUrl = getString(R.string.web_platform_forgotten_password_url)
val url = "$rootUrl/recovery/phone/$recoveryToken"
try {
Log.i("$TAG Trying to open [$url] URL")
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(browserIntent)
} catch (ise: IllegalStateException) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise"
)
} catch (anfe: ActivityNotFoundException) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe"
)
} catch (e: Exception) {
Log.e(
"$TAG Can't start ACTION_VIEW intent for URL [$url]: $e"
)
}
}
}

View file

@ -111,8 +111,11 @@ class LandingFragment : GenericFragment() {
}
binding.setForgottenPasswordClickListener {
val url = getString(R.string.web_platform_forgotten_password_url)
openUrlInBrowser(url)
if (findNavController().currentDestination?.id == R.id.landingFragment) {
val action =
LandingFragmentDirections.actionLandingFragmentToRecoverAccountFragment()
findNavController().navigate(action)
}
}
viewModel.showPassword.observe(viewLifecycleOwner) {

View file

@ -42,7 +42,6 @@ import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantRegisterFragmentBinding
import org.linphone.ui.GenericActivity
import org.linphone.ui.GenericFragment
import org.linphone.ui.assistant.viewmodel.AccountCreationViewModel
import org.linphone.utils.ConfirmationDialogModel
@ -164,15 +163,6 @@ class RegisterFragment : GenericFragment() {
}
}
viewModel.errorHappenedEvent.observe(viewLifecycleOwner) {
it.consume { error ->
(requireActivity() as GenericActivity).showRedToast(
error,
R.drawable.warning_circle
)
}
}
val telephonyManager = requireContext().getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
val countryIso = telephonyManager.networkCountryIso
coreContext.postOnCoreThread {

View file

@ -47,7 +47,6 @@ import org.linphone.core.Dictionary
import org.linphone.core.Factory
import org.linphone.core.tools.Log
import org.linphone.ui.GenericViewModel
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
@ -105,7 +104,7 @@ class AccountCreationViewModel
val accountCreatedEvent = MutableLiveData<Event<Boolean>>()
val errorHappenedEvent: MutableLiveData<Event<String>> by lazy {
val accountRecoveryTokenReceivedEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
@ -113,7 +112,9 @@ class AccountCreationViewModel
private var waitForPushJob: Job? = null
private lateinit var accountManagerServices: AccountManagerServices
private var requestedTokenIsForAccountCreation: Boolean = true
private var accountCreationToken: String? = null
private var accountRecoveryToken: String? = null
private var accountCreatedAuthInfo: AuthInfo? = null
private var accountCreated: Account? = null
@ -124,7 +125,7 @@ class AccountCreationViewModel
request: AccountManagerServicesRequest,
data: String?
) {
Log.i("$TAG Request [$request] was successful, data is [$data]")
Log.i("$TAG Request [${request.type}] was successful, data is [$data]")
operationInProgress.postValue(false)
when (request.type) {
@ -138,6 +139,10 @@ class AccountCreationViewModel
)
}
}
AccountManagerServicesRequest.Type.SendAccountCreationTokenByPush,
AccountManagerServicesRequest.Type.SendAccountRecoveryTokenByPush -> {
Log.i("$TAG Send token by push notification request has been accepted, it should be received soon")
}
AccountManagerServicesRequest.Type.SendPhoneNumberLinkingCodeBySms -> {
goToSmsCodeConfirmationViewEvent.postValue(Event(true))
}
@ -156,7 +161,7 @@ class AccountCreationViewModel
parameterErrors: Dictionary?
) {
Log.e(
"$TAG Request [$request] returned an error with status code [$statusCode] and message [$errorMessage]"
"$TAG Request [${request.type}] returned an error with status code [$statusCode] and message [$errorMessage]"
)
operationInProgress.postValue(false)
@ -174,7 +179,8 @@ class AccountCreationViewModel
}
when (request.type) {
AccountManagerServicesRequest.Type.SendAccountCreationTokenByPush -> {
AccountManagerServicesRequest.Type.SendAccountCreationTokenByPush,
AccountManagerServicesRequest.Type.SendAccountRecoveryTokenByPush -> {
Log.w("$TAG Cancelling job waiting for push notification")
waitingForFlexiApiPushToken = false
waitForPushJob?.cancel()
@ -220,11 +226,19 @@ class AccountCreationViewModel
val token = customPayload.getString("token")
if (token.isNotEmpty()) {
accountCreationToken = token
Log.i(
"$TAG Extracted token [$accountCreationToken] from push payload, creating account"
)
createAccount()
if (requestedTokenIsForAccountCreation) {
accountCreationToken = token
Log.i(
"$TAG Extracted token [$accountCreationToken] from push payload, creating account"
)
createAccount()
} else {
accountRecoveryToken = token
Log.i(
"$TAG Extracted token [$accountRecoveryToken] from push payload, opening browser"
)
accountRecoveryTokenReceivedEvent.postValue(Event(token))
}
} else {
Log.e("$TAG Push payload JSON object has an empty 'token'!")
onFlexiApiTokenRequestError()
@ -317,9 +331,7 @@ class AccountCreationViewModel
normalizedPhoneNumberEvent.postValue(Event(formattedPhoneNumber))
} else {
Log.e("$TAG Account manager services hasn't been initialized!")
errorHappenedEvent.postValue(
Event(AppUtils.getString(R.string.assistant_account_register_unexpected_error))
)
showRedToast(R.string.assistant_account_register_unexpected_error, R.drawable.warning_circle)
}
}
}
@ -330,8 +342,8 @@ class AccountCreationViewModel
coreContext.postOnCoreThread {
if (accountCreationToken.isNullOrEmpty()) {
Log.i("$TAG We don't have a creation token, let's request one")
requestFlexiApiToken()
Log.i("$TAG We don't have an account creation token yet, let's request one")
requestFlexiApiToken(requestAccountCreationToken = true)
} else {
val authInfo = accountCreatedAuthInfo
if (authInfo != null) {
@ -345,6 +357,20 @@ class AccountCreationViewModel
}
}
@UiThread
fun requestAccountRecoveryToken() {
coreContext.postOnCoreThread {
val existingToken = accountRecoveryToken
if (existingToken.isNullOrEmpty()) {
Log.i("$TAG We don't have an account recovery token yet, let's request one")
requestFlexiApiToken(requestAccountCreationToken = false)
} else {
Log.i("$TAG We've already have a token [$existingToken], using it")
accountRecoveryTokenReceivedEvent.postValue(Event(existingToken))
}
}
}
@UiThread
fun toggleShowPassword() {
showPassword.value = showPassword.value == false
@ -365,7 +391,7 @@ class AccountCreationViewModel
val account = accountCreated
if (::accountManagerServices.isInitialized && account != null) {
val code =
"${smsCodeFirstDigit.value}${smsCodeSecondDigit.value}${smsCodeThirdDigit.value}${smsCodeLastDigit.value}"
"${smsCodeFirstDigit.value.orEmpty().trim()}${smsCodeSecondDigit.value.orEmpty().trim()}${smsCodeThirdDigit.value.orEmpty().trim()}${smsCodeLastDigit.value.orEmpty().trim()}"
val identity = account.params.identityAddress
if (identity != null) {
Log.i(
@ -519,7 +545,8 @@ class AccountCreationViewModel
}
@WorkerThread
private fun requestFlexiApiToken() {
private fun requestFlexiApiToken(requestAccountCreationToken: Boolean) {
requestedTokenIsForAccountCreation = requestAccountCreationToken
if (!coreContext.core.isPushNotificationAvailable) {
Log.e(
"$TAG Core says push notification aren't available, can't request a token from FlexiAPI"
@ -545,11 +572,21 @@ class AccountCreationViewModel
}
// Request an auth token, will be sent by push
val request = accountManagerServices.createSendAccountCreationTokenByPushRequest(
provider,
param,
prid
)
val request = if (requestAccountCreationToken) {
Log.i("$TAG Requesting account creation token")
accountManagerServices.createSendAccountCreationTokenByPushRequest(
provider,
param,
prid
)
} else {
Log.i("$TAG Requesting account recovery token")
accountManagerServices.createSendAccountRecoveryTokenByPushRequest(
provider,
param,
prid
)
}
request.addListener(accountManagerServicesListener)
request.submit()
@ -580,12 +617,6 @@ class AccountCreationViewModel
private fun onFlexiApiTokenRequestError() {
Log.e("$TAG Flexi API token request by push error!")
operationInProgress.postValue(false)
errorHappenedEvent.postValue(
Event(
AppUtils.getString(
R.string.assistant_account_register_push_notification_not_received_error
)
)
)
showRedToast(R.string.assistant_account_register_push_notification_not_received_error, R.drawable.warning_circle)
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:pathData="M48,56L48,200a8,8 0,0 1,-16 0L32,56a8,8 0,0 1,16 0ZM140,110.5L120,117L120,96a8,8 0,0 0,-16 0v21L84,110.5a8,8 0,0 0,-5 15.22l20,6.49 -12.34,17a8,8 0,1 0,12.94 9.4l12.34,-17 12.34,17a8,8 0,1 0,12.94 -9.4l-12.34,-17 20,-6.49A8,8 0,0 0,140 110.5ZM246,115.64A8,8 0,0 0,236 110.5L216,117L216,96a8,8 0,0 0,-16 0v21l-20,-6.49a8,8 0,0 0,-4.95 15.22l20,6.49 -12.34,17a8,8 0,1 0,12.94 9.4l12.34,-17 12.34,17a8,8 0,1 0,12.94 -9.4l-12.34,-17 20,-6.49A8,8 0,0 0,246 115.64Z"
android:fillColor="#4e6074"/>
</vector>

View file

@ -0,0 +1,181 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<import type="android.text.InputType" />
<variable
name="backClickListener"
type="View.OnClickListener" />
<variable
name="recoverEmailAccountClickListener"
type="View.OnClickListener" />
<variable
name="recoverPhoneNumberAccountClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.assistant.viewmodel.AccountCreationViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/color_main2_000">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:onClick="@{backClickListener}"
android:id="@+id/back"
android:layout_width="@dimen/top_bar_height"
android:layout_height="@dimen/top_bar_height"
android:padding="15dp"
android:src="@drawable/caret_left"
android:contentDescription="@string/content_description_go_back_icon"
app:tint="?attr/color_main2_500"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/assistant_page_title_style"
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/assistant_forgotten_password_title"
android:textColor="?attr/color_text"
app:layout_constraintTop_toTopOf="@id/back"
app:layout_constraintBottom_toBottomOf="@id/back"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:id="@+id/password_icon"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginTop="68dp"
android:background="@drawable/circle_light_blue_button_background"
android:padding="16dp"
android:src="@drawable/password"
android:contentDescription="@null"
app:tint="?attr/color_main2_500"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/header_style"
android:id="@+id/subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginStart="48dp"
android:layout_marginEnd="48dp"
android:textAlignment="center"
android:text="@string/assistant_forgotten_password_subtitle"
app:layout_constraintTop_toBottomOf="@id/password_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginStart="48dp"
android:layout_marginEnd="48dp"
android:text="@string/assistant_forgotten_password_message"
android:textSize="14sp"
android:textColor="?attr/color_main2_600"
android:gravity="center_horizontal"
app:layout_constraintTop_toBottomOf="@id/subtitle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/primary_button_label_style"
android:id="@+id/recover_email"
android:onClick="@{recoverEmailAccountClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:text="@string/assistant_recover_email_account_label"
app:layout_constraintWidth_max="@dimen/button_max_width"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/message" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/primary_button_label_style"
android:id="@+id/recover_phone_number"
android:onClick="@{recoverPhoneNumberAccountClickListener}"
android:enabled="@{viewModel.pushNotificationsAvailable}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:text="@string/assistant_recover_phone_number_account_label"
app:layout_constraintWidth_max="@dimen/button_max_width"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/recover_email" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/header_style"
android:id="@+id/no_push_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginStart="48dp"
android:layout_marginEnd="48dp"
android:textAlignment="center"
android:visibility="@{viewModel.pushNotificationsAvailable ? View.GONE : View.VISIBLE, default=gone}"
android:text="@string/assistant_recover_phone_number_account_unavailable_no_push_warning"
app:layout_constraintTop_toBottomOf="@id/recover_phone_number"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:id="@+id/mountains"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:src="@drawable/mountains"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:contentDescription="@null"
app:layout_constraintVertical_bias="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/no_push_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:tint="?attr/color_main1_500" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<include
layout="@layout/operation_in_progress"
bind:visibility="@{viewModel.operationInProgress}" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -134,6 +134,7 @@
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:launchSingleTop="true" />
<action
android:id="@+id/action_landingFragment_to_thirdPartySipAccountLoginFragment"
app:destination="@id/thirdPartySipAccountLoginFragment"
@ -143,7 +144,8 @@
app:popExitAnim="@anim/slide_out_right"
app:launchSingleTop="true"
app:popUpTo="@id/landingFragment"
app:popUpToInclusive="true"/>
app:popUpToInclusive="true" />
<action
android:id="@+id/action_landingFragment_to_helpFragment"
app:destination="@id/helpFragment"
@ -152,7 +154,14 @@
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:launchSingleTop="true" />
<action
android:id="@+id/action_landingFragment_to_recoverAccountFragment"
app:destination="@id/recoverAccountFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:launchSingleTop="true" />
</fragment>
<fragment
@ -198,4 +207,10 @@
android:label="DebugFragment"
tools:layout="@layout/help_debug_fragment"/>
<fragment
android:id="@+id/recoverAccountFragment"
android:name="org.linphone.ui.assistant.fragment.RecoverAccountFragment"
android:label="RecoverAccountFragment"
tools:layout="@layout/assistant_recover_account_fragment" />
</navigation>

View file

@ -141,6 +141,12 @@
<string name="assistant_permissions_post_notifications_title"><b>Notifications :</b> Pour vous informer quand vous recevez un message ou un appel.</string>
<string name="assistant_permissions_record_audio_title"><b>Microphone :</b> Pour permettre à vos correspondants de vous entendre.</string>
<string name="assistant_permissions_access_camera_title"><b>Caméra :</b> Pour capturer votre vidéo lors des appels et des conférences.</string>
<string name="assistant_forgotten_password_title">Récupération de compte</string>
<string name="assistant_forgotten_password_subtitle">Choisissez comment récupérer votre compte.</string>
<string name="assistant_forgotten_password_message">Vous avez créé votre compte avec :</string>
<string name="assistant_recover_email_account_label">Un email</string>
<string name="assistant_recover_phone_number_account_label">Un numéro de téléphone</string>
<string name="assistant_recover_phone_number_account_unavailable_no_push_warning">Les notifications push ne semblent pas être disponibles sur votre appareil. Celles-ci sont nécessaires à la récupération dun compte sur lapplication mobile avec un numéro de téléphone.</string>
<!-- Main navigation items -->
<string name="bottom_navigation_contacts_label">Contacts</string>
@ -860,7 +866,7 @@
<!-- Keep <u></u> in the following strings translations! -->
<string name="welcome_carousel_skip"><u>Passer</u></string>
<string name="assistant_forgotten_password"><u>Mot de passe oublié ?</u></string>
<string name="assistant_forgotten_password"><u>Mot de passe oublié ou inconnu ?</u></string>
<string name="call_zrtp_sas_validation_skip"><u>Passer</u></string>
<!-- Keep <b></b> in the following strings translations! -->

View file

@ -37,7 +37,7 @@
<string name="website_privacy_policy_url" translatable="false">https://linphone.org/en/privacy-policy</string>
<string name="website_terms_and_conditions_url" translatable="false">https://linphone.org/en/terms-of-use</string>
<string name="web_platform_register_email_url" translatable="false">https://subscribe.linphone.org/register/email</string>
<string name="web_platform_forgotten_password_url" translatable="false">https://subscribe.linphone.org/</string>
<string name="web_platform_forgotten_password_url" translatable="false">https://subscribe.linphone.org</string>
<string name="website_translate_weblate_url" translatable="false">https://weblate.linphone.org/</string>
<string name="website_open_source_licences_usage_url" translatable="false">https://wiki.linphone.org/xwiki/wiki/public/view/Linphone/Third%20party%20components%20/#Hlinphone-android</string>
<string name="conversation_end_to_end_encrypted_bottom_sheet_link" translatable="false"><u>https://linphone.org/en/features/#security</u></string>
@ -183,6 +183,12 @@
<string name="assistant_permissions_post_notifications_title"><b>Post notifications:</b> To be informed when you receive a message or a call.</string>
<string name="assistant_permissions_record_audio_title"><b>Record audio:</b> So your correspondent can hear you and to record voice messages.</string>
<string name="assistant_permissions_access_camera_title"><b>Access camera:</b> To capture video during video calls and conferences.</string>
<string name="assistant_forgotten_password_title">Account recovery</string>
<string name="assistant_forgotten_password_subtitle">Choose how to recover your account.</string>
<string name="assistant_forgotten_password_message">You created your account using:</string>
<string name="assistant_recover_email_account_label">An email</string>
<string name="assistant_recover_phone_number_account_label">A phone number</string>
<string name="assistant_recover_phone_number_account_unavailable_no_push_warning">Push notifications do not seem to be available on your device, but they are mandatory for recovering a phone number account in the mobile app.</string>
<!-- Main navigation items -->
<string name="bottom_navigation_contacts_label">Contacts</string>
@ -903,7 +909,7 @@
<!-- Keep <u></u> in the following strings translations! -->
<string name="welcome_carousel_skip"><u>Skip</u></string>
<string name="assistant_forgotten_password"><u>Forgotten password?</u></string>
<string name="assistant_forgotten_password"><u>Forgotten or unknown password?</u></string>
<string name="call_zrtp_sas_validation_skip"><u>Skip</u></string>
<!-- Keep <b></b> in the following strings translations! -->