Merge pull request #3355 from element-hq/feature/bma/resetIdentityIteration2
[Identity reset] Remove instruction to reset identity on another client.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
appId: ${MAESTRO_APP_ID}
|
||||
---
|
||||
- extendedWaitUntil:
|
||||
visible: "Confirm that it's you"
|
||||
visible: "Confirm your identity"
|
||||
timeout: 20000
|
||||
|
||||
@@ -32,9 +32,6 @@ interface SecureBackupEntryPoint : FeatureEntryPoint {
|
||||
@Parcelize
|
||||
data object EnterRecoveryKey : InitialTarget
|
||||
|
||||
@Parcelize
|
||||
data object CreateNewRecoveryKey : InitialTarget
|
||||
|
||||
@Parcelize
|
||||
data object ResetIdentity : InitialTarget
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
||||
import io.element.android.features.securebackup.impl.createkey.CreateNewRecoveryKeyNode
|
||||
import io.element.android.features.securebackup.impl.disable.SecureBackupDisableNode
|
||||
import io.element.android.features.securebackup.impl.enable.SecureBackupEnableNode
|
||||
import io.element.android.features.securebackup.impl.enter.SecureBackupEnterRecoveryKeyNode
|
||||
@@ -52,7 +51,6 @@ class SecureBackupFlowNode @AssistedInject constructor(
|
||||
initialElement = when (plugins.filterIsInstance<SecureBackupEntryPoint.Params>().first().initialElement) {
|
||||
SecureBackupEntryPoint.InitialTarget.Root -> NavTarget.Root
|
||||
SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey -> NavTarget.EnterRecoveryKey
|
||||
SecureBackupEntryPoint.InitialTarget.CreateNewRecoveryKey -> NavTarget.CreateNewRecoveryKey
|
||||
is SecureBackupEntryPoint.InitialTarget.ResetIdentity -> NavTarget.ResetIdentity
|
||||
},
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
@@ -79,9 +77,6 @@ class SecureBackupFlowNode @AssistedInject constructor(
|
||||
@Parcelize
|
||||
data object EnterRecoveryKey : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object CreateNewRecoveryKey : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object ResetIdentity : NavTarget
|
||||
}
|
||||
@@ -141,16 +136,9 @@ class SecureBackupFlowNode @AssistedInject constructor(
|
||||
backstack.pop()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateNewRecoveryKey() {
|
||||
backstack.push(NavTarget.CreateNewRecoveryKey)
|
||||
}
|
||||
}
|
||||
createNode<SecureBackupEnterRecoveryKeyNode>(buildContext, plugins = listOf(callback))
|
||||
}
|
||||
NavTarget.CreateNewRecoveryKey -> {
|
||||
createNode<CreateNewRecoveryKeyNode>(buildContext)
|
||||
}
|
||||
is NavTarget.ResetIdentity -> {
|
||||
val callback = object : ResetIdentityFlowNode.Callback {
|
||||
override fun onDone() {
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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 io.element.android.features.securebackup.impl.createkey
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class CreateNewRecoveryKeyNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val buildMeta: BuildMeta,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
CreateNewRecoveryKeyView(
|
||||
desktopApplicationName = buildMeta.desktopApplicationName,
|
||||
modifier = modifier,
|
||||
onBackClick = ::navigateUp,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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 io.element.android.features.securebackup.impl.createkey
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.securebackup.impl.R
|
||||
import io.element.android.libraries.designsystem.atomic.organisms.NumberedListOrganism
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.PageTitle
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CreateNewRecoveryKeyView(
|
||||
desktopApplicationName: String,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(title = {}, navigationIcon = { BackButton(onClick = onBackClick) })
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier.padding(padding)
|
||||
) {
|
||||
PageTitle(
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 40.dp),
|
||||
title = stringResource(R.string.screen_create_new_recovery_key_title),
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.Computer())
|
||||
)
|
||||
Content(desktopApplicationName = desktopApplicationName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(desktopApplicationName: String) {
|
||||
val listItems = buildList {
|
||||
add(AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_1, desktopApplicationName)))
|
||||
add(AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_2)))
|
||||
add(
|
||||
annotatedTextWithBold(
|
||||
text = stringResource(
|
||||
id = R.string.screen_create_new_recovery_key_list_item_3,
|
||||
stringResource(R.string.screen_create_new_recovery_key_list_item_3_reset_all)
|
||||
),
|
||||
boldText = stringResource(R.string.screen_create_new_recovery_key_list_item_3_reset_all)
|
||||
)
|
||||
)
|
||||
add(AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_4)))
|
||||
add(AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_5)))
|
||||
}
|
||||
NumberedListOrganism(modifier = Modifier.padding(horizontal = 16.dp), items = listItems.toImmutableList())
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun CreateNewRecoveryKeyViewPreview() {
|
||||
ElementPreview {
|
||||
CreateNewRecoveryKeyView(
|
||||
desktopApplicationName = "Element",
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,6 @@ class SecureBackupEnterRecoveryKeyNode @AssistedInject constructor(
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
interface Callback : Plugin {
|
||||
fun onEnterRecoveryKeySuccess()
|
||||
fun onCreateNewRecoveryKey()
|
||||
}
|
||||
|
||||
private val callback = plugins<Callback>().first()
|
||||
@@ -48,7 +47,6 @@ class SecureBackupEnterRecoveryKeyNode @AssistedInject constructor(
|
||||
modifier = modifier,
|
||||
onSuccess = callback::onEnterRecoveryKeySuccess,
|
||||
onBackClick = ::navigateUp,
|
||||
onCreateNewRecoveryKey = callback::onCreateNewRecoveryKey
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import io.element.android.libraries.designsystem.components.async.AsyncActionVie
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
@@ -41,7 +40,6 @@ fun SecureBackupEnterRecoveryKeyView(
|
||||
state: SecureBackupEnterRecoveryKeyState,
|
||||
onSuccess: () -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
onCreateNewRecoveryKey: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AsyncActionView(
|
||||
@@ -60,7 +58,7 @@ fun SecureBackupEnterRecoveryKeyView(
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.KeySolid()),
|
||||
title = stringResource(id = R.string.screen_recovery_key_confirm_title),
|
||||
subTitle = stringResource(id = R.string.screen_recovery_key_confirm_description),
|
||||
buttons = { Buttons(state = state, onCreateRecoveryKey = onCreateNewRecoveryKey) }
|
||||
buttons = { Buttons(state = state) }
|
||||
) {
|
||||
Content(state = state)
|
||||
}
|
||||
@@ -86,7 +84,6 @@ private fun Content(
|
||||
@Composable
|
||||
private fun ColumnScope.Buttons(
|
||||
state: SecureBackupEnterRecoveryKeyState,
|
||||
onCreateRecoveryKey: () -> Unit,
|
||||
) {
|
||||
Button(
|
||||
text = stringResource(id = CommonStrings.action_continue),
|
||||
@@ -97,12 +94,6 @@ private fun ColumnScope.Buttons(
|
||||
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit)
|
||||
}
|
||||
)
|
||||
TextButton(
|
||||
text = stringResource(id = R.string.screen_recovery_key_confirm_lost_recovery_key),
|
||||
enabled = !state.submitAction.isLoading(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onCreateRecoveryKey,
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@@ -114,6 +105,5 @@ internal fun SecureBackupEnterRecoveryKeyViewPreview(
|
||||
state = state,
|
||||
onSuccess = {},
|
||||
onBackClick = {},
|
||||
onCreateNewRecoveryKey = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@ class ResetIdentityFlowManager @Inject constructor(
|
||||
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
||||
private val sessionVerificationService: SessionVerificationService,
|
||||
) {
|
||||
private val resetHandleFlow: MutableStateFlow<AsyncData<IdentityResetHandle>> = MutableStateFlow(AsyncData.Uninitialized)
|
||||
val currentHandleFlow: StateFlow<AsyncData<IdentityResetHandle>> = resetHandleFlow
|
||||
private val resetHandleFlow: MutableStateFlow<AsyncData<IdentityResetHandle?>> = MutableStateFlow(AsyncData.Uninitialized)
|
||||
val currentHandleFlow: StateFlow<AsyncData<IdentityResetHandle?>> = resetHandleFlow
|
||||
private var whenResetIsDoneWaitingJob: Job? = null
|
||||
|
||||
fun whenResetIsDone(block: () -> Unit) {
|
||||
@@ -47,7 +47,7 @@ class ResetIdentityFlowManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun getResetHandle(): StateFlow<AsyncData<IdentityResetHandle>> {
|
||||
fun getResetHandle(): StateFlow<AsyncData<IdentityResetHandle?>> {
|
||||
return if (resetHandleFlow.value.isLoading() || resetHandleFlow.value.isSuccess()) {
|
||||
resetHandleFlow
|
||||
} else {
|
||||
@@ -56,13 +56,11 @@ class ResetIdentityFlowManager @Inject constructor(
|
||||
sessionCoroutineScope.launch {
|
||||
matrixClient.encryptionService().startIdentityReset()
|
||||
.onSuccess { handle ->
|
||||
resetHandleFlow.value = if (handle != null) {
|
||||
AsyncData.Success(handle)
|
||||
} else {
|
||||
AsyncData.Failure(IllegalStateException("Could not get a reset identity handle"))
|
||||
}
|
||||
resetHandleFlow.value = AsyncData.Success(handle)
|
||||
}
|
||||
.onFailure {
|
||||
resetHandleFlow.value = AsyncData.Failure(it)
|
||||
}
|
||||
.onFailure { resetHandleFlow.value = AsyncData.Failure(it) }
|
||||
}
|
||||
|
||||
resetHandleFlow
|
||||
|
||||
@@ -140,6 +140,9 @@ class ResetIdentityFlowNode @AssistedInject constructor(
|
||||
}
|
||||
is AsyncData.Success -> {
|
||||
when (val handle = state.data) {
|
||||
null -> {
|
||||
Timber.d("No reset handle return, the reset is done.")
|
||||
}
|
||||
is IdentityOidcResetHandle -> {
|
||||
if (oidcEntryPoint.canUseCustomTab()) {
|
||||
activity.openUrlInChromeCustomTab(null, false, handle.url)
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<string name="screen_encryption_reset_bullet_2">"You will lose your existing message history"</string>
|
||||
<string name="screen_encryption_reset_bullet_3">"You will need to verify all your existing devices and contacts again"</string>
|
||||
<string name="screen_encryption_reset_footer">"Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key."</string>
|
||||
<string name="screen_encryption_reset_subtitle">"If you’re not signed in to any other devices and you’ve lost your recovery key, then you’ll need to reset your identity to continue using the app. "</string>
|
||||
<string name="screen_encryption_reset_subtitle">"If you’re not signed in to any other devices and you’ve lost your recovery key, then you’ll need to reset your identity to continue using the app."</string>
|
||||
<string name="screen_encryption_reset_title">"Reset your identity in case you can’t confirm another way"</string>
|
||||
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Turn off"</string>
|
||||
<string name="screen_key_backup_disable_confirmation_description">"You will lose your encrypted messages if you are signed out of all devices."</string>
|
||||
|
||||
@@ -101,18 +101,6 @@ class SecureBackupEnterRecoveryKeyViewTest {
|
||||
recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.Submit)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `tapping on Lost your recovery key - calls onCreateNewRecoveryKey`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setSecureBackupEnterRecoveryKeyView(
|
||||
aSecureBackupEnterRecoveryKeyState(),
|
||||
onCreateNewRecoveryKey = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_recovery_key_confirm_lost_recovery_key)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when submit action succeeds - calls onDone`() {
|
||||
ensureCalledOnce { callback ->
|
||||
@@ -127,14 +115,12 @@ class SecureBackupEnterRecoveryKeyViewTest {
|
||||
state: SecureBackupEnterRecoveryKeyState,
|
||||
onDone: () -> Unit = EnsureNeverCalled(),
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
onCreateNewRecoveryKey: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
SecureBackupEnterRecoveryKeyView(
|
||||
state = state,
|
||||
onSuccess = onDone,
|
||||
onBackClick = onBackClick,
|
||||
onCreateNewRecoveryKey = onCreateNewRecoveryKey
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,14 +66,16 @@ class ResetIdentityFlowManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getResetHandle - will fail if it receives a null reset handle`() = runTest {
|
||||
fun `getResetHandle - will success if it receives a null reset handle`() = runTest {
|
||||
val startResetLambda = lambdaRecorder<Result<IdentityResetHandle?>> { Result.success(null) }
|
||||
val encryptionService = FakeEncryptionService(startIdentityResetLambda = startResetLambda)
|
||||
val flowManager = createFlowManager(encryptionService = encryptionService)
|
||||
|
||||
flowManager.getResetHandle().test {
|
||||
assertThat(awaitItem().isLoading()).isTrue()
|
||||
assertThat(awaitItem().isFailure()).isTrue()
|
||||
val finalItem = awaitItem()
|
||||
assertThat(finalItem.isSuccess()).isTrue()
|
||||
assertThat(finalItem.dataOrNull()).isNull()
|
||||
startResetLambda.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<string name="screen_identity_confirmation_cannot_confirm">"Can\'t confirm?"</string>
|
||||
<string name="screen_identity_confirmation_create_new_recovery_key">"Create a new recovery key"</string>
|
||||
<string name="screen_identity_confirmation_subtitle">"Verify this device to set up secure messaging."</string>
|
||||
<string name="screen_identity_confirmation_title">"Confirm that it\'s you"</string>
|
||||
<string name="screen_identity_confirmation_title">"Confirm your identity"</string>
|
||||
<string name="screen_identity_confirmation_use_another_device">"Use another device"</string>
|
||||
<string name="screen_identity_confirmation_use_recovery_key">"Use recovery key"</string>
|
||||
<string name="screen_identity_confirmed_subtitle">"Now you can read or send messages securely, and anyone you chat with can also trust this device."</string>
|
||||
|
||||
@@ -72,7 +72,7 @@ interface EncryptionService {
|
||||
/**
|
||||
* A handle to reset the user's identity.
|
||||
*/
|
||||
interface IdentityResetHandle {
|
||||
sealed interface IdentityResetHandle {
|
||||
/**
|
||||
* Cancel the reset process and drops the existing handle in the SDK.
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package io.element.android.libraries.matrix.impl.encryption
|
||||
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.core.extensions.mapFailure
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||
@@ -204,9 +205,9 @@ internal class RustEncryptionService(
|
||||
|
||||
override suspend fun startIdentityReset(): Result<IdentityResetHandle?> {
|
||||
return runCatching {
|
||||
service.resetIdentity()?.let { handle ->
|
||||
RustIdentityResetHandleFactory.create(sessionId, handle)
|
||||
}?.getOrNull()
|
||||
service.resetIdentity()
|
||||
}.flatMap { handle ->
|
||||
RustIdentityResetHandleFactory.create(sessionId, handle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +27,15 @@ import org.matrix.rustcomponents.sdk.CrossSigningResetAuthType
|
||||
object RustIdentityResetHandleFactory {
|
||||
fun create(
|
||||
userId: UserId,
|
||||
identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle
|
||||
): Result<IdentityResetHandle> {
|
||||
identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle?
|
||||
): Result<IdentityResetHandle?> {
|
||||
return runCatching {
|
||||
when (val authType = identityResetHandle.authType()) {
|
||||
is CrossSigningResetAuthType.Oidc -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl)
|
||||
// User interactive authentication (user + password)
|
||||
CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(userId, identityResetHandle)
|
||||
identityResetHandle?.let {
|
||||
when (val authType = identityResetHandle.authType()) {
|
||||
is CrossSigningResetAuthType.Oidc -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl)
|
||||
// User interactive authentication (user + password)
|
||||
CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(userId, identityResetHandle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user