Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions DashWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,7 @@
BD509A68B1A0B6E4599F9C7A /* DWProfileUpdateCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82926C69F3BB675BEF1A1FFD /* DWProfileUpdateCoordinator.swift */; };
C05FA6CE3DDE2D0327D334E5 /* CoinJoinRecovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = B478AD92C1F1C11D09645DF4 /* CoinJoinRecovery.swift */; };
C0DE5A0126C7000000000001 /* SwiftDashSDKCoreLifecycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE5A0126C7000000000002 /* SwiftDashSDKCoreLifecycleTests.swift */; };
C0DE5A1126C7000000000011 /* UIViewControllerUserActionGateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE5A1026C7000000000012 /* UIViewControllerUserActionGateTests.swift */; };
C124DF94278D4238FAE0975D /* OrderPreviewFeeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D1A08496F925D17031493E /* OrderPreviewFeeRow.swift */; };
C1A0B2C3D4E5F60718293A02 /* ShortcutsBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A0B2C3D4E5F60718293A01 /* ShortcutsBarView.swift */; };
C1A0B2C3D4E5F60718293A03 /* ShortcutsBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A0B2C3D4E5F60718293A01 /* ShortcutsBarView.swift */; };
Expand Down Expand Up @@ -3142,6 +3143,7 @@
BCD0199D74601A6150ABC8D1 /* WalletAccountDetailViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletAccountDetailViewModel.swift; sourceTree = "<group>"; };
C0D1A08496F925D17031493E /* OrderPreviewFeeRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OrderPreviewFeeRow.swift; sourceTree = "<group>"; };
C0DE5A0126C7000000000002 /* SwiftDashSDKCoreLifecycleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDashSDKCoreLifecycleTests.swift; sourceTree = "<group>"; };
C0DE5A1026C7000000000012 /* UIViewControllerUserActionGateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerUserActionGateTests.swift; sourceTree = "<group>"; };
C1A0B2C3D4E5F60718293A01 /* ShortcutsBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsBarView.swift; sourceTree = "<group>"; };
C1D2E3F4A5B6C7D8E9F01234 /* TransferAmountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferAmountView.swift; sourceTree = "<group>"; };
C3DAD266246AA6F10001624F /* DWScreenshotWarningViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DWScreenshotWarningViewController.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -7070,6 +7072,7 @@
A17EED0126C7000000000002 /* WalletWipeSerialExecutorTests.swift */,
B7C072AA26C7000000000002 /* DWContestedNameStatusServiceTests.swift */,
C0DE5A0126C7000000000002 /* SwiftDashSDKCoreLifecycleTests.swift */,
C0DE5A1026C7000000000012 /* UIViewControllerUserActionGateTests.swift */,
CB9000012FE1000000000001 /* CoinbaseTransactionMetadataTests.swift */,
CB9100012FE2000000000001 /* CoinbaseTransferAmountTests.swift */,
7A30000130A1000000000001 /* TransactionDirectionTests.swift */,
Expand Down Expand Up @@ -10248,6 +10251,7 @@
A17EED0126C7000000000001 /* WalletWipeSerialExecutorTests.swift in Sources */,
B7C072AA26C7000000000001 /* DWContestedNameStatusServiceTests.swift in Sources */,
C0DE5A0126C7000000000001 /* SwiftDashSDKCoreLifecycleTests.swift in Sources */,
C0DE5A1126C7000000000011 /* UIViewControllerUserActionGateTests.swift in Sources */,
CB9000022FE1000000000002 /* CoinbaseTransactionMetadataTests.swift in Sources */,
CB9100022FE2000000000002 /* CoinbaseTransferAmountTests.swift in Sources */,
7A30000230A1000000000002 /* TransactionDirectionTests.swift in Sources */,
Expand Down
50 changes: 49 additions & 1 deletion DashWallet/Sources/Categories/UIViewController+DashWallet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,58 @@

