mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-28 12:29:51 +00:00
CW-1045-Ledger-Bugs-Enhancements (#2278)
* feat: prepare ledger.dart to use callbacks * feat: set ledger callback using monero_c * fix(cw_monero): async ledger * build: Bump monero_c dependencies * feat: Add "How to connect" to HW Device selection screen * refactor: use monero_c free to clean pointer * fix: use new monero_c deps * fix: merge conflicts regarding new theming * feat: add status bottomsheet indicating an ongoing signing process * fix: getLastLedgerCommand monero.dart generation * reformat send_view_model.dart [skip ci] --------- Co-authored-by: Czarek Nakamoto <cyjan@mrcyjanek.net> Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
parent
e03dcc7fe9
commit
90aee053cd
46 changed files with 660 additions and 315 deletions
|
@ -421,6 +421,7 @@ class CWMonero extends Monero {
|
|||
moneroWallet.setLedgerConnection(connection);
|
||||
}
|
||||
|
||||
@override
|
||||
void resetLedgerConnection() {
|
||||
disableLedgerExchange();
|
||||
}
|
||||
|
@ -428,9 +429,11 @@ class CWMonero extends Monero {
|
|||
@override
|
||||
void setGlobalLedgerConnection(ledger.LedgerConnection connection) {
|
||||
gLedger = connection;
|
||||
keepAlive(connection);
|
||||
}
|
||||
|
||||
@override
|
||||
String? getLastLedgerCommand() => latestLedgerCommand;
|
||||
|
||||
bool isViewOnly() {
|
||||
return isViewOnlyBySpendKey(null);
|
||||
}
|
||||
|
@ -439,5 +442,4 @@ class CWMonero extends Monero {
|
|||
Map<String, List<int>> debugCallLength() {
|
||||
return monero_wallet_api.debugCallLength();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,7 +5,9 @@ import 'package:cake_wallet/generated/i18n.dart';
|
|||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/screens/connect_device/widgets/device_tile.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/themes/core/material_base_theme.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
|
@ -60,6 +62,7 @@ class ConnectDevicePage extends BasePage {
|
|||
onConnectDevice,
|
||||
allowChangeWallet,
|
||||
ledgerVM,
|
||||
currentTheme,
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -68,12 +71,14 @@ class ConnectDevicePageBody extends StatefulWidget {
|
|||
final OnConnectDevice onConnectDevice;
|
||||
final bool allowChangeWallet;
|
||||
final LedgerViewModel ledgerVM;
|
||||
final MaterialThemeBase currentTheme;
|
||||
|
||||
const ConnectDevicePageBody(
|
||||
this.walletType,
|
||||
this.onConnectDevice,
|
||||
this.allowChangeWallet,
|
||||
this.ledgerVM,
|
||||
this.currentTheme,
|
||||
);
|
||||
|
||||
@override
|
||||
|
@ -179,109 +184,126 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint,
|
||||
height: double.infinity,
|
||||
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 24),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Text(
|
||||
Platform.isIOS
|
||||
? S.of(context).connect_your_hardware_wallet_ios
|
||||
: S.of(context).connect_your_hardware_wallet,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: !longWait,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Text(
|
||||
S.of(context).if_you_dont_see_your_device,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => Offstage(
|
||||
offstage: widget.ledgerVM.bleIsEnabled,
|
||||
child: Padding(
|
||||
return Container(
|
||||
width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint,
|
||||
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 24),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Text(
|
||||
S.of(context).ledger_please_enable_bluetooth,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
Platform.isIOS
|
||||
? S.of(context).connect_your_hardware_wallet_ios
|
||||
: S.of(context).connect_your_hardware_wallet,
|
||||
style: Theme.of(context)
|
||||
.textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: !longWait,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Text(
|
||||
S.of(context).if_you_dont_see_your_device,
|
||||
style: Theme.of(context)
|
||||
.textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => Offstage(
|
||||
offstage: widget.ledgerVM.bleIsEnabled,
|
||||
child: Padding(
|
||||
padding:
|
||||
EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Text(
|
||||
S.of(context).ledger_please_enable_bluetooth,
|
||||
style: Theme.of(context)
|
||||
.textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (bleDevices.length > 0) ...[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
S.of(context).bluetooth,
|
||||
style: Theme.of(context)
|
||||
.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
...bleDevices
|
||||
.map(
|
||||
(device) => Padding(
|
||||
padding: EdgeInsets.only(bottom: 20),
|
||||
child: DeviceTile(
|
||||
onPressed: () => _connectToDevice(device),
|
||||
title: device.name,
|
||||
leading: _getDeviceTileLeading(device.deviceInfo),
|
||||
connectionType: device.connectionType,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
],
|
||||
if (usbDevices.length > 0) ...[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
S.of(context).usb,
|
||||
style: Theme.of(context)
|
||||
.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
...usbDevices
|
||||
.map(
|
||||
(device) => Padding(
|
||||
padding: EdgeInsets.only(bottom: 20),
|
||||
child: DeviceTile(
|
||||
onPressed: () => _connectToDevice(device),
|
||||
title: device.name,
|
||||
leading: _getDeviceTileLeading(device.deviceInfo),
|
||||
connectionType: device.connectionType,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
],
|
||||
if (widget.allowChangeWallet) ...[
|
||||
PrimaryButton(
|
||||
text: S.of(context).wallets,
|
||||
color: Theme.of(context)
|
||||
.colorScheme.primary,
|
||||
textColor: Theme.of(context)
|
||||
.colorScheme.onPrimary,
|
||||
onPressed: _onChangeWallet,
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
if (bleDevices.length > 0) ...[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
S.of(context).bluetooth,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
...bleDevices
|
||||
.map(
|
||||
(device) => Padding(
|
||||
padding: EdgeInsets.only(bottom: 20),
|
||||
child: DeviceTile(
|
||||
onPressed: () => _connectToDevice(device),
|
||||
title: device.name,
|
||||
leading: _getDeviceTileLeading(device.deviceInfo),
|
||||
connectionType: device.connectionType,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
],
|
||||
if (usbDevices.length > 0) ...[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
S.of(context).usb,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
...usbDevices
|
||||
.map(
|
||||
(device) => Padding(
|
||||
padding: EdgeInsets.only(bottom: 20),
|
||||
child: DeviceTile(
|
||||
onPressed: () => _connectToDevice(device),
|
||||
title: device.name,
|
||||
leading: _getDeviceTileLeading(device.deviceInfo),
|
||||
connectionType: device.connectionType,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
],
|
||||
if (widget.allowChangeWallet) ...[
|
||||
PrimaryButton(
|
||||
text: S.of(context).wallets,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: _onChangeWallet,
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
PrimaryButton(
|
||||
text: S.of(context).how_to_connect,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
onPressed: () => _onHowToConnect(context),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -293,4 +315,21 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
|||
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false),
|
||||
);
|
||||
}
|
||||
|
||||
void _onHowToConnect(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext bottomSheetContext) => InfoStepsBottomSheet(
|
||||
titleText: S.of(context).how_to_connect,
|
||||
currentTheme: widget.currentTheme,
|
||||
steps: [
|
||||
InfoStep('${S.of(context).step} 1', S.of(context).connect_hw_info_step_1),
|
||||
InfoStep('${S.of(context).step} 2', S.of(context).connect_hw_info_step_2),
|
||||
InfoStep('${S.of(context).step} 3', S.of(context).connect_hw_info_step_3),
|
||||
InfoStep('${S.of(context).step} 4', S.of(context).connect_hw_info_step_4),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -684,29 +684,47 @@ class SendPage extends BasePage {
|
|||
});
|
||||
}
|
||||
|
||||
if (state is IsAwaitingDeviceResponseState) {
|
||||
if (state is IsDeviceSigningResponseState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!context.mounted) return;
|
||||
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
builder: (BuildContext bottomSheetContext) => InfoBottomSheet(
|
||||
currentTheme: currentTheme,
|
||||
titleText: S.of(bottomSheetContext).proceed_on_device,
|
||||
contentImage: 'assets/images/hardware_wallet/ledger_nano_x.png',
|
||||
contentImageColor: Theme.of(context).colorScheme.onSurface,
|
||||
content: S.of(bottomSheetContext).proceed_on_device_description,
|
||||
isTwoAction: false,
|
||||
actionButtonText: S.of(context).cancel,
|
||||
actionButton: () {
|
||||
sendViewModel.state = InitialExecutionState();
|
||||
Navigator.of(bottomSheetContext).pop();
|
||||
},
|
||||
),
|
||||
builder: (context) {
|
||||
dialogContext = context;
|
||||
return LoadingBottomSheet(titleText: S.of(context).device_is_signing);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (state is IsAwaitingDeviceResponseState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!context.mounted) return;
|
||||
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
builder: (context) {
|
||||
dialogContext = context;
|
||||
return InfoBottomSheet(
|
||||
currentTheme: currentTheme,
|
||||
titleText: S.of(context).proceed_on_device,
|
||||
contentImage:
|
||||
'assets/images/hardware_wallet/ledger_nano_x.png',
|
||||
contentImageColor: Theme.of(context).colorScheme.onSurface,
|
||||
content: S.of(context).proceed_on_device_description,
|
||||
isTwoAction: false,
|
||||
actionButtonText: S.of(context).cancel,
|
||||
actionButton: () {
|
||||
sendViewModel.state = InitialExecutionState();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_effectsInstalled = true;
|
||||
|
|
114
lib/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart
Normal file
114
lib/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart
Normal file
|
@ -0,0 +1,114 @@
|
|||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/themes/core/material_base_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'base_bottom_sheet_widget.dart';
|
||||
|
||||
class InfoStep {
|
||||
final String title;
|
||||
final String description;
|
||||
|
||||
const InfoStep(this.title, this.description);
|
||||
}
|
||||
|
||||
class InfoStepsBottomSheet extends BaseBottomSheet {
|
||||
final MaterialThemeBase currentTheme;
|
||||
final List<InfoStep> steps;
|
||||
|
||||
InfoStepsBottomSheet({
|
||||
required String titleText,
|
||||
required this.steps,
|
||||
String? titleIconPath,
|
||||
required this.currentTheme,
|
||||
}) : super(titleText: titleText, titleIconPath: titleIconPath);
|
||||
|
||||
@override
|
||||
Widget contentWidget(BuildContext context) => SizedBox(
|
||||
height: 500,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: steps
|
||||
.map((step) => Container(
|
||||
margin: EdgeInsets.only(
|
||||
bottom: 15, left: 20, right: 20),
|
||||
padding: EdgeInsets.all(10),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: Theme.of(context).cardColor,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.start,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
step.title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 5),
|
||||
child: Text(
|
||||
step.description,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: PrimaryButton(
|
||||
text: S.of(context).close,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: currentTheme.isDark
|
||||
? Theme.of(context).colorScheme.onSurfaceVariant
|
||||
: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget footerWidget(BuildContext context) => SizedBox.shrink();
|
||||
}
|
|
@ -15,8 +15,8 @@ import 'package:cake_wallet/entities/contact_record.dart';
|
|||
import 'package:cake_wallet/entities/evm_transaction_error_fees_handler.dart';
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
import 'package:cake_wallet/entities/parsed_address.dart';
|
||||
import 'package:cake_wallet/entities/template.dart';
|
||||
import 'package:cake_wallet/entities/preferences_key.dart';
|
||||
import 'package:cake_wallet/entities/template.dart';
|
||||
import 'package:cake_wallet/entities/transaction_description.dart';
|
||||
import 'package:cake_wallet/entities/wallet_contact.dart';
|
||||
import 'package:cake_wallet/ethereum/ethereum.dart';
|
||||
|
@ -293,19 +293,18 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
|||
.toList();
|
||||
|
||||
@computed
|
||||
bool get hasCoinControl =>
|
||||
wallet.type == WalletType.bitcoin ||
|
||||
wallet.type == WalletType.litecoin ||
|
||||
wallet.type == WalletType.monero ||
|
||||
wallet.type == WalletType.wownero ||
|
||||
wallet.type == WalletType.decred ||
|
||||
wallet.type == WalletType.bitcoinCash;
|
||||
bool get hasCoinControl => [
|
||||
WalletType.bitcoin,
|
||||
WalletType.litecoin,
|
||||
WalletType.monero,
|
||||
WalletType.wownero,
|
||||
WalletType.decred,
|
||||
WalletType.bitcoinCash
|
||||
].contains(wallet.type);
|
||||
|
||||
@computed
|
||||
bool get isElectrumWallet =>
|
||||
wallet.type == WalletType.bitcoin ||
|
||||
wallet.type == WalletType.litecoin ||
|
||||
wallet.type == WalletType.bitcoinCash;
|
||||
[WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type);
|
||||
|
||||
@observable
|
||||
CryptoCurrency selectedCryptoCurrency;
|
||||
|
@ -444,12 +443,23 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
|||
}
|
||||
}
|
||||
|
||||
Timer? _ledgerTxStateTimer;
|
||||
|
||||
@action
|
||||
Future<PendingTransaction?> createTransaction({ExchangeProvider? provider}) async {
|
||||
try {
|
||||
if (!(state is IsExecutingState)) state = IsExecutingState();
|
||||
|
||||
if (wallet.isHardwareWallet) state = IsAwaitingDeviceResponseState();
|
||||
if (wallet.isHardwareWallet) {
|
||||
state = IsAwaitingDeviceResponseState();
|
||||
if (walletType == WalletType.monero)
|
||||
_ledgerTxStateTimer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
if (monero!.getLastLedgerCommand() == "INS_CLSAG") {
|
||||
timer.cancel();
|
||||
state = IsDeviceSigningResponseState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pendingTransaction = await wallet.createTransaction(_credentials(provider));
|
||||
|
||||
|
@ -475,6 +485,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
|||
state = ExecutedSuccessfullyState();
|
||||
return pendingTransaction;
|
||||
} catch (e) {
|
||||
_ledgerTxStateTimer?.cancel();
|
||||
// if (e is LedgerException) {
|
||||
// final errorCode = e.errorCode.toRadixString(16);
|
||||
// final fallbackMsg =
|
||||
|
@ -592,7 +603,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
|||
));
|
||||
}
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name), DateTime.now().add(Duration(minutes: 1)).toIso8601String());
|
||||
await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name),
|
||||
DateTime.now().add(Duration(minutes: 1)).toIso8601String());
|
||||
state = TransactionCommitted();
|
||||
} catch (e) {
|
||||
state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency));
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:cake_wallet/core/execution_state.dart';
|
||||
|
||||
class IsDeviceSigningResponseState extends IsExecutingState {}
|
||||
class IsAwaitingDeviceResponseState extends IsExecutingState {}
|
||||
class TransactionCommitting extends ExecutionState {}
|
||||
class TransactionCommitted extends ExecutionState {}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue