Skip to content

Commit

Permalink
[ABW-3328] Fix NPSSurvey presentation bug (#1132)
Browse files Browse the repository at this point in the history
  • Loading branch information
matiasbzurovski committed Jul 3, 2024
1 parent 457000b commit c92125a
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,35 +26,9 @@ extension DappInteractor {
func body(content: Content) -> some SwiftUI.View {
ZStack {
content
WithViewStore(store, observe: { $0.currentModal }) { viewStore in
IfLetStore(
store.scope(state: \.$currentModal, action: { .child(.modal($0)) }),
state: /DappInteractor.Modal.State.dappInteraction,
action: DappInteractor.Modal.Action.dappInteraction,
then: { DappInteractionCoordinator.View(store: $0) }
)
.transition(.move(edge: .bottom))
.animation(.linear, value: viewStore.state)
}
dappInteraction
}
.sheet(
store: store.scope(state: \.$currentModal, action: { .child(.modal($0)) }),
state: /DappInteractor.Modal.State.dappInteractionCompletion,
action: DappInteractor.Modal.Action.dappInteractionCompletion,
content: { Completion.View(store: $0) }
)
.alert(
store: store.scope(
state: \.$invalidRequestAlert,
action: { .view(.invalidRequestAlert($0)) }
)
)
.alert(
store: store.scope(
state: \.$responseFailureAlert,
action: { .view(.responseFailureAlert($0)) }
)
)
.destinations(with: store)
.task {
await store.send(.view(.task)).finish()
}
Expand All @@ -65,6 +39,56 @@ extension DappInteractor {
store.send(.view(.moveToBackground))
}
}

@MainActor
private var dappInteraction: some SwiftUI.View {
WithViewStore(store, observe: { $0.destination }) { viewStore in
IfLetStore(
store.destination,
state: /DappInteractor.Destination.State.dappInteraction,
action: DappInteractor.Destination.Action.dappInteraction,
then: { DappInteractionCoordinator.View(store: $0) }
)
.transition(.move(edge: .bottom))
.animation(.linear, value: viewStore.state)
}
}
}
}

private extension StoreOf<DappInteractor> {
var destination: PresentationStoreOf<DappInteractor.Destination> {
func scopeState(state: State) -> PresentationState<DappInteractor.Destination.State> {
state.$destination
}
return scope(state: scopeState, action: Action.destination)
}
}

