Skip to content
Snippets Groups Projects
Commit a020aae2 authored by Dariusz Rybicki's avatar Dariusz Rybicki
Browse files

Implement messenger account deletion example

parent 747e6c60
No related branches found
No related tags found
3 merge requests!102Release 1.0.0,!62Messenger example - account deletion,!61Messenger example - account deletion
This commit is part of merge request !62. Comments created here will be created in the context of that merge request.
......@@ -33,6 +33,7 @@ extension AppEnvironment {
home: {
HomeEnvironment(
messenger: messenger,
db: dbManager.getDB,
mainQueue: mainQueue,
bgQueue: bgQueue,
register: {
......
......@@ -68,7 +68,7 @@ extension AppEnvironment {
let appReducer = Reducer<AppState, AppAction, AppEnvironment>
{ state, action, env in
switch action {
case .start, .welcome(.finished), .restore(.finished):
case .start, .welcome(.finished), .restore(.finished), .home(.didDeleteAccount):
state.screen = .loading
return .run { subscriber in
do {
......
import ComposableArchitecture
extension AlertState {
public static func confirmAccountDeletion() -> AlertState<HomeAction> {
AlertState<HomeAction>(
title: TextState("Delete Account"),
message: TextState("This will permanently delete your account and can't be undone."),
buttons: [
.destructive(TextState("Delete"), action: .send(.deleteAccountConfirmed)),
.cancel(TextState("Cancel"))
]
)
}
public static func accountDeletionFailed(_ error: Error) -> AlertState<HomeAction> {
AlertState<HomeAction>(
title: TextState("Error"),
message: TextState(error.localizedDescription),
buttons: []
)
}
}
import AppCore
import Combine
import ComposableArchitecture
import ComposablePresentation
......@@ -9,18 +10,27 @@ import XXMessengerClient
public struct HomeState: Equatable {
public init(
failure: String? = nil,
register: RegisterState? = nil
register: RegisterState? = nil,
alert: AlertState<HomeAction>? = nil,
isDeletingAccount: Bool = false
) {
self.failure = failure
self.register = register
self.alert = alert
self.isDeletingAccount = isDeletingAccount
}
@BindableState public var failure: String?
@BindableState public var register: RegisterState?
@BindableState public var alert: AlertState<HomeAction>?
@BindableState public var isDeletingAccount: Bool
}
public enum HomeAction: Equatable, BindableAction {
case start
case deleteAccountButtonTapped
case deleteAccountConfirmed
case didDeleteAccount
case binding(BindingAction<HomeState>)
case register(RegisterAction)
}
......@@ -28,17 +38,20 @@ public enum HomeAction: Equatable, BindableAction {
public struct HomeEnvironment {
public init(
messenger: Messenger,
db: DBManagerGetDB,
mainQueue: AnySchedulerOf<DispatchQueue>,
bgQueue: AnySchedulerOf<DispatchQueue>,
register: @escaping () -> RegisterEnvironment
) {
self.messenger = messenger
self.db = db
self.mainQueue = mainQueue
self.bgQueue = bgQueue
self.register = register
}
public var messenger: Messenger
public var db: DBManagerGetDB
public var mainQueue: AnySchedulerOf<DispatchQueue>
public var bgQueue: AnySchedulerOf<DispatchQueue>
public var register: () -> RegisterEnvironment
......@@ -47,6 +60,7 @@ public struct HomeEnvironment {
extension HomeEnvironment {
public static let unimplemented = HomeEnvironment(
messenger: .unimplemented,
db: .unimplemented,
mainQueue: .unimplemented,
bgQueue: .unimplemented,
register: { .unimplemented }
......@@ -83,6 +97,38 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment>
.receive(on: env.mainQueue)
.eraseToEffect()
case .deleteAccountButtonTapped:
state.alert = .confirmAccountDeletion()
return .none
case .deleteAccountConfirmed:
state.isDeletingAccount = true
return .run { subscriber in
do {
let contactId = try env.messenger.e2e.tryGet().getContact().getId()
let contact = try env.db().fetchContacts(.init(id: [contactId])).first
if let username = contact?.username {
let ud = try env.messenger.ud.tryGet()
try ud.permanentDeleteAccount(username: Fact(fact: username, type: 0))
}
try env.messenger.destroy()
try env.db().drop()
subscriber.send(.didDeleteAccount)
} catch {
subscriber.send(.set(\.$isDeletingAccount, false))
subscriber.send(.set(\.$alert, .accountDeletionFailed(error)))
}
subscriber.send(completion: .finished)
return AnyCancellable {}
}
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
case .didDeleteAccount:
state.isDeletingAccount = false
return .none
case .register(.finished):
state.register = nil
return Effect(value: .start)
......
......@@ -12,9 +12,11 @@ public struct HomeView: View {
struct ViewState: Equatable {
var failure: String?
var isDeletingAccount: Bool
init(state: HomeState) {
failure = state.failure
isDeletingAccount = state.isDeletingAccount
}
}
......@@ -34,8 +36,29 @@ public struct HomeView: View {
Text("Error")
}
}
Section {
Button(role: .destructive) {
viewStore.send(.deleteAccountButtonTapped)
} label: {
HStack {
Text("Delete Account")
Spacer()
if viewStore.isDeletingAccount {
ProgressView()
}
}
}
.disabled(viewStore.isDeletingAccount)
} header: {
Text("Account")
}
}
.navigationTitle("Home")
.alert(
store.scope(state: \.alert),
dismiss: HomeAction.set(\.$alert, nil)
)
}
.navigationViewStyle(.stack)
.task { viewStore.send(.start) }
......
......@@ -141,6 +141,36 @@ final class AppFeatureTests: XCTestCase {
}
}
func testHomeDidDeleteAccount() {
let store = TestStore(
initialState: AppState(
screen: .home(HomeState())
),
reducer: appReducer,
environment: .unimplemented
)
let mainQueue = DispatchQueue.test
let bgQueue = DispatchQueue.test
store.environment.mainQueue = mainQueue.eraseToAnyScheduler()
store.environment.bgQueue = bgQueue.eraseToAnyScheduler()
store.environment.dbManager.hasDB.run = { true }
store.environment.messenger.isLoaded.run = { false }
store.environment.messenger.isCreated.run = { false }
store.send(.home(.didDeleteAccount)) {
$0.screen = .loading
}
bgQueue.advance()
mainQueue.advance()
store.receive(.set(\.$screen, .welcome(WelcomeState()))) {
$0.screen = .welcome(WelcomeState())
}
}
func testWelcomeRestoreTapped() {
let store = TestStore(
initialState: AppState(
......
......@@ -3,6 +3,7 @@ import RegisterFeature
import XCTest
import XXClient
import XXMessengerClient
import XXModels
@testable import HomeFeature
final class HomeFeatureTests: XCTestCase {
......@@ -201,4 +202,112 @@ final class HomeFeatureTests: XCTestCase {
$0.failure = error.localizedDescription
}
}
func testAccountDeletion() {
let store = TestStore(
initialState: HomeState(),
reducer: homeReducer,
environment: .unimplemented
)
var dbDidFetchContacts: [XXModels.Contact.Query] = []
var udDidPermanentDeleteAccount: [Fact] = []
var messengerDidDestroy = 0
var dbDidDrop = 0
store.environment.bgQueue = .immediate
store.environment.mainQueue = .immediate
store.environment.messenger.e2e.get = {
var e2e: E2E = .unimplemented
e2e.getContact.run = {
var contact = Contact.unimplemented("contact-data".data(using: .utf8)!)
contact.getIdFromContact.run = { _ in "contact-id".data(using: .utf8)! }
return contact
}
return e2e
}
store.environment.db.run = {
var db: Database = .failing
db.fetchContacts.run = { query in
dbDidFetchContacts.append(query)
return [
XXModels.Contact(
id: "contact-id".data(using: .utf8)!,
marshaled: "contact-data".data(using: .utf8)!,
username: "MyUsername"
)
]
}
db.drop.run = {
dbDidDrop += 1
}
return db
}
store.environment.messenger.ud.get = {
var ud: UserDiscovery = .unimplemented
ud.permanentDeleteAccount.run = { usernameFact in
udDidPermanentDeleteAccount.append(usernameFact)
}
return ud
}
store.environment.messenger.destroy.run = {
messengerDidDestroy += 1
}
store.send(.deleteAccountButtonTapped) {
$0.alert = .confirmAccountDeletion()
}
store.send(.set(\.$alert, nil)) {
$0.alert = nil
}
store.send(.deleteAccountConfirmed) {
$0.isDeletingAccount = true
}
XCTAssertNoDifference(dbDidFetchContacts, [.init(id: ["contact-id".data(using: .utf8)!])])
XCTAssertNoDifference(udDidPermanentDeleteAccount, [Fact(fact: "MyUsername", type: 0)])
XCTAssertNoDifference(messengerDidDestroy, 1)
XCTAssertNoDifference(dbDidDrop, 1)
store.receive(.didDeleteAccount) {
$0.isDeletingAccount = false
}
}
func testAccountDeletionFailure() {
let store = TestStore(
initialState: HomeState(),
reducer: homeReducer,
environment: .unimplemented
)
struct Failure: Error {}
let error = Failure()
store.environment.bgQueue = .immediate
store.environment.mainQueue = .immediate
store.environment.messenger.e2e.get = {
var e2e: E2E = .unimplemented
e2e.getContact.run = {
var contact = Contact.unimplemented("contact-data".data(using: .utf8)!)
contact.getIdFromContact.run = { _ in throw error }
return contact
}
return e2e
}
store.send(.deleteAccountConfirmed) {
$0.isDeletingAccount = true
}
store.receive(.set(\.$isDeletingAccount, false)) {
$0.isDeletingAccount = false
}
store.receive(.set(\.$alert, .accountDeletionFailed(error))) {
$0.alert = .accountDeletionFailed(error)
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment