Skip to content

Commit

Permalink
Merge pull request #18412 from wordpress-mobile/feature/enable-accoun…
Browse files Browse the repository at this point in the history
…t-closure

Enable account closure
  • Loading branch information
mkevins authored May 19, 2023
2 parents 8bda6cf + 6e49556 commit a21210b
Show file tree
Hide file tree
Showing 20 changed files with 739 additions and 6 deletions.
2 changes: 1 addition & 1 deletion RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

22.5
-----

* [*] Adds a button to enable account closure from the account settings screen [https://github.com/wordpress-mobile/WordPress-Android/pull/18412]

22.4
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,8 @@ class HelpActivity : LocaleAwareActivity() {
JETPACK_MIGRATION_HELP("origin:jetpack-migration-help"),
JETPACK_INSTALL_FULL_PLUGIN_ONBOARDING("origin:jp-install-full-plugin-overlay"),
JETPACK_INSTALL_FULL_PLUGIN_ERROR("origin:jp-install-full-plugin-error"),
JETPACK_REMOTE_INSTALL_PLUGIN_ERROR("origin:jp-remote-install-plugin-error");
JETPACK_REMOTE_INSTALL_PLUGIN_ERROR("origin:jp-remote-install-plugin-error"),
ACCOUNT_CLOSURE_DIALOG("origin:account-closure-dialog");

override fun toString(): String {
return stringValue
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.wordpress.android.ui.prefs.accountsettings

import org.wordpress.android.analytics.AnalyticsTracker.Stat
import org.wordpress.android.analytics.AnalyticsTracker.Stat.CLOSED_ACCOUNT
import org.wordpress.android.analytics.AnalyticsTracker.Stat.CLOSE_ACCOUNT_FAILED
import org.wordpress.android.analytics.AnalyticsTracker.Stat.SETTINGS_DID_CHANGE
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsEvent.EMAIL_CHANGED
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsEvent.PASSWORD_CHANGED
Expand All @@ -17,6 +19,7 @@ private const val SOURCE_ACCOUNT_SETTINGS = "account_settings"
private const val TRACK_PROPERTY_FIELD_NAME = "field_name"
private const val TRACK_PROPERTY_PAGE = "page"
private const val TRACK_PROPERTY_PAGE_ACCOUNT_SETTINGS = "account_settings"
private const val KEY_ACCOUNT_CLOSURE_ERROR_CODE = "error_code"

enum class AccountSettingsEvent(val trackProperty: String? = null) {
EMAIL_CHANGED("email"),
Expand Down Expand Up @@ -50,4 +53,16 @@ class AccountSettingsAnalyticsTracker @Inject constructor(private val analyticsT
props[SOURCE] = SOURCE_ACCOUNT_SETTINGS
analyticsTracker.track(stat, props)
}

fun trackAccountClosureFailure(errorCode: String?) {
mutableMapOf<String, String?>().apply {
put(KEY_ACCOUNT_CLOSURE_ERROR_CODE, errorCode ?: "unknown")
}.let { props ->
analyticsTracker.track(CLOSE_ACCOUNT_FAILED, props)
}
}

fun trackAccountClosureSuccess() {
analyticsTracker.track(CLOSED_ACCOUNT)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,21 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ListView
import android.widget.TextView
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import org.wordpress.android.R
import org.wordpress.android.WordPress
import org.wordpress.android.ui.ActivityLauncher
import org.wordpress.android.ui.accounts.HelpActivity
import org.wordpress.android.ui.accounts.signup.BaseUsernameChangerFullScreenDialogFragment
import org.wordpress.android.ui.compose.theme.AppTheme
import org.wordpress.android.ui.pages.SnackbarMessageHolder
import org.wordpress.android.ui.prefs.DetailListPreference
import org.wordpress.android.ui.prefs.EditTextPreferenceWithValidation
Expand All @@ -37,10 +45,13 @@ import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsEvent.USERN
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsEvent.USERNAME_CHANGE_SCREEN_DISPLAYED
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsEvent.WEB_ADDRESS_CHANGED
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsViewModel.AccountSettingsUiState
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsViewModel.AccountClosureUiState
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsViewModel.ChangePasswordSettingsUiState
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsViewModel.EmailSettingsUiState
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsViewModel.PrimarySiteSettingsUiState
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsViewModel.UserNameSettingsUiState
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsViewModel.Companion.AccountClosureAction
import org.wordpress.android.ui.prefs.accountsettings.components.AccountClosureUi
import org.wordpress.android.ui.utils.UiHelpers
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.AppLog.T.SETTINGS
Expand Down Expand Up @@ -90,6 +101,7 @@ class AccountSettingsFragment : PreferenceFragmentLifeCycleOwner(),
addPreferencesFromResource(R.xml.account_settings)
bindPreferences()
setUpListeners()
observeAccountClosureEvents()
emailPreference.configure(
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
validationType = EMAIL
Expand Down Expand Up @@ -117,6 +129,30 @@ class AccountSettingsFragment : PreferenceFragmentLifeCycleOwner(),
changePasswordPreference.summary = EMPTY_STRING
}

private fun observeAccountClosureEvents() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.userActionEvents.collect { handleUserAction(it) }
}
}
lifecycleScope.launch {
// Using `CREATED` state here prevents tracking duplicate events
repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.accountClosureUiState.collect {
when (it) {
is AccountClosureUiState.Opened.Error -> {
analyticsTracker.trackAccountClosureFailure(it.errorType.token)
}
is AccountClosureUiState.Opened.Success -> {
analyticsTracker.trackAccountClosureSuccess()
}
else -> {}
}
}
}
}
}

private fun setUpListeners() {
usernamePreference.onPreferenceClickListener = this@AccountSettingsFragment
primarySitePreference.onPreferenceChangeListener = this@AccountSettingsFragment
Expand Down Expand Up @@ -153,6 +189,21 @@ class AccountSettingsFragment : PreferenceFragmentLifeCycleOwner(),
return coordinatorView
}

@Deprecated("Deprecated")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(view.findViewById<View>(android.R.id.list) as? ListView)?.let { listView ->
listView.addFooterView(ComposeView(context).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
AppTheme {
AccountClosureUi(viewModel)
}
}
})
}
}

