Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
5a42302
feat: Add delete account button to manage account screen
Mandvii Nov 12, 2025
60cdcb3
feat: Add initial structure for Delete Account screen
Mandvii Nov 12, 2025
41a813f
feat: Add delete account button to manage account screen
Mandvii Nov 12, 2025
24beb28
feat: Implement delete account confirmation screen
Mandvii Nov 13, 2025
2e67988
feat: Add ability to delete a person by GUID
Mandvii Nov 13, 2025
4474967
function to delete added in repository and http
Mandvii Nov 13, 2025
c59d0b9
function to delete added in repository and http
Mandvii Nov 13, 2025
fe2953f
Merge branch 'main' into dev-account-deletion
Mandvii Nov 13, 2025
266ca45
undo function to delete added in repository and http
Mandvii Nov 13, 2025
d9075ca
undo function to delete added in repository and http
Mandvii Nov 13, 2025
43d8bed
feat: Add account deletion functionality
Mandvii Nov 14, 2025
bfa2c83
Merge branch 'main' into dev-account-deletion
Mandvii Nov 14, 2025
23f945e
refactor
Mandvii Nov 14, 2025
79893d6
feat: Add account deletion functionality
Mandvii Nov 15, 2025
918e7c6
End user session after account deletion
Mandvii Nov 15, 2025
681fc8e
feat: Remove unused PersonEntityDao.deleteByPersonGuidHash
Mandvii Nov 17, 2025
86abc10
feat: Remove unused PersonEntityDao.deleteByPersonGuidHash
Mandvii Nov 17, 2025
c19a9b5
Merge branch 'main' into dev-account-deletion
Mandvii Nov 17, 2025
685079f
feat: Add findFamilyMembersRelatedToPerson to PersonEntityDao
Mandvii Nov 17, 2025
2244347
Moves `UsernameSuggestionUseCaseServer` to the `account.invite.userna…
Mandvii Nov 17, 2025
8c847e5
The `DeleteAccountUseCase` is simplified by removing the `guid` param…
Mandvii Nov 17, 2025
606a5eb
style: Fix trailing comma
Mandvii Nov 17, 2025
c968a6d
style: Fix trailing comma
Mandvii Nov 17, 2025
37c9a25
style: Fix trailing comma
Mandvii Nov 17, 2025
201d641
Merge branch 'main' into dev-account-deletion
Mandvii Nov 18, 2025
172ec36
refactor
Mandvii Nov 18, 2025
2fe2498
feat: Delete account of the authenticated user
Mandvii Nov 18, 2025
d7c202c
The account deletion logic has been moved from the `PersonDeleteRout…
Mandvii Nov 18, 2025
01fff6b
feat: End user session and navigate to Get Started screen after accou…
Mandvii Nov 19, 2025
dd959d9
feat: End user session and navigate to Get Started screen after accou…
Mandvii Nov 19, 2025
153efc3
Merge branch 'main' into dev-account-deletion
Mandvii Nov 19, 2025
3ce1423
This change simplifies the `DeleteAccountScreen` by replacing manual …
Mandvii Nov 19, 2025
623a75a
The `DeleteAccountViewModel` now observes changes to the user's data …
Mandvii Nov 19, 2025
772c54b
refactor: Remove automatic population of the delete confirmation field
Mandvii Nov 19, 2025
a833c55
Use username instead of full name in delete account screen
Mandvii Nov 20, 2025
753f54c
refactor
Mandvii Nov 20, 2025
8e91964
Merge branch 'main' into dev-account-deletion
Mandvii Nov 20, 2025
5e238ab
Merge branch 'main' into dev-account-deletion
Mandvii Nov 26, 2025
f7cfb70
Merge branch 'main' into dev-account-deletion
Mandvii Dec 2, 2025
7f62957
feat: Add person delete functionality
Mandvii Dec 2, 2025
e5cdd5b
Refactor DeleteAccountUseCaseServer to remove unused dependency
Mandvii Dec 2, 2025
56f9d73
Refactor: Rename schoolDb to schoolDataSource
Mandvii Dec 2, 2025
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
Expand Up @@ -93,6 +93,8 @@ import world.respect.shared.domain.account.RespectTokenManager
import world.respect.shared.domain.account.child.AddChildAccountUseCase
import world.respect.shared.domain.account.authenticatepassword.AuthenticatePasswordUseCase
import world.respect.shared.domain.account.child.AddChildAccountUseCaseDataSource
import world.respect.shared.domain.account.deleteaccount.DeleteAccountUseCase
import world.respect.shared.domain.account.deleteaccount.DeleteAccountUseCaseClient
import world.respect.shared.domain.account.gettokenanduser.GetTokenAndUserProfileWithCredentialUseCase
import world.respect.shared.domain.account.gettokenanduser.GetTokenAndUserProfileWithCredentialUseCaseClient
import world.respect.shared.domain.account.invite.ApproveOrDeclineInviteRequestUseCase
Expand Down Expand Up @@ -191,6 +193,7 @@ import world.respect.shared.viewmodel.person.detail.PersonDetailViewModel
import world.respect.shared.viewmodel.person.edit.PersonEditViewModel
import world.respect.shared.viewmodel.person.list.PersonListViewModel
import world.respect.shared.viewmodel.person.manageaccount.ManageAccountViewModel
import world.respect.shared.viewmodel.person.deleteaccount.DeleteAccountViewModel
import world.respect.shared.viewmodel.person.passkeylist.PasskeyListViewModel
import world.respect.shared.viewmodel.person.setusernameandpassword.SetUsernameAndPasswordViewModel
import world.respect.shared.viewmodel.report.ReportViewModel
Expand Down Expand Up @@ -330,6 +333,7 @@ val appKoinModule = module {
viewModelOf(::AssignmentDetailViewModel)
viewModelOf(::EnrollmentListViewModel)
viewModelOf(::EnrollmentEditViewModel)
viewModelOf(::DeleteAccountViewModel)


single<GetOfflineStorageOptionsUseCase> {
Expand Down Expand Up @@ -748,6 +752,15 @@ val appKoinModule = module {
get<RespectTokenManager>().providerFor(id)
}

factory<DeleteAccountUseCase> {
DeleteAccountUseCaseClient(
schoolUrl = SchoolDirectoryEntryScopeId.parse(id).schoolUrl,
schoolDirectoryEntryDataSource = get<RespectAppDataSource>().schoolDirectoryEntryDataSource,
httpClient = get(),
tokenProvider = get(),
)
}

scoped<RemoteWriteQueue> {
get<RespectAccountSchoolScopeLink>()
val accountScopeId = RespectAccountScopeId.parse(id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import world.respect.app.view.manageuser.otheroptionsignup.OtherOptionsSignupScr
import world.respect.app.view.manageuser.termsandcondition.TermsAndConditionScreen
import world.respect.app.view.onboarding.OnboardingScreen
import world.respect.app.view.person.changepassword.ChangePasswordScreen
import world.respect.app.view.person.deleteaccount.DeleteAccountScreen
import world.respect.app.view.person.detail.PersonDetailScreen
import world.respect.app.view.person.edit.PersonEditScreen
import world.respect.app.view.person.list.PersonListScreen
Expand Down Expand Up @@ -92,6 +93,7 @@ import world.respect.shared.viewmodel.clazz.edit.ClazzEditViewModel
import world.respect.shared.viewmodel.clazz.list.ClazzListViewModel
import world.respect.shared.viewmodel.clazz.detail.ClazzDetailViewModel
import world.respect.shared.navigation.CreateAccount
import world.respect.shared.navigation.DeleteAccount
import world.respect.shared.navigation.EnrollmentEdit
import world.respect.shared.navigation.EnrollmentList
import world.respect.shared.navigation.EnterPasswordSignup
Expand Down Expand Up @@ -512,6 +514,15 @@ fun AppNavHost(
)
)
}

composable<DeleteAccount> {
DeleteAccountScreen(
viewModel = respectViewModel(
onSetAppUiState = onSetAppUiState,
navController = respectNavController
)
)
}
composable<PasskeyList> {
PasskeyListScreen(
viewModel = respectViewModel(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package world.respect.app.view.person.deleteaccount

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource
import world.respect.app.components.defaultItemPadding
import world.respect.app.components.uiTextStringResource
import world.respect.shared.generated.resources.Res
import world.respect.shared.generated.resources.delete_headline
import world.respect.shared.generated.resources.delete_account_message
import world.respect.shared.generated.resources.name
import world.respect.shared.generated.resources.permanently_delete
import world.respect.shared.viewmodel.person.deleteaccount.DeleteAccountUiState
import world.respect.shared.viewmodel.person.deleteaccount.DeleteAccountViewModel


@Composable
fun DeleteAccountScreen(
viewModel: DeleteAccountViewModel
) {
val uiState by viewModel.uiState.collectAsState()

DeleteAccountScreen(
uiState = uiState,
onDeleteAccount = viewModel::onDeleteAccount,
onEntityChanged = viewModel::onEntityChanged
)
}

@Composable
fun DeleteAccountScreen(
uiState: DeleteAccountUiState,
onDeleteAccount: () -> Unit = {},
onEntityChanged: (String) -> Unit = {}
) {

Column(
modifier = Modifier
.fillMaxWidth()
.defaultItemPadding()
) {
Spacer(modifier = Modifier.height(16.dp))

Text(
text = stringResource(Res.string.delete_headline),
style = MaterialTheme.typography.titleSmall
)

Spacer(modifier = Modifier.height(16.dp))

uiState.userName?.let {
Text(
text = stringResource(
Res.string.delete_account_message,
it
),
style = MaterialTheme.typography.bodyMedium
)
}

Spacer(modifier = Modifier.height(24.dp))


OutlinedTextField(
modifier = Modifier
.testTag("name")
.fillMaxWidth(),
value = uiState.enteredName,
label = { Text(stringResource(Res.string.name) + "*") },
onValueChange = onEntityChanged,
isError = uiState.userNameError != null,
singleLine = true,
supportingText = {
uiState.userNameError?.let { errorText ->
Text(uiTextStringResource(errorText))
}
}
)

Button(
modifier = Modifier.fillMaxWidth(),
onClick = onDeleteAccount,
enabled = uiState.userNameError == null && uiState.enteredName.isNotBlank()
) {
Text(
text = stringResource(Res.string.permanently_delete)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Password
import androidx.compose.material.icons.filled.Security
import androidx.compose.material3.Icon
Expand Down Expand Up @@ -43,8 +44,8 @@ fun ManageAccountScreen(
onClickManagePasskey = viewModel::onClickManagePasskey,
onClickChangePassword = viewModel::onClickChangePassword,
onClickHowPasskeysWork = viewModel::onClickHowPasskeysWork,
onDeleteAccountClick = viewModel::onDeleteAccount
)

}

@Composable
Expand All @@ -54,6 +55,7 @@ fun ManageAccountScreen(
onClickHowPasskeysWork: () -> Unit = {},
onClickManagePasskey: () -> Unit = {},
onClickChangePassword: () -> Unit = {},
onDeleteAccountClick:()-> Unit={}
) {
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
Expand Down Expand Up @@ -154,6 +156,19 @@ fun ManageAccountScreen(
}
)


ListItem(
modifier = Modifier.clickable {
onDeleteAccountClick()
},
leadingContent = {
Icon(Icons.Default.Delete, contentDescription = null)
},
headlineContent = {
Text(
text = stringResource(Res.string.delete_account),
maxLines = 1,
)
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,25 @@ class PersonDataSourceDb(
}
}

}
override suspend fun delete(guid: String): Boolean {
val guidHash = uidNumberMapper(guid)

return schoolDb.useWriterConnection { con ->
var deleted = false

con.withTransaction(Transactor.SQLiteTransactionType.IMMEDIATE) {

val rows = schoolDb.getPersonEntityDao()
.deletePerson(guidHash.toString())

deleted = rows > 0
}

deleted
}
}



}

Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ interface PersonEntityDao {
)
suspend fun getAllUsers(sourcedId: String): List<PersonEntity>

@Query("""
DELETE FROM PersonEntity
WHERE pGuid = :id
""")
suspend fun deletePerson(id: String): Int

companion object {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ class PersonDataSourceHttp(
}
}

override suspend fun delete(guid: String): Boolean {
TODO("Not yet implemented")
}

override suspend fun store(list: List<Person>) {
httpClient.post(
url = respectEndpointUrl(PersonDataSource.ENDPOINT_NAME)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ class PersonDataSourceRepository(
)
}

override suspend fun delete(guid: String): Boolean {
TODO("Not yet implemented")
}

override suspend fun store(list: List<Person>) {
local.store(list)
val timeNow = systemTimeInMillis()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ interface PersonDataSource: WritableDataSource<Person> {
): IPagingSourceFactory<Int, PersonListDetails>


suspend fun delete(guid: String): Boolean

/**
* Persists the list to the DataSource. The underlying DataSource WILL set the stored time on
* the data. It WILL NOT set the last-modified time (this should be done by the ViewModel or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,12 @@
<string name="email">Email</string>
<string name="phone_number">Phone number</string>
<string name="create_account">Create account</string>
<string name="delete_account">Delete Account</string>
<string name="permanently_delete">Permanently Delete</string>
<string name="delete_headline">This would delete your personal data permanently from our system. You will lose your learning data. Once completed this action cannot be undone.</string>
<string name="delete_account_message">Enter your username (%1$s) below and then tap on 'Permanently delete' button to delete your account.</string>
<string name="username_label">Username</string>
<string name="error_name_mismatched">Error: name mismatched</string>
<string name="manage_account">Manage account</string>
<string name="passkey_not_supported">Passkeys are not supported</string>
<string name="required">Required*</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package world.respect.shared.domain.account.deleteaccount

interface DeleteAccountUseCase {
suspend operator fun invoke(): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package world.respect.shared.domain.account.deleteaccount

import io.github.aakira.napier.Napier
import io.ktor.client.HttpClient
import io.ktor.client.request.post
import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpStatusCode
import io.ktor.http.Url
import world.respect.datalayer.AuthTokenProvider
import world.respect.datalayer.ext.useTokenProvider
import world.respect.datalayer.http.ext.respectEndpointUrl
import world.respect.datalayer.http.school.SchoolUrlBasedDataSource
import world.respect.datalayer.school.PersonDataSource
import world.respect.datalayer.schooldirectory.SchoolDirectoryEntryDataSource

class DeleteAccountUseCaseClient(
private val httpClient: HttpClient,
override val schoolUrl: Url,
private val tokenProvider: AuthTokenProvider,
override val schoolDirectoryEntryDataSource: SchoolDirectoryEntryDataSource
) : DeleteAccountUseCase, SchoolUrlBasedDataSource {

override suspend fun invoke(): Boolean {
return try {
val response: HttpResponse = httpClient.post(
respectEndpointUrl("${PersonDataSource.ENDPOINT_NAME}/delete")
) {
useTokenProvider(tokenProvider)
}

val success = response.status == HttpStatusCode.OK ||
response.status == HttpStatusCode.NoContent

return success

} catch (e: Exception) {
Napier.e("DeleteAccountUseCase: ${e.message}", e)
false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,11 @@ data class ManageAccount(
val guid: String,
) : RespectAppRoute

@Serializable
data class DeleteAccount(
val guid: String,
) : RespectAppRoute

@Serializable
data class PersonEdit(
val guid: String?,
Expand Down
Loading