@MainActor
private extension View {
func destinations(with store: StoreOf<DappInteractor>) -> some View {
let destinationStore = store.destination
return dappInteractionCompletion(with: destinationStore, store: store)
.invalidRequestAlert(with: destinationStore)
.responseFailureAlert(with: destinationStore)
}

private func dappInteractionCompletion(with destinationStore: PresentationStoreOf<DappInteractor.Destination>, store: StoreOf<DappInteractor>) -> some View {
sheet(
store: destinationStore.scope(state: \.dappInteractionCompletion, action: \.dappInteractionCompletion),
onDismiss: { store.send(.view(.completionDismissed)) }
) {
Completion.View(store: $0)
}
}

private func invalidRequestAlert(with destinationStore: PresentationStoreOf<DappInteractor.Destination>) -> some View {
alert(store: destinationStore.scope(state: \.invalidRequest, action: \.invalidRequest))
}

private func responseFailureAlert(with destinationStore: PresentationStoreOf<DappInteractor.Destination>) -> some View {
alert(store: destinationStore.scope(state: \.responseFailure, action: \.responseFailure))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,16 @@ struct DappInteractor: Sendable, FeatureReducer {
var requestQueue: IdentifiedArrayOf<RequestEnvelope> = []

@PresentationState
var currentModal: Modal.State?
public var destination: Destination.State?

@PresentationState
var responseFailureAlert: AlertState<ViewAction.ResponseFailureAlertAction>?

@PresentationState
var invalidRequestAlert: AlertState<ViewAction.InvalidRequestAlertAction>?
fileprivate var shouldIncrementOnCompletionDismiss = false
}

enum ViewAction: Sendable, Equatable {
case task
case moveToBackground
case moveToForeground
case responseFailureAlert(PresentationAction<ResponseFailureAlertAction>)
case invalidRequestAlert(PresentationAction<InvalidRequestAlertAction>)

enum ResponseFailureAlertAction: Sendable, Hashable {
case cancelButtonTapped(RequestEnvelope)
case retryButtonTapped(WalletToDappInteractionResponse, for: RequestEnvelope, DappMetadata)
}

enum InvalidRequestAlertAction: Sendable, Hashable {
case ok(P2P.RTCOutgoingMessage.Response, origin: P2P.Route)
}
case completionDismissed
}

enum InternalAction: Sendable, Equatable {
Expand Down Expand Up @@ -73,26 +59,37 @@ struct DappInteractor: Sendable, FeatureReducer {
)
}

enum ChildAction: Sendable, Equatable {
case modal(PresentationAction<Modal.Action>)
}

struct Modal: Sendable, Reducer {
struct Destination: Sendable, DestinationReducer {
@CasePathable
enum State: Sendable, Hashable {
case dappInteraction(DappInteractionCoordinator.State)
case dappInteractionCompletion(Completion.State)
case responseFailure(AlertState<Action.ResponseFailure>)
case invalidRequest(AlertState<Action.InvalidRequest>)
}

@CasePathable
enum Action: Sendable, Equatable {
case dappInteraction(DappInteractionCoordinator.Action)
case dappInteractionCompletion(Completion.Action)
case responseFailure(ResponseFailure)
case invalidRequest(InvalidRequest)

enum ResponseFailure: Sendable, Hashable {
case cancelButtonTapped(RequestEnvelope)
case retryButtonTapped(WalletToDappInteractionResponse, for: RequestEnvelope, DappMetadata)
}

enum InvalidRequest: Sendable, Hashable {
case ok(P2P.RTCOutgoingMessage.Response, origin: P2P.Route)
}
}

var body: some ReducerOf<Self> {
Scope(state: /State.dappInteraction, action: /Action.dappInteraction) {
Scope(state: \.dappInteraction, action: \.dappInteraction) {
DappInteractionCoordinator()
}
Scope(state: /State.dappInteractionCompletion, action: /Action.dappInteractionCompletion) {
Scope(state: \.dappInteractionCompletion, action: \.dappInteractionCompletion) {
Completion()
}
}
Expand All @@ -106,47 +103,22 @@ struct DappInteractor: Sendable, FeatureReducer {
@Dependency(\.rolaClient) var rolaClient
@Dependency(\.appPreferencesClient) var appPreferencesClient
@Dependency(\.dappInteractionClient) var dappInteractionClient
@Dependency(\.npsSurveyClient) var npsSurveyClient

var body: some ReducerOf<Self> {
Reduce(core)
.ifLet(\.$currentModal, action: /Action.child .. ChildAction.modal) {
Modal()
.ifLet(destinationPath, action: /Action.destination) {
Destination()
}
.ifLet(\.$responseFailureAlert, action: /Action.view .. ViewAction.responseFailureAlert)
.ifLet(\.$invalidRequestAlert, action: /Action.view .. ViewAction.invalidRequestAlert)
}

private let destinationPath: WritableKeyPath<State, PresentationState<Destination.State>> = \.$destination

func reduce(into state: inout State, viewAction: ViewAction) -> Effect<Action> {
switch viewAction {
case .task:
return handleIncomingRequests()

case let .responseFailureAlert(action):
switch action {
case .dismiss:
return .none
case let .presented(.cancelButtonTapped(request)):
dismissCurrentModalAndRequest(request, for: &state)
return .send(.internal(.presentQueuedRequestIfNeeded))
case let .presented(.retryButtonTapped(response, request, dappMetadata)):
return sendResponseToDappEffect(response, for: request, dappMetadata: dappMetadata)
}

case let .invalidRequestAlert(action):
switch action {
case .dismiss:
return .none
case let .presented(.ok(response, route)):
return .run { send in
do {
try await dappInteractionClient.completeInteraction(.response(response, origin: route))
} catch {
errorQueue.schedule(error)
}
await send(.internal(.presentQueuedRequestIfNeeded))
}
}

case .moveToBackground:
return .run { _ in
await radixConnectClient.disconnectAll()
Expand All @@ -156,18 +128,24 @@ struct DappInteractor: Sendable, FeatureReducer {
return .run { _ in
_ = await radixConnectClient.loadP2PLinksAndConnectAll()
}

case .completionDismissed:
if state.shouldIncrementOnCompletionDismiss {
npsSurveyClient.incrementTransactionCompleteCounter()
}
return .none
}
}

func reduce(into state: inout State, internalAction: InternalAction) -> Effect<Action> {
switch internalAction {
case let .receivedRequestFromDapp(request):

switch state.currentModal {
switch state.destination {
case .some(.dappInteractionCompletion):
// FIXME: this is a temporary hack, to solve bug where incoming requests
// are ignored since completion is believed to be shown, but is not.
state.currentModal = nil
state.destination = nil
default: break
}

Expand All @@ -192,7 +170,7 @@ struct DappInteractor: Sendable, FeatureReducer {
return .send(.internal(.presentResponseFailureAlert(response, for: request, metadata, reason: reason)))

case let .presentResponseFailureAlert(response, for: request, dappMetadata, reason):
state.responseFailureAlert = .init(
state.destination = .responseFailure(.init(
title: { TextState(L10n.Common.errorAlertTitle) },
actions: {
ButtonState(role: .cancel, action: .cancelButtonTapped(request)) {
Expand All @@ -209,7 +187,8 @@ struct DappInteractor: Sendable, FeatureReducer {
TextState(L10n.DAppRequest.ResponseFailureAlert.message)
#endif
}
)
))

return .none

case let .presentInvalidRequest(invalidRequest, reason, route, isDeveloperModeEnabled):
Expand All @@ -219,7 +198,7 @@ struct DappInteractor: Sendable, FeatureReducer {
message: reason.responseMessage()
)

state.invalidRequestAlert = .init(
state.destination = .invalidRequest(.init(
title: { TextState(L10n.Error.DappRequest.invalidRequest) },
actions: {
ButtonState(
Expand All @@ -230,11 +209,12 @@ struct DappInteractor: Sendable, FeatureReducer {
}
},
message: { TextState(reason.alertMessage(isDeveloperModeEnabled)) }
)
))
return .none

case let .presentResponseSuccessView(dappMetadata, txID, p2pRoute):
state.currentModal = .dappInteractionCompletion(
state.shouldIncrementOnCompletionDismiss = txID != nil
state.destination = .dappInteractionCompletion(
.init(
txID: txID,
dappMetadata: dappMetadata,
Expand All @@ -245,10 +225,10 @@ struct DappInteractor: Sendable, FeatureReducer {
}
}

func reduce(into state: inout State, childAction: ChildAction) -> Effect<Action> {
switch childAction {
case let .modal(.presented(.dappInteraction(.delegate(delegateAction)))):
guard case let .dappInteraction(dappInteraction) = state.currentModal else {
func reduce(into state: inout State, presentedAction: Destination.Action) -> Effect<Action> {
switch presentedAction {
case let .dappInteraction(.delegate(delegateAction)):
guard case let .dappInteraction(dappInteraction) = state.destination else {
let message = "We should only get actions from this modal if it is showing"
assertionFailure(message)
loggerGlobal.error(.init(stringLiteral: message))
Expand All @@ -267,10 +247,32 @@ struct DappInteractor: Sendable, FeatureReducer {
return delayedMediumEffect(internal: .presentQueuedRequestIfNeeded)
}

case .modal(.presented(.dappInteractionCompletion(.delegate(.dismiss)))):
state.currentModal = nil
case .dappInteractionCompletion(.delegate(.dismiss)):
state.destination = nil
return delayedMediumEffect(internal: .presentQueuedRequestIfNeeded)

case let .responseFailure(action):
switch action {
case let .cancelButtonTapped(request):
dismissCurrentModalAndRequest(request, for: &state)
return .send(.internal(.presentQueuedRequestIfNeeded))
case let .retryButtonTapped(response, request, dappMetadata):
return sendResponseToDappEffect(response, for: request, dappMetadata: dappMetadata)
}

case let .invalidRequest(action):
switch action {
case let .ok(response, route):
return .run { send in
do {
try await dappInteractionClient.completeInteraction(.response(response, origin: route))
} catch {
errorQueue.schedule(error)
}
await send(.internal(.presentQueuedRequestIfNeeded))
}
}

default:
return .none
}
Expand All @@ -281,12 +283,12 @@ struct DappInteractor: Sendable, FeatureReducer {
) -> Effect<Action> {
guard
let next = state.requestQueue.first,
state.currentModal == nil
state.destination == nil
else {
return .none
}

state.currentModal = .dappInteraction(.init(request: next))
state.destination = .dappInteraction(.init(request: next))

return .none
}
Expand Down Expand Up @@ -343,7 +345,7 @@ struct DappInteractor: Sendable, FeatureReducer {

func dismissCurrentModalAndRequest(_ request: RequestEnvelope, for state: inout State) {
state.requestQueue.remove(id: request.id)
state.currentModal = nil
state.destination = nil
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ public struct SubmitTransaction: Sendable, FeatureReducer {
@Dependency(\.submitTXClient) var submitTXClient
@Dependency(\.errorQueue) var errorQueue
@Dependency(\.accountPortfoliosClient) var accountPortfoliosClient
@Dependency(\.npsSurveyClient) var npsSurveyClient

public init() {}

Expand Down Expand Up @@ -163,7 +162,6 @@ public struct SubmitTransaction: Sendable, FeatureReducer {
private func transactionCommittedSuccesfully(_ state: State) -> Effect<Action> {
// TODO: Could probably be moved in other place. TransactionClient? AccountPortfolio?
accountPortfoliosClient.updateAfterCommittedTransaction(state.notarizedTX.intent)
npsSurveyClient.incrementTransactionCompleteCounter()
return .send(.delegate(.committedSuccessfully(state.notarizedTX.txID)))
}
}
Expand Down

0 comments on commit c92125a

Please sign in to comment.