Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send exception logs to email for users that experience incompatible profile #1000

Merged
merged 3 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,70 +1,173 @@
@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)

package com.babylon.wallet.android.presentation.incompatibleprofile

import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.babylon.wallet.android.MainActivity
import com.babylon.wallet.android.R
import com.babylon.wallet.android.designsystem.composable.RadixTextButton
import com.babylon.wallet.android.designsystem.theme.RadixTheme
import com.babylon.wallet.android.presentation.ui.composables.BasicPromptAlertDialog
import com.babylon.wallet.android.presentation.incompatibleprofile.IncompatibleProfileViewModel.Event
import com.babylon.wallet.android.presentation.ui.RadixWalletPreviewTheme
import com.babylon.wallet.android.utils.Constants
import com.babylon.wallet.android.utils.openEmail

const val ROUTE_INCOMPATIBLE_PROFILE = "incompatible_profile_route"

@Composable
fun IncompatibleProfileContent(
fun IncompatibleProfileScreen(
modifier: Modifier = Modifier,
viewModel: IncompatibleProfileViewModel,
onProfileDeleted: () -> Unit,
modifier: Modifier = Modifier
) {
val activity = (LocalContext.current as MainActivity)
BackHandler {
activity.finish()
}

LaunchedEffect(Unit) {
viewModel.oneOffEvent.collect {
when (it) {
IncompatibleProfileEvent.ProfileDeleted -> onProfileDeleted()
is Event.ProfileDeleted -> onProfileDeleted()
is Event.OnSendLogsToSupport -> activity.openEmail(
recipientAddress = Constants.RADIX_SUPPORT_EMAIL_ADDRESS,
subject = Constants.RADIX_SUPPORT_EMAIL_SUBJECT,
body = it.body
)
}
}
}

val state by viewModel.state.collectAsStateWithLifecycle()

IncompatibleWalletContent(
modifier = modifier,
state = state,
onDismiss = {
activity.finish()
},
onDeleteProfile = viewModel::deleteProfile,
onSendLogs = viewModel::sendLogsToSupportClick
)
}

@Composable
private fun IncompatibleWalletContent(
modifier: Modifier = Modifier,
state: IncompatibleProfileViewModel.State,
onDismiss: () -> Unit,
onDeleteProfile: () -> Unit,
onSendLogs: () -> Unit
) {
BackHandler(onBack = onDismiss)
Box(
modifier = modifier
.fillMaxSize()
.background(RadixTheme.colors.blue1)
.padding(horizontal = RadixTheme.dimensions.paddingDefault)
) {
BasicPromptAlertDialog(
finish = {
if (it) {
viewModel.deleteProfile()
} else {
activity.finish()
BasicAlertDialog(
onDismissRequest = onDismiss
) {
Surface(
shape = RadixTheme.shapes.roundedRectSmall,
color = RadixTheme.colors.defaultBackground,
tonalElevation = AlertDialogDefaults.TonalElevation
) {
Column(
modifier = Modifier.padding(RadixTheme.dimensions.paddingLarge),
) {
Text(
modifier = Modifier.padding(bottom = RadixTheme.dimensions.paddingDefault),
text = stringResource(id = R.string.splash_incompatibleProfileVersionAlert_title),
style = RadixTheme.typography.body2Header,
color = RadixTheme.colors.gray1
)

Text(
modifier = Modifier.padding(bottom = RadixTheme.dimensions.paddingLarge),
text = stringResource(id = R.string.splash_incompatibleProfileVersionAlert_message),
style = RadixTheme.typography.body2Regular,
color = RadixTheme.colors.gray1
)

FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
verticalArrangement = Arrangement.Bottom
) {
RadixTextButton(
text = stringResource(id = R.string.common_cancel),
onClick = onDismiss,
contentColor = RadixTheme.colors.blue2
)

if (state.incompatibleCause != null) {
RadixTextButton(
text = stringResource(id = R.string.troubleshooting_contactSupport_title),
onClick = onSendLogs,
contentColor = RadixTheme.colors.blue2
)
}

RadixTextButton(
text = stringResource(id = R.string.splash_incompatibleProfileVersionAlert_delete),
onClick = onDeleteProfile,
contentColor = RadixTheme.colors.red1
)
}
}
},
title = {
Text(
text = stringResource(id = R.string.splash_incompatibleProfileVersionAlert_title),
style = RadixTheme.typography.body2Header,
color = RadixTheme.colors.gray1
)
},
message = {
Text(
text = stringResource(id = R.string.splash_incompatibleProfileVersionAlert_message),
style = RadixTheme.typography.body2Regular,
color = RadixTheme.colors.gray1
)
},
confirmText = stringResource(id = R.string.splash_incompatibleProfileVersionAlert_delete),
confirmTextColor = RadixTheme.colors.red1
}
}
}
}

@Preview
@Composable
private fun IncompatibleWalletWithCausePreview() {
RadixWalletPreviewTheme {
IncompatibleWalletContent(
state = IncompatibleProfileViewModel.State(
incompatibleCause = RuntimeException("Some error")
),
onDismiss = {},
onDeleteProfile = {},
onSendLogs = {}
)
}
}

@Preview
@Composable
private fun IncompatibleWalletWithoutCausePreview() {
RadixWalletPreviewTheme {
IncompatibleWalletContent(
state = IncompatibleProfileViewModel.State(),
onDismiss = {},
onDeleteProfile = {},
onSendLogs = {}
)
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,83 @@
package com.babylon.wallet.android.presentation.incompatibleprofile

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.babylon.wallet.android.domain.usecases.DeleteWalletUseCase
import com.babylon.wallet.android.presentation.common.OneOffEvent
import com.babylon.wallet.android.presentation.common.OneOffEventHandler
import com.babylon.wallet.android.presentation.common.OneOffEventHandlerImpl
import com.babylon.wallet.android.presentation.common.StateViewModel
import com.babylon.wallet.android.presentation.common.UiState
import com.babylon.wallet.android.utils.DeviceCapabilityHelper
import com.radixdlt.sargon.CommonException
import com.radixdlt.sargon.errorCodeFromError
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import rdx.works.core.domain.ProfileState
import rdx.works.profile.data.repository.ProfileRepository
import javax.inject.Inject

@HiltViewModel
class IncompatibleProfileViewModel @Inject constructor(
private val deleteWalletUseCase: DeleteWalletUseCase
) : ViewModel(), OneOffEventHandler<IncompatibleProfileEvent> by OneOffEventHandlerImpl() {
private val profileRepository: ProfileRepository,
private val deleteWalletUseCase: DeleteWalletUseCase,
private val deviceCapabilityHelper: DeviceCapabilityHelper
) : StateViewModel<IncompatibleProfileViewModel.State>(),
OneOffEventHandler<IncompatibleProfileViewModel.Event> by OneOffEventHandlerImpl() {

override fun initialState(): State = State()

init {
viewModelScope.launch {
val incompatibleProfile = profileRepository.profileState.filterIsInstance<ProfileState.Incompatible>().firstOrNull()
_state.update { it.copy(incompatibleCause = incompatibleProfile?.cause) }
}
}

fun sendLogsToSupportClick() {
viewModelScope.launch {
val body = StringBuilder()
body.append(deviceCapabilityHelper.supportEmailTemplate)
body.appendLine("=========================")
body.appendLine("Incompatible Wallet Profile")

val cause = _state.value.incompatibleCause
if (cause != null) {
if (cause is CommonException) {
body.appendLine("Error Code: ${errorCodeFromError(cause)}")

if (cause is CommonException.FailedToDeserializeJsonToValue) {
body.appendLine(_state.value.incompatibleCause?.message)
body.appendLine()
body.appendLine(_state.value.incompatibleCause?.stackTraceToString())
}
} else {
body.appendLine(_state.value.incompatibleCause?.message)
body.appendLine()
body.appendLine(_state.value.incompatibleCause?.stackTraceToString())
}
}
sendEvent(Event.OnSendLogsToSupport(body = body.toString()))
}
}

fun deleteProfile() {
viewModelScope.launch {
deleteWalletUseCase()
sendEvent(IncompatibleProfileEvent.ProfileDeleted)
sendEvent(Event.ProfileDeleted)
}
}
}

internal sealed interface IncompatibleProfileEvent : OneOffEvent {
object ProfileDeleted : IncompatibleProfileEvent
data class State(
val incompatibleCause: Throwable? = null
) : UiState

sealed interface Event : OneOffEvent {
data object ProfileDeleted : Event
data class OnSendLogsToSupport(
val body: String
) : Event
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -347,14 +347,14 @@ data class OlympiaErrorState(
sealed interface AppState {
data object OnBoarding : AppState
data object Wallet : AppState
data object IncompatibleProfile : AppState
data class IncompatibleProfile(val cause: Throwable) : AppState
data object Loading : AppState

companion object {
fun from(
profileState: ProfileState
) = when (profileState) {
is ProfileState.Incompatible -> IncompatibleProfile
is ProfileState.Incompatible -> IncompatibleProfile(cause = profileState.cause)
is ProfileState.Restored -> if (profileState.hasNetworks()) {
Wallet
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import com.babylon.wallet.android.presentation.account.settings.thirdpartydeposi
import com.babylon.wallet.android.presentation.dapp.authorized.dappLoginAuthorizedNavGraph
import com.babylon.wallet.android.presentation.dapp.completion.ChooseAccountsCompletionScreen
import com.babylon.wallet.android.presentation.dapp.unauthorized.dappLoginUnauthorizedNavGraph
import com.babylon.wallet.android.presentation.incompatibleprofile.IncompatibleProfileContent
import com.babylon.wallet.android.presentation.incompatibleprofile.IncompatibleProfileScreen
import com.babylon.wallet.android.presentation.incompatibleprofile.ROUTE_INCOMPATIBLE_PROFILE
import com.babylon.wallet.android.presentation.main.MAIN_ROUTE
import com.babylon.wallet.android.presentation.main.MainUiState
Expand Down Expand Up @@ -456,9 +456,12 @@ fun NavigationHost(
composable(
route = ROUTE_INCOMPATIBLE_PROFILE
) {
IncompatibleProfileContent(hiltViewModel(), onProfileDeleted = {
navController.popBackStack(MAIN_ROUTE, false)
})
IncompatibleProfileScreen(
viewModel = hiltViewModel(),
onProfileDeleted = {
navController.popBackStack(MAIN_ROUTE, false)
}
)
}
composable(
route = ROUTE_ROOT_DETECTION
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import androidx.annotation.StringRes
import com.babylon.wallet.android.R
import com.babylon.wallet.android.domain.model.SecurityProblem
import com.babylon.wallet.android.presentation.ui.composables.DSR
import com.babylon.wallet.android.utils.Constants.RADIX_SUPPORT_EMAIL_ADDRESS
import com.babylon.wallet.android.utils.Constants.RADIX_SUPPORT_EMAIL_SUBJECT
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf

Expand Down Expand Up @@ -111,8 +113,8 @@ sealed interface SettingsItem {
data object ImportFromLegacyWallet : Troubleshooting
data class ContactSupport(
val body: String,
val supportAddress: String = "[email protected]",
val subject: String = "Customer Support Case"
val supportAddress: String = RADIX_SUPPORT_EMAIL_ADDRESS,
val subject: String = RADIX_SUPPORT_EMAIL_SUBJECT
) : Troubleshooting

data object Discord : Troubleshooting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ object Constants {
const val RADIX_START_PAGE_URL = "https://wallet.radixdlt.com/?wallet=downloaded"
const val DEFAULT_ACCOUNT_NAME = "Unnamed"
const val MAX_ITEMS_PER_ENTITY_DETAILS_REQUEST = 20

const val RADIX_SUPPORT_EMAIL_ADDRESS = "[email protected]"
const val RADIX_SUPPORT_EMAIL_SUBJECT = "Customer Support Case"
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class FakeProfileRepository(
Profile.fromJson(jsonString = content)
}.fold(
onSuccess = { ProfileState.Restored(it) },
onFailure = { ProfileState.Incompatible }
onFailure = { ProfileState.Incompatible(it) }
)

fun update(onUpdate: (Profile) -> Profile): Profile {
Expand Down
6 changes: 3 additions & 3 deletions core/src/main/java/rdx/works/core/domain/ProfileState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ sealed class ProfileState {

/**
* The [Profile]'s version saved in the internal storage is lower than the
* [ProfileSnapshotVersion.V100], so it is incompatible. Currently the user can only
* create a new profile.
* [ProfileSnapshotVersion.V100], so it is incompatible or an error during deserialization has occurred.
* Currently the user can only create a new profile.
*/
data object Incompatible : ProfileState()
data class Incompatible(val cause: Throwable) : ProfileState()

/**
* A compatible [ProfileSnapshot] exists and the user can derive the [Profile].
Expand Down
2 changes: 1 addition & 1 deletion gradle/libraries.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ hiltNavigation = "1.2.0"
biometricKtx = "1.2.0-alpha05"
coilCompose = "2.5.0"
kotlinxSerialization = "1.6.2"
sargon = "1.0.10-b1fb6d6f"
sargon = "1.0.14-31dd1aba"
okhttpBom = "5.0.0-alpha.11"
retrofit = "2.9.0"
retrofitKoltinxConverter = "1.0.0"
Expand Down
Loading
Loading