import UIKit
import MessageUI
import ObjectiveC
import SwiftDashSDK

@objc
private final class UserActionGate {
private var isInProgress = false

func begin() -> Bool {
guard !isInProgress else { return false }

isInProgress = true
return true
}

func end() {
isInProgress = false
}
}

private var userActionGateKey: UInt8 = 0

extension UIViewController {
/// Starts a UI action only when this controller does not already have one
/// in flight. Call this synchronously, before navigation, presentation, or
/// an asynchronous operation can yield back to the main run loop.
///
/// End the action when a recoverable failure/cancel occurs or when the
/// source controller becomes visible again. Successful one-way transitions
/// intentionally leave it acquired so queued taps cannot replay afterward.
@objc(dw_beginExclusiveUserAction)
func dw_beginExclusiveUserAction() -> Bool {
userActionGate.begin()
}

@objc(dw_endExclusiveUserAction)
func dw_endExclusiveUserAction() {
userActionGate.end()
}

private var userActionGate: UserActionGate {
if let gate = objc_getAssociatedObject(self, &userActionGateKey) as? UserActionGate {
return gate
}

let gate = UserActionGate()
objc_setAssociatedObject(
self,
&userActionGateKey,
gate,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return gate
}

@objc
func topController() -> UIViewController {
if let vc = self as? UITabBarController {
Expand Down
63 changes: 61 additions & 2 deletions DashWallet/Sources/UI/LockScreen/DWLockScreenViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#import "DWLockActionButton.h"
#import "DWLockPinInputView.h"
#import "DWLockScreenModel.h"
#import "DWQRScanModel.h"
#import "DWRecoverViewController.h"
#import "DWSetPinViewController.h"
#import "DWUIKit.h"
Expand Down Expand Up @@ -70,7 +71,8 @@ static CGFloat ActionButtonsHeight(void) {
@interface DWLockScreenViewController () <DWLockPinInputViewDelegate,
DWLockScreenModelDelegate,
DWRecoverViewControllerDelegate,
DWSetPinViewControllerDelegate>
DWSetPinViewControllerDelegate,
UIAdaptivePresentationControllerDelegate>

@property (strong, nonatomic) DWLockScreenModel *model;

Expand All @@ -87,6 +89,9 @@ @interface DWLockScreenViewController () <DWLockPinInputViewDelegate,

@property (nonatomic, assign) BOOL biometricsAuthorizationAttemptWasMade;

- (BOOL)beginExclusiveUserAction;
- (void)endExclusiveUserAction;

@end

@implementation DWLockScreenViewController
Expand Down Expand Up @@ -138,6 +143,8 @@ - (void)viewWillAppear:(BOOL)animated {
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];

[self endExclusiveUserAction];

if (self.unlockMode == DWLockScreenViewControllerUnlockMode_Instantly) {
[self tryOnceToUnlockUsingBiometrics];
}
Expand All @@ -152,6 +159,10 @@ - (void)viewWillDisappear:(BOOL)animated {
#pragma mark - Actions

- (IBAction)forgotPinButtonAction:(UIButton *)sender {
if (![self beginExclusiveUserAction]) {
return;
}

[self.model stopCheckingAuthState];

// No PIN record at all (partial keychain restore, interrupted setup):
Expand Down Expand Up @@ -188,6 +199,7 @@ - (IBAction)forgotPinButtonAction:(UIButton *)sender {
style:UIAlertActionStyleCancel
handler:^(UIAlertAction *action) {
[self.model startCheckingAuthState];
[self endExclusiveUserAction];
}]];
[self presentViewController:sheet animated:YES completion:nil];
}
Expand Down Expand Up @@ -219,6 +231,7 @@ - (void)forgotPinRecoveryCancelAction:(id)sender {
[self dismissViewControllerAnimated:YES
completion:^{
[self.model startCheckingAuthState];
[self endExclusiveUserAction];
}];
}

Expand All @@ -231,6 +244,7 @@ - (void)confirmWipeWallet {
style:UIAlertActionStyleCancel
handler:^(UIAlertAction *action) {
[self.model startCheckingAuthState];
[self endExclusiveUserAction];
}]];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Wipe wallet", nil)
style:UIAlertActionStyleDestructive
Expand Down Expand Up @@ -308,20 +322,44 @@ - (void)setPinViewControllerDidCancel:(DWSetPinViewController *)controller {
}

- (IBAction)receiveButtonAction:(DWLockActionButton *)sender {
if (![self beginExclusiveUserAction]) {
return;
}

// SwiftUI receive surface (Transparent / Platform / Shielded toggle),
// narrowed to the Receive tab for the locked context.
UIViewController *controller = [DWPaymentsLandingHostingController quickReceiveController];
[self presentViewController:controller animated:YES completion:nil];
controller.presentationController.delegate = self;
}

- (IBAction)loginButtonAction:(DWLockActionButton *)sender {
[self performBiometricAuthentication];
}

- (IBAction)scanToPayButtonAction:(DWLockActionButton *)sender {
if (![self beginExclusiveUserAction]) {
return;
}

[self performScanQRCodeAction];
}

#pragma mark - UIAdaptivePresentationControllerDelegate

- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController {
[self endExclusiveUserAction];
}

#pragma mark - DWQRScanModelDelegate

- (void)qrScanModelDidCancel:(DWQRScanModel *)viewModel {
[self dismissViewControllerAnimated:YES
completion:^{
[self endExclusiveUserAction];
}];
}

#pragma mark - DWNavigationFullscreenable

- (BOOL)requiresNoNavigationBar {
Expand Down Expand Up @@ -388,7 +426,9 @@ - (void)lockScreenModel:(DWLockScreenModel *)model
- (void)lockPinInputView:(DWLockPinInputView *)view didFinishInputWithText:(NSString *)text {
BOOL isPinValid = [self.model checkPin:text];
if (isPinValid) {
[self.delegate lockScreenViewControllerDidUnlock:self];
if ([self beginExclusiveUserAction]) {
[self.delegate lockScreenViewControllerDidUnlock:self];
}
}
else {
[view clearAndShakePinField];
Expand Down Expand Up @@ -481,17 +521,36 @@ - (void)performBiometricAuthentication {
}

if (self.model.isBiometricAuthenticationAllowed) {
if (![self beginExclusiveUserAction]) {
return;
}

[self.model authenticateUsingBiometricsOnlyCompletion:^(BOOL authenticated) {
if (authenticated) {
[self.delegate lockScreenViewControllerDidUnlock:self];
}
else {
[self endExclusiveUserAction];
[self hideLoginButtonIfNeeded];
}
}];
}
}

- (BOOL)beginExclusiveUserAction {
if (![self dw_beginExclusiveUserAction]) {
return NO;
}

self.view.userInteractionEnabled = NO;
return YES;
}

- (void)endExclusiveUserAction {
[self dw_endExclusiveUserAction];
self.view.userInteractionEnabled = YES;
}

- (void)hideLoginButtonIfNeeded {
self.loginButton.hidden = !self.model.hasPinSet || !self.model.isBiometricAuthenticationAllowed;
}
Expand Down
5 changes: 5 additions & 0 deletions DashWallet/Sources/UI/Onboarding/DWOnboardingViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ - (void)viewDidLayoutSubviews {
#pragma mark - Actions

- (IBAction)skipButtonAction:(id)sender {
if (![self dw_beginExclusiveUserAction]) {
return;
}
self.view.userInteractionEnabled = NO;

[self.delegate onboardingViewControllerDidFinish:self];
}

Expand Down
13 changes: 9 additions & 4 deletions DashWallet/Sources/UI/Payments/PaymentsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ class PaymentsViewController: BaseViewController {

@IBAction
func closeButtonAction() {
guard dw_beginExclusiveUserAction() else { return }
view.isUserInteractionEnabled = false
delegate?.paymentsViewControllerDidCancel(self)
}

Expand All @@ -108,6 +110,13 @@ class PaymentsViewController: BaseViewController {
configureHierarchy()
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

dw_endExclusiveUserAction()
view.isUserInteractionEnabled = true
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

Expand All @@ -119,10 +128,6 @@ class PaymentsViewController: BaseViewController {
}
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}

class func controller() -> PaymentsViewController {
sb("Payments").vc(PaymentsViewController.self)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ @interface DWAppRootViewController () <DWSetupViewControllerDelegate,
@property (nullable, nonatomic, strong) NSURL *deferredURLToProcess;
@property (nullable, nonatomic, strong) NSURL *deferredDeeplinkToProcess;
@property (nonatomic, assign) BOOL walletWipeInProgress;
@property (nonatomic, assign) BOOL setupCompletionInProgress;
#if DASHPAY
@property (null_resettable, nonatomic, strong) DWInvitationSetupState *invitationSetup;
#endif
Expand Down Expand Up @@ -325,6 +326,11 @@ - (void)presentInitialControllerWhenKeyMigrationSettles:(NSDate *)deadline {
#pragma mark - DWSetupViewControllerDelegate

- (void)setupViewControllerDidFinish:(DWSetupViewController *)controller {
if (self.setupCompletionInProgress) {
return;
}
self.setupCompletionInProgress = YES;

[self.model setupDidFinish];

UIViewController *mainController = self.mainController;
Expand All @@ -345,6 +351,8 @@ - (void)setupViewControllerDidFinish:(DWSetupViewController *)controller {
#pragma mark - DWWipeDelegate

- (void)didWipeWallet {
self.setupCompletionInProgress = NO;

UIViewController *setupController = [self setupController];
[self transitionToController:setupController
transitionType:DWContainerTransitionType_ScaleAndCrossDissolve];
Expand All @@ -365,6 +373,7 @@ - (void)beginWipeWallet {
return;
}
self.walletWipeInProgress = YES;
self.setupCompletionInProgress = NO;

UIViewController *setupController = [self setupController];
[self transitionToController:setupController
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
@interface DWInitialViewController () <DWOnboardingViewControllerDelegate>

@property (nonatomic, assign) BOOL launchingWasDeferred;
@property (nonatomic, assign) BOOL onboardingCompletionInProgress;
@property (nullable, nonatomic, strong) DWAppRootViewController *rootController;

#if DASHPAY
Expand Down Expand Up @@ -92,6 +93,11 @@ - (void)handleURL:(NSURL *)url {
#pragma mark - DWOnboardingViewControllerDelegate

- (void)onboardingViewControllerDidFinish:(DWOnboardingViewController *)controller {
if (self.onboardingCompletionInProgress) {
return;
}
self.onboardingCompletionInProgress = YES;

[self onboardingDidFinish];

// Reinstall detection: if SDK wallet keychain material survived while
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#import "DWBiometricAuthModel.h"
#import "DWUIKit.h"
#import "dashwallet-Swift.h"

NS_ASSUME_NONNULL_BEGIN

Expand Down Expand Up @@ -63,6 +64,9 @@ - (BOOL)requiresNoNavigationBar {
#pragma mark - Actions

- (IBAction)enableBiometricButtonAction:(id)sender {
if (![self dw_beginExclusiveUserAction]) {
return;
}
self.view.userInteractionEnabled = NO;

__weak typeof(self) weakSelf = self;
Expand All @@ -82,6 +86,11 @@ - (IBAction)enableBiometricButtonAction:(id)sender {
}

- (IBAction)skipBiometricButtonAction:(id)sender {
if (![self dw_beginExclusiveUserAction]) {
return;
}
self.view.userInteractionEnabled = NO;

[self.model disableBiometricAuth];
[self.delegate biometricAuthViewControllerDidFinish:self];
}
Expand Down
Loading
Loading