Merge pull request #2753 from vector-im/feature/bca/sso_reauth
Feature/bca/sso reauth
This commit is contained in:
commit
bc22647b48
@ -2,10 +2,12 @@ Changes in Element 1.0.15 (2020-XX-XX)
|
|||||||
===================================================
|
===================================================
|
||||||
|
|
||||||
Features ✨:
|
Features ✨:
|
||||||
-
|
- Social Login support
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
-
|
- SSO support for cross signing (#1062)
|
||||||
|
- Deactivate account when logged in with SSO (#1264)
|
||||||
|
- SSO UIA doesn't work (#2754)
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
- Fix clear cache issue: sometimes, after a clear cache, there is still a token, so the init sync service is not started.
|
- Fix clear cache issue: sometimes, after a clear cache, there is still a token, so the init sync service is not started.
|
||||||
|
@ -16,8 +16,18 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.account
|
package org.matrix.android.sdk.account
|
||||||
|
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.FixMethodOrder
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.JUnit4
|
||||||
|
import org.junit.runners.MethodSorters
|
||||||
import org.matrix.android.sdk.InstrumentedTest
|
import org.matrix.android.sdk.InstrumentedTest
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
import org.matrix.android.sdk.api.auth.data.LoginFlowResult
|
import org.matrix.android.sdk.api.auth.data.LoginFlowResult
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
|
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.MatrixError
|
import org.matrix.android.sdk.api.failure.MatrixError
|
||||||
@ -25,12 +35,8 @@ import org.matrix.android.sdk.common.CommonTestHelper
|
|||||||
import org.matrix.android.sdk.common.SessionTestParams
|
import org.matrix.android.sdk.common.SessionTestParams
|
||||||
import org.matrix.android.sdk.common.TestConstants
|
import org.matrix.android.sdk.common.TestConstants
|
||||||
import org.matrix.android.sdk.common.TestMatrixCallback
|
import org.matrix.android.sdk.common.TestMatrixCallback
|
||||||
import org.junit.Assert.assertTrue
|
import kotlin.coroutines.Continuation
|
||||||
import org.junit.FixMethodOrder
|
import kotlin.coroutines.resume
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.junit.runners.JUnit4
|
|
||||||
import org.junit.runners.MethodSorters
|
|
||||||
|
|
||||||
@RunWith(JUnit4::class)
|
@RunWith(JUnit4::class)
|
||||||
@FixMethodOrder(MethodSorters.JVM)
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
@ -44,7 +50,18 @@ class DeactivateAccountTest : InstrumentedTest {
|
|||||||
|
|
||||||
// Deactivate the account
|
// Deactivate the account
|
||||||
commonTestHelper.runBlockingTest {
|
commonTestHelper.runBlockingTest {
|
||||||
session.deactivateAccount(TestConstants.PASSWORD, false)
|
session.deactivateAccount(
|
||||||
|
object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
promise.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
|
user = session.myUserId,
|
||||||
|
password = TestConstants.PASSWORD,
|
||||||
|
session = flowResponse.session
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to login on the previous account, it will fail (M_USER_DEACTIVATED)
|
// Try to login on the previous account, it will fail (M_USER_DEACTIVATED)
|
||||||
|
@ -19,6 +19,18 @@ package org.matrix.android.sdk.common
|
|||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||||
@ -36,17 +48,10 @@ import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
|||||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertNotNull
|
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||||
|
|
||||||
@ -304,10 +309,18 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
|||||||
fun initializeCrossSigning(session: Session) {
|
fun initializeCrossSigning(session: Session) {
|
||||||
mTestHelper.doSync<Unit> {
|
mTestHelper.doSync<Unit> {
|
||||||
session.cryptoService().crossSigningService()
|
session.cryptoService().crossSigningService()
|
||||||
.initializeCrossSigning(UserPasswordAuth(
|
.initializeCrossSigning(
|
||||||
|
object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
promise.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
user = session.myUserId,
|
user = session.myUserId,
|
||||||
password = TestConstants.PASSWORD
|
password = TestConstants.PASSWORD,
|
||||||
), it)
|
session = flowResponse.session
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,18 @@
|
|||||||
package org.matrix.android.sdk.internal.crypto
|
package org.matrix.android.sdk.internal.crypto
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import org.amshove.kluent.shouldBe
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.FixMethodOrder
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.MethodSorters
|
||||||
import org.matrix.android.sdk.InstrumentedTest
|
import org.matrix.android.sdk.InstrumentedTest
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
@ -30,19 +41,13 @@ import org.matrix.android.sdk.common.CryptoTestHelper
|
|||||||
import org.matrix.android.sdk.common.TestConstants
|
import org.matrix.android.sdk.common.TestConstants
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
|
import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
|
import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
|
||||||
import org.amshove.kluent.shouldBe
|
|
||||||
import org.junit.Assert
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.FixMethodOrder
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.junit.runners.MethodSorters
|
|
||||||
import org.matrix.olm.OlmSession
|
import org.matrix.olm.OlmSession
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ref:
|
* Ref:
|
||||||
@ -202,10 +207,18 @@ class UnwedgingTest : InstrumentedTest {
|
|||||||
// It's a trick to force key request on fail to decrypt
|
// It's a trick to force key request on fail to decrypt
|
||||||
mTestHelper.doSync<Unit> {
|
mTestHelper.doSync<Unit> {
|
||||||
bobSession.cryptoService().crossSigningService()
|
bobSession.cryptoService().crossSigningService()
|
||||||
.initializeCrossSigning(UserPasswordAuth(
|
.initializeCrossSigning(
|
||||||
|
object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
promise.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
user = bobSession.myUserId,
|
user = bobSession.myUserId,
|
||||||
password = TestConstants.PASSWORD
|
password = TestConstants.PASSWORD,
|
||||||
), it)
|
session = flowResponse.session
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait until we received back the key
|
// Wait until we received back the key
|
||||||
|
@ -17,14 +17,6 @@
|
|||||||
package org.matrix.android.sdk.internal.crypto.crosssigning
|
package org.matrix.android.sdk.internal.crypto.crosssigning
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.matrix.android.sdk.InstrumentedTest
|
|
||||||
import org.matrix.android.sdk.common.CommonTestHelper
|
|
||||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
|
||||||
import org.matrix.android.sdk.common.SessionTestParams
|
|
||||||
import org.matrix.android.sdk.common.TestConstants
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
@ -35,6 +27,19 @@ import org.junit.FixMethodOrder
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.MethodSorters
|
import org.junit.runners.MethodSorters
|
||||||
|
import org.matrix.android.sdk.InstrumentedTest
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
|
import org.matrix.android.sdk.common.CommonTestHelper
|
||||||
|
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||||
|
import org.matrix.android.sdk.common.SessionTestParams
|
||||||
|
import org.matrix.android.sdk.common.TestConstants
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||||
@ -49,10 +54,17 @@ class XSigningTest : InstrumentedTest {
|
|||||||
|
|
||||||
mTestHelper.doSync<Unit> {
|
mTestHelper.doSync<Unit> {
|
||||||
aliceSession.cryptoService().crossSigningService()
|
aliceSession.cryptoService().crossSigningService()
|
||||||
.initializeCrossSigning(UserPasswordAuth(
|
.initializeCrossSigning(object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
promise.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
user = aliceSession.myUserId,
|
user = aliceSession.myUserId,
|
||||||
password = TestConstants.PASSWORD
|
password = TestConstants.PASSWORD,
|
||||||
), it)
|
session = flowResponse.session
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val myCrossSigningKeys = aliceSession.cryptoService().crossSigningService().getMyCrossSigningKeys()
|
val myCrossSigningKeys = aliceSession.cryptoService().crossSigningService().getMyCrossSigningKeys()
|
||||||
@ -86,8 +98,18 @@ class XSigningTest : InstrumentedTest {
|
|||||||
password = TestConstants.PASSWORD
|
password = TestConstants.PASSWORD
|
||||||
)
|
)
|
||||||
|
|
||||||
mTestHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) }
|
mTestHelper.doSync<Unit> {
|
||||||
mTestHelper.doSync<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) }
|
aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
promise.resume(aliceAuthParams)
|
||||||
|
}
|
||||||
|
}, it)
|
||||||
|
}
|
||||||
|
mTestHelper.doSync<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
promise.resume(bobAuthParams)
|
||||||
|
}
|
||||||
|
}, it) }
|
||||||
|
|
||||||
// Check that alice can see bob keys
|
// Check that alice can see bob keys
|
||||||
mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) }
|
mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) }
|
||||||
@ -122,8 +144,16 @@ class XSigningTest : InstrumentedTest {
|
|||||||
password = TestConstants.PASSWORD
|
password = TestConstants.PASSWORD
|
||||||
)
|
)
|
||||||
|
|
||||||
mTestHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) }
|
mTestHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
|
||||||
mTestHelper.doSync<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) }
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
promise.resume(aliceAuthParams)
|
||||||
|
}
|
||||||
|
}, it) }
|
||||||
|
mTestHelper.doSync<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
promise.resume(bobAuthParams)
|
||||||
|
}
|
||||||
|
}, it) }
|
||||||
|
|
||||||
// Check that alice can see bob keys
|
// Check that alice can see bob keys
|
||||||
val bobUserId = bobSession.myUserId
|
val bobUserId = bobSession.myUserId
|
||||||
|
@ -18,7 +18,21 @@ package org.matrix.android.sdk.internal.crypto.gossiping
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import junit.framework.TestCase.assertNotNull
|
||||||
|
import junit.framework.TestCase.assertTrue
|
||||||
|
import junit.framework.TestCase.fail
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.FixMethodOrder
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.MethodSorters
|
||||||
import org.matrix.android.sdk.InstrumentedTest
|
import org.matrix.android.sdk.InstrumentedTest
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
||||||
@ -28,6 +42,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxStat
|
|||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
||||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||||
import org.matrix.android.sdk.common.CommonTestHelper
|
import org.matrix.android.sdk.common.CommonTestHelper
|
||||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||||
import org.matrix.android.sdk.common.SessionTestParams
|
import org.matrix.android.sdk.common.SessionTestParams
|
||||||
@ -40,19 +55,9 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
|
|||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
|
||||||
import junit.framework.TestCase.assertEquals
|
|
||||||
import junit.framework.TestCase.assertNotNull
|
|
||||||
import junit.framework.TestCase.assertTrue
|
|
||||||
import junit.framework.TestCase.fail
|
|
||||||
import org.junit.Assert
|
|
||||||
import org.junit.FixMethodOrder
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.junit.runners.MethodSorters
|
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@FixMethodOrder(MethodSorters.JVM)
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
@ -200,10 +205,17 @@ class KeyShareTests : InstrumentedTest {
|
|||||||
|
|
||||||
mTestHelper.doSync<Unit> {
|
mTestHelper.doSync<Unit> {
|
||||||
aliceSession1.cryptoService().crossSigningService()
|
aliceSession1.cryptoService().crossSigningService()
|
||||||
.initializeCrossSigning(UserPasswordAuth(
|
.initializeCrossSigning(
|
||||||
|
object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
promise.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
user = aliceSession1.myUserId,
|
user = aliceSession1.myUserId,
|
||||||
password = TestConstants.PASSWORD
|
password = TestConstants.PASSWORD
|
||||||
), it)
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also bootstrap keybackup on first session
|
// Also bootstrap keybackup on first session
|
||||||
@ -305,10 +317,18 @@ class KeyShareTests : InstrumentedTest {
|
|||||||
|
|
||||||
mTestHelper.doSync<Unit> {
|
mTestHelper.doSync<Unit> {
|
||||||
aliceSession.cryptoService().crossSigningService()
|
aliceSession.cryptoService().crossSigningService()
|
||||||
.initializeCrossSigning(UserPasswordAuth(
|
.initializeCrossSigning(
|
||||||
|
object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
promise.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
user = aliceSession.myUserId,
|
user = aliceSession.myUserId,
|
||||||
password = TestConstants.PASSWORD
|
password = TestConstants.PASSWORD,
|
||||||
), it)
|
session = flowResponse.session
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create an encrypted room and send a couple of messages
|
// Create an encrypted room and send a couple of messages
|
||||||
@ -332,10 +352,18 @@ class KeyShareTests : InstrumentedTest {
|
|||||||
val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true))
|
val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true))
|
||||||
mTestHelper.doSync<Unit> {
|
mTestHelper.doSync<Unit> {
|
||||||
bobSession.cryptoService().crossSigningService()
|
bobSession.cryptoService().crossSigningService()
|
||||||
.initializeCrossSigning(UserPasswordAuth(
|
.initializeCrossSigning(
|
||||||
|
object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
promise.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
user = bobSession.myUserId,
|
user = bobSession.myUserId,
|
||||||
password = TestConstants.PASSWORD
|
password = TestConstants.PASSWORD,
|
||||||
), it)
|
session = flowResponse.session
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let alice invite bob
|
// Let alice invite bob
|
||||||
|
@ -17,20 +17,25 @@
|
|||||||
package org.matrix.android.sdk.internal.crypto.verification.qrcode
|
package org.matrix.android.sdk.internal.crypto.verification.qrcode
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.matrix.android.sdk.InstrumentedTest
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
|
||||||
import org.matrix.android.sdk.common.CommonTestHelper
|
|
||||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
|
||||||
import org.matrix.android.sdk.common.TestConstants
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
|
|
||||||
import org.amshove.kluent.shouldBe
|
import org.amshove.kluent.shouldBe
|
||||||
import org.junit.FixMethodOrder
|
import org.junit.FixMethodOrder
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.MethodSorters
|
import org.junit.runners.MethodSorters
|
||||||
|
import org.matrix.android.sdk.InstrumentedTest
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||||
|
import org.matrix.android.sdk.common.CommonTestHelper
|
||||||
|
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||||
|
import org.matrix.android.sdk.common.TestConstants
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@FixMethodOrder(MethodSorters.JVM)
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
@ -157,18 +162,34 @@ class VerificationTest : InstrumentedTest {
|
|||||||
|
|
||||||
mTestHelper.doSync<Unit> { callback ->
|
mTestHelper.doSync<Unit> { callback ->
|
||||||
aliceSession.cryptoService().crossSigningService()
|
aliceSession.cryptoService().crossSigningService()
|
||||||
.initializeCrossSigning(UserPasswordAuth(
|
.initializeCrossSigning(
|
||||||
|
object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
promise.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
user = aliceSession.myUserId,
|
user = aliceSession.myUserId,
|
||||||
password = TestConstants.PASSWORD
|
password = TestConstants.PASSWORD,
|
||||||
), callback)
|
session = flowResponse.session
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
mTestHelper.doSync<Unit> { callback ->
|
mTestHelper.doSync<Unit> { callback ->
|
||||||
bobSession.cryptoService().crossSigningService()
|
bobSession.cryptoService().crossSigningService()
|
||||||
.initializeCrossSigning(UserPasswordAuth(
|
.initializeCrossSigning(
|
||||||
|
object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
promise.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
user = bobSession.myUserId,
|
user = bobSession.myUserId,
|
||||||
password = TestConstants.PASSWORD
|
password = TestConstants.PASSWORD,
|
||||||
), callback)
|
session = flowResponse.session
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
val aliceVerificationService = aliceSession.cryptoService().verificationService()
|
val aliceVerificationService = aliceSession.cryptoService().verificationService()
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.matrix.android.sdk.api.auth
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides the authentication data by using user and password
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class TokenBasedAuth(
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a session identifier that the client must pass back to the homeserver,
|
||||||
|
* if one is provided, in subsequent attempts to authenticate in the same API call.
|
||||||
|
*/
|
||||||
|
@Json(name = "session")
|
||||||
|
override val session: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A client may receive a login token via some external service, such as email or SMS.
|
||||||
|
* Note that a login token is separate from an access token, the latter providing general authentication to various API endpoints.
|
||||||
|
*/
|
||||||
|
@Json(name = "token")
|
||||||
|
val token: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The txn_id should be a random string generated by the client for the request.
|
||||||
|
* The same txn_id should be used if retrying the request.
|
||||||
|
* The txn_id may be used by the server to disallow other devices from using the token,
|
||||||
|
* thus providing "single use" tokens while still allowing the device to retry the request.
|
||||||
|
* This would be done by tying the token to the txn_id server side, as well as potentially invalidating
|
||||||
|
* the token completely once the device has successfully logged in
|
||||||
|
* (e.g. when we receive a request from the newly provisioned access_token).
|
||||||
|
*/
|
||||||
|
@Json(name = "txn_id")
|
||||||
|
val transactionId: String? = null,
|
||||||
|
|
||||||
|
// registration information
|
||||||
|
@Json(name = "type")
|
||||||
|
val type: String? = LoginFlowTypes.TOKEN
|
||||||
|
|
||||||
|
) : UIABaseAuth {
|
||||||
|
override fun hasAuthInfo() = token != null
|
||||||
|
|
||||||
|
override fun copyWithSession(session: String) = this.copy(session = session)
|
||||||
|
|
||||||
|
override fun asMap(): Map<String, *> = mapOf(
|
||||||
|
"session" to session,
|
||||||
|
"token" to token,
|
||||||
|
"transactionId" to transactionId,
|
||||||
|
"type" to type
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.api.auth
|
||||||
|
|
||||||
|
interface UIABaseAuth {
|
||||||
|
/**
|
||||||
|
* This is a session identifier that the client must pass back to the homeserver,
|
||||||
|
* if one is provided, in subsequent attempts to authenticate in the same API call.
|
||||||
|
*/
|
||||||
|
val session: String?
|
||||||
|
|
||||||
|
fun hasAuthInfo(): Boolean
|
||||||
|
|
||||||
|
fun copyWithSession(session: String): UIABaseAuth
|
||||||
|
|
||||||
|
fun asMap() : Map<String, *>
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.api.auth
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some API endpoints require authentication that interacts with the user.
|
||||||
|
* The homeserver may provide many different ways of authenticating, such as user/password auth, login via a social network (OAuth2),
|
||||||
|
* login by confirming a token sent to their email address, etc.
|
||||||
|
*
|
||||||
|
* The process takes the form of one or more 'stages'.
|
||||||
|
* At each stage the client submits a set of data for a given authentication type and awaits a response from the server,
|
||||||
|
* which will either be a final success or a request to perform an additional stage.
|
||||||
|
* This exchange continues until the final success.
|
||||||
|
*
|
||||||
|
* For each endpoint, a server offers one or more 'flows' that the client can use to authenticate itself.
|
||||||
|
* Each flow comprises a series of stages, as described above.
|
||||||
|
* The client is free to choose which flow it follows, however the flow's stages must be completed in order.
|
||||||
|
* Failing to follow the flows in order must result in an HTTP 401 response.
|
||||||
|
* When all stages in a flow are complete, authentication is complete and the API call succeeds.
|
||||||
|
*/
|
||||||
|
interface UserInteractiveAuthInterceptor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the API needs additional auth, this will be called.
|
||||||
|
* Implementation should check the flows from flow response and act accordingly.
|
||||||
|
* Updated auth should be provided using promise.resume, this allow implementation to perform
|
||||||
|
* an async operation (prompt for user password, open sso fallback) and then resume initial API call when done.
|
||||||
|
*/
|
||||||
|
fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>)
|
||||||
|
}
|
@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.matrix.android.sdk.internal.crypto.model.rest
|
package org.matrix.android.sdk.api.auth
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
@ -27,7 +27,7 @@ data class UserPasswordAuth(
|
|||||||
|
|
||||||
// device device session id
|
// device device session id
|
||||||
@Json(name = "session")
|
@Json(name = "session")
|
||||||
val session: String? = null,
|
override val session: String? = null,
|
||||||
|
|
||||||
// registration information
|
// registration information
|
||||||
@Json(name = "type")
|
@Json(name = "type")
|
||||||
@ -38,4 +38,16 @@ data class UserPasswordAuth(
|
|||||||
|
|
||||||
@Json(name = "password")
|
@Json(name = "password")
|
||||||
val password: String? = null
|
val password: String? = null
|
||||||
|
) : UIABaseAuth {
|
||||||
|
|
||||||
|
override fun hasAuthInfo() = password != null
|
||||||
|
|
||||||
|
override fun copyWithSession(session: String) = this.copy(session = session)
|
||||||
|
|
||||||
|
override fun asMap(): Map<String, *> = mapOf(
|
||||||
|
"session" to session,
|
||||||
|
"user" to user,
|
||||||
|
"password" to password,
|
||||||
|
"type" to type
|
||||||
)
|
)
|
||||||
|
}
|
@ -38,15 +38,24 @@ data class SsoIdentityProvider(
|
|||||||
* If present then it must be an HTTPS URL to an image resource.
|
* If present then it must be an HTTPS URL to an image resource.
|
||||||
* This should be hosted by the homeserver service provider to not leak the client's IP address unnecessarily.
|
* This should be hosted by the homeserver service provider to not leak the client's IP address unnecessarily.
|
||||||
*/
|
*/
|
||||||
@Json(name = "icon") val iconUrl: String?
|
@Json(name = "icon") val iconUrl: String?,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `brand` field is **optional**. It allows the client to style the login
|
||||||
|
* button to suit a particular brand. It should be a string matching the
|
||||||
|
* "Common namespaced identifier grammar" as defined in
|
||||||
|
* [MSC2758](https://github.com/matrix-org/matrix-doc/pull/2758).
|
||||||
|
*/
|
||||||
|
@Json(name = "brand") val brand: String?
|
||||||
|
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// Not really defined by the spec, but we may define some ids here
|
const val BRAND_GOOGLE = "org.matrix.google"
|
||||||
const val ID_GOOGLE = "google"
|
const val BRAND_GITHUB = "org.matrix.github"
|
||||||
const val ID_GITHUB = "github"
|
const val BRAND_APPLE = "org.matrix.apple"
|
||||||
const val ID_APPLE = "apple"
|
const val BRAND_FACEBOOK = "org.matrix.facebook"
|
||||||
const val ID_FACEBOOK = "facebook"
|
const val BRAND_TWITTER = "org.matrix.twitter"
|
||||||
const val ID_TWITTER = "twitter"
|
const val BRAND_GITLAB = "org.matrix.gitlab"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,14 +14,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.auth.registration
|
package org.matrix.android.sdk.api.auth.registration
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
import org.matrix.android.sdk.api.auth.registration.FlowResult
|
|
||||||
import org.matrix.android.sdk.api.auth.registration.Stage
|
|
||||||
import org.matrix.android.sdk.api.auth.registration.TermPolicies
|
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
import org.matrix.android.sdk.internal.auth.data.InteractiveAuthenticationFlow
|
import org.matrix.android.sdk.internal.auth.data.InteractiveAuthenticationFlow
|
||||||
|
|
||||||
@ -109,3 +106,8 @@ fun RegistrationFlowResponse.toFlowResult(): FlowResult {
|
|||||||
|
|
||||||
return FlowResult(missingStage, completedStage)
|
return FlowResult(missingStage, completedStage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun RegistrationFlowResponse.nextUncompletedStage(flowIndex: Int = 0): String? {
|
||||||
|
val completed = completedStages ?: emptyList()
|
||||||
|
return flows?.getOrNull(flowIndex)?.stages?.firstOrNull { completed.contains(it).not() }
|
||||||
|
}
|
@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.api.failure
|
package org.matrix.android.sdk.api.failure
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
|
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import javax.net.ssl.HttpsURLConnection
|
import javax.net.ssl.HttpsURLConnection
|
||||||
@ -43,6 +43,12 @@ fun Throwable.isInvalidPassword(): Boolean {
|
|||||||
&& error.message == "Invalid password"
|
&& error.message == "Invalid password"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Throwable.isInvalidUIAAuth(): Boolean {
|
||||||
|
return this is Failure.ServerError
|
||||||
|
&& error.code == MatrixError.M_FORBIDDEN
|
||||||
|
&& error.flows != null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible
|
* Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible
|
||||||
*/
|
*/
|
||||||
@ -53,6 +59,16 @@ fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? {
|
|||||||
.adapter(RegistrationFlowResponse::class.java)
|
.adapter(RegistrationFlowResponse::class.java)
|
||||||
.fromJson(this.errorBody)
|
.fromJson(this.errorBody)
|
||||||
}
|
}
|
||||||
|
} else if (this is Failure.ServerError && this.httpCode == 401 && this.error.code == MatrixError.M_FORBIDDEN) {
|
||||||
|
// This happens when the submission for this stage was bad (like bad password)
|
||||||
|
if (this.error.session != null && this.error.flows != null) {
|
||||||
|
RegistrationFlowResponse(
|
||||||
|
flows = this.error.flows,
|
||||||
|
session = this.error.session,
|
||||||
|
completedStages = this.error.completedStages,
|
||||||
|
params = this.error.params
|
||||||
|
)
|
||||||
|
} else null
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.api.failure
|
package org.matrix.android.sdk.api.failure
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
|
|
||||||
import org.matrix.android.sdk.internal.network.ssl.Fingerprint
|
import org.matrix.android.sdk.internal.network.ssl.Fingerprint
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
|
@ -18,6 +18,8 @@ package org.matrix.android.sdk.api.failure
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
|
import org.matrix.android.sdk.internal.auth.data.InteractiveAuthenticationFlow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This data class holds the error defined by the matrix specifications.
|
* This data class holds the error defined by the matrix specifications.
|
||||||
@ -42,7 +44,17 @@ data class MatrixError(
|
|||||||
@Json(name = "soft_logout") val isSoftLogout: Boolean = false,
|
@Json(name = "soft_logout") val isSoftLogout: Boolean = false,
|
||||||
// For M_INVALID_PEPPER
|
// For M_INVALID_PEPPER
|
||||||
// {"error": "pepper does not match 'erZvr'", "lookup_pepper": "pQgMS", "algorithm": "sha256", "errcode": "M_INVALID_PEPPER"}
|
// {"error": "pepper does not match 'erZvr'", "lookup_pepper": "pQgMS", "algorithm": "sha256", "errcode": "M_INVALID_PEPPER"}
|
||||||
@Json(name = "lookup_pepper") val newLookupPepper: String? = null
|
@Json(name = "lookup_pepper") val newLookupPepper: String? = null,
|
||||||
|
|
||||||
|
// For M_FORBIDDEN UIA
|
||||||
|
@Json(name = "session")
|
||||||
|
val session: String? = null,
|
||||||
|
@Json(name = "completed")
|
||||||
|
val completedStages: List<String>? = null,
|
||||||
|
@Json(name = "flows")
|
||||||
|
val flows: List<InteractiveAuthenticationFlow>? = null,
|
||||||
|
@Json(name = "params")
|
||||||
|
val params: JsonDict? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -245,6 +245,8 @@ interface Session :
|
|||||||
|
|
||||||
val sharedSecretStorageService: SharedSecretStorageService
|
val sharedSecretStorageService: SharedSecretStorageService
|
||||||
|
|
||||||
|
fun getUiaSsoFallbackUrl(authenticationSessionId: String): String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maintenance API, allows to print outs info on DB size to logcat
|
* Maintenance API, allows to print outs info on DB size to logcat
|
||||||
*/
|
*/
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.api.session.account
|
package org.matrix.android.sdk.api.session.account
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This interface defines methods to manage the account. It's implemented at the session level.
|
* This interface defines methods to manage the account. It's implemented at the session level.
|
||||||
*/
|
*/
|
||||||
@ -43,5 +45,5 @@ interface AccountService {
|
|||||||
* @param eraseAllData set to true to forget all messages that have been sent. Warning: this will cause future users to see
|
* @param eraseAllData set to true to forget all messages that have been sent. Warning: this will cause future users to see
|
||||||
* an incomplete view of conversations
|
* an incomplete view of conversations
|
||||||
*/
|
*/
|
||||||
suspend fun deactivateAccount(password: String, eraseAllData: Boolean)
|
suspend fun deactivateAccount(userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, eraseAllData: Boolean)
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import android.content.Context
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.PagedList
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
|
||||||
@ -53,7 +54,7 @@ interface CryptoService {
|
|||||||
|
|
||||||
fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>)
|
fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>)
|
||||||
|
|
||||||
fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>)
|
fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>)
|
||||||
|
|
||||||
fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>)
|
fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>)
|
||||||
|
|
||||||
|
@ -18,10 +18,10 @@ package org.matrix.android.sdk.api.session.crypto.crosssigning
|
|||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustResult
|
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustResult
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.UserTrustResult
|
import org.matrix.android.sdk.internal.crypto.crosssigning.UserTrustResult
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
|
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
|
||||||
|
|
||||||
interface CrossSigningService {
|
interface CrossSigningService {
|
||||||
@ -40,7 +40,7 @@ interface CrossSigningService {
|
|||||||
* Initialize cross signing for this user.
|
* Initialize cross signing for this user.
|
||||||
* Users needs to enter credentials
|
* Users needs to enter credentials
|
||||||
*/
|
*/
|
||||||
fun initializeCrossSigning(authParams: UserPasswordAuth?,
|
fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?,
|
||||||
callback: MatrixCallback<Unit>)
|
callback: MatrixCallback<Unit>)
|
||||||
|
|
||||||
fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null
|
fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null
|
||||||
|
@ -20,6 +20,7 @@ package org.matrix.android.sdk.api.session.profile
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||||
import org.matrix.android.sdk.api.util.Cancelable
|
import org.matrix.android.sdk.api.util.Cancelable
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
@ -107,8 +108,7 @@ interface ProfileService {
|
|||||||
* Finalize adding a 3Pids. Call this method once the user has validated that he owns the ThreePid
|
* Finalize adding a 3Pids. Call this method once the user has validated that he owns the ThreePid
|
||||||
*/
|
*/
|
||||||
fun finalizeAddingThreePid(threePid: ThreePid,
|
fun finalizeAddingThreePid(threePid: ThreePid,
|
||||||
uiaSession: String?,
|
userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
|
||||||
accountPassword: String?,
|
|
||||||
matrixCallback: MatrixCallback<Unit>): Cancelable
|
matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,3 +36,6 @@ internal const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect"
|
|||||||
internal const val MSC2858_SSO_REDIRECT_PATH = "/_matrix/client/unstable/org.matrix.msc2858/login/sso/redirect"
|
internal const val MSC2858_SSO_REDIRECT_PATH = "/_matrix/client/unstable/org.matrix.msc2858/login/sso/redirect"
|
||||||
|
|
||||||
internal const val SSO_REDIRECT_URL_PARAM = "redirectUrl"
|
internal const val SSO_REDIRECT_URL_PARAM = "redirectUrl"
|
||||||
|
|
||||||
|
// Ref: https://matrix.org/docs/spec/client_server/r0.6.1#single-sign-on
|
||||||
|
internal const val SSO_UIA_FALLBACK_PATH = "/_matrix/client/r0/auth/m.login.sso/fallback/web"
|
||||||
|
@ -43,5 +43,6 @@ internal data class LoginFlow(
|
|||||||
* See MSC #2858
|
* See MSC #2858
|
||||||
*/
|
*/
|
||||||
@Json(name = "org.matrix.msc2858.identity_providers")
|
@Json(name = "org.matrix.msc2858.identity_providers")
|
||||||
val ssoIdentityProvider: List<SsoIdentityProvider>?
|
val ssoIdentityProvider: List<SsoIdentityProvider>? = null
|
||||||
|
|
||||||
)
|
)
|
||||||
|
@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
|||||||
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
||||||
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
|
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
|
||||||
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
|
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.toFlowResult
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.Failure.RegistrationFlowError
|
import org.matrix.android.sdk.api.failure.Failure.RegistrationFlowError
|
||||||
import org.matrix.android.sdk.api.util.Cancelable
|
import org.matrix.android.sdk.api.util.Cancelable
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.auth.registration
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
|
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
|
import timber.log.Timber
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
internal suspend fun handleUIA(failure: Throwable, interceptor: UserInteractiveAuthInterceptor, retryBlock: suspend (UIABaseAuth) -> Unit): Boolean {
|
||||||
|
Timber.d("## UIA: check error ${failure.message}")
|
||||||
|
val flowResponse = failure.toRegistrationFlowResponse()
|
||||||
|
?: return false.also {
|
||||||
|
Timber.d("## UIA: not a UIA error")
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("## UIA: error can be passed to interceptor")
|
||||||
|
Timber.d("## UIA: type = ${flowResponse.flows}")
|
||||||
|
|
||||||
|
Timber.d("## UIA: delegate to interceptor...")
|
||||||
|
val authUpdate = try {
|
||||||
|
suspendCoroutine<UIABaseAuth> { continuation ->
|
||||||
|
interceptor.performStage(flowResponse, (failure as? Failure.ServerError)?.error?.code, continuation)
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.w(failure, "## UIA: failed to participate")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("## UIA: updated auth $authUpdate")
|
||||||
|
return try {
|
||||||
|
retryBlock(authUpdate)
|
||||||
|
true
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
handleUIA(failure, interceptor, retryBlock)
|
||||||
|
}
|
||||||
|
}
|
@ -30,6 +30,7 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
@ -207,9 +208,9 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) {
|
override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>) {
|
||||||
deleteDeviceTask
|
deleteDeviceTask
|
||||||
.configureWith(DeleteDeviceTask.Params(deviceId)) {
|
.configureWith(DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null)) {
|
||||||
this.executionThread = TaskThread.CRYPTO
|
this.executionThread = TaskThread.CRYPTO
|
||||||
this.callback = callback
|
this.callback = callback
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import androidx.work.ExistingWorkPolicy
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||||
@ -29,7 +30,6 @@ import org.matrix.android.sdk.api.util.Optional
|
|||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
|
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
|
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
|
||||||
@ -150,11 +150,11 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
* - Sign the keys and upload them
|
* - Sign the keys and upload them
|
||||||
* - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures
|
* - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures
|
||||||
*/
|
*/
|
||||||
override fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback<Unit>) {
|
override fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?, callback: MatrixCallback<Unit>) {
|
||||||
Timber.d("## CrossSigning initializeCrossSigning")
|
Timber.d("## CrossSigning initializeCrossSigning")
|
||||||
|
|
||||||
val params = InitializeCrossSigningTask.Params(
|
val params = InitializeCrossSigningTask.Params(
|
||||||
authParams = authParams
|
interactiveAuthInterceptor = uiaInterceptor
|
||||||
)
|
)
|
||||||
initializeCrossSigningTask.configureWith(params) {
|
initializeCrossSigningTask.configureWith(params) {
|
||||||
this.callbackThread = TaskThread.CRYPTO
|
this.callbackThread = TaskThread.CRYPTO
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.crypto.model.rest
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
|
|
||||||
|
data class DefaultBaseAuth(
|
||||||
|
/**
|
||||||
|
* This is a session identifier that the client must pass back to the homeserver,
|
||||||
|
* if one is provided, in subsequent attempts to authenticate in the same API call.
|
||||||
|
*/
|
||||||
|
override val session: String? = null
|
||||||
|
|
||||||
|
) : UIABaseAuth {
|
||||||
|
override fun hasAuthInfo() = true
|
||||||
|
|
||||||
|
override fun copyWithSession(session: String) = this.copy(session = session)
|
||||||
|
|
||||||
|
override fun asMap(): Map<String, *> = mapOf("session" to session)
|
||||||
|
}
|
@ -24,5 +24,5 @@ import com.squareup.moshi.JsonClass
|
|||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class DeleteDeviceParams(
|
internal data class DeleteDeviceParams(
|
||||||
@Json(name = "auth")
|
@Json(name = "auth")
|
||||||
val userPasswordAuth: UserPasswordAuth? = null
|
val auth: Map<String, *>? = null
|
||||||
)
|
)
|
||||||
|
@ -30,5 +30,5 @@ internal data class UploadSigningKeysBody(
|
|||||||
val userSigningKey: RestKeyInfo? = null,
|
val userSigningKey: RestKeyInfo? = null,
|
||||||
|
|
||||||
@Json(name = "auth")
|
@Json(name = "auth")
|
||||||
val auth: UserPasswordAuth? = null
|
val auth: Map<String, *>? = null
|
||||||
)
|
)
|
||||||
|
@ -16,18 +16,22 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.tasks
|
package org.matrix.android.sdk.internal.crypto.tasks
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
|
import org.matrix.android.sdk.internal.auth.registration.handleUIA
|
||||||
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
|
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
|
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||||
import org.matrix.android.sdk.internal.network.executeRequest
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
import org.matrix.android.sdk.internal.task.Task
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> {
|
internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> {
|
||||||
data class Params(
|
data class Params(
|
||||||
val deviceId: String
|
val deviceId: String,
|
||||||
|
val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?,
|
||||||
|
val userAuthParam: UIABaseAuth?
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,12 +43,17 @@ internal class DefaultDeleteDeviceTask @Inject constructor(
|
|||||||
override suspend fun execute(params: DeleteDeviceTask.Params) {
|
override suspend fun execute(params: DeleteDeviceTask.Params) {
|
||||||
try {
|
try {
|
||||||
executeRequest<Unit>(globalErrorReceiver) {
|
executeRequest<Unit>(globalErrorReceiver) {
|
||||||
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams())
|
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams(params.userAuthParam?.asMap()))
|
||||||
}
|
}
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
throw throwable.toRegistrationFlowResponse()
|
if (params.userInteractiveAuthInterceptor == null
|
||||||
?.let { Failure.RegistrationFlowError(it) }
|
|| !handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth ->
|
||||||
?: throwable
|
execute(params.copy(userAuthParam = auth))
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Timber.d("## UIA: propagate failure")
|
||||||
|
throw throwable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.crypto.tasks
|
|||||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
|
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
|
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||||
import org.matrix.android.sdk.internal.network.executeRequest
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
@ -44,12 +44,12 @@ internal class DefaultDeleteDeviceWithUserPasswordTask @Inject constructor(
|
|||||||
return executeRequest(globalErrorReceiver) {
|
return executeRequest(globalErrorReceiver) {
|
||||||
apiCall = cryptoApi.deleteDevice(params.deviceId,
|
apiCall = cryptoApi.deleteDevice(params.deviceId,
|
||||||
DeleteDeviceParams(
|
DeleteDeviceParams(
|
||||||
userPasswordAuth = UserPasswordAuth(
|
auth = UserPasswordAuth(
|
||||||
type = LoginFlowTypes.PASSWORD,
|
type = LoginFlowTypes.PASSWORD,
|
||||||
session = params.authSession,
|
session = params.authSession,
|
||||||
user = userId,
|
user = userId,
|
||||||
password = params.password
|
password = params.password
|
||||||
)
|
).asMap()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
package org.matrix.android.sdk.internal.crypto.tasks
|
package org.matrix.android.sdk.internal.crypto.tasks
|
||||||
|
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
|
import org.matrix.android.sdk.internal.auth.registration.handleUIA
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||||
import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder
|
import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable
|
import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable
|
||||||
@ -24,7 +26,6 @@ import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
|
|||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
|
import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
|
||||||
import org.matrix.android.sdk.internal.crypto.model.KeyUsage
|
import org.matrix.android.sdk.internal.crypto.model.KeyUsage
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
import org.matrix.android.sdk.internal.task.Task
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
||||||
@ -34,7 +35,7 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
internal interface InitializeCrossSigningTask : Task<InitializeCrossSigningTask.Params, InitializeCrossSigningTask.Result> {
|
internal interface InitializeCrossSigningTask : Task<InitializeCrossSigningTask.Params, InitializeCrossSigningTask.Result> {
|
||||||
data class Params(
|
data class Params(
|
||||||
val authParams: UserPasswordAuth?
|
val interactiveAuthInterceptor: UserInteractiveAuthInterceptor?
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Result(
|
data class Result(
|
||||||
@ -117,10 +118,21 @@ internal class DefaultInitializeCrossSigningTask @Inject constructor(
|
|||||||
.key(sskPublicKey)
|
.key(sskPublicKey)
|
||||||
.signature(userId, masterPublicKey, signedSSK)
|
.signature(userId, masterPublicKey, signedSSK)
|
||||||
.build(),
|
.build(),
|
||||||
userPasswordAuth = params.authParams
|
userAuthParam = null
|
||||||
|
// userAuthParam = params.authParams
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
uploadSigningKeysTask.execute(uploadSigningKeysParams)
|
uploadSigningKeysTask.execute(uploadSigningKeysParams)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
if (params.interactiveAuthInterceptor == null
|
||||||
|
|| !handleUIA(failure, params.interactiveAuthInterceptor) { authUpdate ->
|
||||||
|
uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate))
|
||||||
|
}) {
|
||||||
|
Timber.d("## UIA: propagate failure")
|
||||||
|
throw failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sign the current device with SSK
|
// Sign the current device with SSK
|
||||||
val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder()
|
val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder()
|
||||||
|
@ -16,14 +16,12 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.tasks
|
package org.matrix.android.sdk.internal.crypto.tasks
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
|
|
||||||
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
|
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
|
||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
|
import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
|
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody
|
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.toRest
|
import org.matrix.android.sdk.internal.crypto.model.toRest
|
||||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||||
import org.matrix.android.sdk.internal.network.executeRequest
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
@ -39,15 +37,9 @@ internal interface UploadSigningKeysTask : Task<UploadSigningKeysTask.Params, Un
|
|||||||
// the SSK
|
// the SSK
|
||||||
val selfSignedKey: CryptoCrossSigningKey,
|
val selfSignedKey: CryptoCrossSigningKey,
|
||||||
/**
|
/**
|
||||||
* - If null:
|
* Authorisation info (User Interactive flow)
|
||||||
* - no retry will be performed
|
|
||||||
* - If not null, it may or may not contain a sessionId:
|
|
||||||
* - If sessionId is null:
|
|
||||||
* - password should not be null: the task will perform a first request to get a sessionId, and then a second one
|
|
||||||
* - If sessionId is not null:
|
|
||||||
* - password should not be null as well, and no retry will be performed
|
|
||||||
*/
|
*/
|
||||||
val userPasswordAuth: UserPasswordAuth?
|
val userAuthParam: UIABaseAuth?
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,31 +51,13 @@ internal class DefaultUploadSigningKeysTask @Inject constructor(
|
|||||||
) : UploadSigningKeysTask {
|
) : UploadSigningKeysTask {
|
||||||
|
|
||||||
override suspend fun execute(params: UploadSigningKeysTask.Params) {
|
override suspend fun execute(params: UploadSigningKeysTask.Params) {
|
||||||
val paramsHaveSessionId = params.userPasswordAuth?.session != null
|
|
||||||
|
|
||||||
val uploadQuery = UploadSigningKeysBody(
|
val uploadQuery = UploadSigningKeysBody(
|
||||||
masterKey = params.masterKey.toRest(),
|
masterKey = params.masterKey.toRest(),
|
||||||
userSigningKey = params.userKey.toRest(),
|
userSigningKey = params.userKey.toRest(),
|
||||||
selfSigningKey = params.selfSignedKey.toRest(),
|
selfSigningKey = params.selfSignedKey.toRest(),
|
||||||
// If sessionId is provided, use the userPasswordAuth
|
auth = params.userAuthParam?.asMap()
|
||||||
auth = params.userPasswordAuth.takeIf { paramsHaveSessionId }
|
|
||||||
)
|
)
|
||||||
try {
|
|
||||||
doRequest(uploadQuery)
|
doRequest(uploadQuery)
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
val registrationFlowResponse = throwable.toRegistrationFlowResponse()
|
|
||||||
if (registrationFlowResponse != null
|
|
||||||
&& registrationFlowResponse.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true }
|
|
||||||
&& params.userPasswordAuth?.password != null
|
|
||||||
&& !paramsHaveSessionId
|
|
||||||
) {
|
|
||||||
// Retry with authentication
|
|
||||||
doRequest(uploadQuery.copy(auth = params.userPasswordAuth.copy(session = registrationFlowResponse.session)))
|
|
||||||
} else {
|
|
||||||
// Other error
|
|
||||||
throw throwable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun doRequest(uploadQuery: UploadSigningKeysBody) {
|
private suspend fun doRequest(uploadQuery: UploadSigningKeysBody) {
|
||||||
|
@ -52,6 +52,8 @@ import org.matrix.android.sdk.api.session.terms.TermsService
|
|||||||
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
|
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
|
||||||
import org.matrix.android.sdk.api.session.user.UserService
|
import org.matrix.android.sdk.api.session.user.UserService
|
||||||
import org.matrix.android.sdk.api.session.widgets.WidgetService
|
import org.matrix.android.sdk.api.session.widgets.WidgetService
|
||||||
|
import org.matrix.android.sdk.api.util.appendParamToUrl
|
||||||
|
import org.matrix.android.sdk.internal.auth.SSO_UIA_FALLBACK_PATH
|
||||||
import org.matrix.android.sdk.internal.auth.SessionParamsStore
|
import org.matrix.android.sdk.internal.auth.SessionParamsStore
|
||||||
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
|
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
|
||||||
import org.matrix.android.sdk.internal.database.tools.RealmDebugTools
|
import org.matrix.android.sdk.internal.database.tools.RealmDebugTools
|
||||||
@ -273,6 +275,18 @@ internal class DefaultSession @Inject constructor(
|
|||||||
return "$myUserId - ${sessionParams.deviceId}"
|
return "$myUserId - ${sessionParams.deviceId}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getUiaSsoFallbackUrl(authenticationSessionId: String): String {
|
||||||
|
val hsBas = sessionParams.homeServerConnectionConfig
|
||||||
|
.homeServerUri
|
||||||
|
.toString()
|
||||||
|
.trim { it == '/' }
|
||||||
|
return buildString {
|
||||||
|
append(hsBas)
|
||||||
|
append(SSO_UIA_FALLBACK_PATH)
|
||||||
|
appendParamToUrl("session", authenticationSessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun logDbUsageInfo() {
|
override fun logDbUsageInfo() {
|
||||||
RealmDebugTools(realmConfiguration).logInfo("Session")
|
RealmDebugTools(realmConfiguration).logInfo("Session")
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.session.account
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to pass request parameters to update the password.
|
* Class to pass request parameters to update the password.
|
||||||
|
@ -18,21 +18,21 @@ package org.matrix.android.sdk.internal.session.account
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class DeactivateAccountParams(
|
internal data class DeactivateAccountParams(
|
||||||
@Json(name = "auth")
|
|
||||||
val auth: UserPasswordAuth? = null,
|
|
||||||
|
|
||||||
// Set to true to erase all data of the account
|
// Set to true to erase all data of the account
|
||||||
@Json(name = "erase")
|
@Json(name = "erase")
|
||||||
val erase: Boolean
|
val erase: Boolean,
|
||||||
|
|
||||||
|
@Json(name = "auth")
|
||||||
|
val auth: Map<String, *>? = null
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun create(userId: String, password: String, erase: Boolean): DeactivateAccountParams {
|
fun create(auth: UIABaseAuth?, erase: Boolean): DeactivateAccountParams {
|
||||||
return DeactivateAccountParams(
|
return DeactivateAccountParams(
|
||||||
auth = UserPasswordAuth(user = userId, password = password),
|
auth = auth?.asMap(),
|
||||||
erase = erase
|
erase = erase
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,9 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.account
|
package org.matrix.android.sdk.internal.session.account
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
|
import org.matrix.android.sdk.internal.auth.registration.handleUIA
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||||
import org.matrix.android.sdk.internal.network.executeRequest
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
@ -27,8 +30,9 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
internal interface DeactivateAccountTask : Task<DeactivateAccountTask.Params, Unit> {
|
internal interface DeactivateAccountTask : Task<DeactivateAccountTask.Params, Unit> {
|
||||||
data class Params(
|
data class Params(
|
||||||
val password: String,
|
val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
|
||||||
val eraseAllData: Boolean
|
val eraseAllData: Boolean,
|
||||||
|
val userAuthParam: UIABaseAuth? = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,12 +45,21 @@ internal class DefaultDeactivateAccountTask @Inject constructor(
|
|||||||
) : DeactivateAccountTask {
|
) : DeactivateAccountTask {
|
||||||
|
|
||||||
override suspend fun execute(params: DeactivateAccountTask.Params) {
|
override suspend fun execute(params: DeactivateAccountTask.Params) {
|
||||||
val deactivateAccountParams = DeactivateAccountParams.create(userId, params.password, params.eraseAllData)
|
val deactivateAccountParams = DeactivateAccountParams.create(params.userAuthParam, params.eraseAllData)
|
||||||
|
|
||||||
|
try {
|
||||||
executeRequest<Unit>(globalErrorReceiver) {
|
executeRequest<Unit>(globalErrorReceiver) {
|
||||||
apiCall = accountAPI.deactivate(deactivateAccountParams)
|
apiCall = accountAPI.deactivate(deactivateAccountParams)
|
||||||
}
|
}
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
if (!handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth ->
|
||||||
|
execute(params.copy(userAuthParam = auth))
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Timber.d("## UIA: propagate failure")
|
||||||
|
throw throwable
|
||||||
|
}
|
||||||
|
}
|
||||||
// Logout from identity server if any, ignoring errors
|
// Logout from identity server if any, ignoring errors
|
||||||
runCatching { identityDisconnectTask.execute(Unit) }
|
runCatching { identityDisconnectTask.execute(Unit) }
|
||||||
.onFailure { Timber.w(it, "Unable to disconnect identity server") }
|
.onFailure { Timber.w(it, "Unable to disconnect identity server") }
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.account
|
package org.matrix.android.sdk.internal.session.account
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.session.account.AccountService
|
import org.matrix.android.sdk.api.session.account.AccountService
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -26,7 +27,7 @@ internal class DefaultAccountService @Inject constructor(private val changePassw
|
|||||||
changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword))
|
changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deactivateAccount(password: String, eraseAllData: Boolean) {
|
override suspend fun deactivateAccount(userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, eraseAllData: Boolean) {
|
||||||
deactivateAccountTask.execute(DeactivateAccountTask.Params(password, eraseAllData))
|
deactivateAccountTask.execute(DeactivateAccountTask.Params(userInteractiveAuthInterceptor, eraseAllData))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData
|
|||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import io.realm.kotlin.where
|
import io.realm.kotlin.where
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||||
import org.matrix.android.sdk.api.session.profile.ProfileService
|
import org.matrix.android.sdk.api.session.profile.ProfileService
|
||||||
import org.matrix.android.sdk.api.util.Cancelable
|
import org.matrix.android.sdk.api.util.Cancelable
|
||||||
@ -170,14 +171,12 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun finalizeAddingThreePid(threePid: ThreePid,
|
override fun finalizeAddingThreePid(threePid: ThreePid,
|
||||||
uiaSession: String?,
|
userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
|
||||||
accountPassword: String?,
|
|
||||||
matrixCallback: MatrixCallback<Unit>): Cancelable {
|
matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||||
return finalizeAddingThreePidTask
|
return finalizeAddingThreePidTask
|
||||||
.configureWith(FinalizeAddingThreePidTask.Params(
|
.configureWith(FinalizeAddingThreePidTask.Params(
|
||||||
threePid = threePid,
|
threePid = threePid,
|
||||||
session = uiaSession,
|
userInteractiveAuthInterceptor = userInteractiveAuthInterceptor,
|
||||||
accountPassword = accountPassword,
|
|
||||||
userWantsToCancel = false
|
userWantsToCancel = false
|
||||||
)) {
|
)) {
|
||||||
callback = alsoRefresh(matrixCallback)
|
callback = alsoRefresh(matrixCallback)
|
||||||
@ -189,8 +188,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
|
|||||||
return finalizeAddingThreePidTask
|
return finalizeAddingThreePidTask
|
||||||
.configureWith(FinalizeAddingThreePidTask.Params(
|
.configureWith(FinalizeAddingThreePidTask.Params(
|
||||||
threePid = threePid,
|
threePid = threePid,
|
||||||
session = null,
|
userInteractiveAuthInterceptor = null,
|
||||||
accountPassword = null,
|
|
||||||
userWantsToCancel = true
|
userWantsToCancel = true
|
||||||
)) {
|
)) {
|
||||||
callback = alsoRefresh(matrixCallback)
|
callback = alsoRefresh(matrixCallback)
|
||||||
|
@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.session.profile
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class FinalizeAddThreePidBody(
|
internal data class FinalizeAddThreePidBody(
|
||||||
@ -37,5 +36,5 @@ internal data class FinalizeAddThreePidBody(
|
|||||||
* Additional authentication information for the user-interactive authentication API.
|
* Additional authentication information for the user-interactive authentication API.
|
||||||
*/
|
*/
|
||||||
@Json(name = "auth")
|
@Json(name = "auth")
|
||||||
val auth: UserPasswordAuth?
|
val auth: Map<String, *>? = null
|
||||||
)
|
)
|
||||||
|
@ -17,10 +17,12 @@
|
|||||||
package org.matrix.android.sdk.internal.session.profile
|
package org.matrix.android.sdk.internal.session.profile
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
|
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
import org.matrix.android.sdk.internal.auth.registration.handleUIA
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
|
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
|
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
|
||||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
@ -29,13 +31,14 @@ import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
|||||||
import org.matrix.android.sdk.internal.network.executeRequest
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
import org.matrix.android.sdk.internal.task.Task
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal abstract class FinalizeAddingThreePidTask : Task<FinalizeAddingThreePidTask.Params, Unit> {
|
internal abstract class FinalizeAddingThreePidTask : Task<FinalizeAddingThreePidTask.Params, Unit> {
|
||||||
data class Params(
|
data class Params(
|
||||||
val threePid: ThreePid,
|
val threePid: ThreePid,
|
||||||
val session: String?,
|
val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?,
|
||||||
val accountPassword: String?,
|
val userAuthParam: UIABaseAuth? = null,
|
||||||
val userWantsToCancel: Boolean
|
val userWantsToCancel: Boolean
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -62,22 +65,23 @@ internal class DefaultFinalizeAddingThreePidTask @Inject constructor(
|
|||||||
val body = FinalizeAddThreePidBody(
|
val body = FinalizeAddThreePidBody(
|
||||||
clientSecret = pendingThreePids.clientSecret,
|
clientSecret = pendingThreePids.clientSecret,
|
||||||
sid = pendingThreePids.sid,
|
sid = pendingThreePids.sid,
|
||||||
auth = if (params.session != null && params.accountPassword != null) {
|
auth = params.userAuthParam?.asMap()
|
||||||
UserPasswordAuth(
|
|
||||||
session = params.session,
|
|
||||||
user = userId,
|
|
||||||
password = params.accountPassword
|
|
||||||
)
|
|
||||||
} else null
|
|
||||||
)
|
)
|
||||||
apiCall = profileAPI.finalizeAddThreePid(body)
|
apiCall = profileAPI.finalizeAddThreePid(body)
|
||||||
}
|
}
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
|
if (params.userInteractiveAuthInterceptor == null
|
||||||
|
|| !handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth ->
|
||||||
|
execute(params.copy(userAuthParam = auth))
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Timber.d("## UIA: propagate failure")
|
||||||
throw throwable.toRegistrationFlowResponse()
|
throw throwable.toRegistrationFlowResponse()
|
||||||
?.let { Failure.RegistrationFlowError(it) }
|
?.let { Failure.RegistrationFlowError(it) }
|
||||||
?: throwable
|
?: throwable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cleanupDatabase(params)
|
cleanupDatabase(params)
|
||||||
}
|
}
|
||||||
|
@ -143,9 +143,11 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean {
|
private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean {
|
||||||
return (params.enableEncryptionIfInvitedUsersSupportIt
|
return params.enableEncryptionIfInvitedUsersSupportIt
|
||||||
&& crossSigningService.isCrossSigningVerified()
|
// Parity with web, enable if users have encryption ready devices
|
||||||
&& params.invite3pids.isEmpty())
|
// for now remove checks on cross signing and 3pid invites
|
||||||
|
// && crossSigningService.isCrossSigningVerified()
|
||||||
|
&& params.invite3pids.isEmpty()
|
||||||
&& params.invitedUserIds.isNotEmpty()
|
&& params.invitedUserIds.isNotEmpty()
|
||||||
&& params.invitedUserIds.let { userIds ->
|
&& params.invitedUserIds.let { userIds ->
|
||||||
val keys = deviceListManager.downloadKeys(userIds, forceDownload = false)
|
val keys = deviceListManager.downloadKeys(userIds, forceDownload = false)
|
||||||
|
@ -42,13 +42,18 @@ import org.junit.Rule
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.matrix.android.sdk.api.Matrix
|
import org.matrix.android.sdk.api.Matrix
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@ -67,10 +72,18 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
|
|||||||
existingSession = createAccountAndSync(matrix, userName, password, true)
|
existingSession = createAccountAndSync(matrix, userName, password, true)
|
||||||
doSync<Unit> {
|
doSync<Unit> {
|
||||||
existingSession!!.cryptoService().crossSigningService()
|
existingSession!!.cryptoService().crossSigningService()
|
||||||
.initializeCrossSigning(UserPasswordAuth(
|
.initializeCrossSigning(
|
||||||
|
object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
promise.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
user = existingSession!!.myUserId,
|
user = existingSession!!.myUserId,
|
||||||
password = "password"
|
password = "password",
|
||||||
), it)
|
session = flowResponse.session
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,8 +46,13 @@ import org.junit.Rule
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.matrix.android.sdk.api.Matrix
|
import org.matrix.android.sdk.api.Matrix
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@ -67,17 +72,35 @@ class VerifySessionPassphraseTest : VerificationTestBase() {
|
|||||||
existingSession = createAccountAndSync(matrix, userName, password, true)
|
existingSession = createAccountAndSync(matrix, userName, password, true)
|
||||||
doSync<Unit> {
|
doSync<Unit> {
|
||||||
existingSession!!.cryptoService().crossSigningService()
|
existingSession!!.cryptoService().crossSigningService()
|
||||||
.initializeCrossSigning(UserPasswordAuth(
|
.initializeCrossSigning(
|
||||||
|
object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
promise.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
user = existingSession!!.myUserId,
|
user = existingSession!!.myUserId,
|
||||||
password = "password"
|
password = "password",
|
||||||
), it)
|
session = flowResponse.session
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val task = BootstrapCrossSigningTask(existingSession!!, StringProvider(context.resources))
|
val task = BootstrapCrossSigningTask(existingSession!!, StringProvider(context.resources))
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
task.execute(Params(
|
task.execute(Params(
|
||||||
userPasswordAuth = UserPasswordAuth(password = password),
|
userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
promise.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
|
user = existingSession!!.myUserId,
|
||||||
|
password = password,
|
||||||
|
session = flowResponse.session
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
passphrase = passphrase,
|
passphrase = passphrase,
|
||||||
setupMode = SetupMode.NORMAL
|
setupMode = SetupMode.NORMAL
|
||||||
))
|
))
|
||||||
|
@ -241,6 +241,27 @@
|
|||||||
<activity android:name=".features.home.room.detail.search.SearchActivity" />
|
<activity android:name=".features.home.room.detail.search.SearchActivity" />
|
||||||
<activity android:name=".features.usercode.UserCodeActivity" />
|
<activity android:name=".features.usercode.UserCodeActivity" />
|
||||||
|
|
||||||
|
<!-- Single instance is very important for the custom scheme callback-->
|
||||||
|
<activity android:name=".features.auth.ReAuthActivity"
|
||||||
|
android:launchMode="singleInstance"
|
||||||
|
android:exported="false">
|
||||||
|
|
||||||
|
<!-- XXX: UIA SSO has only web fallback, i.e no url redirect, so for now we comment this out
|
||||||
|
hopefully, we would use it when finally available
|
||||||
|
-->
|
||||||
|
<!-- Add intent filter to handle redirection URL after SSO login in external browser -->
|
||||||
|
<!-- <intent-filter>-->
|
||||||
|
<!-- <action android:name="android.intent.action.VIEW" />-->
|
||||||
|
|
||||||
|
<!-- <category android:name="android.intent.category.DEFAULT" />-->
|
||||||
|
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
|
||||||
|
|
||||||
|
<!-- <data-->
|
||||||
|
<!-- android:host="reauth"-->
|
||||||
|
<!-- android:scheme="element" />-->
|
||||||
|
<!-- </intent-filter>-->
|
||||||
|
</activity>
|
||||||
|
|
||||||
<!-- Services -->
|
<!-- Services -->
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
@ -28,7 +28,7 @@ import im.vector.app.features.crypto.keysbackup.settings.KeysBackupSettingsFragm
|
|||||||
import im.vector.app.features.crypto.quads.SharedSecuredStorageKeyFragment
|
import im.vector.app.features.crypto.quads.SharedSecuredStorageKeyFragment
|
||||||
import im.vector.app.features.crypto.quads.SharedSecuredStoragePassphraseFragment
|
import im.vector.app.features.crypto.quads.SharedSecuredStoragePassphraseFragment
|
||||||
import im.vector.app.features.crypto.quads.SharedSecuredStorageResetAllFragment
|
import im.vector.app.features.crypto.quads.SharedSecuredStorageResetAllFragment
|
||||||
import im.vector.app.features.crypto.recover.BootstrapAccountPasswordFragment
|
import im.vector.app.features.crypto.recover.BootstrapReAuthFragment
|
||||||
import im.vector.app.features.crypto.recover.BootstrapConclusionFragment
|
import im.vector.app.features.crypto.recover.BootstrapConclusionFragment
|
||||||
import im.vector.app.features.crypto.recover.BootstrapConfirmPassphraseFragment
|
import im.vector.app.features.crypto.recover.BootstrapConfirmPassphraseFragment
|
||||||
import im.vector.app.features.crypto.recover.BootstrapEnterPassphraseFragment
|
import im.vector.app.features.crypto.recover.BootstrapEnterPassphraseFragment
|
||||||
@ -522,8 +522,8 @@ interface FragmentModule {
|
|||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(BootstrapAccountPasswordFragment::class)
|
@FragmentKey(BootstrapReAuthFragment::class)
|
||||||
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
|
fun bindBootstrapReAuthFragment(fragment: BootstrapReAuthFragment): Fragment
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
|
@ -25,6 +25,7 @@ import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
|
|||||||
import im.vector.app.core.error.ErrorFormatter
|
import im.vector.app.core.error.ErrorFormatter
|
||||||
import im.vector.app.core.preference.UserAvatarPreference
|
import im.vector.app.core.preference.UserAvatarPreference
|
||||||
import im.vector.app.features.MainActivity
|
import im.vector.app.features.MainActivity
|
||||||
|
import im.vector.app.features.auth.ReAuthActivity
|
||||||
import im.vector.app.features.call.CallControlsBottomSheet
|
import im.vector.app.features.call.CallControlsBottomSheet
|
||||||
import im.vector.app.features.call.VectorCallActivity
|
import im.vector.app.features.call.VectorCallActivity
|
||||||
import im.vector.app.features.call.conference.VectorJitsiActivity
|
import im.vector.app.features.call.conference.VectorJitsiActivity
|
||||||
@ -145,6 +146,7 @@ interface ScreenComponent {
|
|||||||
fun inject(activity: VectorJitsiActivity)
|
fun inject(activity: VectorJitsiActivity)
|
||||||
fun inject(activity: SearchActivity)
|
fun inject(activity: SearchActivity)
|
||||||
fun inject(activity: UserCodeActivity)
|
fun inject(activity: UserCodeActivity)
|
||||||
|
fun inject(activity: ReAuthActivity)
|
||||||
|
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
* BottomSheets
|
* BottomSheets
|
||||||
|
@ -105,11 +105,13 @@ class DefaultErrorFormatter @Inject constructor(
|
|||||||
HttpURLConnection.HTTP_NOT_FOUND ->
|
HttpURLConnection.HTTP_NOT_FOUND ->
|
||||||
// homeserver not found
|
// homeserver not found
|
||||||
stringProvider.getString(R.string.login_error_no_homeserver_found)
|
stringProvider.getString(R.string.login_error_no_homeserver_found)
|
||||||
|
HttpURLConnection.HTTP_UNAUTHORIZED ->
|
||||||
|
// uia errors?
|
||||||
|
stringProvider.getString(R.string.error_unauthorized)
|
||||||
else ->
|
else ->
|
||||||
throwable.localizedMessage
|
throwable.localizedMessage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is SsoFlowNotSupportedYet -> stringProvider.getString(R.string.error_sso_flow_not_supported_yet)
|
|
||||||
else -> throwable.localizedMessage
|
else -> throwable.localizedMessage
|
||||||
}
|
}
|
||||||
?: stringProvider.getString(R.string.unknown_error)
|
?: stringProvider.getString(R.string.unknown_error)
|
||||||
|
@ -200,6 +200,7 @@ abstract class VectorBaseFragment<VB: ViewBinding> : BaseMvRxFragment(), HasScre
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected fun showLoadingDialog(message: CharSequence? = null, cancelable: Boolean = false) {
|
protected fun showLoadingDialog(message: CharSequence? = null, cancelable: Boolean = false) {
|
||||||
|
progress?.dismiss()
|
||||||
progress = ProgressDialog(requireContext()).apply {
|
progress = ProgressDialog(requireContext()).apply {
|
||||||
setCancelable(cancelable)
|
setCancelable(cancelable)
|
||||||
setMessage(message ?: getString(R.string.please_wait))
|
setMessage(message ?: getString(R.string.please_wait))
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.app.core.ui.list
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic button list item.
|
||||||
|
*/
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_positive_button)
|
||||||
|
abstract class GenericPositiveButtonItem : VectorEpoxyModel<GenericPositiveButtonItem.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var text: String? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var buttonClickAction: View.OnClickListener? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
@ColorInt
|
||||||
|
var textColor: Int? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
@DrawableRes
|
||||||
|
var iconRes: Int? = null
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
super.bind(holder)
|
||||||
|
holder.button.text = text
|
||||||
|
if (iconRes != null) {
|
||||||
|
holder.button.setIconResource(iconRes!!)
|
||||||
|
} else {
|
||||||
|
holder.button.icon = null
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonClickAction?.let { holder.button.setOnClickListener(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
val button by bind<MaterialButton>(R.id.itemGenericItemButton)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.auth
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.airbnb.mvrx.activityViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.extensions.showPassword
|
||||||
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.app.databinding.FragmentReauthConfirmBinding
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
|
|
||||||
|
class PromptFragment : VectorBaseFragment<FragmentReauthConfirmBinding>() {
|
||||||
|
|
||||||
|
private val viewModel: ReAuthViewModel by activityViewModel()
|
||||||
|
|
||||||
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) =
|
||||||
|
FragmentReauthConfirmBinding.inflate(layoutInflater, container, false)
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
views.reAuthConfirmButton.debouncedClicks {
|
||||||
|
onButtonClicked()
|
||||||
|
}
|
||||||
|
views.passwordReveal.debouncedClicks {
|
||||||
|
viewModel.handle(ReAuthActions.StartSSOFallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
views.passwordReveal.debouncedClicks {
|
||||||
|
viewModel.handle(ReAuthActions.TogglePassVisibility)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onButtonClicked() = withState(viewModel) { state ->
|
||||||
|
when (state.flowType) {
|
||||||
|
LoginFlowTypes.SSO -> {
|
||||||
|
viewModel.handle(ReAuthActions.StartSSOFallback)
|
||||||
|
}
|
||||||
|
LoginFlowTypes.PASSWORD -> {
|
||||||
|
val password = views.passwordField.text.toString()
|
||||||
|
if (password.isBlank()) {
|
||||||
|
// Prompt to enter something
|
||||||
|
views.passwordFieldTil.error = getString(R.string.error_empty_field_your_password)
|
||||||
|
} else {
|
||||||
|
views.passwordFieldTil.error = null
|
||||||
|
viewModel.handle(ReAuthActions.ReAuthWithPass(password))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// not supported
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) {
|
||||||
|
when (it.flowType) {
|
||||||
|
LoginFlowTypes.SSO -> {
|
||||||
|
views.passwordContainer.isVisible = false
|
||||||
|
views.reAuthConfirmButton.text = getString(R.string.auth_login_sso)
|
||||||
|
}
|
||||||
|
LoginFlowTypes.PASSWORD -> {
|
||||||
|
views.passwordContainer.isVisible = true
|
||||||
|
views.reAuthConfirmButton.text = getString(R.string._continue)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// This login flow is not supported, you should use web?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
views.passwordField.showPassword(it.passwordVisible)
|
||||||
|
|
||||||
|
if (it.passwordVisible) {
|
||||||
|
views.passwordReveal.setImageResource(R.drawable.ic_eye_closed)
|
||||||
|
views.passwordReveal.contentDescription = getString(R.string.a11y_hide_password)
|
||||||
|
} else {
|
||||||
|
views.passwordReveal.setImageResource(R.drawable.ic_eye)
|
||||||
|
views.passwordReveal.contentDescription = getString(R.string.a11y_show_password)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it.lastErrorCode != null) {
|
||||||
|
when (it.flowType) {
|
||||||
|
LoginFlowTypes.SSO -> {
|
||||||
|
views.genericErrorText.isVisible = true
|
||||||
|
views.genericErrorText.text = getString(R.string.authentication_error)
|
||||||
|
}
|
||||||
|
LoginFlowTypes.PASSWORD -> {
|
||||||
|
views.passwordFieldTil.error = getString(R.string.authentication_error)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// nop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
views.passwordFieldTil.error = null
|
||||||
|
views.genericErrorText.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.auth
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
|
sealed class ReAuthActions : VectorViewModelAction {
|
||||||
|
object StartSSOFallback : ReAuthActions()
|
||||||
|
object FallBackPageLoaded : ReAuthActions()
|
||||||
|
object FallBackPageClosed : ReAuthActions()
|
||||||
|
object TogglePassVisibility : ReAuthActions()
|
||||||
|
data class ReAuthWithPass(val password: String) : ReAuthActions()
|
||||||
|
}
|
@ -0,0 +1,228 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.auth
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.browser.customtabs.CustomTabsCallback
|
||||||
|
import androidx.browser.customtabs.CustomTabsClient
|
||||||
|
import androidx.browser.customtabs.CustomTabsServiceConnection
|
||||||
|
import androidx.browser.customtabs.CustomTabsSession
|
||||||
|
import com.airbnb.mvrx.MvRx
|
||||||
|
import com.airbnb.mvrx.viewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.di.ScreenComponent
|
||||||
|
import im.vector.app.core.extensions.addFragment
|
||||||
|
import im.vector.app.core.platform.SimpleFragmentActivity
|
||||||
|
import im.vector.app.core.utils.openUrlInChromeCustomTab
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory {
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Args(
|
||||||
|
val flowType: String?,
|
||||||
|
val title: String?,
|
||||||
|
val session: String?,
|
||||||
|
val lastErrorCode: String?,
|
||||||
|
val resultKeyStoreAlias: String
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
// For sso
|
||||||
|
private var customTabsServiceConnection: CustomTabsServiceConnection? = null
|
||||||
|
private var customTabsClient: CustomTabsClient? = null
|
||||||
|
private var customTabsSession: CustomTabsSession? = null
|
||||||
|
|
||||||
|
@Inject lateinit var authenticationService: AuthenticationService
|
||||||
|
@Inject lateinit var reAuthViewModelFactory: ReAuthViewModel.Factory
|
||||||
|
|
||||||
|
override fun create(initialState: ReAuthState) = reAuthViewModelFactory.create(initialState)
|
||||||
|
|
||||||
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
|
super.injectWith(injector)
|
||||||
|
injector.inject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sharedViewModel: ReAuthViewModel by viewModel()
|
||||||
|
|
||||||
|
// override fun getTitleRes() = R.string.re_authentication_activity_title
|
||||||
|
|
||||||
|
override fun initUiAndData() {
|
||||||
|
super.initUiAndData()
|
||||||
|
|
||||||
|
val title = intent.extras?.getString(EXTRA_REASON_TITLE) ?: getString(R.string.re_authentication_activity_title)
|
||||||
|
supportActionBar?.setTitle(title) ?: run { setTitle(title) }
|
||||||
|
|
||||||
|
// val authArgs = intent.getParcelableExtra<Args>(MvRx.KEY_ARG)
|
||||||
|
|
||||||
|
// For the sso flow we can for now only rely on the fallback flow, that handles all
|
||||||
|
// the UI, due to the sandbox nature of CCT (chrome custom tab) we cannot get much information
|
||||||
|
// on how the process did go :/
|
||||||
|
// so we assume that after the user close the tab we return success and let caller retry the UIA flow :/
|
||||||
|
if (isFirstCreation()) {
|
||||||
|
addFragment(
|
||||||
|
R.id.container,
|
||||||
|
PromptFragment::class.java
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
sharedViewModel.observeViewEvents {
|
||||||
|
when (it) {
|
||||||
|
is ReAuthEvents.OpenSsoURl -> {
|
||||||
|
openInCustomTab(it.url)
|
||||||
|
}
|
||||||
|
ReAuthEvents.Dismiss -> {
|
||||||
|
setResult(RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
is ReAuthEvents.PasswordFinishSuccess -> {
|
||||||
|
setResult(RESULT_OK, Intent().apply {
|
||||||
|
putExtra(RESULT_FLOW_TYPE, LoginFlowTypes.PASSWORD)
|
||||||
|
putExtra(RESULT_VALUE, it.passwordSafeForIntent)
|
||||||
|
})
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
// It's the only way we have to know if sso falback flow was successful
|
||||||
|
withState(sharedViewModel) {
|
||||||
|
if (it.ssoFallbackPageWasShown) {
|
||||||
|
Timber.d("## UIA ssoFallbackPageWasShown tentative success")
|
||||||
|
setResult(RESULT_OK, Intent().apply {
|
||||||
|
putExtra(RESULT_FLOW_TYPE, LoginFlowTypes.SSO)
|
||||||
|
})
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
|
||||||
|
withState(sharedViewModel) { state ->
|
||||||
|
if (state.ssoFallbackPageWasShown) {
|
||||||
|
sharedViewModel.handle(ReAuthActions.FallBackPageClosed)
|
||||||
|
return@withState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val packageName = CustomTabsClient.getPackageName(this, null)
|
||||||
|
|
||||||
|
// packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device
|
||||||
|
if (packageName != null) {
|
||||||
|
customTabsServiceConnection = object : CustomTabsServiceConnection() {
|
||||||
|
override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
|
||||||
|
Timber.d("## CustomTab onCustomTabsServiceConnected($name)")
|
||||||
|
customTabsClient = client
|
||||||
|
.also { it.warmup(0L) }
|
||||||
|
customTabsSession = customTabsClient?.newSession(object : CustomTabsCallback() {
|
||||||
|
// override fun onPostMessage(message: String, extras: Bundle?) {
|
||||||
|
// Timber.v("## CustomTab onPostMessage($message)")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// override fun onMessageChannelReady(extras: Bundle?) {
|
||||||
|
// Timber.v("## CustomTab onMessageChannelReady()")
|
||||||
|
// }
|
||||||
|
|
||||||
|
override fun onNavigationEvent(navigationEvent: Int, extras: Bundle?) {
|
||||||
|
Timber.v("## CustomTab onNavigationEvent($navigationEvent), $extras")
|
||||||
|
super.onNavigationEvent(navigationEvent, extras)
|
||||||
|
if (navigationEvent == NAVIGATION_FINISHED) {
|
||||||
|
// sharedViewModel.handle(ReAuthActions.FallBackPageLoaded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRelationshipValidationResult(relation: Int, requestedOrigin: Uri, result: Boolean, extras: Bundle?) {
|
||||||
|
Timber.v("## CustomTab onRelationshipValidationResult($relation), $requestedOrigin")
|
||||||
|
super.onRelationshipValidationResult(relation, requestedOrigin, result, extras)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
Timber.d("## CustomTab onServiceDisconnected($name)")
|
||||||
|
}
|
||||||
|
}.also {
|
||||||
|
CustomTabsClient.bindCustomTabsService(
|
||||||
|
this,
|
||||||
|
// Despite the API, packageName cannot be null
|
||||||
|
packageName,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
customTabsServiceConnection?.let { this.unbindService(it) }
|
||||||
|
customTabsServiceConnection = null
|
||||||
|
customTabsSession = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openInCustomTab(ssoUrl: String) {
|
||||||
|
openUrlInChromeCustomTab(this, customTabsSession, ssoUrl)
|
||||||
|
val channelOpened = customTabsSession?.requestPostMessageChannel(Uri.parse("https://element.io"))
|
||||||
|
Timber.d("## CustomTab channelOpened: $channelOpened")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val EXTRA_AUTH_TYPE = "EXTRA_AUTH_TYPE"
|
||||||
|
const val EXTRA_REASON_TITLE = "EXTRA_REASON_TITLE"
|
||||||
|
const val RESULT_FLOW_TYPE = "RESULT_FLOW_TYPE"
|
||||||
|
const val RESULT_VALUE = "RESULT_VALUE"
|
||||||
|
const val DEFAULT_RESULT_KEYSTORE_ALIAS = "ReAuthActivity"
|
||||||
|
|
||||||
|
fun newIntent(context: Context,
|
||||||
|
fromError: RegistrationFlowResponse,
|
||||||
|
lastErrorCode: String?,
|
||||||
|
reasonTitle: String?,
|
||||||
|
resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS): Intent {
|
||||||
|
val authType = when (fromError.nextUncompletedStage()) {
|
||||||
|
LoginFlowTypes.PASSWORD -> {
|
||||||
|
LoginFlowTypes.PASSWORD
|
||||||
|
}
|
||||||
|
LoginFlowTypes.SSO -> {
|
||||||
|
LoginFlowTypes.SSO
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// TODO, support more auth type?
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Intent(context, ReAuthActivity::class.java).apply {
|
||||||
|
putExtra(MvRx.KEY_ARG, Args(authType, reasonTitle, fromError.session, lastErrorCode, resultKeyStoreAlias))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2020 New Vector Ltd
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -14,6 +14,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.app.core.error
|
package im.vector.app.features.auth
|
||||||
|
|
||||||
class SsoFlowNotSupportedYet : Throwable()
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
|
||||||
|
sealed class ReAuthEvents : VectorViewEvents {
|
||||||
|
data class OpenSsoURl(val url: String) : ReAuthEvents()
|
||||||
|
object Dismiss : ReAuthEvents()
|
||||||
|
data class PasswordFinishSuccess(val passwordSafeForIntent: String) : ReAuthEvents()
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.auth
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
|
||||||
|
data class ReAuthState(
|
||||||
|
val title: String? = null,
|
||||||
|
val session: String? = null,
|
||||||
|
val flowType: String? = null,
|
||||||
|
val ssoFallbackPageWasShown: Boolean = false,
|
||||||
|
val passwordVisible: Boolean = false,
|
||||||
|
val lastErrorCode: String? = null,
|
||||||
|
val resultKeyStoreAlias: String = ""
|
||||||
|
) : MvRxState {
|
||||||
|
constructor(args: ReAuthActivity.Args) : this(
|
||||||
|
args.title,
|
||||||
|
args.session,
|
||||||
|
args.flowType,
|
||||||
|
lastErrorCode = args.lastErrorCode,
|
||||||
|
resultKeyStoreAlias = args.resultKeyStoreAlias
|
||||||
|
)
|
||||||
|
|
||||||
|
constructor() : this(null, null)
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.auth
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.ActivityViewModelContext
|
||||||
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
|
class ReAuthViewModel @AssistedInject constructor(
|
||||||
|
@Assisted val initialState: ReAuthState,
|
||||||
|
private val session: Session
|
||||||
|
) : VectorViewModel<ReAuthState, ReAuthActions, ReAuthEvents>(initialState) {
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(initialState: ReAuthState): ReAuthViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<ReAuthViewModel, ReAuthState> {
|
||||||
|
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: ReAuthState): ReAuthViewModel? {
|
||||||
|
val factory = when (viewModelContext) {
|
||||||
|
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
|
||||||
|
is ActivityViewModelContext -> viewModelContext.activity as? Factory
|
||||||
|
}
|
||||||
|
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: ReAuthActions) = withState { state ->
|
||||||
|
when (action) {
|
||||||
|
ReAuthActions.StartSSOFallback -> {
|
||||||
|
if (state.flowType == LoginFlowTypes.SSO) {
|
||||||
|
setState { copy(ssoFallbackPageWasShown = true) }
|
||||||
|
val ssoURL = session.getUiaSsoFallbackUrl(initialState.session ?: "")
|
||||||
|
_viewEvents.post(ReAuthEvents.OpenSsoURl(ssoURL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ReAuthActions.FallBackPageLoaded -> {
|
||||||
|
setState { copy(ssoFallbackPageWasShown = true) }
|
||||||
|
}
|
||||||
|
ReAuthActions.FallBackPageClosed -> {
|
||||||
|
// Should we do something here?
|
||||||
|
}
|
||||||
|
ReAuthActions.TogglePassVisibility -> {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
passwordVisible = !state.passwordVisible
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ReAuthActions.ReAuthWithPass -> {
|
||||||
|
val safeForIntentCypher = ByteArrayOutputStream().also {
|
||||||
|
it.use {
|
||||||
|
session.securelyStoreObject(action.password, initialState.resultKeyStoreAlias, it)
|
||||||
|
}
|
||||||
|
}.toByteArray().toBase64NoPadding()
|
||||||
|
_viewEvents.post(ReAuthEvents.PasswordFinishSuccess(safeForIntentCypher))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,110 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2020 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package im.vector.app.features.crypto.recover
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.inputmethod.EditorInfo
|
|
||||||
import androidx.core.text.toSpannable
|
|
||||||
import com.airbnb.mvrx.parentFragmentViewModel
|
|
||||||
import com.airbnb.mvrx.withState
|
|
||||||
import com.jakewharton.rxbinding3.widget.editorActionEvents
|
|
||||||
import com.jakewharton.rxbinding3.widget.textChanges
|
|
||||||
import im.vector.app.R
|
|
||||||
import im.vector.app.core.extensions.hideKeyboard
|
|
||||||
import im.vector.app.core.extensions.showPassword
|
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
|
||||||
import im.vector.app.core.resources.ColorProvider
|
|
||||||
import im.vector.app.core.utils.colorizeMatchingText
|
|
||||||
import im.vector.app.databinding.FragmentBootstrapEnterAccountPasswordBinding
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class BootstrapAccountPasswordFragment @Inject constructor(
|
|
||||||
private val colorProvider: ColorProvider
|
|
||||||
) : VectorBaseFragment<FragmentBootstrapEnterAccountPasswordBinding>() {
|
|
||||||
|
|
||||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentBootstrapEnterAccountPasswordBinding {
|
|
||||||
return FragmentBootstrapEnterAccountPasswordBinding.inflate(inflater, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
val recPassPhrase = getString(R.string.account_password)
|
|
||||||
views.bootstrapDescriptionText.text = getString(R.string.enter_account_password, recPassPhrase)
|
|
||||||
.toSpannable()
|
|
||||||
.colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
|
||||||
|
|
||||||
views.bootstrapAccountPasswordEditText.hint = getString(R.string.account_password)
|
|
||||||
|
|
||||||
views.bootstrapAccountPasswordEditText.editorActionEvents()
|
|
||||||
.throttleFirst(300, TimeUnit.MILLISECONDS)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe {
|
|
||||||
if (it.actionId == EditorInfo.IME_ACTION_DONE) {
|
|
||||||
submit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.disposeOnDestroyView()
|
|
||||||
|
|
||||||
views.bootstrapAccountPasswordEditText.textChanges()
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.subscribe {
|
|
||||||
if (!it.isNullOrBlank()) {
|
|
||||||
views.bootstrapAccountPasswordTil.error = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.disposeOnDestroyView()
|
|
||||||
|
|
||||||
views.ssssViewShowPassword.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) }
|
|
||||||
views.bootstrapPasswordButton.debouncedClicks { submit() }
|
|
||||||
|
|
||||||
withState(sharedViewModel) { state ->
|
|
||||||
(state.step as? BootstrapStep.AccountPassword)?.failure?.let {
|
|
||||||
views.bootstrapAccountPasswordTil.error = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun submit() = withState(sharedViewModel) { state ->
|
|
||||||
if (state.step !is BootstrapStep.AccountPassword) {
|
|
||||||
return@withState
|
|
||||||
}
|
|
||||||
val accountPassword = views.bootstrapAccountPasswordEditText.text?.toString()
|
|
||||||
if (accountPassword.isNullOrBlank()) {
|
|
||||||
views.bootstrapAccountPasswordTil.error = getString(R.string.error_empty_field_your_password)
|
|
||||||
} else {
|
|
||||||
view?.hideKeyboard()
|
|
||||||
sharedViewModel.handle(BootstrapActions.ReAuth(accountPassword))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun invalidate() = withState(sharedViewModel) { state ->
|
|
||||||
if (state.step is BootstrapStep.AccountPassword) {
|
|
||||||
val isPasswordVisible = state.step.isPasswordVisible
|
|
||||||
views.bootstrapAccountPasswordEditText.showPassword(isPasswordVisible, updateCursor = false)
|
|
||||||
views.ssssViewShowPassword.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed else R.drawable.ic_eye)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -37,7 +37,7 @@ sealed class BootstrapActions : VectorViewModelAction {
|
|||||||
object TogglePasswordVisibility : BootstrapActions()
|
object TogglePasswordVisibility : BootstrapActions()
|
||||||
data class UpdateCandidatePassphrase(val pass: String) : BootstrapActions()
|
data class UpdateCandidatePassphrase(val pass: String) : BootstrapActions()
|
||||||
data class UpdateConfirmCandidatePassphrase(val pass: String) : BootstrapActions()
|
data class UpdateConfirmCandidatePassphrase(val pass: String) : BootstrapActions()
|
||||||
data class ReAuth(val pass: String) : BootstrapActions()
|
// data class ReAuth(val pass: String) : BootstrapActions()
|
||||||
object RecoveryKeySaved : BootstrapActions()
|
object RecoveryKeySaved : BootstrapActions()
|
||||||
object Completed : BootstrapActions()
|
object Completed : BootstrapActions()
|
||||||
object SaveReqQueryStarted : BootstrapActions()
|
object SaveReqQueryStarted : BootstrapActions()
|
||||||
@ -47,4 +47,8 @@ sealed class BootstrapActions : VectorViewModelAction {
|
|||||||
object HandleForgotBackupPassphrase : BootstrapActions()
|
object HandleForgotBackupPassphrase : BootstrapActions()
|
||||||
data class DoMigrateWithPassphrase(val passphrase: String) : BootstrapActions()
|
data class DoMigrateWithPassphrase(val passphrase: String) : BootstrapActions()
|
||||||
data class DoMigrateWithRecoveryKey(val recoveryKey: String) : BootstrapActions()
|
data class DoMigrateWithRecoveryKey(val recoveryKey: String) : BootstrapActions()
|
||||||
|
|
||||||
|
object SsoAuthDone: BootstrapActions()
|
||||||
|
data class PasswordAuthDone(val password: String): BootstrapActions()
|
||||||
|
object ReAuthCancelled: BootstrapActions()
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.app.features.crypto.recover
|
package im.vector.app.features.crypto.recover
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -36,9 +37,12 @@ import im.vector.app.R
|
|||||||
import im.vector.app.core.di.ScreenComponent
|
import im.vector.app.core.di.ScreenComponent
|
||||||
import im.vector.app.core.extensions.commitTransaction
|
import im.vector.app.core.extensions.commitTransaction
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
|
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||||
import im.vector.app.databinding.BottomSheetBootstrapBinding
|
import im.vector.app.databinding.BottomSheetBootstrapBinding
|
||||||
|
import im.vector.app.features.auth.ReAuthActivity
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
@ -64,6 +68,25 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetBoot
|
|||||||
return BottomSheetBootstrapBinding.inflate(inflater, container, false)
|
return BottomSheetBootstrapBinding.inflate(inflater, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
|
||||||
|
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||||
|
when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
|
||||||
|
LoginFlowTypes.SSO -> {
|
||||||
|
viewModel.handle(BootstrapActions.SsoAuthDone)
|
||||||
|
}
|
||||||
|
LoginFlowTypes.PASSWORD -> {
|
||||||
|
val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
|
||||||
|
viewModel.handle(BootstrapActions.PasswordAuthDone(password))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
viewModel.handle(BootstrapActions.ReAuthCancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viewModel.handle(BootstrapActions.ReAuthCancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
viewModel.observeViewEvents { event ->
|
viewModel.observeViewEvents { event ->
|
||||||
@ -85,6 +108,14 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetBoot
|
|||||||
is BootstrapViewEvents.SkipBootstrap -> {
|
is BootstrapViewEvents.SkipBootstrap -> {
|
||||||
promptSkip()
|
promptSkip()
|
||||||
}
|
}
|
||||||
|
is BootstrapViewEvents.RequestReAuth -> {
|
||||||
|
ReAuthActivity.newIntent(requireContext(),
|
||||||
|
event.flowResponse,
|
||||||
|
event.lastErrorCode,
|
||||||
|
getString(R.string.initialize_cross_signing)).let { intent ->
|
||||||
|
reAuthActivityResultLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,11 +180,11 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetBoot
|
|||||||
views.bootstrapTitleText.text = getString(R.string.set_a_security_phrase_title)
|
views.bootstrapTitleText.text = getString(R.string.set_a_security_phrase_title)
|
||||||
showFragment(BootstrapConfirmPassphraseFragment::class, Bundle())
|
showFragment(BootstrapConfirmPassphraseFragment::class, Bundle())
|
||||||
}
|
}
|
||||||
is BootstrapStep.AccountPassword -> {
|
is BootstrapStep.AccountReAuth -> {
|
||||||
views.bootstrapIcon.isVisible = true
|
views.bootstrapIcon.isVisible = true
|
||||||
views.bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user))
|
views.bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user))
|
||||||
views.bootstrapTitleText.text = getString(R.string.account_password)
|
views.bootstrapTitleText.text = getString(R.string.re_authentication_activity_title)
|
||||||
showFragment(BootstrapAccountPasswordFragment::class, Bundle())
|
showFragment(BootstrapReAuthFragment::class, Bundle())
|
||||||
}
|
}
|
||||||
is BootstrapStep.Initializing -> {
|
is BootstrapStep.Initializing -> {
|
||||||
views.bootstrapIcon.isVisible = true
|
views.bootstrapIcon.isVisible = true
|
||||||
|
@ -20,10 +20,9 @@ import im.vector.app.R
|
|||||||
import im.vector.app.core.platform.ViewModelTask
|
import im.vector.app.core.platform.ViewModelTask
|
||||||
import im.vector.app.core.platform.WaitingViewData
|
import im.vector.app.core.platform.WaitingViewData
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.MatrixError
|
import org.matrix.android.sdk.api.failure.MatrixError
|
||||||
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
|
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||||
@ -38,7 +37,6 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreat
|
|||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
|
||||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@ -51,16 +49,12 @@ sealed class BootstrapResult {
|
|||||||
|
|
||||||
abstract class Failure(val error: String?) : BootstrapResult()
|
abstract class Failure(val error: String?) : BootstrapResult()
|
||||||
|
|
||||||
class UnsupportedAuthFlow : Failure(null)
|
|
||||||
|
|
||||||
data class GenericError(val failure: Throwable) : Failure(failure.localizedMessage)
|
data class GenericError(val failure: Throwable) : Failure(failure.localizedMessage)
|
||||||
data class InvalidPasswordError(val matrixError: MatrixError) : Failure(null)
|
data class InvalidPasswordError(val matrixError: MatrixError) : Failure(null)
|
||||||
class FailedToCreateSSSSKey(failure: Throwable) : Failure(failure.localizedMessage)
|
class FailedToCreateSSSSKey(failure: Throwable) : Failure(failure.localizedMessage)
|
||||||
class FailedToSetDefaultSSSSKey(failure: Throwable) : Failure(failure.localizedMessage)
|
class FailedToSetDefaultSSSSKey(failure: Throwable) : Failure(failure.localizedMessage)
|
||||||
class FailedToStorePrivateKeyInSSSS(failure: Throwable) : Failure(failure.localizedMessage)
|
class FailedToStorePrivateKeyInSSSS(failure: Throwable) : Failure(failure.localizedMessage)
|
||||||
object MissingPrivateKey : Failure(null)
|
object MissingPrivateKey : Failure(null)
|
||||||
|
|
||||||
data class PasswordAuthFlowMissing(val sessionId: String) : Failure(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BootstrapProgressListener {
|
interface BootstrapProgressListener {
|
||||||
@ -68,7 +62,7 @@ interface BootstrapProgressListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class Params(
|
data class Params(
|
||||||
val userPasswordAuth: UserPasswordAuth? = null,
|
val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
|
||||||
val progressListener: BootstrapProgressListener? = null,
|
val progressListener: BootstrapProgressListener? = null,
|
||||||
val passphrase: String?,
|
val passphrase: String?,
|
||||||
val keySpec: SsssKeySpec? = null,
|
val keySpec: SsssKeySpec? = null,
|
||||||
@ -101,7 +95,10 @@ class BootstrapCrossSigningTask @Inject constructor(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
awaitCallback<Unit> {
|
awaitCallback<Unit> {
|
||||||
crossSigningService.initializeCrossSigning(params.userPasswordAuth, it)
|
crossSigningService.initializeCrossSigning(
|
||||||
|
params.userInteractiveAuthInterceptor,
|
||||||
|
it
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) {
|
if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) {
|
||||||
return BootstrapResult.SuccessCrossSigningOnly
|
return BootstrapResult.SuccessCrossSigningOnly
|
||||||
@ -312,16 +309,6 @@ class BootstrapCrossSigningTask @Inject constructor(
|
|||||||
private fun handleInitializeXSigningError(failure: Throwable): BootstrapResult {
|
private fun handleInitializeXSigningError(failure: Throwable): BootstrapResult {
|
||||||
if (failure is Failure.ServerError && failure.error.code == MatrixError.M_FORBIDDEN) {
|
if (failure is Failure.ServerError && failure.error.code == MatrixError.M_FORBIDDEN) {
|
||||||
return BootstrapResult.InvalidPasswordError(failure.error)
|
return BootstrapResult.InvalidPasswordError(failure.error)
|
||||||
} else {
|
|
||||||
val registrationFlowResponse = failure.toRegistrationFlowResponse()
|
|
||||||
if (registrationFlowResponse != null) {
|
|
||||||
return if (registrationFlowResponse.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true }) {
|
|
||||||
BootstrapResult.PasswordAuthFlowMissing(registrationFlowResponse.session ?: "")
|
|
||||||
} else {
|
|
||||||
// can't do this from here
|
|
||||||
BootstrapResult.UnsupportedAuthFlow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return BootstrapResult.GenericError(failure)
|
return BootstrapResult.GenericError(failure)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.crypto.recover
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.airbnb.mvrx.parentFragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.app.core.extensions.setTextOrHide
|
||||||
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.app.core.resources.ColorProvider
|
||||||
|
import im.vector.app.databinding.FragmentBootstrapReauthBinding
|
||||||
|
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class BootstrapReAuthFragment @Inject constructor(
|
||||||
|
private val colorProvider: ColorProvider
|
||||||
|
) : VectorBaseFragment<FragmentBootstrapReauthBinding>() {
|
||||||
|
|
||||||
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentBootstrapReauthBinding {
|
||||||
|
return FragmentBootstrapReauthBinding.inflate(inflater, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
views.bootstrapRetryButton.debouncedClicks { submit() }
|
||||||
|
views.bootstrapCancelButton.debouncedClicks { cancel() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun submit() = withState(sharedViewModel) { state ->
|
||||||
|
if (state.step !is BootstrapStep.AccountReAuth) {
|
||||||
|
return@withState
|
||||||
|
}
|
||||||
|
if (state.passphrase != null) {
|
||||||
|
sharedViewModel.handle(BootstrapActions.DoInitialize(state.passphrase))
|
||||||
|
} else {
|
||||||
|
sharedViewModel.handle(BootstrapActions.DoInitializeGeneratedKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancel() = withState(sharedViewModel) { state ->
|
||||||
|
if (state.step !is BootstrapStep.AccountReAuth) {
|
||||||
|
return@withState
|
||||||
|
}
|
||||||
|
sharedViewModel.handle(BootstrapActions.GoBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||||
|
if (state.step !is BootstrapStep.AccountReAuth) {
|
||||||
|
return@withState
|
||||||
|
}
|
||||||
|
val failure = state.step.failure
|
||||||
|
views.reAuthFailureText.setTextOrHide(failure)
|
||||||
|
if (failure == null) {
|
||||||
|
views.waitingProgress.isVisible = true
|
||||||
|
views.bootstrapCancelButton.isVisible = false
|
||||||
|
views.bootstrapRetryButton.isVisible = false
|
||||||
|
} else {
|
||||||
|
views.waitingProgress.isVisible = false
|
||||||
|
views.bootstrapCancelButton.isVisible = true
|
||||||
|
views.bootstrapRetryButton.isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -26,25 +26,35 @@ import com.airbnb.mvrx.Uninitialized
|
|||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import com.nulabinc.zxcvbn.Zxcvbn
|
import com.nulabinc.zxcvbn.Zxcvbn
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.error.ErrorFormatter
|
import im.vector.app.core.error.ErrorFormatter
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.core.platform.WaitingViewData
|
import im.vector.app.core.platform.WaitingViewData
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import im.vector.app.features.auth.ReAuthActivity
|
||||||
import im.vector.app.features.login.ReAuthHelper
|
import im.vector.app.features.login.ReAuthHelper
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
|
import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
|
||||||
|
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
class BootstrapSharedViewModel @AssistedInject constructor(
|
class BootstrapSharedViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: BootstrapViewState,
|
@Assisted initialState: BootstrapViewState,
|
||||||
@ -66,7 +76,10 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
fun create(initialState: BootstrapViewState, args: BootstrapBottomSheet.Args): BootstrapSharedViewModel
|
fun create(initialState: BootstrapViewState, args: BootstrapBottomSheet.Args): BootstrapSharedViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
private var _pendingSession: String? = null
|
// private var _pendingSession: String? = null
|
||||||
|
|
||||||
|
var uiaContinuation: Continuation<UIABaseAuth>? = null
|
||||||
|
var pendingAuth: UIABaseAuth? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
@ -81,7 +94,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
SetupMode.CROSS_SIGNING_ONLY -> {
|
SetupMode.CROSS_SIGNING_ONLY -> {
|
||||||
// Go straight to account password
|
// Go straight to account password
|
||||||
setState {
|
setState {
|
||||||
copy(step = BootstrapStep.AccountPassword(false))
|
copy(step = BootstrapStep.AccountReAuth())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SetupMode.NORMAL -> {
|
SetupMode.NORMAL -> {
|
||||||
@ -149,10 +162,8 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is BootstrapStep.AccountPassword -> {
|
is BootstrapStep.AccountReAuth -> {
|
||||||
setState {
|
// nop
|
||||||
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is BootstrapStep.GetBackupSecretPassForMigration -> {
|
is BootstrapStep.GetBackupSecretPassForMigration -> {
|
||||||
setState {
|
setState {
|
||||||
@ -196,16 +207,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
is BootstrapActions.DoInitialize -> {
|
is BootstrapActions.DoInitialize -> {
|
||||||
if (state.passphrase == state.passphraseRepeat) {
|
if (state.passphrase == state.passphraseRepeat) {
|
||||||
val userPassword = reAuthHelper.data
|
startInitializeFlow(state)
|
||||||
if (userPassword == null) {
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
step = BootstrapStep.AccountPassword(false)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
startInitializeFlow(userPassword)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
@ -215,24 +217,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is BootstrapActions.DoInitializeGeneratedKey -> {
|
is BootstrapActions.DoInitializeGeneratedKey -> {
|
||||||
val userPassword = reAuthHelper.data
|
startInitializeFlow(state)
|
||||||
if (userPassword == null) {
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
passphrase = null,
|
|
||||||
passphraseRepeat = null,
|
|
||||||
step = BootstrapStep.AccountPassword(false)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
passphrase = null,
|
|
||||||
passphraseRepeat = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
startInitializeFlow(userPassword)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
BootstrapActions.RecoveryKeySaved -> {
|
BootstrapActions.RecoveryKeySaved -> {
|
||||||
_viewEvents.post(BootstrapViewEvents.RecoveryKeySaved)
|
_viewEvents.post(BootstrapViewEvents.RecoveryKeySaved)
|
||||||
@ -263,7 +248,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
BootstrapActions.GoToEnterAccountPassword -> {
|
BootstrapActions.GoToEnterAccountPassword -> {
|
||||||
setState {
|
setState {
|
||||||
copy(step = BootstrapStep.AccountPassword(false))
|
copy(step = BootstrapStep.AccountReAuth())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BootstrapActions.HandleForgotBackupPassphrase -> {
|
BootstrapActions.HandleForgotBackupPassphrase -> {
|
||||||
@ -273,15 +258,33 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
} else return@withState
|
} else return@withState
|
||||||
}
|
}
|
||||||
is BootstrapActions.ReAuth -> {
|
// is BootstrapActions.ReAuth -> {
|
||||||
startInitializeFlow(action.pass)
|
// startInitializeFlow(action.pass)
|
||||||
}
|
// }
|
||||||
is BootstrapActions.DoMigrateWithPassphrase -> {
|
is BootstrapActions.DoMigrateWithPassphrase -> {
|
||||||
startMigrationFlow(state.step, action.passphrase, null)
|
startMigrationFlow(state.step, action.passphrase, null)
|
||||||
}
|
}
|
||||||
is BootstrapActions.DoMigrateWithRecoveryKey -> {
|
is BootstrapActions.DoMigrateWithRecoveryKey -> {
|
||||||
startMigrationFlow(state.step, null, action.recoveryKey)
|
startMigrationFlow(state.step, null, action.recoveryKey)
|
||||||
}
|
}
|
||||||
|
BootstrapActions.SsoAuthDone -> {
|
||||||
|
uiaContinuation?.resume(DefaultBaseAuth(session = pendingAuth?.session ?: ""))
|
||||||
|
}
|
||||||
|
is BootstrapActions.PasswordAuthDone -> {
|
||||||
|
val decryptedPass = session.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
|
||||||
|
uiaContinuation?.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
|
session = pendingAuth?.session,
|
||||||
|
password = decryptedPass,
|
||||||
|
user = session.myUserId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
BootstrapActions.ReAuthCancelled -> {
|
||||||
|
setState {
|
||||||
|
copy(step = BootstrapStep.AccountReAuth(stringProvider.getString(R.string.authentication_error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,7 +296,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
startInitializeFlow(null)
|
startInitializeFlow(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,16 +349,16 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
migrationRecoveryKey = recoveryKey
|
migrationRecoveryKey = recoveryKey
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val userPassword = reAuthHelper.data
|
// val userPassword = reAuthHelper.data
|
||||||
if (userPassword == null) {
|
// if (userPassword == null) {
|
||||||
setState {
|
// setState {
|
||||||
copy(
|
// copy(
|
||||||
step = BootstrapStep.AccountPassword(false)
|
// step = BootstrapStep.AccountPassword(false)
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
startInitializeFlow(userPassword)
|
withState { startInitializeFlow(it) }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
is BackupToQuadSMigrationTask.Result.Failure -> {
|
is BackupToQuadSMigrationTask.Result.Failure -> {
|
||||||
_viewEvents.post(
|
_viewEvents.post(
|
||||||
@ -372,7 +375,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startInitializeFlow(userPassword: String?) = withState { state ->
|
private fun startInitializeFlow(state: BootstrapViewState) {
|
||||||
val previousStep = state.step
|
val previousStep = state.step
|
||||||
|
|
||||||
setState {
|
setState {
|
||||||
@ -389,19 +392,45 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
val interceptor = object : UserInteractiveAuthInterceptor {
|
||||||
val userPasswordAuth = userPassword?.let {
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
UserPasswordAuth(
|
when (flowResponse.nextUncompletedStage()) {
|
||||||
|
LoginFlowTypes.PASSWORD -> {
|
||||||
|
pendingAuth = UserPasswordAuth(
|
||||||
// Note that _pendingSession may or may not be null, this is OK, it will be managed by the task
|
// Note that _pendingSession may or may not be null, this is OK, it will be managed by the task
|
||||||
session = _pendingSession,
|
session = flowResponse.session,
|
||||||
user = session.myUserId,
|
user = session.myUserId,
|
||||||
password = it
|
password = null
|
||||||
|
)
|
||||||
|
uiaContinuation = promise
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
step = BootstrapStep.AccountReAuth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
_viewEvents.post(BootstrapViewEvents.RequestReAuth(flowResponse, errCode))
|
||||||
|
}
|
||||||
|
LoginFlowTypes.SSO -> {
|
||||||
|
pendingAuth = DefaultBaseAuth(flowResponse.session)
|
||||||
|
uiaContinuation = promise
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
step = BootstrapStep.AccountReAuth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_viewEvents.post(BootstrapViewEvents.RequestReAuth(flowResponse, errCode))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
promise.resumeWith(Result.failure(UnsupportedOperationException()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
bootstrapTask.invoke(this,
|
bootstrapTask.invoke(this,
|
||||||
Params(
|
Params(
|
||||||
userPasswordAuth = userPasswordAuth,
|
userInteractiveAuthInterceptor = interceptor,
|
||||||
progressListener = progressListener,
|
progressListener = progressListener,
|
||||||
passphrase = state.passphrase,
|
passphrase = state.passphrase,
|
||||||
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } },
|
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } },
|
||||||
@ -410,7 +439,6 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
) { bootstrapResult ->
|
) { bootstrapResult ->
|
||||||
when (bootstrapResult) {
|
when (bootstrapResult) {
|
||||||
is BootstrapResult.SuccessCrossSigningOnly -> {
|
is BootstrapResult.SuccessCrossSigningOnly -> {
|
||||||
// TPD
|
|
||||||
_viewEvents.post(BootstrapViewEvents.Dismiss(true))
|
_viewEvents.post(BootstrapViewEvents.Dismiss(true))
|
||||||
}
|
}
|
||||||
is BootstrapResult.Success -> {
|
is BootstrapResult.Success -> {
|
||||||
@ -424,26 +452,11 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is BootstrapResult.PasswordAuthFlowMissing -> {
|
|
||||||
// Ask the password to the user
|
|
||||||
_pendingSession = bootstrapResult.sessionId
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
step = BootstrapStep.AccountPassword(false)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is BootstrapResult.UnsupportedAuthFlow -> {
|
|
||||||
_viewEvents.post(BootstrapViewEvents.ModalError(stringProvider.getString(R.string.auth_flow_not_supported)))
|
|
||||||
_viewEvents.post(BootstrapViewEvents.Dismiss(false))
|
|
||||||
}
|
|
||||||
is BootstrapResult.InvalidPasswordError -> {
|
is BootstrapResult.InvalidPasswordError -> {
|
||||||
// it's a bad password
|
// it's a bad password / auth
|
||||||
// We clear the auth session, to avoid 'Requested operation has changed during the UI authentication session' error
|
|
||||||
_pendingSession = null
|
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
step = BootstrapStep.AccountPassword(false, stringProvider.getString(R.string.auth_invalid_login_param))
|
step = BootstrapStep.AccountReAuth(stringProvider.getString(R.string.auth_invalid_login_param))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -516,7 +529,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is BootstrapStep.AccountPassword -> {
|
is BootstrapStep.AccountReAuth -> {
|
||||||
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
|
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
|
||||||
}
|
}
|
||||||
BootstrapStep.Initializing -> {
|
BootstrapStep.Initializing -> {
|
||||||
|
@ -52,11 +52,11 @@ package im.vector.app.features.crypto.recover
|
|||||||
* │ │ BootstrapStep.ConfirmPassphrase │──┐
|
* │ │ BootstrapStep.ConfirmPassphrase │──┐
|
||||||
* │ └────────────────────────────────────┘ │
|
* │ └────────────────────────────────────┘ │
|
||||||
* │ │ │
|
* │ │ │
|
||||||
* │ is password needed? │
|
* │ is password/reauth needed? │
|
||||||
* │ │ │
|
* │ │ │
|
||||||
* │ ▼ │
|
* │ ▼ │
|
||||||
* │ ┌────────────────────────────────────┐ │
|
* │ ┌────────────────────────────────────┐ │
|
||||||
* │ │ BootstrapStep.AccountPassword │ │
|
* │ │ BootstrapStep.AccountReAuth │ │
|
||||||
* │ └────────────────────────────────────┘ │
|
* │ └────────────────────────────────────┘ │
|
||||||
* │ │ │
|
* │ │ │
|
||||||
* │ │ │
|
* │ │ │
|
||||||
@ -94,7 +94,7 @@ sealed class BootstrapStep {
|
|||||||
data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
|
data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
|
||||||
data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
|
data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
|
||||||
|
|
||||||
data class AccountPassword(val isPasswordVisible: Boolean, val failure: String? = null) : BootstrapStep()
|
data class AccountReAuth(val failure: String? = null) : BootstrapStep()
|
||||||
|
|
||||||
abstract class GetBackupSecretForMigration : BootstrapStep()
|
abstract class GetBackupSecretForMigration : BootstrapStep()
|
||||||
data class GetBackupSecretPassForMigration(val isPasswordVisible: Boolean, val useKey: Boolean) : GetBackupSecretForMigration()
|
data class GetBackupSecretPassForMigration(val isPasswordVisible: Boolean, val useKey: Boolean) : GetBackupSecretForMigration()
|
||||||
|
@ -17,10 +17,12 @@
|
|||||||
package im.vector.app.features.crypto.recover
|
package im.vector.app.features.crypto.recover
|
||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewEvents
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
|
|
||||||
sealed class BootstrapViewEvents : VectorViewEvents {
|
sealed class BootstrapViewEvents : VectorViewEvents {
|
||||||
data class Dismiss(val success: Boolean) : BootstrapViewEvents()
|
data class Dismiss(val success: Boolean) : BootstrapViewEvents()
|
||||||
data class ModalError(val error: String) : BootstrapViewEvents()
|
data class ModalError(val error: String) : BootstrapViewEvents()
|
||||||
object RecoveryKeySaved : BootstrapViewEvents()
|
object RecoveryKeySaved : BootstrapViewEvents()
|
||||||
data class SkipBootstrap(val genKeyOption: Boolean = true) : BootstrapViewEvents()
|
data class SkipBootstrap(val genKeyOption: Boolean = true) : BootstrapViewEvents()
|
||||||
|
data class RequestReAuth(val flowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : BootstrapViewEvents()
|
||||||
}
|
}
|
||||||
|
@ -21,29 +21,37 @@ import com.airbnb.mvrx.MvRx
|
|||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.features.login.ReAuthHelper
|
import im.vector.app.features.login.ReAuthHelper
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.pushrules.RuleIds
|
import org.matrix.android.sdk.api.pushrules.RuleIds
|
||||||
import org.matrix.android.sdk.api.session.InitialSyncProgressService
|
import org.matrix.android.sdk.api.session.InitialSyncProgressService
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
|
||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
|
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||||
import org.matrix.android.sdk.rx.asObservable
|
import org.matrix.android.sdk.rx.asObservable
|
||||||
import org.matrix.android.sdk.rx.rx
|
import org.matrix.android.sdk.rx.rx
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
class HomeActivityViewModel @AssistedInject constructor(
|
class HomeActivityViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: HomeActivityViewState,
|
@Assisted initialState: HomeActivityViewState,
|
||||||
@ -74,7 +82,6 @@ class HomeActivityViewModel @AssistedInject constructor(
|
|||||||
init {
|
init {
|
||||||
cleanupFiles()
|
cleanupFiles()
|
||||||
observeInitialSync()
|
observeInitialSync()
|
||||||
mayBeInitializeCrossSigning()
|
|
||||||
checkSessionPushIsOn()
|
checkSessionPushIsOn()
|
||||||
observeCrossSigningReset()
|
observeCrossSigningReset()
|
||||||
}
|
}
|
||||||
@ -125,7 +132,7 @@ class HomeActivityViewModel @AssistedInject constructor(
|
|||||||
is InitialSyncProgressService.Status.Idle -> {
|
is InitialSyncProgressService.Status.Idle -> {
|
||||||
if (checkBootstrap) {
|
if (checkBootstrap) {
|
||||||
checkBootstrap = false
|
checkBootstrap = false
|
||||||
maybeBootstrapCrossSigning()
|
maybeBootstrapCrossSigningAfterInitialSync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,29 +146,6 @@ class HomeActivityViewModel @AssistedInject constructor(
|
|||||||
.disposeOnClear()
|
.disposeOnClear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mayBeInitializeCrossSigning() {
|
|
||||||
if (args.accountCreation) {
|
|
||||||
val password = reAuthHelper.data ?: return Unit.also {
|
|
||||||
Timber.w("No password to init cross signing")
|
|
||||||
}
|
|
||||||
|
|
||||||
val session = activeSessionHolder.getSafeActiveSession() ?: return Unit.also {
|
|
||||||
Timber.w("No session to init cross signing")
|
|
||||||
}
|
|
||||||
|
|
||||||
// We do not use the viewModel context because we do not want to cancel this action
|
|
||||||
Timber.d("Initialize cross signing")
|
|
||||||
session.cryptoService().crossSigningService().initializeCrossSigning(
|
|
||||||
authParams = UserPasswordAuth(
|
|
||||||
session = null,
|
|
||||||
user = session.myUserId,
|
|
||||||
password = password
|
|
||||||
),
|
|
||||||
callback = NoOpMatrixCallback()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After migration from riot to element some users reported that their
|
* After migration from riot to element some users reported that their
|
||||||
* push setting for the session was set to off
|
* push setting for the session was set to off
|
||||||
@ -197,15 +181,18 @@ class HomeActivityViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun maybeBootstrapCrossSigning() {
|
private fun maybeBootstrapCrossSigningAfterInitialSync() {
|
||||||
// In case of account creation, it is already done before
|
// We do not use the viewModel context because we do not want to tie this action to activity view model
|
||||||
if (args.accountCreation) return
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val session = activeSessionHolder.getSafeActiveSession() ?: return@launch
|
||||||
|
|
||||||
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
tryOrNull("## MaybeBootstrapCrossSigning: Failed to download keys") {
|
||||||
|
awaitCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
|
||||||
|
session.cryptoService().downloadKeys(listOf(session.myUserId), true, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure keys of the user are downloaded
|
// From there we are up to date with server
|
||||||
session.cryptoService().downloadKeys(listOf(session.myUserId), true, object : MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
|
|
||||||
override fun onSuccess(data: MXUsersDevicesMap<CryptoDeviceInfo>) {
|
|
||||||
// Is there already cross signing keys here?
|
// Is there already cross signing keys here?
|
||||||
val mxCrossSigningInfo = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
|
val mxCrossSigningInfo = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
|
||||||
if (mxCrossSigningInfo != null) {
|
if (mxCrossSigningInfo != null) {
|
||||||
@ -222,31 +209,38 @@ class HomeActivityViewModel @AssistedInject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Initialize cross-signing
|
// Try to initialize cross signing in background if possible
|
||||||
val password = reAuthHelper.data
|
Timber.d("Initialize cross signing...")
|
||||||
|
awaitCallback<Unit> {
|
||||||
if (password == null) {
|
try {
|
||||||
// Check this is not an SSO account
|
|
||||||
if (session.getHomeServerCapabilities().canChangePassword) {
|
|
||||||
// Ask password to the user: Upgrade security
|
|
||||||
_viewEvents.post(HomeActivityViewEvents.AskPasswordToInitCrossSigning(session.getUser(session.myUserId)?.toMatrixItem()))
|
|
||||||
}
|
|
||||||
// Else (SSO) just ignore for the moment
|
|
||||||
} else {
|
|
||||||
// We do not use the viewModel context because we do not want to cancel this action
|
|
||||||
Timber.d("Initialize cross signing")
|
|
||||||
session.cryptoService().crossSigningService().initializeCrossSigning(
|
session.cryptoService().crossSigningService().initializeCrossSigning(
|
||||||
authParams = UserPasswordAuth(
|
object : UserInteractiveAuthInterceptor {
|
||||||
session = null,
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
// We missed server grace period or it's not setup, see if we remember locally password
|
||||||
|
if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD
|
||||||
|
&& errCode == null
|
||||||
|
&& reAuthHelper.data != null) {
|
||||||
|
promise.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
|
session = flowResponse.session,
|
||||||
user = session.myUserId,
|
user = session.myUserId,
|
||||||
password = password
|
password = reAuthHelper.data
|
||||||
),
|
|
||||||
callback = NoOpMatrixCallback()
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
promise.resumeWith(Result.failure(Exception("Cannot silently initialize cross signing, UIA missing")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
callback = it
|
||||||
|
)
|
||||||
|
Timber.d("Initialize cross signing SUCCESS")
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e(failure, "Failed to initialize cross signing")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: HomeActivityViewActions) {
|
override fun handle(action: HomeActivityViewActions) {
|
||||||
|
@ -83,25 +83,28 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs:
|
|||||||
ssoIdentityProviders?.forEach { identityProvider ->
|
ssoIdentityProviders?.forEach { identityProvider ->
|
||||||
// Use some heuristic to render buttons according to branding guidelines
|
// Use some heuristic to render buttons according to branding guidelines
|
||||||
val button: MaterialButton = cachedViews[identityProvider.id]
|
val button: MaterialButton = cachedViews[identityProvider.id]
|
||||||
?: when (identityProvider.id) {
|
?: when (identityProvider.brand) {
|
||||||
SsoIdentityProvider.ID_GOOGLE -> {
|
SsoIdentityProvider.BRAND_GOOGLE -> {
|
||||||
MaterialButton(context, null, R.attr.vctr_social_login_button_google_style)
|
MaterialButton(context, null, R.attr.vctr_social_login_button_google_style)
|
||||||
}
|
}
|
||||||
SsoIdentityProvider.ID_GITHUB -> {
|
SsoIdentityProvider.BRAND_GITHUB -> {
|
||||||
MaterialButton(context, null, R.attr.vctr_social_login_button_github_style)
|
MaterialButton(context, null, R.attr.vctr_social_login_button_github_style)
|
||||||
}
|
}
|
||||||
SsoIdentityProvider.ID_APPLE -> {
|
SsoIdentityProvider.BRAND_APPLE -> {
|
||||||
MaterialButton(context, null, R.attr.vctr_social_login_button_apple_style)
|
MaterialButton(context, null, R.attr.vctr_social_login_button_apple_style)
|
||||||
}
|
}
|
||||||
SsoIdentityProvider.ID_FACEBOOK -> {
|
SsoIdentityProvider.BRAND_FACEBOOK -> {
|
||||||
MaterialButton(context, null, R.attr.vctr_social_login_button_facebook_style)
|
MaterialButton(context, null, R.attr.vctr_social_login_button_facebook_style)
|
||||||
}
|
}
|
||||||
SsoIdentityProvider.ID_TWITTER -> {
|
SsoIdentityProvider.BRAND_TWITTER -> {
|
||||||
MaterialButton(context, null, R.attr.vctr_social_login_button_twitter_style)
|
MaterialButton(context, null, R.attr.vctr_social_login_button_twitter_style)
|
||||||
}
|
}
|
||||||
|
SsoIdentityProvider.BRAND_GITLAB -> {
|
||||||
|
MaterialButton(context, null, R.attr.vctr_social_login_button_gitlab_style)
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// TODO Use iconUrl
|
// TODO Use iconUrl
|
||||||
MaterialButton(context, null, R.attr.materialButtonStyle).apply {
|
MaterialButton(context, null, R.attr.materialButtonOutlinedStyle).apply {
|
||||||
transformationMethod = null
|
transformationMethod = null
|
||||||
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||||
}
|
}
|
||||||
@ -131,12 +134,13 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs:
|
|||||||
clipChildren = false
|
clipChildren = false
|
||||||
if (isInEditMode) {
|
if (isInEditMode) {
|
||||||
ssoIdentityProviders = listOf(
|
ssoIdentityProviders = listOf(
|
||||||
SsoIdentityProvider(SsoIdentityProvider.ID_GOOGLE, "Google", null),
|
SsoIdentityProvider("Google", "Google", null, SsoIdentityProvider.BRAND_GOOGLE),
|
||||||
SsoIdentityProvider(SsoIdentityProvider.ID_FACEBOOK, "Facebook", null),
|
SsoIdentityProvider("Facebook", "Facebook", null, SsoIdentityProvider.BRAND_FACEBOOK),
|
||||||
SsoIdentityProvider(SsoIdentityProvider.ID_APPLE, "Apple", null),
|
SsoIdentityProvider("Apple", "Apple", null, SsoIdentityProvider.BRAND_APPLE),
|
||||||
SsoIdentityProvider(SsoIdentityProvider.ID_GITHUB, "GitHub", null),
|
SsoIdentityProvider("GitHub", "GitHub", null, SsoIdentityProvider.BRAND_GITHUB),
|
||||||
SsoIdentityProvider(SsoIdentityProvider.ID_TWITTER, "Twitter", null),
|
SsoIdentityProvider("Twitter", "Twitter", null, SsoIdentityProvider.BRAND_TWITTER),
|
||||||
SsoIdentityProvider("Custom_pro", "SSO", null)
|
SsoIdentityProvider("Gitlab", "Gitlab", null, SsoIdentityProvider.BRAND_GITLAB),
|
||||||
|
SsoIdentityProvider("Custom_pro", "SSO", null, null)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.SocialLoginButtonsView, 0, 0)
|
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.SocialLoginButtonsView, 0, 0)
|
||||||
|
@ -229,10 +229,13 @@ class DefaultNavigator @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun openKeysBackupSetup(context: Context, showManualExport: Boolean) {
|
override fun openKeysBackupSetup(context: Context, showManualExport: Boolean) {
|
||||||
// if cross signing is enabled we should propose full 4S
|
// if cross signing is enabled and trusted or not set up at all we should propose full 4S
|
||||||
sessionHolder.getSafeActiveSession()?.let { session ->
|
sessionHolder.getSafeActiveSession()?.let { session ->
|
||||||
if (session.cryptoService().crossSigningService().canCrossSign() && context is AppCompatActivity) {
|
if (session.cryptoService().crossSigningService().getMyCrossSigningKeys() == null
|
||||||
BootstrapBottomSheet.show(context.supportFragmentManager, SetupMode.NORMAL)
|
|| session.cryptoService().crossSigningService().canCrossSign()) {
|
||||||
|
(context as? AppCompatActivity)?.let {
|
||||||
|
BootstrapBottomSheet.show(it.supportFragmentManager, SetupMode.NORMAL)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport))
|
context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport))
|
||||||
}
|
}
|
||||||
|
@ -311,10 +311,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
mCrossSigningStatePreference.isVisible = true
|
mCrossSigningStatePreference.isVisible = true
|
||||||
if (!vectorPreferences.developerMode()) {
|
|
||||||
// When not in developer mode, intercept click on this preference
|
|
||||||
mCrossSigningStatePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { true }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val saveMegolmStartForActivityResult = registerStartForActivityResult {
|
private val saveMegolmStartForActivityResult = registerStartForActivityResult {
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.settings.account.deactivation
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
|
sealed class DeactivateAccountAction : VectorViewModelAction {
|
||||||
|
object TogglePassword : DeactivateAccountAction()
|
||||||
|
data class DeactivateAccount(val eraseAllData: Boolean) : DeactivateAccountAction()
|
||||||
|
|
||||||
|
object SsoAuthDone: DeactivateAccountAction()
|
||||||
|
data class PasswordAuthDone(val password: String): DeactivateAccountAction()
|
||||||
|
object ReAuthCancelled: DeactivateAccountAction()
|
||||||
|
}
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.app.features.settings.account.deactivation
|
package im.vector.app.features.settings.account.deactivation
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@ -23,16 +24,16 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
|
||||||
import com.jakewharton.rxbinding3.widget.textChanges
|
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.extensions.showPassword
|
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.databinding.FragmentDeactivateAccountBinding
|
import im.vector.app.databinding.FragmentDeactivateAccountBinding
|
||||||
import im.vector.app.features.MainActivity
|
import im.vector.app.features.MainActivity
|
||||||
import im.vector.app.features.MainActivityArgs
|
import im.vector.app.features.MainActivityArgs
|
||||||
|
import im.vector.app.features.auth.ReAuthActivity
|
||||||
import im.vector.app.features.settings.VectorSettingsActivity
|
import im.vector.app.features.settings.VectorSettingsActivity
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -46,6 +47,25 @@ class DeactivateAccountFragment @Inject constructor(
|
|||||||
return FragmentDeactivateAccountBinding.inflate(inflater, container, false)
|
return FragmentDeactivateAccountBinding.inflate(inflater, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
|
||||||
|
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||||
|
when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
|
||||||
|
LoginFlowTypes.SSO -> {
|
||||||
|
viewModel.handle(DeactivateAccountAction.SsoAuthDone)
|
||||||
|
}
|
||||||
|
LoginFlowTypes.PASSWORD -> {
|
||||||
|
val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
|
||||||
|
viewModel.handle(DeactivateAccountAction.PasswordAuthDone(password))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
viewModel.handle(DeactivateAccountAction.ReAuthCancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viewModel.handle(DeactivateAccountAction.ReAuthCancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
(activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.deactivate_account_title)
|
(activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.deactivate_account_title)
|
||||||
@ -66,29 +86,15 @@ class DeactivateAccountFragment @Inject constructor(
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
setupUi()
|
|
||||||
setupViewListeners()
|
setupViewListeners()
|
||||||
observeViewEvents()
|
observeViewEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupUi() {
|
|
||||||
views.deactivateAccountPassword.textChanges()
|
|
||||||
.subscribe {
|
|
||||||
views.deactivateAccountPasswordTil.error = null
|
|
||||||
views.deactivateAccountSubmit.isEnabled = it.isNotEmpty()
|
|
||||||
}
|
|
||||||
.disposeOnDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupViewListeners() {
|
private fun setupViewListeners() {
|
||||||
views.deactivateAccountPasswordReveal.setOnClickListener {
|
|
||||||
viewModel.handle(DeactivateAccountAction.TogglePassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
views.deactivateAccountSubmit.debouncedClicks {
|
views.deactivateAccountSubmit.debouncedClicks {
|
||||||
viewModel.handle(DeactivateAccountAction.DeactivateAccount(
|
viewModel.handle(DeactivateAccountAction.DeactivateAccount(
|
||||||
views.deactivateAccountPassword.text.toString(),
|
views.deactivateAccountEraseCheckbox.isChecked)
|
||||||
views.deactivateAccountEraseCheckbox.isChecked))
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,26 +105,27 @@ class DeactivateAccountFragment @Inject constructor(
|
|||||||
settingsActivity?.ignoreInvalidTokenError = true
|
settingsActivity?.ignoreInvalidTokenError = true
|
||||||
showLoadingDialog(it.message)
|
showLoadingDialog(it.message)
|
||||||
}
|
}
|
||||||
DeactivateAccountViewEvents.EmptyPassword -> {
|
DeactivateAccountViewEvents.InvalidAuth -> {
|
||||||
|
dismissLoadingDialog()
|
||||||
settingsActivity?.ignoreInvalidTokenError = false
|
settingsActivity?.ignoreInvalidTokenError = false
|
||||||
views.deactivateAccountPasswordTil.error = getString(R.string.error_empty_field_your_password)
|
|
||||||
}
|
|
||||||
DeactivateAccountViewEvents.InvalidPassword -> {
|
|
||||||
settingsActivity?.ignoreInvalidTokenError = false
|
|
||||||
views.deactivateAccountPasswordTil.error = getString(R.string.settings_fail_to_update_password_invalid_current_password)
|
|
||||||
}
|
}
|
||||||
is DeactivateAccountViewEvents.OtherFailure -> {
|
is DeactivateAccountViewEvents.OtherFailure -> {
|
||||||
settingsActivity?.ignoreInvalidTokenError = false
|
settingsActivity?.ignoreInvalidTokenError = false
|
||||||
|
dismissLoadingDialog()
|
||||||
displayErrorDialog(it.throwable)
|
displayErrorDialog(it.throwable)
|
||||||
}
|
}
|
||||||
DeactivateAccountViewEvents.Done ->
|
DeactivateAccountViewEvents.Done -> {
|
||||||
MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCredentials = true, isAccountDeactivated = true))
|
MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCredentials = true, isAccountDeactivated = true))
|
||||||
|
}
|
||||||
|
is DeactivateAccountViewEvents.RequestReAuth -> {
|
||||||
|
ReAuthActivity.newIntent(requireContext(),
|
||||||
|
it.registrationFlowResponse,
|
||||||
|
it.lastErrorCode,
|
||||||
|
getString(R.string.deactivate_account_title)).let { intent ->
|
||||||
|
reAuthActivityResultLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(viewModel) { state ->
|
|
||||||
views.deactivateAccountPassword.showPassword(state.passwordShown)
|
|
||||||
views.deactivateAccountPasswordReveal.setImageResource(if (state.passwordShown) R.drawable.ic_eye_closed else R.drawable.ic_eye)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -17,14 +17,15 @@
|
|||||||
package im.vector.app.features.settings.account.deactivation
|
package im.vector.app.features.settings.account.deactivation
|
||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewEvents
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transient events for deactivate account settings screen
|
* Transient events for deactivate account settings screen
|
||||||
*/
|
*/
|
||||||
sealed class DeactivateAccountViewEvents : VectorViewEvents {
|
sealed class DeactivateAccountViewEvents : VectorViewEvents {
|
||||||
data class Loading(val message: CharSequence? = null) : DeactivateAccountViewEvents()
|
data class Loading(val message: CharSequence? = null) : DeactivateAccountViewEvents()
|
||||||
object EmptyPassword : DeactivateAccountViewEvents()
|
object InvalidAuth : DeactivateAccountViewEvents()
|
||||||
object InvalidPassword : DeactivateAccountViewEvents()
|
|
||||||
data class OtherFailure(val throwable: Throwable) : DeactivateAccountViewEvents()
|
data class OtherFailure(val throwable: Throwable) : DeactivateAccountViewEvents()
|
||||||
object Done : DeactivateAccountViewEvents()
|
object Done : DeactivateAccountViewEvents()
|
||||||
|
data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DeactivateAccountViewEvents()
|
||||||
}
|
}
|
||||||
|
@ -21,25 +21,28 @@ import com.airbnb.mvrx.MvRxState
|
|||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.core.platform.VectorViewModelAction
|
import im.vector.app.features.auth.ReAuthActivity
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.failure.isInvalidPassword
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
|
import org.matrix.android.sdk.api.failure.isInvalidUIAAuth
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import java.lang.Exception
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
|
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
|
import timber.log.Timber
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
data class DeactivateAccountViewState(
|
data class DeactivateAccountViewState(
|
||||||
val passwordShown: Boolean = false
|
val passwordShown: Boolean = false
|
||||||
) : MvRxState
|
) : MvRxState
|
||||||
|
|
||||||
sealed class DeactivateAccountAction : VectorViewModelAction {
|
|
||||||
object TogglePassword : DeactivateAccountAction()
|
|
||||||
data class DeactivateAccount(val password: String, val eraseAllData: Boolean) : DeactivateAccountAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private val initialState: DeactivateAccountViewState,
|
class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private val initialState: DeactivateAccountViewState,
|
||||||
private val session: Session)
|
private val session: Session)
|
||||||
: VectorViewModel<DeactivateAccountViewState, DeactivateAccountAction, DeactivateAccountViewEvents>(initialState) {
|
: VectorViewModel<DeactivateAccountViewState, DeactivateAccountAction, DeactivateAccountViewEvents>(initialState) {
|
||||||
@ -49,10 +52,37 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v
|
|||||||
fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel
|
fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var uiaContinuation: Continuation<UIABaseAuth>? = null
|
||||||
|
var pendingAuth: UIABaseAuth? = null
|
||||||
|
|
||||||
override fun handle(action: DeactivateAccountAction) {
|
override fun handle(action: DeactivateAccountAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
DeactivateAccountAction.TogglePassword -> handleTogglePassword()
|
DeactivateAccountAction.TogglePassword -> handleTogglePassword()
|
||||||
is DeactivateAccountAction.DeactivateAccount -> handleDeactivateAccount(action)
|
is DeactivateAccountAction.DeactivateAccount -> handleDeactivateAccount(action)
|
||||||
|
DeactivateAccountAction.SsoAuthDone -> {
|
||||||
|
Timber.d("## UIA - FallBack success")
|
||||||
|
if (pendingAuth != null) {
|
||||||
|
uiaContinuation?.resume(pendingAuth!!)
|
||||||
|
} else {
|
||||||
|
uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is DeactivateAccountAction.PasswordAuthDone -> {
|
||||||
|
val decryptedPass = session.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
|
||||||
|
uiaContinuation?.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
|
session = pendingAuth?.session,
|
||||||
|
password = decryptedPass,
|
||||||
|
user = session.myUserId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DeactivateAccountAction.ReAuthCancelled -> {
|
||||||
|
Timber.d("## UIA - Reauth cancelled")
|
||||||
|
uiaContinuation?.resumeWith(Result.failure((Exception())))
|
||||||
|
uiaContinuation = null
|
||||||
|
pendingAuth = null
|
||||||
|
}
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,20 +93,22 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDeactivateAccount(action: DeactivateAccountAction.DeactivateAccount) {
|
private fun handleDeactivateAccount(action: DeactivateAccountAction.DeactivateAccount) {
|
||||||
if (action.password.isEmpty()) {
|
|
||||||
_viewEvents.post(DeactivateAccountViewEvents.EmptyPassword)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_viewEvents.post(DeactivateAccountViewEvents.Loading())
|
_viewEvents.post(DeactivateAccountViewEvents.Loading())
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val event = try {
|
val event = try {
|
||||||
session.deactivateAccount(action.password, action.eraseAllData)
|
session.deactivateAccount(
|
||||||
|
object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
_viewEvents.post(DeactivateAccountViewEvents.RequestReAuth(flowResponse, errCode))
|
||||||
|
pendingAuth = DefaultBaseAuth(session = flowResponse.session)
|
||||||
|
uiaContinuation = promise
|
||||||
|
}
|
||||||
|
}, action.eraseAllData)
|
||||||
DeactivateAccountViewEvents.Done
|
DeactivateAccountViewEvents.Done
|
||||||
} catch (failure: Exception) {
|
} catch (failure: Exception) {
|
||||||
if (failure.isInvalidPassword()) {
|
if (failure.isInvalidUIAAuth()) {
|
||||||
DeactivateAccountViewEvents.InvalidPassword
|
DeactivateAccountViewEvents.InvalidAuth
|
||||||
} else {
|
} else {
|
||||||
DeactivateAccountViewEvents.OtherFailure(failure)
|
DeactivateAccountViewEvents.OtherFailure(failure)
|
||||||
}
|
}
|
||||||
|
@ -18,4 +18,9 @@ package im.vector.app.features.settings.crosssigning
|
|||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewModelAction
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
sealed class CrossSigningSettingsAction : VectorViewModelAction
|
sealed class CrossSigningSettingsAction : VectorViewModelAction {
|
||||||
|
object InitializeCrossSigning: CrossSigningSettingsAction()
|
||||||
|
object SsoAuthDone: CrossSigningSettingsAction()
|
||||||
|
data class PasswordAuthDone(val password: String): CrossSigningSettingsAction()
|
||||||
|
object ReAuthCancelled: CrossSigningSettingsAction()
|
||||||
|
}
|
||||||
|
@ -19,8 +19,11 @@ import com.airbnb.epoxy.TypedEpoxyController
|
|||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.resources.ColorProvider
|
import im.vector.app.core.resources.ColorProvider
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import im.vector.app.core.ui.list.genericButtonItem
|
||||||
import im.vector.app.core.ui.list.genericItem
|
import im.vector.app.core.ui.list.genericItem
|
||||||
import im.vector.app.core.ui.list.genericItemWithValue
|
import im.vector.app.core.ui.list.genericItemWithValue
|
||||||
|
import im.vector.app.core.ui.list.genericPositiveButtonItem
|
||||||
|
import im.vector.app.core.utils.DebouncedClickListener
|
||||||
import im.vector.app.core.utils.DimensionConverter
|
import im.vector.app.core.utils.DimensionConverter
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -31,7 +34,9 @@ class CrossSigningSettingsController @Inject constructor(
|
|||||||
private val dimensionConverter: DimensionConverter
|
private val dimensionConverter: DimensionConverter
|
||||||
) : TypedEpoxyController<CrossSigningSettingsViewState>() {
|
) : TypedEpoxyController<CrossSigningSettingsViewState>() {
|
||||||
|
|
||||||
interface InteractionListener
|
interface InteractionListener {
|
||||||
|
fun didTapInitializeCrossSigning()
|
||||||
|
}
|
||||||
|
|
||||||
var interactionListener: InteractionListener? = null
|
var interactionListener: InteractionListener? = null
|
||||||
|
|
||||||
@ -44,6 +49,13 @@ class CrossSigningSettingsController @Inject constructor(
|
|||||||
titleIconResourceId(R.drawable.ic_shield_trusted)
|
titleIconResourceId(R.drawable.ic_shield_trusted)
|
||||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete))
|
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete))
|
||||||
}
|
}
|
||||||
|
genericButtonItem {
|
||||||
|
id("Reset")
|
||||||
|
text(stringProvider.getString(R.string.reset_cross_signing))
|
||||||
|
buttonClickAction(DebouncedClickListener({
|
||||||
|
interactionListener?.didTapInitializeCrossSigning()
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
data.xSigningKeysAreTrusted -> {
|
data.xSigningKeysAreTrusted -> {
|
||||||
genericItem {
|
genericItem {
|
||||||
@ -51,6 +63,13 @@ class CrossSigningSettingsController @Inject constructor(
|
|||||||
titleIconResourceId(R.drawable.ic_shield_custom)
|
titleIconResourceId(R.drawable.ic_shield_custom)
|
||||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted))
|
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted))
|
||||||
}
|
}
|
||||||
|
genericButtonItem {
|
||||||
|
id("Reset")
|
||||||
|
text(stringProvider.getString(R.string.reset_cross_signing))
|
||||||
|
buttonClickAction(DebouncedClickListener({
|
||||||
|
interactionListener?.didTapInitializeCrossSigning()
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
data.xSigningIsEnableInAccount -> {
|
data.xSigningIsEnableInAccount -> {
|
||||||
genericItem {
|
genericItem {
|
||||||
@ -58,12 +77,27 @@ class CrossSigningSettingsController @Inject constructor(
|
|||||||
titleIconResourceId(R.drawable.ic_shield_black)
|
titleIconResourceId(R.drawable.ic_shield_black)
|
||||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted))
|
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted))
|
||||||
}
|
}
|
||||||
|
genericButtonItem {
|
||||||
|
id("Reset")
|
||||||
|
text(stringProvider.getString(R.string.reset_cross_signing))
|
||||||
|
buttonClickAction(DebouncedClickListener({
|
||||||
|
interactionListener?.didTapInitializeCrossSigning()
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
genericItem {
|
genericItem {
|
||||||
id("not")
|
id("not")
|
||||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled))
|
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
genericPositiveButtonItem {
|
||||||
|
id("Initialize")
|
||||||
|
text(stringProvider.getString(R.string.initialize_cross_signing))
|
||||||
|
buttonClickAction(DebouncedClickListener({
|
||||||
|
interactionListener?.didTapInitializeCrossSigning()
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,20 +15,26 @@
|
|||||||
*/
|
*/
|
||||||
package im.vector.app.features.settings.crosssigning
|
package im.vector.app.features.settings.crosssigning
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.cleanup
|
import im.vector.app.core.extensions.cleanup
|
||||||
import im.vector.app.core.extensions.configureWith
|
import im.vector.app.core.extensions.configureWith
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
|
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||||
|
import im.vector.app.core.extensions.setTextOrHide
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.databinding.FragmentGenericRecyclerBinding
|
import im.vector.app.databinding.FragmentGenericRecyclerBinding
|
||||||
|
import im.vector.app.features.auth.ReAuthActivity
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -47,19 +53,55 @@ class CrossSigningSettingsFragment @Inject constructor(
|
|||||||
|
|
||||||
private val viewModel: CrossSigningSettingsViewModel by fragmentViewModel()
|
private val viewModel: CrossSigningSettingsViewModel by fragmentViewModel()
|
||||||
|
|
||||||
|
private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
|
||||||
|
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||||
|
when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
|
||||||
|
LoginFlowTypes.SSO -> {
|
||||||
|
viewModel.handle(CrossSigningSettingsAction.SsoAuthDone)
|
||||||
|
}
|
||||||
|
LoginFlowTypes.PASSWORD -> {
|
||||||
|
val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
|
||||||
|
viewModel.handle(CrossSigningSettingsAction.PasswordAuthDone(password))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
viewModel.handle(CrossSigningSettingsAction.ReAuthCancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// activityResult.data?.extras?.getString(ReAuthActivity.RESULT_TOKEN)?.let { token ->
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
viewModel.handle(CrossSigningSettingsAction.ReAuthCancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
viewModel.observeViewEvents {
|
viewModel.observeViewEvents { event ->
|
||||||
when (it) {
|
when (event) {
|
||||||
is CrossSigningSettingsViewEvents.Failure -> {
|
is CrossSigningSettingsViewEvents.Failure -> {
|
||||||
AlertDialog.Builder(requireContext())
|
AlertDialog.Builder(requireContext())
|
||||||
.setTitle(R.string.dialog_title_error)
|
.setTitle(R.string.dialog_title_error)
|
||||||
.setMessage(errorFormatter.toHumanReadable(it.throwable))
|
.setMessage(errorFormatter.toHumanReadable(event.throwable))
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
.show()
|
.show()
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
|
is CrossSigningSettingsViewEvents.RequestReAuth -> {
|
||||||
|
ReAuthActivity.newIntent(requireContext(),
|
||||||
|
event.registrationFlowResponse,
|
||||||
|
event.lastErrorCode,
|
||||||
|
getString(R.string.initialize_cross_signing)).let { intent ->
|
||||||
|
reAuthActivityResultLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is CrossSigningSettingsViewEvents.ShowModalWaitingView -> {
|
||||||
|
views.waitingView.waitingView.isVisible = true
|
||||||
|
views.waitingView.waitingStatusText.setTextOrHide(event.status)
|
||||||
|
}
|
||||||
|
CrossSigningSettingsViewEvents.HideModalWaitingView -> {
|
||||||
|
views.waitingView.waitingView.isVisible = false
|
||||||
|
}
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,4 +125,8 @@ class CrossSigningSettingsFragment @Inject constructor(
|
|||||||
controller.interactionListener = null
|
controller.interactionListener = null
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun didTapInitializeCrossSigning() {
|
||||||
|
viewModel.handle(CrossSigningSettingsAction.InitializeCrossSigning)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,10 +17,14 @@
|
|||||||
package im.vector.app.features.settings.crosssigning
|
package im.vector.app.features.settings.crosssigning
|
||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewEvents
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transient events for cross signing settings screen
|
* Transient events for cross signing settings screen
|
||||||
*/
|
*/
|
||||||
sealed class CrossSigningSettingsViewEvents : VectorViewEvents {
|
sealed class CrossSigningSettingsViewEvents : VectorViewEvents {
|
||||||
data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents()
|
data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents()
|
||||||
|
data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : CrossSigningSettingsViewEvents()
|
||||||
|
data class ShowModalWaitingView(val status: String?) : CrossSigningSettingsViewEvents()
|
||||||
|
object HideModalWaitingView : CrossSigningSettingsViewEvents()
|
||||||
}
|
}
|
||||||
|
@ -15,25 +15,48 @@
|
|||||||
*/
|
*/
|
||||||
package im.vector.app.features.settings.crosssigning
|
package im.vector.app.features.settings.crosssigning
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.airbnb.mvrx.FragmentViewModelContext
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import im.vector.app.features.auth.ReAuthActivity
|
||||||
|
import im.vector.app.features.login.ReAuthHelper
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.functions.BiFunction
|
import io.reactivex.functions.BiFunction
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified
|
import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||||
|
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||||
import org.matrix.android.sdk.rx.rx
|
import org.matrix.android.sdk.rx.rx
|
||||||
|
import timber.log.Timber
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted private val initialState: CrossSigningSettingsViewState,
|
class CrossSigningSettingsViewModel @AssistedInject constructor(
|
||||||
private val session: Session)
|
@Assisted private val initialState: CrossSigningSettingsViewState,
|
||||||
: VectorViewModel<CrossSigningSettingsViewState, CrossSigningSettingsAction, CrossSigningSettingsViewEvents>(initialState) {
|
private val session: Session,
|
||||||
|
private val reAuthHelper: ReAuthHelper,
|
||||||
|
private val stringProvider: StringProvider
|
||||||
|
) : VectorViewModel<CrossSigningSettingsViewState, CrossSigningSettingsAction, CrossSigningSettingsViewEvents>(initialState) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Observable.combineLatest<List<DeviceInfo>, Optional<MXCrossSigningInfo>, Pair<List<DeviceInfo>, Optional<MXCrossSigningInfo>>>(
|
Observable.combineLatest<List<DeviceInfo>, Optional<MXCrossSigningInfo>, Pair<List<DeviceInfo>, Optional<MXCrossSigningInfo>>>(
|
||||||
@ -58,15 +81,82 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var uiaContinuation: Continuation<UIABaseAuth>? = null
|
||||||
|
var pendingAuth: UIABaseAuth? = null
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel
|
fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: CrossSigningSettingsAction) {
|
override fun handle(action: CrossSigningSettingsAction) {
|
||||||
// No op for the moment
|
when (action) {
|
||||||
// when (action) {
|
CrossSigningSettingsAction.InitializeCrossSigning -> {
|
||||||
// }.exhaustive
|
_viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null))
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
awaitCallback<Unit> {
|
||||||
|
session.cryptoService().crossSigningService().initializeCrossSigning(
|
||||||
|
object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse,
|
||||||
|
errCode: String?,
|
||||||
|
promise: Continuation<UIABaseAuth>) {
|
||||||
|
Timber.d("## UIA : initializeCrossSigning UIA")
|
||||||
|
if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD
|
||||||
|
&& reAuthHelper.data != null && errCode == null) {
|
||||||
|
UserPasswordAuth(
|
||||||
|
session = null,
|
||||||
|
user = session.myUserId,
|
||||||
|
password = reAuthHelper.data
|
||||||
|
).let { promise.resume(it) }
|
||||||
|
} else {
|
||||||
|
Timber.d("## UIA : initializeCrossSigning UIA > start reauth activity")
|
||||||
|
_viewEvents.post(CrossSigningSettingsViewEvents.RequestReAuth(flowResponse, errCode))
|
||||||
|
pendingAuth = DefaultBaseAuth(session = flowResponse.session)
|
||||||
|
uiaContinuation = promise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, it)
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
handleInitializeXSigningError(failure)
|
||||||
|
} finally {
|
||||||
|
_viewEvents.post(CrossSigningSettingsViewEvents.HideModalWaitingView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
is CrossSigningSettingsAction.SsoAuthDone -> {
|
||||||
|
Timber.d("## UIA - FallBack success")
|
||||||
|
if (pendingAuth != null) {
|
||||||
|
uiaContinuation?.resume(pendingAuth!!)
|
||||||
|
} else {
|
||||||
|
uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is CrossSigningSettingsAction.PasswordAuthDone -> {
|
||||||
|
val decryptedPass = session.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
|
||||||
|
uiaContinuation?.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
|
session = pendingAuth?.session,
|
||||||
|
password = decryptedPass,
|
||||||
|
user = session.myUserId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CrossSigningSettingsAction.ReAuthCancelled -> {
|
||||||
|
Timber.d("## UIA - Reauth cancelled")
|
||||||
|
_viewEvents.post(CrossSigningSettingsViewEvents.HideModalWaitingView)
|
||||||
|
uiaContinuation?.resumeWith(Result.failure((Exception())))
|
||||||
|
uiaContinuation = null
|
||||||
|
pendingAuth = null
|
||||||
|
}
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleInitializeXSigningError(failure: Throwable) {
|
||||||
|
Timber.e(failure, "## CrossSigning - Failed to initialize cross signing")
|
||||||
|
_viewEvents.post(CrossSigningSettingsViewEvents.Failure(Exception(stringProvider.getString(R.string.failed_to_initialize_cross_signing))))
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : MvRxViewModelFactory<CrossSigningSettingsViewModel, CrossSigningSettingsViewState> {
|
companion object : MvRxViewModelFactory<CrossSigningSettingsViewModel, CrossSigningSettingsViewState> {
|
||||||
|
@ -22,7 +22,7 @@ import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
|||||||
sealed class DevicesAction : VectorViewModelAction {
|
sealed class DevicesAction : VectorViewModelAction {
|
||||||
object Refresh : DevicesAction()
|
object Refresh : DevicesAction()
|
||||||
data class Delete(val deviceId: String) : DevicesAction()
|
data class Delete(val deviceId: String) : DevicesAction()
|
||||||
data class Password(val password: String) : DevicesAction()
|
// data class Password(val password: String) : DevicesAction()
|
||||||
data class Rename(val deviceId: String, val newName: String) : DevicesAction()
|
data class Rename(val deviceId: String, val newName: String) : DevicesAction()
|
||||||
|
|
||||||
data class PromptRename(val deviceId: String) : DevicesAction()
|
data class PromptRename(val deviceId: String) : DevicesAction()
|
||||||
@ -30,4 +30,8 @@ sealed class DevicesAction : VectorViewModelAction {
|
|||||||
data class VerifyMyDeviceManually(val deviceId: String) : DevicesAction()
|
data class VerifyMyDeviceManually(val deviceId: String) : DevicesAction()
|
||||||
object CompleteSecurity : DevicesAction()
|
object CompleteSecurity : DevicesAction()
|
||||||
data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction()
|
data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction()
|
||||||
|
|
||||||
|
object SsoAuthDone: DevicesAction()
|
||||||
|
data class PasswordAuthDone(val password: String): DevicesAction()
|
||||||
|
object ReAuthCancelled: DevicesAction()
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ package im.vector.app.features.settings.devices
|
|||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewEvents
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||||
|
|
||||||
@ -27,9 +28,12 @@ import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
|||||||
*/
|
*/
|
||||||
sealed class DevicesViewEvents : VectorViewEvents {
|
sealed class DevicesViewEvents : VectorViewEvents {
|
||||||
data class Loading(val message: CharSequence? = null) : DevicesViewEvents()
|
data class Loading(val message: CharSequence? = null) : DevicesViewEvents()
|
||||||
|
// object HideLoading : DevicesViewEvents()
|
||||||
data class Failure(val throwable: Throwable) : DevicesViewEvents()
|
data class Failure(val throwable: Throwable) : DevicesViewEvents()
|
||||||
|
|
||||||
object RequestPassword : DevicesViewEvents()
|
// object RequestPassword : DevicesViewEvents()
|
||||||
|
|
||||||
|
data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DevicesViewEvents()
|
||||||
|
|
||||||
data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvents()
|
data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvents()
|
||||||
|
|
||||||
|
@ -27,16 +27,21 @@ import com.airbnb.mvrx.Success
|
|||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import im.vector.app.core.error.SsoFlowNotSupportedYet
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.R
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import im.vector.app.features.auth.ReAuthActivity
|
||||||
|
import im.vector.app.features.login.ReAuthHelper
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.functions.BiFunction
|
import io.reactivex.functions.BiFunction
|
||||||
import io.reactivex.subjects.PublishSubject
|
import io.reactivex.subjects.PublishSubject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
||||||
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
@ -44,13 +49,22 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
|||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
|
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
|
||||||
|
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
|
||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||||
import org.matrix.android.sdk.rx.rx
|
import org.matrix.android.sdk.rx.rx
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.net.ssl.HttpsURLConnection
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
data class DevicesViewState(
|
data class DevicesViewState(
|
||||||
val myDeviceId: String = "",
|
val myDeviceId: String = "",
|
||||||
@ -70,9 +84,14 @@ data class DeviceFullInfo(
|
|||||||
|
|
||||||
class DevicesViewModel @AssistedInject constructor(
|
class DevicesViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: DevicesViewState,
|
@Assisted initialState: DevicesViewState,
|
||||||
private val session: Session
|
private val session: Session,
|
||||||
|
private val reAuthHelper: ReAuthHelper,
|
||||||
|
private val stringProvider: StringProvider
|
||||||
) : VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
|
) : VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
|
||||||
|
|
||||||
|
var uiaContinuation: Continuation<UIABaseAuth>? = null
|
||||||
|
var pendingAuth: UIABaseAuth? = null
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(initialState: DevicesViewState): DevicesViewModel
|
fun create(initialState: DevicesViewState): DevicesViewModel
|
||||||
@ -87,10 +106,6 @@ class DevicesViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// temp storage when we ask for the user password
|
|
||||||
private var _currentDeviceId: String? = null
|
|
||||||
private var _currentSession: String? = null
|
|
||||||
|
|
||||||
private val refreshPublisher: PublishSubject<Unit> = PublishSubject.create()
|
private val refreshPublisher: PublishSubject<Unit> = PublishSubject.create()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -189,13 +204,43 @@ class DevicesViewModel @AssistedInject constructor(
|
|||||||
return when (action) {
|
return when (action) {
|
||||||
is DevicesAction.Refresh -> queryRefreshDevicesList()
|
is DevicesAction.Refresh -> queryRefreshDevicesList()
|
||||||
is DevicesAction.Delete -> handleDelete(action)
|
is DevicesAction.Delete -> handleDelete(action)
|
||||||
is DevicesAction.Password -> handlePassword(action)
|
|
||||||
is DevicesAction.Rename -> handleRename(action)
|
is DevicesAction.Rename -> handleRename(action)
|
||||||
is DevicesAction.PromptRename -> handlePromptRename(action)
|
is DevicesAction.PromptRename -> handlePromptRename(action)
|
||||||
is DevicesAction.VerifyMyDevice -> handleInteractiveVerification(action)
|
is DevicesAction.VerifyMyDevice -> handleInteractiveVerification(action)
|
||||||
is DevicesAction.CompleteSecurity -> handleCompleteSecurity()
|
is DevicesAction.CompleteSecurity -> handleCompleteSecurity()
|
||||||
is DevicesAction.MarkAsManuallyVerified -> handleVerifyManually(action)
|
is DevicesAction.MarkAsManuallyVerified -> handleVerifyManually(action)
|
||||||
is DevicesAction.VerifyMyDeviceManually -> handleShowDeviceCryptoInfo(action)
|
is DevicesAction.VerifyMyDeviceManually -> handleShowDeviceCryptoInfo(action)
|
||||||
|
is DevicesAction.SsoAuthDone -> {
|
||||||
|
// we should use token based auth
|
||||||
|
// _viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null))
|
||||||
|
// will release the interactive auth interceptor
|
||||||
|
Timber.d("## UIA - FallBack success $pendingAuth , continuation: $uiaContinuation")
|
||||||
|
if (pendingAuth != null) {
|
||||||
|
uiaContinuation?.resume(pendingAuth!!)
|
||||||
|
} else {
|
||||||
|
uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException())))
|
||||||
|
}
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
is DevicesAction.PasswordAuthDone -> {
|
||||||
|
val decryptedPass = session.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
|
||||||
|
uiaContinuation?.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
|
session = pendingAuth?.session,
|
||||||
|
password = decryptedPass,
|
||||||
|
user = session.myUserId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
DevicesAction.ReAuthCancelled -> {
|
||||||
|
Timber.d("## UIA - Reauth cancelled")
|
||||||
|
// _viewEvents.post(DevicesViewEvents.Loading)
|
||||||
|
uiaContinuation?.resumeWith(Result.failure((Exception())))
|
||||||
|
uiaContinuation = null
|
||||||
|
pendingAuth = null
|
||||||
|
Unit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,95 +330,48 @@ class DevicesViewModel @AssistedInject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
session.cryptoService().deleteDevice(deviceId, object : MatrixCallback<Unit> {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
override fun onFailure(failure: Throwable) {
|
try {
|
||||||
var isPasswordRequestFound = false
|
awaitCallback<Unit> {
|
||||||
|
session.cryptoService().deleteDevice(deviceId, object : UserInteractiveAuthInterceptor {
|
||||||
if (failure is Failure.RegistrationFlowError) {
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
// We only support LoginFlowTypes.PASSWORD
|
Timber.d("## UIA : deleteDevice UIA")
|
||||||
// Check if we can provide the user password
|
if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errCode == null) {
|
||||||
failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow ->
|
UserPasswordAuth(
|
||||||
isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true
|
session = null,
|
||||||
|
user = session.myUserId,
|
||||||
|
password = reAuthHelper.data
|
||||||
|
).let { promise.resume(it) }
|
||||||
|
} else {
|
||||||
|
Timber.d("## UIA : deleteDevice UIA > start reauth activity")
|
||||||
|
_viewEvents.post(DevicesViewEvents.RequestReAuth(flowResponse, errCode))
|
||||||
|
pendingAuth = DefaultBaseAuth(session = flowResponse.session)
|
||||||
|
uiaContinuation = promise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPasswordRequestFound) {
|
|
||||||
_currentDeviceId = deviceId
|
|
||||||
_currentSession = failure.registrationFlowResponse.session
|
|
||||||
|
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
request = Success(Unit)
|
request = Success(Unit)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// force settings update
|
||||||
_viewEvents.post(DevicesViewEvents.RequestPassword)
|
queryRefreshDevicesList()
|
||||||
}
|
} catch (failure: Throwable) {
|
||||||
}
|
|
||||||
|
|
||||||
if (!isPasswordRequestFound) {
|
|
||||||
// LoginFlowTypes.PASSWORD not supported, and this is the only one Element supports so far...
|
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
request = Fail(failure)
|
request = Fail(failure)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) {
|
||||||
_viewEvents.post(DevicesViewEvents.Failure(SsoFlowNotSupportedYet()))
|
_viewEvents.post(DevicesViewEvents.Failure(Exception(stringProvider.getString(R.string.authentication_error))))
|
||||||
|
} else {
|
||||||
|
_viewEvents.post(DevicesViewEvents.Failure(Exception(stringProvider.getString(R.string.matrix_error))))
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
Timber.e(failure, "failed to delete session")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSuccess(data: Unit) {
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
request = Success(data)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// force settings update
|
|
||||||
queryRefreshDevicesList()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handlePassword(action: DevicesAction.Password) {
|
|
||||||
val currentDeviceId = _currentDeviceId
|
|
||||||
if (currentDeviceId.isNullOrBlank()) {
|
|
||||||
// Abort
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
request = Loading()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
session.cryptoService().deleteDeviceWithUserPassword(currentDeviceId, _currentSession, action.password, object : MatrixCallback<Unit> {
|
|
||||||
override fun onSuccess(data: Unit) {
|
|
||||||
_currentDeviceId = null
|
|
||||||
_currentSession = null
|
|
||||||
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
request = Success(data)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// force settings update
|
|
||||||
queryRefreshDevicesList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
_currentDeviceId = null
|
|
||||||
_currentSession = null
|
|
||||||
|
|
||||||
// Password is maybe not good
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
request = Fail(failure)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
_viewEvents.post(DevicesViewEvents.Failure(failure))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.app.features.settings.devices
|
package im.vector.app.features.settings.devices
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -29,14 +30,16 @@ import com.airbnb.mvrx.fragmentViewModel
|
|||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.dialogs.ManuallyVerifyDialog
|
import im.vector.app.core.dialogs.ManuallyVerifyDialog
|
||||||
import im.vector.app.core.dialogs.PromptPasswordDialog
|
|
||||||
import im.vector.app.core.extensions.cleanup
|
import im.vector.app.core.extensions.cleanup
|
||||||
import im.vector.app.core.extensions.configureWith
|
import im.vector.app.core.extensions.configureWith
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
|
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.databinding.DialogBaseEditTextBinding
|
import im.vector.app.databinding.DialogBaseEditTextBinding
|
||||||
import im.vector.app.databinding.FragmentGenericRecyclerBinding
|
import im.vector.app.databinding.FragmentGenericRecyclerBinding
|
||||||
|
import im.vector.app.features.auth.ReAuthActivity
|
||||||
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -52,7 +55,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
|
|||||||
|
|
||||||
// used to avoid requesting to enter the password for each deletion
|
// used to avoid requesting to enter the password for each deletion
|
||||||
// Note: Sonar does not like to use password for member name.
|
// Note: Sonar does not like to use password for member name.
|
||||||
private var mAccountPass: String = ""
|
// private var mAccountPass: String = ""
|
||||||
|
|
||||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentGenericRecyclerBinding {
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentGenericRecyclerBinding {
|
||||||
return FragmentGenericRecyclerBinding.inflate(inflater, container, false)
|
return FragmentGenericRecyclerBinding.inflate(inflater, container, false)
|
||||||
@ -71,7 +74,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
|
|||||||
when (it) {
|
when (it) {
|
||||||
is DevicesViewEvents.Loading -> showLoading(it.message)
|
is DevicesViewEvents.Loading -> showLoading(it.message)
|
||||||
is DevicesViewEvents.Failure -> showFailure(it.throwable)
|
is DevicesViewEvents.Failure -> showFailure(it.throwable)
|
||||||
is DevicesViewEvents.RequestPassword -> maybeShowDeleteDeviceWithPasswordDialog()
|
is DevicesViewEvents.RequestReAuth -> askForReAuthentication(it)
|
||||||
is DevicesViewEvents.PromptRenameDevice -> displayDeviceRenameDialog(it.deviceInfo)
|
is DevicesViewEvents.PromptRenameDevice -> displayDeviceRenameDialog(it.deviceInfo)
|
||||||
is DevicesViewEvents.ShowVerifyDevice -> {
|
is DevicesViewEvents.ShowVerifyDevice -> {
|
||||||
VerificationBottomSheet.withArgs(
|
VerificationBottomSheet.withArgs(
|
||||||
@ -93,13 +96,6 @@ class VectorSettingsDevicesFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showFailure(throwable: Throwable) {
|
|
||||||
super.showFailure(throwable)
|
|
||||||
|
|
||||||
// Password is maybe not good, for safety measure, reset it here
|
|
||||||
mAccountPass = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
devicesController.callback = null
|
devicesController.callback = null
|
||||||
views.genericRecyclerView.cleanup()
|
views.genericRecyclerView.cleanup()
|
||||||
@ -119,14 +115,6 @@ class VectorSettingsDevicesFragment @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// override fun onDeleteDevice(deviceInfo: DeviceInfo) {
|
|
||||||
// devicesViewModel.handle(DevicesAction.Delete(deviceInfo))
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun onRenameDevice(deviceInfo: DeviceInfo) {
|
|
||||||
// displayDeviceRenameDialog(deviceInfo)
|
|
||||||
// }
|
|
||||||
|
|
||||||
override fun retry() {
|
override fun retry() {
|
||||||
viewModel.handle(DevicesAction.Refresh)
|
viewModel.handle(DevicesAction.Refresh)
|
||||||
}
|
}
|
||||||
@ -154,17 +142,34 @@ class VectorSettingsDevicesFragment @Inject constructor(
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
|
||||||
* Show a dialog to ask for user password, or use a previously entered password.
|
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||||
*/
|
when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
|
||||||
private fun maybeShowDeleteDeviceWithPasswordDialog() {
|
LoginFlowTypes.SSO -> {
|
||||||
if (mAccountPass.isNotEmpty()) {
|
viewModel.handle(DevicesAction.SsoAuthDone)
|
||||||
viewModel.handle(DevicesAction.Password(mAccountPass))
|
|
||||||
} else {
|
|
||||||
PromptPasswordDialog().show(requireActivity()) { password ->
|
|
||||||
mAccountPass = password
|
|
||||||
viewModel.handle(DevicesAction.Password(mAccountPass))
|
|
||||||
}
|
}
|
||||||
|
LoginFlowTypes.PASSWORD -> {
|
||||||
|
val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
|
||||||
|
viewModel.handle(DevicesAction.PasswordAuthDone(password))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
viewModel.handle(DevicesAction.ReAuthCancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viewModel.handle(DevicesAction.ReAuthCancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch the re auth activity to get credentials
|
||||||
|
*/
|
||||||
|
private fun askForReAuthentication(reAuthReq: DevicesViewEvents.RequestReAuth) {
|
||||||
|
ReAuthActivity.newIntent(requireContext(),
|
||||||
|
reAuthReq.registrationFlowResponse,
|
||||||
|
reAuthReq.lastErrorCode,
|
||||||
|
getString(R.string.devices_delete_dialog_title)).let { intent ->
|
||||||
|
reAuthActivityResultLauncher.launch(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,11 @@ sealed class ThreePidsSettingsAction : VectorViewModelAction {
|
|||||||
data class SubmitCode(val threePid: ThreePid.Msisdn, val code: String) : ThreePidsSettingsAction()
|
data class SubmitCode(val threePid: ThreePid.Msisdn, val code: String) : ThreePidsSettingsAction()
|
||||||
data class ContinueThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()
|
data class ContinueThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()
|
||||||
data class CancelThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()
|
data class CancelThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()
|
||||||
data class AccountPassword(val password: String) : ThreePidsSettingsAction()
|
|
||||||
|
// data class AccountPassword(val password: String) : ThreePidsSettingsAction()
|
||||||
data class DeleteThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()
|
data class DeleteThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()
|
||||||
|
|
||||||
|
object SsoAuthDone : ThreePidsSettingsAction()
|
||||||
|
data class PasswordAuthDone(val password: String) : ThreePidsSettingsAction()
|
||||||
|
object ReAuthCancelled : ThreePidsSettingsAction()
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.app.features.settings.threepids
|
package im.vector.app.features.settings.threepids
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@ -26,7 +27,6 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.dialogs.PromptPasswordDialog
|
|
||||||
import im.vector.app.core.dialogs.withColoredButton
|
import im.vector.app.core.dialogs.withColoredButton
|
||||||
import im.vector.app.core.extensions.cleanup
|
import im.vector.app.core.extensions.cleanup
|
||||||
import im.vector.app.core.extensions.configureWith
|
import im.vector.app.core.extensions.configureWith
|
||||||
@ -35,10 +35,12 @@ import im.vector.app.core.extensions.getFormattedValue
|
|||||||
import im.vector.app.core.extensions.hideKeyboard
|
import im.vector.app.core.extensions.hideKeyboard
|
||||||
import im.vector.app.core.extensions.isEmail
|
import im.vector.app.core.extensions.isEmail
|
||||||
import im.vector.app.core.extensions.isMsisdn
|
import im.vector.app.core.extensions.isMsisdn
|
||||||
|
import im.vector.app.core.extensions.registerStartForActivityResult
|
||||||
import im.vector.app.core.platform.OnBackPressed
|
import im.vector.app.core.platform.OnBackPressed
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.databinding.FragmentGenericRecyclerBinding
|
import im.vector.app.databinding.FragmentGenericRecyclerBinding
|
||||||
|
import im.vector.app.features.auth.ReAuthActivity
|
||||||
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -65,14 +67,41 @@ class ThreePidsSettingsFragment @Inject constructor(
|
|||||||
viewModel.observeViewEvents {
|
viewModel.observeViewEvents {
|
||||||
when (it) {
|
when (it) {
|
||||||
is ThreePidsSettingsViewEvents.Failure -> displayErrorDialog(it.throwable)
|
is ThreePidsSettingsViewEvents.Failure -> displayErrorDialog(it.throwable)
|
||||||
ThreePidsSettingsViewEvents.RequestPassword -> askUserPassword()
|
is ThreePidsSettingsViewEvents.RequestReAuth -> askAuthentication(it)
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun askUserPassword() {
|
// private fun askUserPassword() {
|
||||||
PromptPasswordDialog().show(requireActivity()) { password ->
|
// PromptPasswordDialog().show(requireActivity()) { password ->
|
||||||
viewModel.handle(ThreePidsSettingsAction.AccountPassword(password))
|
// viewModel.handle(ThreePidsSettingsAction.AccountPassword(password))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
private fun askAuthentication(event: ThreePidsSettingsViewEvents.RequestReAuth) {
|
||||||
|
ReAuthActivity.newIntent(requireContext(),
|
||||||
|
event.registrationFlowResponse,
|
||||||
|
event.lastErrorCode,
|
||||||
|
getString(R.string.settings_add_email_address)).let { intent ->
|
||||||
|
reAuthActivityResultLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
|
||||||
|
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||||
|
when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
|
||||||
|
LoginFlowTypes.SSO -> {
|
||||||
|
viewModel.handle(ThreePidsSettingsAction.SsoAuthDone)
|
||||||
|
}
|
||||||
|
LoginFlowTypes.PASSWORD -> {
|
||||||
|
val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
|
||||||
|
viewModel.handle(ThreePidsSettingsAction.PasswordAuthDone(password))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
viewModel.handle(ThreePidsSettingsAction.ReAuthCancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viewModel.handle(ThreePidsSettingsAction.ReAuthCancelled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,8 +17,10 @@
|
|||||||
package im.vector.app.features.settings.threepids
|
package im.vector.app.features.settings.threepids
|
||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewEvents
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
|
|
||||||
sealed class ThreePidsSettingsViewEvents : VectorViewEvents {
|
sealed class ThreePidsSettingsViewEvents : VectorViewEvents {
|
||||||
data class Failure(val throwable: Throwable) : ThreePidsSettingsViewEvents()
|
data class Failure(val throwable: Throwable) : ThreePidsSettingsViewEvents()
|
||||||
object RequestPassword : ThreePidsSettingsViewEvents()
|
// object RequestPassword : ThreePidsSettingsViewEvents()
|
||||||
|
data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : ThreePidsSettingsViewEvents()
|
||||||
}
|
}
|
||||||
|
@ -24,21 +24,28 @@ import com.airbnb.mvrx.Loading
|
|||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.error.SsoFlowNotSupportedYet
|
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.core.utils.ReadOnceTrue
|
import im.vector.app.core.utils.ReadOnceTrue
|
||||||
|
import im.vector.app.features.auth.ReAuthActivity
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||||
|
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
import org.matrix.android.sdk.rx.rx
|
import org.matrix.android.sdk.rx.rx
|
||||||
|
import timber.log.Timber
|
||||||
|
import kotlin.coroutines.Continuation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
class ThreePidsSettingsViewModel @AssistedInject constructor(
|
class ThreePidsSettingsViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: ThreePidsSettingsViewState,
|
@Assisted initialState: ThreePidsSettingsViewState,
|
||||||
@ -48,36 +55,16 @@ class ThreePidsSettingsViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
// UIA session
|
// UIA session
|
||||||
private var pendingThreePid: ThreePid? = null
|
private var pendingThreePid: ThreePid? = null
|
||||||
private var pendingSession: String? = null
|
// private var pendingSession: String? = null
|
||||||
|
|
||||||
private val loadingCallback: MatrixCallback<Unit> = object : MatrixCallback<Unit> {
|
private val loadingCallback: MatrixCallback<Unit> = object : MatrixCallback<Unit> {
|
||||||
override fun onFailure(failure: Throwable) {
|
override fun onFailure(failure: Throwable) {
|
||||||
isLoading(false)
|
isLoading(false)
|
||||||
|
|
||||||
if (failure is Failure.RegistrationFlowError) {
|
|
||||||
var isPasswordRequestFound = false
|
|
||||||
|
|
||||||
// We only support LoginFlowTypes.PASSWORD
|
|
||||||
// Check if we can provide the user password
|
|
||||||
failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow ->
|
|
||||||
isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPasswordRequestFound) {
|
|
||||||
pendingSession = failure.registrationFlowResponse.session
|
|
||||||
_viewEvents.post(ThreePidsSettingsViewEvents.RequestPassword)
|
|
||||||
} else {
|
|
||||||
// LoginFlowTypes.PASSWORD not supported, and this is the only one Element supports so far...
|
|
||||||
_viewEvents.post(ThreePidsSettingsViewEvents.Failure(SsoFlowNotSupportedYet()))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_viewEvents.post(ThreePidsSettingsViewEvents.Failure(failure))
|
_viewEvents.post(ThreePidsSettingsViewEvents.Failure(failure))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(data: Unit) {
|
override fun onSuccess(data: Unit) {
|
||||||
pendingThreePid = null
|
pendingThreePid = null
|
||||||
pendingSession = null
|
|
||||||
isLoading(false)
|
isLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -146,12 +133,46 @@ class ThreePidsSettingsViewModel @AssistedInject constructor(
|
|||||||
is ThreePidsSettingsAction.ContinueThreePid -> handleContinueThreePid(action)
|
is ThreePidsSettingsAction.ContinueThreePid -> handleContinueThreePid(action)
|
||||||
is ThreePidsSettingsAction.SubmitCode -> handleSubmitCode(action)
|
is ThreePidsSettingsAction.SubmitCode -> handleSubmitCode(action)
|
||||||
is ThreePidsSettingsAction.CancelThreePid -> handleCancelThreePid(action)
|
is ThreePidsSettingsAction.CancelThreePid -> handleCancelThreePid(action)
|
||||||
is ThreePidsSettingsAction.AccountPassword -> handleAccountPassword(action)
|
|
||||||
is ThreePidsSettingsAction.DeleteThreePid -> handleDeleteThreePid(action)
|
is ThreePidsSettingsAction.DeleteThreePid -> handleDeleteThreePid(action)
|
||||||
is ThreePidsSettingsAction.ChangeUiState -> handleChangeUiState(action)
|
is ThreePidsSettingsAction.ChangeUiState -> handleChangeUiState(action)
|
||||||
|
ThreePidsSettingsAction.SsoAuthDone -> {
|
||||||
|
Timber.d("## UIA - FallBack success")
|
||||||
|
if (pendingAuth != null) {
|
||||||
|
uiaContinuation?.resume(pendingAuth!!)
|
||||||
|
} else {
|
||||||
|
uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ThreePidsSettingsAction.PasswordAuthDone -> {
|
||||||
|
val decryptedPass = session.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
|
||||||
|
uiaContinuation?.resume(
|
||||||
|
UserPasswordAuth(
|
||||||
|
session = pendingAuth?.session,
|
||||||
|
password = decryptedPass,
|
||||||
|
user = session.myUserId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ThreePidsSettingsAction.ReAuthCancelled -> {
|
||||||
|
Timber.d("## UIA - Reauth cancelled")
|
||||||
|
uiaContinuation?.resumeWith(Result.failure((Exception())))
|
||||||
|
uiaContinuation = null
|
||||||
|
pendingAuth = null
|
||||||
|
}
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var uiaContinuation: Continuation<UIABaseAuth>? = null
|
||||||
|
var pendingAuth: UIABaseAuth? = null
|
||||||
|
|
||||||
|
private val uiaInterceptor = object : UserInteractiveAuthInterceptor {
|
||||||
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
|
_viewEvents.post(ThreePidsSettingsViewEvents.RequestReAuth(flowResponse, errCode))
|
||||||
|
pendingAuth = DefaultBaseAuth(session = flowResponse.session)
|
||||||
|
uiaContinuation = promise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleSubmitCode(action: ThreePidsSettingsAction.SubmitCode) {
|
private fun handleSubmitCode(action: ThreePidsSettingsAction.SubmitCode) {
|
||||||
isLoading(true)
|
isLoading(true)
|
||||||
setState {
|
setState {
|
||||||
@ -168,7 +189,7 @@ class ThreePidsSettingsViewModel @AssistedInject constructor(
|
|||||||
override fun onSuccess(data: Unit) {
|
override fun onSuccess(data: Unit) {
|
||||||
// then finalize
|
// then finalize
|
||||||
pendingThreePid = action.threePid
|
pendingThreePid = action.threePid
|
||||||
session.finalizeAddingThreePid(action.threePid, null, null, loadingCallback)
|
session.finalizeAddingThreePid(action.threePid, uiaInterceptor, loadingCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
override fun onFailure(failure: Throwable) {
|
||||||
@ -232,7 +253,7 @@ class ThreePidsSettingsViewModel @AssistedInject constructor(
|
|||||||
isLoading(true)
|
isLoading(true)
|
||||||
pendingThreePid = action.threePid
|
pendingThreePid = action.threePid
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
session.finalizeAddingThreePid(action.threePid, null, null, loadingCallback)
|
session.finalizeAddingThreePid(action.threePid, uiaInterceptor, loadingCallback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,16 +264,14 @@ class ThreePidsSettingsViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleAccountPassword(action: ThreePidsSettingsAction.AccountPassword) {
|
// private fun handleAccountPassword(action: ThreePidsSettingsAction.AccountPassword) {
|
||||||
val safeSession = pendingSession ?: return Unit
|
// val safeThreePid = pendingThreePid ?: return Unit
|
||||||
.also { _viewEvents.post(ThreePidsSettingsViewEvents.Failure(IllegalStateException("No pending session"))) }
|
// .also { _viewEvents.post(ThreePidsSettingsViewEvents.Failure(IllegalStateException("No pending threePid"))) }
|
||||||
val safeThreePid = pendingThreePid ?: return Unit
|
// isLoading(true)
|
||||||
.also { _viewEvents.post(ThreePidsSettingsViewEvents.Failure(IllegalStateException("No pending threePid"))) }
|
// viewModelScope.launch {
|
||||||
isLoading(true)
|
// session.finalizeAddingThreePid(safeThreePid, uiaInterceptor, loadingCallback)
|
||||||
viewModelScope.launch {
|
// }
|
||||||
session.finalizeAddingThreePid(safeThreePid, safeSession, action.password, loadingCallback)
|
// }
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleDeleteThreePid(action: ThreePidsSettingsAction.DeleteThreePid) {
|
private fun handleDeleteThreePid(action: ThreePidsSettingsAction.DeleteThreePid) {
|
||||||
isLoading(true)
|
isLoading(true)
|
||||||
|
@ -115,8 +115,10 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS
|
|||||||
|
|
||||||
// So recovery is not setup
|
// So recovery is not setup
|
||||||
// Check if cross signing is enabled and local secrets known
|
// Check if cross signing is enabled and local secrets known
|
||||||
if (crossSigningInfo.getOrNull()?.isTrusted() == true
|
if (
|
||||||
&& pInfo.getOrNull()?.allKnown().orFalse()
|
crossSigningInfo.getOrNull() == null
|
||||||
|
|| (crossSigningInfo.getOrNull()?.isTrusted() == true
|
||||||
|
&& pInfo.getOrNull()?.allKnown().orFalse())
|
||||||
) {
|
) {
|
||||||
// So 4S is not setup and we have local secrets,
|
// So 4S is not setup and we have local secrets,
|
||||||
return@Function4 BannerState.Setup(numberOfKeys = getNumberOfKeysToBackup())
|
return@Function4 BannerState.Setup(numberOfKeys = getNumberOfKeysToBackup())
|
||||||
|
48
vector/src/main/res/drawable/ic_social_gitlab.xml
Normal file
48
vector/src/main/res/drawable/ic_social_gitlab.xml
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="38dp"
|
||||||
|
android:height="38dp"
|
||||||
|
android:viewportWidth="38"
|
||||||
|
android:viewportHeight="38">
|
||||||
|
<path
|
||||||
|
android:pathData="M19.4782,26.0077l0,0l2.8609,-8.8002l-5.7177,0z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:fillColor="#E24329"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M12.6142,17.2076L11.743,19.8812C11.6642,20.124 11.7493,20.392 11.9574,20.5433L19.4782,26.0077L12.6142,17.2076Z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:fillColor="#FCA326"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M12.6142,17.2076L16.6214,17.2076L14.8968,11.9077C14.8086,11.6366 14.4239,11.6366 14.3325,11.9077L12.6142,17.2076Z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:fillColor="#E24329"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M26.3464,17.2076L27.2145,19.8812C27.2933,20.124 27.2082,20.392 27.0001,20.5433L19.4782,26.0077L26.3464,17.2076Z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:fillColor="#FCA326"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M26.3464,17.2076L22.3392,17.2076L24.0606,11.9077C24.1489,11.6366 24.5335,11.6366 24.625,11.9077L26.3464,17.2076Z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:fillColor="#E24329"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M19.4782,26.0077l2.8609,-8.8001l4.0073,0z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:fillColor="#FC6D26"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M19.4782,26.0077l-6.864,-8.8001l4.0072,0z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:fillColor="#FC6D26"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#00000000"/>
|
||||||
|
</vector>
|
68
vector/src/main/res/layout/fragment_bootstrap_reauth.xml
Normal file
68
vector/src/main/res/layout/fragment_bootstrap_reauth.xml
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/bootstrapDescriptionText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/waitingProgress"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="@string/re_authentication_activity_title" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/waitingProgress"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/reAuthFailureText"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/bootstrapDescriptionText" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/reAuthFailureText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:textColor="?colorError"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/buttonFlow"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/waitingProgress"
|
||||||
|
tools:text="Authentication failed" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/bootstrapCancelButton"
|
||||||
|
style="@style/VectorButtonStyleText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/layout_vertical_margin"
|
||||||
|
android:text="@string/cancel"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/bootstrapRetryButton"
|
||||||
|
style="@style/VectorButtonStyleText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/layout_vertical_margin"
|
||||||
|
android:text="@string/global_retry"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.helper.widget.Flow
|
||||||
|
android:id="@+id/buttonFlow"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:constraint_referenced_ids="bootstrapCancelButton, bootstrapRetryButton"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/reAuthFailureText"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -31,75 +31,14 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountContent" />
|
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountContent" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/deactivateAccountPromptPassword"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:text="@string/deactivate_account_prompt_password"
|
|
||||||
android:textColor="?riotx_text_primary"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountEraseCheckbox" />
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/deactivateAccountPasswordContainer"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:hint="@string/auth_password_placeholder"
|
|
||||||
android:inputType="textPassword"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:nextFocusDown="@+id/login_password"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountPromptPassword">
|
|
||||||
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
android:id="@+id/deactivateAccountPasswordTil"
|
|
||||||
style="@style/VectorTextInputLayout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/login_signup_password_hint"
|
|
||||||
app:errorEnabled="true"
|
|
||||||
app:errorIconDrawable="@null">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:id="@+id/deactivateAccountPassword"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ems="10"
|
|
||||||
android:inputType="textPassword"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:paddingEnd="48dp"
|
|
||||||
tools:ignore="RtlSymmetry" />
|
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/deactivateAccountPasswordReveal"
|
|
||||||
android:layout_width="@dimen/layout_touch_size"
|
|
||||||
android:layout_height="@dimen/layout_touch_size"
|
|
||||||
android:layout_gravity="end"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:background="?attr/selectableItemBackground"
|
|
||||||
android:scaleType="center"
|
|
||||||
android:src="@drawable/ic_eye"
|
|
||||||
tools:contentDescription="@string/a11y_show_password"
|
|
||||||
app:tint="?attr/colorAccent"
|
|
||||||
tools:ignore="MissingPrefix" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/deactivateAccountSubmit"
|
android:id="@+id/deactivateAccountSubmit"
|
||||||
style="@style/VectorButtonStyleDestructive"
|
style="@style/VectorButtonStyleDestructive"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
android:text="@string/deactivate_account_submit"
|
android:text="@string/deactivate_account_submit"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountPasswordContainer" />
|
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountEraseCheckbox" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
@ -143,7 +143,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="8dp"
|
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
@ -88,7 +88,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="8dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
112
vector/src/main/res/layout/fragment_reauth_confirm.xml
Normal file
112
vector/src/main/res/layout/fragment_reauth_confirm.xml
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/reAuthConfirmText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:text="@string/re_authentication_default_confirm_text"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/reAuthConfirmButton"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="0"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/passwordContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/reAuthConfirmText">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/passwordFieldTil"
|
||||||
|
style="@style/VectorTextInputLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/login_signup_password_hint"
|
||||||
|
app:errorEnabled="true"
|
||||||
|
app:errorIconDrawable="@null">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/passwordField"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ems="10"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:paddingEnd="48dp"
|
||||||
|
tools:ignore="RtlSymmetry" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/passwordReveal"
|
||||||
|
android:layout_width="@dimen/layout_touch_size"
|
||||||
|
android:layout_height="@dimen/layout_touch_size"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_eye"
|
||||||
|
app:tint="?attr/colorAccent"
|
||||||
|
tools:contentDescription="@string/a11y_show_password"
|
||||||
|
tools:ignore="MissingPrefix" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<!-- <TextView-->
|
||||||
|
<!-- android:id="@+id/loginPasswordNotice"-->
|
||||||
|
<!-- android:layout_width="wrap_content"-->
|
||||||
|
<!-- android:layout_height="wrap_content"-->
|
||||||
|
<!-- android:gravity="start"-->
|
||||||
|
<!-- android:text="@string/login_signin_matrix_id_password_notice"-->
|
||||||
|
<!-- android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"-->
|
||||||
|
<!-- android:visibility="gone"-->
|
||||||
|
<!-- app:layout_constraintEnd_toEndOf="parent"-->
|
||||||
|
<!-- app:layout_constraintStart_toStartOf="parent"-->
|
||||||
|
<!-- app:layout_constraintTop_toBottomOf="@id/passwordContainer"-->
|
||||||
|
<!-- tools:visibility="visible" />-->
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/genericErrorText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="start"
|
||||||
|
android:text="@string/authentication_error"
|
||||||
|
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
|
||||||
|
android:textColor="?colorError"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/passwordContainer"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/reAuthConfirmButton"
|
||||||
|
style="@style/VectorButtonStylePositive"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:text="@string/_continue"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/genericErrorText" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</ScrollView>
|
19
vector/src/main/res/layout/item_positive_button.xml
Normal file
19
vector/src/main/res/layout/item_positive_button.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/itemGenericItemButton"
|
||||||
|
style="@style/VectorButtonStylePositive"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
app:iconGravity="textStart"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:text="Action Name" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -46,6 +46,7 @@
|
|||||||
<attr name="vctr_social_login_button_facebook_style" format="reference" />
|
<attr name="vctr_social_login_button_facebook_style" format="reference" />
|
||||||
<attr name="vctr_social_login_button_twitter_style" format="reference" />
|
<attr name="vctr_social_login_button_twitter_style" format="reference" />
|
||||||
<attr name="vctr_social_login_button_apple_style" format="reference" />
|
<attr name="vctr_social_login_button_apple_style" format="reference" />
|
||||||
|
<attr name="vctr_social_login_button_gitlab_style" format="reference" />
|
||||||
|
|
||||||
<attr name="vctr_chat_effect_snow_background" format="color" />
|
<attr name="vctr_chat_effect_snow_background" format="color" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
@ -333,6 +333,7 @@
|
|||||||
<string name="login_error_ssl_handshake">Your device is using an outdated TLS security protocol, vulnerable to attack, for your security you will not be able to connect</string>
|
<string name="login_error_ssl_handshake">Your device is using an outdated TLS security protocol, vulnerable to attack, for your security you will not be able to connect</string>
|
||||||
|
|
||||||
<string name="login_error_forbidden">Invalid username/password</string>
|
<string name="login_error_forbidden">Invalid username/password</string>
|
||||||
|
<string name="error_unauthorized">Unauthorized, missing valid authentication credentials</string>
|
||||||
<string name="login_error_unknown_token">The access token specified was not recognised</string>
|
<string name="login_error_unknown_token">The access token specified was not recognised</string>
|
||||||
<string name="login_error_bad_json">Malformed JSON</string>
|
<string name="login_error_bad_json">Malformed JSON</string>
|
||||||
<string name="login_error_not_json">Did not contain valid JSON</string>
|
<string name="login_error_not_json">Did not contain valid JSON</string>
|
||||||
@ -2623,6 +2624,7 @@
|
|||||||
<string name="mark_as_verified">Mark as Trusted</string>
|
<string name="mark_as_verified">Mark as Trusted</string>
|
||||||
|
|
||||||
<string name="error_sso_flow_not_supported_yet">Sorry, this operation is not possible yet for accounts connected using Single Sign-On.</string>
|
<string name="error_sso_flow_not_supported_yet">Sorry, this operation is not possible yet for accounts connected using Single Sign-On.</string>
|
||||||
|
<string name="failed_to_initialize_cross_signing">Failed to set up Cross Signing</string>
|
||||||
|
|
||||||
<string name="error_empty_field_choose_user_name">Please choose a username.</string>
|
<string name="error_empty_field_choose_user_name">Please choose a username.</string>
|
||||||
<string name="error_empty_field_choose_password">Please choose a password.</string>
|
<string name="error_empty_field_choose_password">Please choose a password.</string>
|
||||||
@ -2792,4 +2794,8 @@
|
|||||||
<string name="warning_unsaved_change_discard">Discard changes</string>
|
<string name="warning_unsaved_change_discard">Discard changes</string>
|
||||||
|
|
||||||
<string name="matrix_to_card_title">Matrix Link</string>
|
<string name="matrix_to_card_title">Matrix Link</string>
|
||||||
|
|
||||||
|
<string name="re_authentication_activity_title">Re-Authentication Needed</string>
|
||||||
|
<string name="re_authentication_default_confirm_text">Element requires you to enter your credentials to perform this action.</string>
|
||||||
|
<string name="authentication_error">Failed to authenticate</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="WidgetButtonSocialLogin" parent="Widget.MaterialComponents.Button">
|
<style name="WidgetButtonSocialLogin" parent="Widget.MaterialComponents.Button.OutlinedButton">
|
||||||
<item name="android:textAllCaps">false</item>
|
<item name="android:textAllCaps">false</item>
|
||||||
<item name="fontFamily">sans-serif-medium</item>
|
<item name="fontFamily">sans-serif-medium</item>
|
||||||
<item name="android:layout_width">wrap_content</item>
|
<item name="android:layout_width">wrap_content</item>
|
||||||
<item name="android:layout_height">wrap_content</item>
|
<item name="android:layout_height">wrap_content</item>
|
||||||
<item name="iconGravity">start</item>
|
<item name="iconGravity">start</item>
|
||||||
<item name="android:textSize">14sp</item>
|
<item name="android:textSize">14sp</item>
|
||||||
<item name="android:textAlignment">textStart</item>
|
<item name="android:textAlignment">center</item>
|
||||||
<item name="android:paddingStart">2dp</item>
|
<item name="android:paddingStart">2dp</item>
|
||||||
<item name="android:paddingEnd">8dp</item>
|
<!-- Compensate icon size to center text correctly-->
|
||||||
|
<item name="android:paddingEnd">38dp</item>
|
||||||
<item name="android:clipToPadding">false</item>
|
<item name="android:clipToPadding">false</item>
|
||||||
|
<item name="iconSize">38dp</item>
|
||||||
|
<item name="strokeColor">@color/black_54</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="WidgetButtonSocialLogin.Google">
|
<style name="WidgetButtonSocialLogin.Google">
|
||||||
@ -99,4 +102,21 @@
|
|||||||
<item name="android:backgroundTint">@color/white</item>
|
<item name="android:backgroundTint">@color/white</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="WidgetButtonSocialLogin.Gitlab" parent="WidgetButtonSocialLogin">
|
||||||
|
<item name="icon">@drawable/ic_social_gitlab</item>
|
||||||
|
<item name="iconTint">@android:color/transparent</item>
|
||||||
|
<item name="iconTintMode">add</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="WidgetButtonSocialLogin.Gitlab.Light">
|
||||||
|
<item name="android:textColor">@color/black</item>
|
||||||
|
<item name="android:backgroundTint">@color/white</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="WidgetButtonSocialLogin.Gitlab.Dark">
|
||||||
|
<item name="android:textColor">@color/white</item>
|
||||||
|
<item name="android:backgroundTint">@color/black</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
@ -200,6 +200,7 @@
|
|||||||
<item name="vctr_social_login_button_facebook_style">@style/WidgetButtonSocialLogin.Facebook.Dark</item>
|
<item name="vctr_social_login_button_facebook_style">@style/WidgetButtonSocialLogin.Facebook.Dark</item>
|
||||||
<item name="vctr_social_login_button_twitter_style">@style/WidgetButtonSocialLogin.Twitter.Dark</item>
|
<item name="vctr_social_login_button_twitter_style">@style/WidgetButtonSocialLogin.Twitter.Dark</item>
|
||||||
<item name="vctr_social_login_button_apple_style">@style/WidgetButtonSocialLogin.Apple.Dark</item>
|
<item name="vctr_social_login_button_apple_style">@style/WidgetButtonSocialLogin.Apple.Dark</item>
|
||||||
|
<item name="vctr_social_login_button_gitlab_style">@style/WidgetButtonSocialLogin.Gitlab.Dark</item>
|
||||||
|
|
||||||
<!-- chat effect -->
|
<!-- chat effect -->
|
||||||
<item name="vctr_chat_effect_snow_background">@android:color/transparent</item>
|
<item name="vctr_chat_effect_snow_background">@android:color/transparent</item>
|
||||||
|
@ -202,6 +202,7 @@
|
|||||||
<item name="vctr_social_login_button_facebook_style">@style/WidgetButtonSocialLogin.Facebook.Light</item>
|
<item name="vctr_social_login_button_facebook_style">@style/WidgetButtonSocialLogin.Facebook.Light</item>
|
||||||
<item name="vctr_social_login_button_twitter_style">@style/WidgetButtonSocialLogin.Twitter.Light</item>
|
<item name="vctr_social_login_button_twitter_style">@style/WidgetButtonSocialLogin.Twitter.Light</item>
|
||||||
<item name="vctr_social_login_button_apple_style">@style/WidgetButtonSocialLogin.Apple.Light</item>
|
<item name="vctr_social_login_button_apple_style">@style/WidgetButtonSocialLogin.Apple.Light</item>
|
||||||
|
<item name="vctr_social_login_button_gitlab_style">@style/WidgetButtonSocialLogin.Gitlab.Light</item>
|
||||||
|
|
||||||
<!-- chat effect -->
|
<!-- chat effect -->
|
||||||
<item name="vctr_chat_effect_snow_background">@color/black_alpha</item>
|
<item name="vctr_chat_effect_snow_background">@color/black_alpha</item>
|
||||||
|
Loading…
Reference in New Issue
Block a user