diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityService.kt index 3a5a5df284..09462b9b41 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityService.kt @@ -40,15 +40,19 @@ interface IdentityService { fun setNewIdentityServer(url: String?, callback: MatrixCallback): Cancelable /** - * This will ask the identity server to send an email or an SMS to let the user confirm he owns the ThreePid, - * and then the threePid will be associated with the matrix account + * This will ask the identity server to send an email or an SMS to let the user confirm he owns the ThreePid */ fun startBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable /** - * This will perform the actual association of ThreePid and Matrix account + * This will cancel a pending binding of threePid. */ - fun finalizeBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable + fun cancelBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable + + /** + * This will ask the identity server to send an new email or a new SMS to let the user confirm he owns the ThreePid + */ + fun sendAgainValidationCode(threePid: ThreePid, callback: MatrixCallback): Cancelable /** * Submit the code that the identity server has sent to the user (in email or SMS) @@ -58,6 +62,12 @@ interface IdentityService { fun submitValidationToken(threePid: ThreePid, code: String, callback: MatrixCallback): Cancelable /** + * This will perform the actual association of ThreePid and Matrix account + */ + fun finalizeBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable + + /** + * Unbind a threePid * The request will actually be done on the homeserver */ fun unbindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt index ce3e555c66..0311cac64d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt @@ -123,7 +123,19 @@ internal class DefaultIdentityService @Inject constructor( override fun startBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable { return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { - identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid)) + identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid, false)) + } + } + + override fun cancelBindThreePid(threePid: ThreePid, callback: MatrixCallback): Cancelable { + return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + identityServiceStore.deletePendingBinding(threePid) + } + } + + override fun sendAgainValidationCode(threePid: ThreePid, callback: MatrixCallback): Cancelable { + return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid, true)) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRequestTokenForBindingTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRequestTokenForBindingTask.kt index 1f7b2af701..e06a0e3b9a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRequestTokenForBindingTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRequestTokenForBindingTask.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.session.identity +import im.vector.matrix.android.api.session.identity.IdentityServiceError import im.vector.matrix.android.api.session.identity.ThreePid import im.vector.matrix.android.api.session.identity.getCountryCode import im.vector.matrix.android.internal.di.UserId @@ -30,7 +31,9 @@ import javax.inject.Inject internal interface IdentityRequestTokenForBindingTask : Task { data class Params( - val threePid: ThreePid + val threePid: ThreePid, + // True to request the identity server to send again the email or the SMS + val sendAgain: Boolean ) } @@ -45,6 +48,10 @@ internal class DefaultIdentityRequestTokenForBindingTask @Inject constructor( val pendingBindingEntity = identityServiceStore.getPendingBinding(params.threePid) + if (params.sendAgain && pendingBindingEntity == null) { + throw IdentityServiceError.NoCurrentBindingError + } + val clientSecret = pendingBindingEntity?.clientSecret ?: UUID.randomUUID().toString() val sendAttempt = pendingBindingEntity?.sendAttempt?.inc() ?: 1 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingQuery.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingQuery.kt index ffbc586c4e..e358be6bbb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingQuery.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingQuery.kt @@ -32,10 +32,7 @@ internal fun IdentityPendingBindingEntity.Companion.getOrCreate(realm: Realm, th } internal fun IdentityPendingBindingEntity.Companion.delete(realm: Realm, threePid: ThreePid) { - realm.where() - .equalTo(IdentityPendingBindingEntityFields.THREE_PID, threePid.toPrimaryKey()) - .findAll() - .deleteAllFromRealm() + get(realm, threePid)?.deleteFromRealm() } internal fun IdentityPendingBindingEntity.Companion.deleteAll(realm: Realm) { diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsController.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsController.kt index 30c571ed08..ca0ae787b0 100644 --- a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsController.kt +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsController.kt @@ -21,6 +21,7 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized import com.google.i18n.phonenumbers.PhoneNumberUtil import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.identity.SharedState @@ -28,6 +29,7 @@ import im.vector.matrix.android.api.session.identity.ThreePid import im.vector.riotx.R import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider import timber.log.Timber @@ -111,15 +113,15 @@ class DiscoverySettingsController @Inject constructor( colorProvider(colorProvider) stringProvider(stringProvider) when (piState.isShared) { - is Loading -> buttonIndeterminate(true) + is Loading -> { + buttonIndeterminate(true) + } is Fail -> { buttonType(SettingsTextButtonItem.ButtonType.NORMAL) buttonStyle(SettingsTextButtonItem.ButtonStyle.DESTRUCTIVE) buttonTitle(stringProvider.getString(R.string.global_retry)) infoMessage(piState.isShared.error.message) - buttonClickListener(View.OnClickListener { - listener?.onTapRetryToRetrieveBindings() - }) + buttonClickListener { listener?.onTapRetryToRetrieveBindings() } } is Success -> when (piState.isShared()) { SharedState.SHARED, @@ -143,8 +145,8 @@ class DiscoverySettingsController @Inject constructor( } when (piState.isShared()) { SharedState.BINDING_IN_PROGRESS -> { - val errorText = if (piState.isTokenSubmitted is Fail) { - val error = piState.isTokenSubmitted.error + val errorText = if (piState.finalRequest is Fail) { + val error = piState.finalRequest.error // Deal with error 500 //Ref: https://github.com/matrix-org/sydent/issues/292 if (error is Failure.ServerError @@ -160,18 +162,22 @@ class DiscoverySettingsController @Inject constructor( id("tverif" + piState.threePid.value) descriptionText(stringProvider.getString(R.string.settings_text_message_sent, phoneNumber)) errorText(errorText) - inProgress(piState.isTokenSubmitted is Loading) + inProgress(piState.finalRequest is Loading) interactionListener(object : SettingsItemEditText.Listener { override fun onValidate(code: String) { if (piState.threePid is ThreePid.Msisdn) { listener?.sendMsisdnVerificationCode(piState.threePid, code) } } + + override fun onCancel() { + listener?.cancelBinding(piState.threePid) + } }) } } else -> Unit - } + }.exhaustive } } } @@ -210,15 +216,17 @@ class DiscoverySettingsController @Inject constructor( colorProvider(colorProvider) stringProvider(stringProvider) when (piState.isShared) { - is Loading -> buttonIndeterminate(true) + is Loading -> { + buttonIndeterminate(true) + showBottomButtons(false) + } is Fail -> { buttonType(SettingsTextButtonItem.ButtonType.NORMAL) buttonStyle(SettingsTextButtonItem.ButtonStyle.DESTRUCTIVE) buttonTitle(stringProvider.getString(R.string.global_retry)) infoMessage(piState.isShared.error.message) - buttonClickListener(View.OnClickListener { - listener?.onTapRetryToRetrieveBindings() - }) + buttonClickListener { listener?.onTapRetryToRetrieveBindings() } + showBottomButtons(false) } is Success -> when (piState.isShared()) { SharedState.SHARED, @@ -235,14 +243,32 @@ class DiscoverySettingsController @Inject constructor( } SharedState.BINDING_IN_PROGRESS -> { buttonType(SettingsTextButtonItem.ButtonType.NORMAL) - buttonTitleId(R.string._continue) - infoMessageTintColorId(R.color.vector_info_color) - infoMessage(stringProvider.getString(R.string.settings_discovery_confirm_mail, piState.threePid.value)) - buttonClickListener(View.OnClickListener { + buttonTitle(null) + showBottomButtons(true) + when (piState.finalRequest) { + is Uninitialized -> { + infoMessage(stringProvider.getString(R.string.settings_discovery_confirm_mail, piState.threePid.value)) + infoMessageTintColorId(R.color.vector_info_color) + showBottomLoading(false) + } + is Loading -> { + infoMessage(stringProvider.getString(R.string.settings_discovery_confirm_mail, piState.threePid.value)) + infoMessageTintColorId(R.color.vector_info_color) + showBottomLoading(true) + } + is Fail -> { + infoMessage(stringProvider.getString(R.string.settings_discovery_confirm_mail_not_clicked, piState.threePid.value)) + infoMessageTintColorId(R.color.riotx_destructive_accent) + showBottomLoading(false) + } + is Success -> Unit /* Cannot happen */ + } + cancelClickListener { listener?.cancelBinding(piState.threePid) } + continueClickListener { if (piState.threePid is ThreePid.Email) { listener?.checkEmailVerification(piState.threePid) } - }) + } } } } @@ -318,6 +344,7 @@ class DiscoverySettingsController @Inject constructor( fun onTapRevoke(threePid: ThreePid) fun onTapShare(threePid: ThreePid) fun checkEmailVerification(threePid: ThreePid.Email) + fun cancelBinding(threePid: ThreePid) fun sendMsisdnVerificationCode(threePid: ThreePid.Msisdn, code: String) fun onTapChangeIdentityServer() fun onTapDisconnectIdentityServer() diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt index 9358b791c8..0c77c8d1a2 100644 --- a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt @@ -130,6 +130,10 @@ class DiscoverySettingsFragment @Inject constructor( viewModel.handle(DiscoverySettingsAction.SubmitMsisdnToken(threePid, code)) } + override fun cancelBinding(threePid: ThreePid) { + viewModel.handle(DiscoverySettingsAction.CancelBinding(threePid)) + } + override fun onTapChangeIdentityServer() = withState(viewModel) { state -> //we should prompt if there are bound items with current is val pidList = state.emailList().orEmpty() + state.phoneNumbersList().orEmpty() diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewModel.kt index dbb54b6dce..707d018412 100644 --- a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewModel.kt @@ -44,7 +44,8 @@ data class PidInfo( // Retrieved from IdentityServer, or transient state val isShared: Async, // Contains information about a current request to submit the token (for instance SMS code received by SMS) - val isTokenSubmitted: Async = Uninitialized + // Or a current binding finalization, for email + val finalRequest: Async = Uninitialized ) data class DiscoverySettingsState( @@ -64,6 +65,7 @@ sealed class DiscoverySettingsAction : VectorViewModelAction { data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction() data class FinalizeBind3pid(val threePid: ThreePid) : DiscoverySettingsAction() data class SubmitMsisdnToken(val threePid: ThreePid.Msisdn, val code: String) : DiscoverySettingsAction() + data class CancelBinding(val threePid: ThreePid) : DiscoverySettingsAction() } sealed class DiscoverySettingsViewEvents : VectorViewEvents { @@ -133,6 +135,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( is DiscoverySettingsAction.ShareThreePid -> shareThreePid(action) is DiscoverySettingsAction.FinalizeBind3pid -> finalizeBind3pid(action) is DiscoverySettingsAction.SubmitMsisdnToken -> submitMsisdnToken(action) + is DiscoverySettingsAction.CancelBinding -> cancelBinding(action) }.exhaustive } @@ -214,7 +217,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( emailList = Success( currentMails.map { if (it.threePid == threePid) { - it.copy(isTokenSubmitted = submitState) + it.copy(finalRequest = submitState) } else { it } @@ -223,7 +226,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( phoneNumbersList = Success( phones.map { if (it.threePid == threePid) { - it.copy(isTokenSubmitted = submitState) + it.copy(finalRequest = submitState) } else { it } @@ -274,6 +277,18 @@ class DiscoverySettingsViewModel @AssistedInject constructor( }) } + private fun cancelBinding(action: DiscoverySettingsAction.CancelBinding) { + identityService.cancelBindThreePid(action.threePid, object : MatrixCallback { + override fun onSuccess(data: Unit) { + changeThreePidState(action.threePid, Success(SharedState.NOT_SHARED)) + } + + override fun onFailure(failure: Throwable) { + // This could never fail + } + }) + } + private fun startListenToIdentityManager() { identityService.addListener(identityServerManagerListener) } @@ -362,25 +377,23 @@ class DiscoverySettingsViewModel @AssistedInject constructor( private fun finalizeBind3pid(action: DiscoverySettingsAction.FinalizeBind3pid) = withState { state -> val threePid = when (action.threePid) { is ThreePid.Email -> { - changeThreePidState(action.threePid, Loading()) state.emailList()?.find { it.threePid.value == action.threePid.email }?.threePid ?: return@withState } is ThreePid.Msisdn -> { - changeThreePidState(action.threePid, Loading()) state.phoneNumbersList()?.find { it.threePid.value == action.threePid.msisdn }?.threePid ?: return@withState } } + changeThreePidSubmitState(action.threePid, Loading()) + identityService.finalizeBindThreePid(threePid, object : MatrixCallback { override fun onSuccess(data: Unit) { + changeThreePidSubmitState(action.threePid, Uninitialized) changeThreePidState(action.threePid, Success(SharedState.SHARED)) } override fun onFailure(failure: Throwable) { - _viewEvents.post(DiscoverySettingsViewEvents.Failure(failure)) - - // Restore previous state after an error - changeThreePidState(action.threePid, Success(SharedState.BINDING_IN_PROGRESS)) + changeThreePidSubmitState(action.threePid, Fail(failure)) } }) diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/SettingsItemEditText.kt b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsItemEditText.kt index 16a4b3ebcc..35c91989f8 100644 --- a/vector/src/main/java/im/vector/riotx/features/discovery/SettingsItemEditText.kt +++ b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsItemEditText.kt @@ -49,6 +49,10 @@ abstract class SettingsItemEditText : EpoxyModelWithHolder(R.id.settings_item_edittext) val textInputLayout by bind(R.id.settings_item_enter_til) val validateButton by bind