@Deprecated("Deprecated")
override fun onStart() {
super.onStart()
Expand Down Expand Up @@ -349,4 +400,26 @@ class AccountSettingsFragment : PreferenceFragmentLifeCycleOwner(),
}
}
}

private fun handleUserAction(action: AccountClosureAction) {
when (action) {
AccountClosureAction.HELP_VIEWED -> viewHelp()
AccountClosureAction.ACCOUNT_CLOSED -> signOut()
AccountClosureAction.USER_LOGGED_OUT -> {
ActivityLauncher.showMainActivity(context, true)
}
}
}
private fun viewHelp() = ActivityLauncher.viewHelp(
context,
HelpActivity.Origin.ACCOUNT_CLOSURE_DIALOG,
null,
null,
)

private fun signOut() {
(activity.application as? WordPress)?.let {
viewModel.signOutWordPress(it)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,30 @@ import androidx.lifecycle.viewModelScope
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.wordpress.android.R
import org.wordpress.android.WordPress
import org.wordpress.android.fluxc.network.rest.wpcom.account.CloseAccountResult
import org.wordpress.android.fluxc.store.AccountStore.AccountError
import org.wordpress.android.fluxc.store.AccountStore.AccountErrorType.SETTINGS_FETCH_GENERIC_ERROR
import org.wordpress.android.fluxc.store.AccountStore.AccountErrorType.SETTINGS_FETCH_REAUTHORIZATION_REQUIRED_ERROR
import org.wordpress.android.fluxc.store.AccountStore.AccountErrorType.SETTINGS_POST_ERROR
import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged
import org.wordpress.android.modules.BG_THREAD
import org.wordpress.android.modules.UI_THREAD
import org.wordpress.android.ui.pages.SnackbarMessageHolder
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsViewModel.AccountClosureUiState.Dismissed
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsViewModel.AccountClosureUiState.Opened.Error
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsViewModel.AccountClosureUiState.Opened.Default
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsViewModel.AccountClosureUiState.Opened.Success
import org.wordpress.android.ui.prefs.accountsettings.usecase.AccountClosureUseCase
import org.wordpress.android.ui.prefs.accountsettings.usecase.FetchAccountSettingsUseCase
import org.wordpress.android.ui.prefs.accountsettings.usecase.GetAccountUseCase
import org.wordpress.android.ui.prefs.accountsettings.usecase.GetSitesUseCase
Expand All @@ -38,15 +49,21 @@ class AccountSettingsViewModel @Inject constructor(
private val resourceProvider: ResourceProvider,
networkUtilsWrapper: NetworkUtilsWrapper,
@Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher,
@Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher,
private val fetchAccountSettingsUseCase: FetchAccountSettingsUseCase,
private val pushAccountSettingsUseCase: PushAccountSettingsUseCase,
private val getAccountUseCase: GetAccountUseCase,
private val getSitesUseCase: GetSitesUseCase,
private val optimisticUpdateHandler: AccountSettingsOptimisticUpdateHandler
private val optimisticUpdateHandler: AccountSettingsOptimisticUpdateHandler,
private val accountClosureUseCase: AccountClosureUseCase,
) : ScopedViewModel(mainDispatcher) {
private var fetchNewSettingsJob: Job? = null
private var _accountSettingsUiState = MutableStateFlow(getAccountSettingsUiState(true))
val accountSettingsUiState: StateFlow<AccountSettingsUiState> = _accountSettingsUiState.asStateFlow()
private var _accountClosureUiState = MutableStateFlow<AccountClosureUiState>(Dismissed)
val accountClosureUiState: StateFlow<AccountClosureUiState> = _accountClosureUiState
private var _userActionEvents = MutableSharedFlow<AccountClosureAction>()
val userActionEvents: SharedFlow<AccountClosureAction> = _userActionEvents

init {
viewModelScope.launch {
Expand Down Expand Up @@ -288,8 +305,73 @@ class AccountSettingsViewModel @Inject constructor(
val toastMessage: String?
)

sealed class AccountClosureUiState {
object Dismissed: AccountClosureUiState()

sealed class Opened: AccountClosureUiState() {
data class Default(val username: String?, val isPending: Boolean = false): Opened()
data class Error(val errorType: CloseAccountResult.ErrorType): Opened()
object Success: Opened()
}
}

fun openAccountClosureDialog() {
launch {
_accountClosureUiState.value = if (getSitesUseCase.getAtomic().isNotEmpty()) {
Error(CloseAccountResult.ErrorType.ATOMIC_SITE)
} else {
Default(username = getAccountUseCase.account.userName)
}
}
}
fun dismissAccountClosureDialog() {
_accountClosureUiState.value = Dismissed
}

fun closeAccount() {
(accountClosureUiState.value as? Default)?.let { uiState ->
_accountClosureUiState.value = uiState.copy(isPending = true)

launch {
accountClosureUseCase.closeAccount(
onResult = {
when(it) {
is CloseAccountResult.Success -> {
_accountClosureUiState.value = Success
}
is CloseAccountResult.Failure -> {
_accountClosureUiState.value = Error(it.error.errorType)
}
}
}
)
}
}
}

fun signOutWordPress(application: WordPress) {
launch {
withContext(bgDispatcher) {
application.wordPressComSignOut()
userAction(AccountClosureAction.USER_LOGGED_OUT)
}
}
}

fun userAction(action: AccountClosureAction) {
launch {
_userActionEvents.emit(action)
}
}

override fun onCleared() {
pushAccountSettingsUseCase.onCleared()
super.onCleared()
}

companion object {
enum class AccountClosureAction {
HELP_VIEWED, ACCOUNT_CLOSED, USER_LOGGED_OUT;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.wordpress.android.ui.prefs.accountsettings.components

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog

@Composable
fun AccountClosureDialog(
onDismissRequest: () -> Unit,
content: @Composable ColumnScope.() -> Unit,
) {
val padding = 10.dp
Dialog(onDismissRequest = onDismissRequest) {
Column(
modifier = Modifier
.clip(shape = RoundedCornerShape(padding))
.background(MaterialTheme.colors.background)
.padding(padding),
content = content,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.wordpress.android.ui.prefs.accountsettings.components

import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsViewModel
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsViewModel.AccountClosureUiState.Opened
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsViewModel.Companion.AccountClosureAction.ACCOUNT_CLOSED
import org.wordpress.android.ui.prefs.accountsettings.AccountSettingsViewModel.Companion.AccountClosureAction.HELP_VIEWED

@Composable
fun AccountClosureUi(viewModel: AccountSettingsViewModel) {
val uiState = viewModel.accountClosureUiState.collectAsState()

CloseAccountButton(onClick = { viewModel.openAccountClosureDialog() })

(uiState.value as? Opened)?.let {
AccountClosureDialog(
onDismissRequest = { viewModel.dismissAccountClosureDialog() },
) {
when(it) {
is Opened.Default -> it.username?.let { currentUsername ->
DialogUi(
currentUsername = currentUsername,
isPending = it.isPending,
onCancel = { viewModel.dismissAccountClosureDialog() },
onConfirm = { viewModel.closeAccount() },
)
}

is Opened.Error -> DialogErrorUi(
onDismissRequest = { viewModel.dismissAccountClosureDialog() },
onHelpRequested = { viewModel.userAction(HELP_VIEWED) },
it.errorType,
)
is Opened.Success -> DialogSuccessUi(
onDismissRequest = { viewModel.userAction(ACCOUNT_CLOSED) }
)
}
}
}
}
Loading

0 comments on commit a21210b

Please sign in to comment.