Skip to content

Commit

Permalink
Merge pull request #3220 from CruGlobal/deleteAccount
Browse files Browse the repository at this point in the history
GT-2178 Add Support for a user deleting their account
  • Loading branch information
frett authored Nov 13, 2023
2 parents 8c5aeee + 621eb67 commit df95e5b
Show file tree
Hide file tree
Showing 17 changed files with 740 additions and 5 deletions.
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
id("godtools.application-conventions")
id("kotlin-parcelize")
alias(libs.plugins.firebase.appdistribution)
alias(libs.plugins.firebase.crashlytics)
alias(libs.plugins.firebase.perf)
Expand Down Expand Up @@ -205,6 +206,8 @@ dependencies {
implementation(libs.godtoolsShared.common)

api(libs.eventbus)
implementation(libs.circuit.codegen.annotations)
implementation(libs.circuit.foundation)
implementation(libs.coil.compose)
implementation(libs.compose.reorderable)
implementation(libs.hilt)
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@
android:name=".ui.account.AccountActivity"
android:parentActivityName=".ui.dashboard.DashboardActivity" />

<activity
android:name=".ui.account.delete.DeleteAccountActivity"
android:parentActivityName=".ui.dashboard.DashboardActivity" />

<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
Expand Down
33 changes: 33 additions & 0 deletions app/src/main/kotlin/org/cru/godtools/dagger/CircuitModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.cru.godtools.dagger

import com.slack.circuit.foundation.Circuit
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.ui.Ui
import dagger.Module
import dagger.Provides
import dagger.Reusable
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.Multibinds

@Module
@InstallIn(SingletonComponent::class)
abstract class CircuitModule {
@Multibinds
abstract fun presenterFactories(): Set<Presenter.Factory>

@Multibinds
abstract fun uiFactories(): Set<Ui.Factory>

companion object {
@Provides
@Reusable
fun circuit(
presenterFactories: Set<@JvmSuppressWildcards Presenter.Factory>,
uiFactories: Set<@JvmSuppressWildcards Ui.Factory>
) = Circuit.Builder()
.addPresenterFactories(presenterFactories)
.addUiFactories(uiFactories)
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.cru.godtools.ui.account.delete

import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import com.slack.circuit.backstack.rememberSaveableBackStack
import com.slack.circuit.foundation.Circuit
import com.slack.circuit.foundation.CircuitCompositionLocals
import com.slack.circuit.foundation.NavigableCircuitContent
import com.slack.circuit.foundation.rememberCircuitNavigator
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.cru.godtools.base.ui.activity.BaseActivity
import org.cru.godtools.base.ui.theme.GodToolsTheme

fun Context.startDeleteAccountActivity() = startActivity(Intent(this, DeleteAccountActivity::class.java))

@AndroidEntryPoint
class DeleteAccountActivity : BaseActivity() {
@Inject
lateinit var circuit: Circuit

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CircuitCompositionLocals(circuit) {
GodToolsTheme {
val backStack = rememberSaveableBackStack { push(DeleteAccountScreen) }
val navigator = rememberCircuitNavigator(backStack)
NavigableCircuitContent(navigator, backStack)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.cru.godtools.ui.account.delete

import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import org.cru.godtools.base.ui.theme.GodToolsTheme

@Preview
@Composable
private fun DeleteAccountLayoutDisplayPreview() {
GodToolsTheme { DeleteAccountLayout(DeleteAccountScreen.State.Display { }) }
}

@Preview
@Composable
private fun DeleteAccountLayoutDeletingPreview() {
GodToolsTheme { DeleteAccountLayout(DeleteAccountScreen.State.Deleting { }) }
}

@Preview
@Composable
private fun DeleteAccountLayoutErrorPreview() {
GodToolsTheme { DeleteAccountLayout(DeleteAccountScreen.State.Error { }) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package org.cru.godtools.ui.account.delete

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.slack.circuit.codegen.annotations.CircuitInject
import dagger.hilt.components.SingletonComponent
import org.ccci.gto.android.common.androidx.compose.foundation.layout.padding
import org.cru.godtools.R
import org.cru.godtools.ui.account.delete.DeleteAccountScreen.Event
import org.cru.godtools.ui.account.delete.DeleteAccountScreen.State

private val MARGIN_HORIZONTAL = 32.dp

internal const val TEST_TAG_ICON_CLOSE = "icon_close"
internal const val TEST_TAG_BUTTON_DELETE = "button_delete"
internal const val TEST_TAG_BUTTON_CANCEL = "button_cancel"
internal const val TEST_TAG_ERROR_DIALOG = "error_dialog"
internal const val TEST_TAG_ERROR_DIALOG_BUTTON_CONFIRM = "error_dialog_button_confirm"

@Composable
@OptIn(ExperimentalMaterial3Api::class)
@CircuitInject(DeleteAccountScreen::class, SingletonComponent::class)
fun DeleteAccountLayout(state: State, modifier: Modifier = Modifier) {
DeleteAccountError(state)

Scaffold(modifier = modifier) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(it)
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
TopAppBar(
title = {},
navigationIcon = {
IconButton(
onClick = { state.eventSink(Event.Close) },
modifier = Modifier.testTag(TEST_TAG_ICON_CLOSE)
) {
Icon(Icons.Default.Close, null)
}
},
)

Spacer(Modifier.weight(1f))
Image(
painterResource(R.drawable.banner_account_login),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth(),
)
Spacer(Modifier.weight(1f))

Text(
stringResource(R.string.account_delete_heading),
style = MaterialTheme.typography.displayMedium,
modifier = Modifier
.padding(horizontal = MARGIN_HORIZONTAL)
.align(Alignment.Start)
)
Text(
stringResource(R.string.account_delete_description),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(horizontal = MARGIN_HORIZONTAL, top = 8.dp, bottom = 32.dp)
)

val actionsEnabled = state !is State.Deleting && state !is State.Error
OutlinedButton(
enabled = actionsEnabled,
onClick = { state.eventSink(Event.DeleteAccount) },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = MARGIN_HORIZONTAL)
.testTag(TEST_TAG_BUTTON_DELETE)
) {
Text(stringResource(R.string.account_delete_action_delete))
}
Button(
enabled = actionsEnabled,
onClick = { state.eventSink(Event.Close) },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = MARGIN_HORIZONTAL)
.testTag(TEST_TAG_BUTTON_CANCEL)
) {
Text(stringResource(R.string.account_delete_action_cancel))
}
Spacer(Modifier.height(32.dp))
}
}
}

@Composable
private fun DeleteAccountError(state: State) {
if (state is State.Error) {
AlertDialog(
text = { Text(stringResource(R.string.account_delete_error)) },
confirmButton = {
TextButton(
onClick = { state.eventSink(Event.ClearError) },
modifier = Modifier.testTag(TEST_TAG_ERROR_DIALOG_BUTTON_CONFIRM),
) {
Text(stringResource(R.string.account_delete_error_dismiss))
}
},
onDismissRequest = { state.eventSink(Event.ClearError) },
modifier = Modifier.testTag(TEST_TAG_ERROR_DIALOG),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.cru.godtools.ui.account.delete

import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.ui.Ui
import com.slack.circuit.runtime.ui.ui
import dagger.Module
import dagger.Provides
import dagger.Reusable
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet

// TODO: switch to codegen for this once https://github.com/slackhq/circuit/pull/963 is released
@Module
@InstallIn(SingletonComponent::class)
object DeleteAccountModule {
@Provides
@Reusable
@IntoSet
fun presenterFactory(factory: DeleteAccountPresenter.Factory) = Presenter.Factory { screen, navigator, _ ->
when (screen) {
DeleteAccountScreen -> factory.create(navigator = navigator)
else -> null
}
}

@Provides
@Reusable
@IntoSet
fun uiFactory() = Ui.Factory { screen, _ ->
when (screen) {
DeleteAccountScreen -> ui<DeleteAccountScreen.State> { state, modifier ->
DeleteAccountLayout(state = state, modifier = modifier)
}
else -> null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.cru.godtools.ui.account.delete

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import com.slack.circuit.codegen.annotations.CircuitInject
import com.slack.circuit.retained.rememberRetained
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.launch
import org.cru.godtools.account.GodToolsAccountManager
import org.cru.godtools.ui.account.delete.DeleteAccountScreen.Event

class DeleteAccountPresenter @AssistedInject constructor(
@Assisted private val navigator: Navigator,
private val accountManager: GodToolsAccountManager,
) : Presenter<DeleteAccountScreen.State> {
@Composable
override fun present(): DeleteAccountScreen.State {
val coroutineScope = rememberCoroutineScope()

var deleting by remember { mutableStateOf(false) }
var error by rememberRetained { mutableStateOf(false) }

val eventSink: (Event) -> Unit = remember {
{
when (it) {
Event.DeleteAccount -> {
deleting = true
coroutineScope.launch {
if (accountManager.deleteAccount()) {
navigator.pop()
} else {
error = true
deleting = false
}
}
}
Event.ClearError -> error = false
Event.Close -> navigator.pop()
}
}
}

return when {
error -> DeleteAccountScreen.State.Error(eventSink)
deleting -> DeleteAccountScreen.State.Deleting(eventSink)
else -> DeleteAccountScreen.State.Display(eventSink)
}
}

@AssistedFactory
@CircuitInject(DeleteAccountScreen::class, SingletonComponent::class)
interface Factory {
fun create(navigator: Navigator): DeleteAccountPresenter
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.cru.godtools.ui.account.delete

import com.slack.circuit.runtime.CircuitUiEvent
import com.slack.circuit.runtime.CircuitUiState
import com.slack.circuit.runtime.screen.Screen
import kotlinx.parcelize.Parcelize

@Parcelize
data object DeleteAccountScreen : Screen {
sealed class State : CircuitUiState {
data class Display(override val eventSink: (Event) -> Unit) : State()
data class Deleting(override val eventSink: (Event) -> Unit) : State()
data class Error(override val eventSink: (Event) -> Unit) : State()

abstract val eventSink: (Event) -> Unit
}

sealed class Event : CircuitUiEvent {
data object DeleteAccount : Event()
data object ClearError : Event()
data object Close : Event()
}
}
Loading

0 comments on commit df95e5b

Please sign in to comment.