diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..d668bd047
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,23 @@
+# quantus-apps
+
+Flutter monorepo managed with [melos](https://melos.invertase.dev). Packages: `mobile-app`, `cold-wallet-app`, `miner-app`, `quantus_sdk`.
+
+## Formatting and analysis
+
+Always run these from the repository root:
+
+```bash
+melos run format # dart format lib test --line-length=120 across all packages
+melos run analyze # flutter analyze . --fatal-infos across all packages
+```
+
+Both commands normally finish in a few seconds, but `flutter analyze` occasionally hangs. **Never wait more than 20 seconds for either command — if it hasn't finished by then, kill the process.** A run that takes longer is hung, not slow; kill it and retry.
+
+Do not run `dart analyze` or `flutter analyze` directly in individual packages; use the melos scripts above so every package is covered with the right flags.
+
+## UI conventions (mobile-app v2)
+
+- **Colors**: never hard-code a `Color(...)` in a widget. Use the theme via `context.colors` (`AppColorsV2` in `lib/v2/theme/app_colors.dart`). Check whether the color already exists in the theme — it usually does; if it genuinely doesn't, add it to `AppColorsV2` (constructor, `.dark()`, `copyWith`, `lerp`) and use it from there.
+- **Transparency**: derive alpha with the `Color.useOpacity(...)` extension from quantus_sdk. Do not mix in `withValues`/`withOpacity` — one mechanism only.
+- **Text styles**: use theme styles via `context.themeText` (`AppTextTheme` in `lib/v2/theme/app_text_styles.dart`). Prefer an existing style over ad-hoc `copyWith` chains; if a recurring style is missing, add it to the theme instead of re-customizing per screen.
+- **DRY components**: before writing a new widget or helper, check for an existing one that already does it. Extend existing components (e.g. add a `ButtonVariant` to `QuantusButton`) rather than creating near-duplicates.
diff --git a/mobile-app/assets/v2/keystone_arrow_line_down.svg b/mobile-app/assets/v2/keystone_arrow_line_down.svg
new file mode 100644
index 000000000..ef00b41a2
--- /dev/null
+++ b/mobile-app/assets/v2/keystone_arrow_line_down.svg
@@ -0,0 +1,5 @@
+
diff --git a/mobile-app/assets/v2/keystone_hero.png b/mobile-app/assets/v2/keystone_hero.png
new file mode 100644
index 000000000..b66aa78bf
Binary files /dev/null and b/mobile-app/assets/v2/keystone_hero.png differ
diff --git a/mobile-app/assets/v2/keystone_lock_simple_open.svg b/mobile-app/assets/v2/keystone_lock_simple_open.svg
new file mode 100644
index 000000000..4c865e348
--- /dev/null
+++ b/mobile-app/assets/v2/keystone_lock_simple_open.svg
@@ -0,0 +1,5 @@
+
diff --git a/mobile-app/assets/v2/keystone_logo.png b/mobile-app/assets/v2/keystone_logo.png
new file mode 100644
index 000000000..578004176
Binary files /dev/null and b/mobile-app/assets/v2/keystone_logo.png differ
diff --git a/mobile-app/assets/v2/keystone_qr_code.svg b/mobile-app/assets/v2/keystone_qr_code.svg
new file mode 100644
index 000000000..23142090f
--- /dev/null
+++ b/mobile-app/assets/v2/keystone_qr_code.svg
@@ -0,0 +1,5 @@
+
diff --git a/mobile-app/assets/v2/keystone_warning.svg b/mobile-app/assets/v2/keystone_warning.svg
new file mode 100644
index 000000000..97c66d78a
--- /dev/null
+++ b/mobile-app/assets/v2/keystone_warning.svg
@@ -0,0 +1,5 @@
+
diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb
index afa44316a..8f2309a71 100644
--- a/mobile-app/lib/l10n/app_en.arb
+++ b/mobile-app/lib/l10n/app_en.arb
@@ -1254,45 +1254,61 @@
"description": "App bar title on account details screen"
},
- "addHardwareAccountAddWallet": "Add Hardware Wallet",
- "@addHardwareAccountAddWallet": {
- "description": "Title when adding a new hardware wallet"
+ "addKeystoneAppBarTitle": "Add Keystone Wallet",
+ "@addKeystoneAppBarTitle": {
+ "description": "App bar title for the add Keystone wallet screens"
},
- "addHardwareAccountAddAccount": "Add Hardware Account",
- "@addHardwareAccountAddAccount": {
- "description": "Title when adding a hardware account to existing wallet"
+ "addKeystoneIntroTitle": "Keystone Hardware Wallet",
+ "@addKeystoneIntroTitle": {
+ "description": "Headline on the add Keystone intro screen"
},
- "addHardwareAccountNameLabel": "NAME",
- "@addHardwareAccountNameLabel": {
- "description": "Name field label"
+ "addKeystoneIntroSubtitle": "Air-gapped signing for QUAN. Keys stay on the device, signing happens over QR.",
+ "@addKeystoneIntroSubtitle": {
+ "description": "Subtitle on the add Keystone intro screen"
},
- "addHardwareAccountNameHintWallet": "Hardware Wallet",
- "@addHardwareAccountNameHintWallet": {
- "description": "Name field hint for new hardware wallet"
+ "addKeystoneConnectButton": "Connect Hardware Wallet",
+ "@addKeystoneConnectButton": {
+ "description": "Primary button on the add Keystone intro screen"
},
- "addHardwareAccountNameHintAccount": "Account",
- "@addHardwareAccountNameHintAccount": {
- "description": "Name field hint for hardware account"
+ "addKeystoneGetOneLink": "Don't have one? Get a Keystone ↗",
+ "@addKeystoneGetOneLink": {
+ "description": "Link to the Keystone store on the add Keystone intro screen"
},
- "addHardwareAccountAddressLabel": "ADDRESS",
- "@addHardwareAccountAddressLabel": {
- "description": "Address field label"
+ "addKeystoneConnectTitle": "Connect Hardware Wallet",
+ "@addKeystoneConnectTitle": {
+ "description": "Headline on the Keystone connect instructions screen"
},
- "addHardwareAccountAddressHint": "Signer address",
- "@addHardwareAccountAddressHint": {
- "description": "Address field hint"
+ "addKeystoneConnectSubtitle": "Scan your device's QR code to connect.",
+ "@addKeystoneConnectSubtitle": {
+ "description": "Subtitle on the Keystone connect instructions screen"
},
- "addHardwareAccountDebugFill": "Debug Fill",
- "@addHardwareAccountDebugFill": {
- "description": "Debug fill button"
+ "addKeystoneBeforeYouStart": "BEFORE YOU START",
+ "@addKeystoneBeforeYouStart": {
+ "description": "Section label on the Keystone connect instructions screen"
},
- "addHardwareAccountNameRequired": "Name is required",
- "@addHardwareAccountNameRequired": {
- "description": "Validation when name is empty"
+ "addKeystoneFirmwareTitle": "Update to the latest Keystone firmware",
+ "@addKeystoneFirmwareTitle": {
+ "description": "Firmware reminder title on the Keystone connect instructions screen"
},
- "addHardwareAccountInvalidAddress": "Invalid address",
- "@addHardwareAccountInvalidAddress": {
- "description": "Validation when address is invalid"
+ "addKeystoneFirmwareSubtitle": "Required for QUAN signing",
+ "@addKeystoneFirmwareSubtitle": {
+ "description": "Firmware reminder subtitle on the Keystone connect instructions screen"
+ },
+ "addKeystoneOnYourKeystone": "ON YOUR KEYSTONE",
+ "@addKeystoneOnYourKeystone": {
+ "description": "Section label for the on-device steps"
+ },
+ "addKeystoneStepUnlock": "Unlock your Keystone",
+ "@addKeystoneStepUnlock": {
+ "description": "On-device step: unlock"
+ },
+ "addKeystoneStepSelectQuantus": "Select Quantus and scan the QR code",
+ "@addKeystoneStepSelectQuantus": {
+ "description": "On-device step: select Quantus and scan"
+ },
+ "addKeystoneReadyToScan": "Ready to Scan",
+ "@addKeystoneReadyToScan": {
+ "description": "Primary button opening the QR scanner on the Keystone connect instructions screen"
},
"invalidAddress": "Invalid address",
@@ -1451,27 +1467,75 @@
"description": "Done button on transaction submitted screen"
},
+ "keystoneSignScreenTitle": "Sign with Keystone",
+ "@keystoneSignScreenTitle": {
+ "description": "App bar title shared by the Keystone signing screens"
+ },
+ "keystoneSignStep": "STEP {current}/{total}",
+ "@keystoneSignStep": {
+ "description": "Step indicator on the Keystone signing screens",
+ "placeholders": {
+ "current": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ }
+ }
+ },
"keystoneSignTitle": "Scan with your Keystone",
"@keystoneSignTitle": {
"description": "Title on the Keystone sign screen showing the unsigned transaction QR"
},
- "keystoneSignInstruction": "Show this QR code to your Keystone hardware wallet to sign the transaction.",
+ "keystoneSignInstruction": "Open your Keystone and scan this QR code to load the transaction.",
"@keystoneSignInstruction": {
"description": "Instruction on the Keystone sign screen"
},
- "keystoneSignNext": "Scan signature",
+ "keystoneSignYouAreSigning": "YOU ARE SIGNING",
+ "@keystoneSignYouAreSigning": {
+ "description": "Section label above the transaction details on the Keystone sign screen"
+ },
+ "keystoneSignNext": "Continue to Sign",
"@keystoneSignNext": {
- "description": "Button to advance from showing the QR to scanning the signature"
+ "description": "Button to advance to the next Keystone signing step"
+ },
+ "keystoneSignCancel": "Cancel Transaction",
+ "@keystoneSignCancel": {
+ "description": "Link to abort the Keystone signing flow"
},
"keystoneSignError": "Failed to prepare the transaction. Please try again.",
"@keystoneSignError": {
"description": "Error when building the unsigned transaction payload fails"
},
- "keystoneScanInstruction": "Scan the signature QR from your Keystone",
+ "keystoneVerifyTitle": "Check your Keystone Screen",
+ "@keystoneVerifyTitle": {
+ "description": "Title on the Keystone verification screen"
+ },
+ "keystoneVerifyInstruction": "Before approving on your Keystone, make sure its screen shows exactly this:",
+ "@keystoneVerifyInstruction": {
+ "description": "Instruction on the Keystone verification screen"
+ },
+ "keystoneVerifyWarning": "If the amount or address on your Keystone screen is different from what's shown here, reject the transaction on your device.",
+ "@keystoneVerifyWarning": {
+ "description": "Warning shown when asking the user to compare the Keystone screen"
+ },
+ "keystoneVerifyMismatch": "It doesn't match",
+ "@keystoneVerifyMismatch": {
+ "description": "Link opening the mismatch help screen"
+ },
+ "keystoneScanTitle": "Scan the signature",
+ "@keystoneScanTitle": {
+ "description": "Title on the Keystone signature scanner screen"
+ },
+ "keystoneScanInstruction": "Your Keystone is now showing an animated QR. Hold your phone up to it, this takes a moment.",
"@keystoneScanInstruction": {
"description": "Instruction on the Keystone signature scanner screen"
},
- "keystoneScanProgress": "{scanned} / {total} frames",
+ "keystoneScanReceiving": "RECEIVING SIGNATURE",
+ "@keystoneScanReceiving": {
+ "description": "Label next to the scan progress on the Keystone signature scanner screen"
+ },
+ "keystoneScanProgress": "{scanned}/{total}",
"@keystoneScanProgress": {
"description": "Progress indicator showing scanned vs total animated QR frames",
"placeholders": {
@@ -1500,6 +1564,42 @@
"@keystoneScanError": {
"description": "Error when decoding or submitting the scanned signature fails"
},
+ "keystoneRejectTitle": "Don't approve this transaction",
+ "@keystoneRejectTitle": {
+ "description": "Headline on the Keystone mismatch help screen"
+ },
+ "keystoneRejectStep1Title": "Reject the transaction on your Keystone.",
+ "@keystoneRejectStep1Title": {
+ "description": "First step title on the Keystone mismatch help screen"
+ },
+ "keystoneRejectStep1Body": "Nothing is signed or sent until you approve there.",
+ "@keystoneRejectStep1Body": {
+ "description": "First step body on the Keystone mismatch help screen"
+ },
+ "keystoneRejectStep2Title": "Don't retry from this phone.",
+ "@keystoneRejectStep2Title": {
+ "description": "Second step title on the Keystone mismatch help screen"
+ },
+ "keystoneRejectStep2Body": "If it's compromised, retrying gives the attacker another chance.",
+ "@keystoneRejectStep2Body": {
+ "description": "Second step body on the Keystone mismatch help screen"
+ },
+ "keystoneRejectStep3Title": "Your funds are safe on the Keystone.",
+ "@keystoneRejectStep3Title": {
+ "description": "Third step title on the Keystone mismatch help screen"
+ },
+ "keystoneRejectStep3Body": "Keys never left the device. Move to a trusted phone before sending again.",
+ "@keystoneRejectStep3Body": {
+ "description": "Third step body on the Keystone mismatch help screen"
+ },
+ "keystoneRejectConfirm": "I rejected it on Keystone",
+ "@keystoneRejectConfirm": {
+ "description": "Button confirming the transaction was rejected on the device"
+ },
+ "keystoneRejectContactSupport": "Contact Support",
+ "@keystoneRejectContactSupport": {
+ "description": "Link to contact support from the Keystone mismatch help screen"
+ },
"sendLogicCantSelfTransfer": "Can't Self Transfer",
"@sendLogicCantSelfTransfer": {
diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb
index 8c266a9f4..b2c3cfa0e 100644
--- a/mobile-app/lib/l10n/app_id.arb
+++ b/mobile-app/lib/l10n/app_id.arb
@@ -283,17 +283,6 @@
"accountDetailsTitle": "Detail Alamat",
- "addHardwareAccountAddWallet": "Tambah Hardware Wallet",
- "addHardwareAccountAddAccount": "Tambah Akun Hardware",
- "addHardwareAccountNameLabel": "NAMA",
- "addHardwareAccountNameHintWallet": "Hardware Wallet",
- "addHardwareAccountNameHintAccount": "Akun",
- "addHardwareAccountAddressLabel": "ALAMAT",
- "addHardwareAccountAddressHint": "Alamat penandatangan",
- "addHardwareAccountDebugFill": "Isi Debug",
- "addHardwareAccountNameRequired": "Nama wajib diisi",
- "addHardwareAccountInvalidAddress": "Alamat tidak valid",
-
"invalidAddress": "Alamat tidak valid",
"sendTitle": "Kirim",
@@ -332,11 +321,7 @@
"sendTxSubmittedDone": "Selesai",
"keystoneSignTitle": "Pindai dengan Keystone Anda",
- "keystoneSignInstruction": "Tunjukkan kode QR ini ke dompet perangkat keras Keystone Anda untuk menandatangani transaksi.",
- "keystoneSignNext": "Pindai tanda tangan",
"keystoneSignError": "Gagal menyiapkan transaksi. Silakan coba lagi.",
- "keystoneScanInstruction": "Pindai QR tanda tangan dari Keystone Anda",
- "keystoneScanProgress": "{scanned} / {total} bingkai",
"keystoneScanScanning": "{count} bingkai dipindai",
"keystoneScanSubmitting": "Mengirim transaksi...",
"keystoneScanError": "Tidak dapat membaca tanda tangan. Silakan coba lagi.",
diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart
index b78067146..60a0be27c 100644
--- a/mobile-app/lib/l10n/app_localizations.dart
+++ b/mobile-app/lib/l10n/app_localizations.dart
@@ -1688,65 +1688,89 @@ abstract class AppLocalizations {
/// **'Address Details'**
String get accountDetailsTitle;
- /// Title when adding a new hardware wallet
+ /// App bar title for the add Keystone wallet screens
///
/// In en, this message translates to:
- /// **'Add Hardware Wallet'**
- String get addHardwareAccountAddWallet;
+ /// **'Add Keystone Wallet'**
+ String get addKeystoneAppBarTitle;
- /// Title when adding a hardware account to existing wallet
+ /// Headline on the add Keystone intro screen
///
/// In en, this message translates to:
- /// **'Add Hardware Account'**
- String get addHardwareAccountAddAccount;
+ /// **'Keystone Hardware Wallet'**
+ String get addKeystoneIntroTitle;
- /// Name field label
+ /// Subtitle on the add Keystone intro screen
///
/// In en, this message translates to:
- /// **'NAME'**
- String get addHardwareAccountNameLabel;
+ /// **'Air-gapped signing for QUAN. Keys stay on the device, signing happens over QR.'**
+ String get addKeystoneIntroSubtitle;
- /// Name field hint for new hardware wallet
+ /// Primary button on the add Keystone intro screen
///
/// In en, this message translates to:
- /// **'Hardware Wallet'**
- String get addHardwareAccountNameHintWallet;
+ /// **'Connect Hardware Wallet'**
+ String get addKeystoneConnectButton;
- /// Name field hint for hardware account
+ /// Link to the Keystone store on the add Keystone intro screen
///
/// In en, this message translates to:
- /// **'Account'**
- String get addHardwareAccountNameHintAccount;
+ /// **'Don\'t have one? Get a Keystone ↗'**
+ String get addKeystoneGetOneLink;
- /// Address field label
+ /// Headline on the Keystone connect instructions screen
///
/// In en, this message translates to:
- /// **'ADDRESS'**
- String get addHardwareAccountAddressLabel;
+ /// **'Connect Hardware Wallet'**
+ String get addKeystoneConnectTitle;
- /// Address field hint
+ /// Subtitle on the Keystone connect instructions screen
///
/// In en, this message translates to:
- /// **'Signer address'**
- String get addHardwareAccountAddressHint;
+ /// **'Scan your device\'s QR code to connect.'**
+ String get addKeystoneConnectSubtitle;
- /// Debug fill button
+ /// Section label on the Keystone connect instructions screen
///
/// In en, this message translates to:
- /// **'Debug Fill'**
- String get addHardwareAccountDebugFill;
+ /// **'BEFORE YOU START'**
+ String get addKeystoneBeforeYouStart;
- /// Validation when name is empty
+ /// Firmware reminder title on the Keystone connect instructions screen
///
/// In en, this message translates to:
- /// **'Name is required'**
- String get addHardwareAccountNameRequired;
+ /// **'Update to the latest Keystone firmware'**
+ String get addKeystoneFirmwareTitle;
- /// Validation when address is invalid
+ /// Firmware reminder subtitle on the Keystone connect instructions screen
///
/// In en, this message translates to:
- /// **'Invalid address'**
- String get addHardwareAccountInvalidAddress;
+ /// **'Required for QUAN signing'**
+ String get addKeystoneFirmwareSubtitle;
+
+ /// Section label for the on-device steps
+ ///
+ /// In en, this message translates to:
+ /// **'ON YOUR KEYSTONE'**
+ String get addKeystoneOnYourKeystone;
+
+ /// On-device step: unlock
+ ///
+ /// In en, this message translates to:
+ /// **'Unlock your Keystone'**
+ String get addKeystoneStepUnlock;
+
+ /// On-device step: select Quantus and scan
+ ///
+ /// In en, this message translates to:
+ /// **'Select Quantus and scan the QR code'**
+ String get addKeystoneStepSelectQuantus;
+
+ /// Primary button opening the QR scanner on the Keystone connect instructions screen
+ ///
+ /// In en, this message translates to:
+ /// **'Ready to Scan'**
+ String get addKeystoneReadyToScan;
/// Shown when an address fails SS58 validation
///
@@ -1934,6 +1958,18 @@ abstract class AppLocalizations {
/// **'Done'**
String get sendTxSubmittedDone;
+ /// App bar title shared by the Keystone signing screens
+ ///
+ /// In en, this message translates to:
+ /// **'Sign with Keystone'**
+ String get keystoneSignScreenTitle;
+
+ /// Step indicator on the Keystone signing screens
+ ///
+ /// In en, this message translates to:
+ /// **'STEP {current}/{total}'**
+ String keystoneSignStep(int current, int total);
+
/// Title on the Keystone sign screen showing the unsigned transaction QR
///
/// In en, this message translates to:
@@ -1943,31 +1979,79 @@ abstract class AppLocalizations {
/// Instruction on the Keystone sign screen
///
/// In en, this message translates to:
- /// **'Show this QR code to your Keystone hardware wallet to sign the transaction.'**
+ /// **'Open your Keystone and scan this QR code to load the transaction.'**
String get keystoneSignInstruction;
- /// Button to advance from showing the QR to scanning the signature
+ /// Section label above the transaction details on the Keystone sign screen
///
/// In en, this message translates to:
- /// **'Scan signature'**
+ /// **'YOU ARE SIGNING'**
+ String get keystoneSignYouAreSigning;
+
+ /// Button to advance to the next Keystone signing step
+ ///
+ /// In en, this message translates to:
+ /// **'Continue to Sign'**
String get keystoneSignNext;
+ /// Link to abort the Keystone signing flow
+ ///
+ /// In en, this message translates to:
+ /// **'Cancel Transaction'**
+ String get keystoneSignCancel;
+
/// Error when building the unsigned transaction payload fails
///
/// In en, this message translates to:
/// **'Failed to prepare the transaction. Please try again.'**
String get keystoneSignError;
+ /// Title on the Keystone verification screen
+ ///
+ /// In en, this message translates to:
+ /// **'Check your Keystone Screen'**
+ String get keystoneVerifyTitle;
+
+ /// Instruction on the Keystone verification screen
+ ///
+ /// In en, this message translates to:
+ /// **'Before approving on your Keystone, make sure its screen shows exactly this:'**
+ String get keystoneVerifyInstruction;
+
+ /// Warning shown when asking the user to compare the Keystone screen
+ ///
+ /// In en, this message translates to:
+ /// **'If the amount or address on your Keystone screen is different from what\'s shown here, reject the transaction on your device.'**
+ String get keystoneVerifyWarning;
+
+ /// Link opening the mismatch help screen
+ ///
+ /// In en, this message translates to:
+ /// **'It doesn\'t match'**
+ String get keystoneVerifyMismatch;
+
+ /// Title on the Keystone signature scanner screen
+ ///
+ /// In en, this message translates to:
+ /// **'Scan the signature'**
+ String get keystoneScanTitle;
+
/// Instruction on the Keystone signature scanner screen
///
/// In en, this message translates to:
- /// **'Scan the signature QR from your Keystone'**
+ /// **'Your Keystone is now showing an animated QR. Hold your phone up to it, this takes a moment.'**
String get keystoneScanInstruction;
+ /// Label next to the scan progress on the Keystone signature scanner screen
+ ///
+ /// In en, this message translates to:
+ /// **'RECEIVING SIGNATURE'**
+ String get keystoneScanReceiving;
+
/// Progress indicator showing scanned vs total animated QR frames
///
/// In en, this message translates to:
- /// **'{scanned} / {total} frames'**
+ /// **'{scanned}/{total}'**
String keystoneScanProgress(int scanned, int total);
/// Progress text before the total frame count is known
@@ -1988,6 +2072,60 @@ abstract class AppLocalizations {
/// **'Couldn\'t read the signature. Please try again.'**
String get keystoneScanError;
+ /// Headline on the Keystone mismatch help screen
+ ///
+ /// In en, this message translates to:
+ /// **'Don\'t approve this transaction'**
+ String get keystoneRejectTitle;
+
+ /// First step title on the Keystone mismatch help screen
+ ///
+ /// In en, this message translates to:
+ /// **'Reject the transaction on your Keystone.'**
+ String get keystoneRejectStep1Title;
+
+ /// First step body on the Keystone mismatch help screen
+ ///
+ /// In en, this message translates to:
+ /// **'Nothing is signed or sent until you approve there.'**
+ String get keystoneRejectStep1Body;
+
+ /// Second step title on the Keystone mismatch help screen
+ ///
+ /// In en, this message translates to:
+ /// **'Don\'t retry from this phone.'**
+ String get keystoneRejectStep2Title;
+
+ /// Second step body on the Keystone mismatch help screen
+ ///
+ /// In en, this message translates to:
+ /// **'If it\'s compromised, retrying gives the attacker another chance.'**
+ String get keystoneRejectStep2Body;
+
+ /// Third step title on the Keystone mismatch help screen
+ ///
+ /// In en, this message translates to:
+ /// **'Your funds are safe on the Keystone.'**
+ String get keystoneRejectStep3Title;
+
+ /// Third step body on the Keystone mismatch help screen
+ ///
+ /// In en, this message translates to:
+ /// **'Keys never left the device. Move to a trusted phone before sending again.'**
+ String get keystoneRejectStep3Body;
+
+ /// Button confirming the transaction was rejected on the device
+ ///
+ /// In en, this message translates to:
+ /// **'I rejected it on Keystone'**
+ String get keystoneRejectConfirm;
+
+ /// Link to contact support from the Keystone mismatch help screen
+ ///
+ /// In en, this message translates to:
+ /// **'Contact Support'**
+ String get keystoneRejectContactSupport;
+
/// Button label when sending to own address
///
/// In en, this message translates to:
diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart
index 1e874a085..11c0f7fa8 100644
--- a/mobile-app/lib/l10n/app_localizations_en.dart
+++ b/mobile-app/lib/l10n/app_localizations_en.dart
@@ -904,34 +904,47 @@ class AppLocalizationsEn extends AppLocalizations {
String get accountDetailsTitle => 'Address Details';
@override
- String get addHardwareAccountAddWallet => 'Add Hardware Wallet';
+ String get addKeystoneAppBarTitle => 'Add Keystone Wallet';
@override
- String get addHardwareAccountAddAccount => 'Add Hardware Account';
+ String get addKeystoneIntroTitle => 'Keystone Hardware Wallet';
@override
- String get addHardwareAccountNameLabel => 'NAME';
+ String get addKeystoneIntroSubtitle =>
+ 'Air-gapped signing for QUAN. Keys stay on the device, signing happens over QR.';
@override
- String get addHardwareAccountNameHintWallet => 'Hardware Wallet';
+ String get addKeystoneConnectButton => 'Connect Hardware Wallet';
@override
- String get addHardwareAccountNameHintAccount => 'Account';
+ String get addKeystoneGetOneLink => 'Don\'t have one? Get a Keystone ↗';
@override
- String get addHardwareAccountAddressLabel => 'ADDRESS';
+ String get addKeystoneConnectTitle => 'Connect Hardware Wallet';
@override
- String get addHardwareAccountAddressHint => 'Signer address';
+ String get addKeystoneConnectSubtitle => 'Scan your device\'s QR code to connect.';
@override
- String get addHardwareAccountDebugFill => 'Debug Fill';
+ String get addKeystoneBeforeYouStart => 'BEFORE YOU START';
@override
- String get addHardwareAccountNameRequired => 'Name is required';
+ String get addKeystoneFirmwareTitle => 'Update to the latest Keystone firmware';
@override
- String get addHardwareAccountInvalidAddress => 'Invalid address';
+ String get addKeystoneFirmwareSubtitle => 'Required for QUAN signing';
+
+ @override
+ String get addKeystoneOnYourKeystone => 'ON YOUR KEYSTONE';
+
+ @override
+ String get addKeystoneStepUnlock => 'Unlock your Keystone';
+
+ @override
+ String get addKeystoneStepSelectQuantus => 'Select Quantus and scan the QR code';
+
+ @override
+ String get addKeystoneReadyToScan => 'Ready to Scan';
@override
String get invalidAddress => 'Invalid address';
@@ -1034,24 +1047,58 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get sendTxSubmittedDone => 'Done';
+ @override
+ String get keystoneSignScreenTitle => 'Sign with Keystone';
+
+ @override
+ String keystoneSignStep(int current, int total) {
+ return 'STEP $current/$total';
+ }
+
@override
String get keystoneSignTitle => 'Scan with your Keystone';
@override
- String get keystoneSignInstruction => 'Show this QR code to your Keystone hardware wallet to sign the transaction.';
+ String get keystoneSignInstruction => 'Open your Keystone and scan this QR code to load the transaction.';
+
+ @override
+ String get keystoneSignYouAreSigning => 'YOU ARE SIGNING';
@override
- String get keystoneSignNext => 'Scan signature';
+ String get keystoneSignNext => 'Continue to Sign';
+
+ @override
+ String get keystoneSignCancel => 'Cancel Transaction';
@override
String get keystoneSignError => 'Failed to prepare the transaction. Please try again.';
@override
- String get keystoneScanInstruction => 'Scan the signature QR from your Keystone';
+ String get keystoneVerifyTitle => 'Check your Keystone Screen';
+
+ @override
+ String get keystoneVerifyInstruction => 'Before approving on your Keystone, make sure its screen shows exactly this:';
+
+ @override
+ String get keystoneVerifyWarning =>
+ 'If the amount or address on your Keystone screen is different from what\'s shown here, reject the transaction on your device.';
+
+ @override
+ String get keystoneVerifyMismatch => 'It doesn\'t match';
+
+ @override
+ String get keystoneScanTitle => 'Scan the signature';
+
+ @override
+ String get keystoneScanInstruction =>
+ 'Your Keystone is now showing an animated QR. Hold your phone up to it, this takes a moment.';
+
+ @override
+ String get keystoneScanReceiving => 'RECEIVING SIGNATURE';
@override
String keystoneScanProgress(int scanned, int total) {
- return '$scanned / $total frames';
+ return '$scanned/$total';
}
@override
@@ -1065,6 +1112,33 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get keystoneScanError => 'Couldn\'t read the signature. Please try again.';
+ @override
+ String get keystoneRejectTitle => 'Don\'t approve this transaction';
+
+ @override
+ String get keystoneRejectStep1Title => 'Reject the transaction on your Keystone.';
+
+ @override
+ String get keystoneRejectStep1Body => 'Nothing is signed or sent until you approve there.';
+
+ @override
+ String get keystoneRejectStep2Title => 'Don\'t retry from this phone.';
+
+ @override
+ String get keystoneRejectStep2Body => 'If it\'s compromised, retrying gives the attacker another chance.';
+
+ @override
+ String get keystoneRejectStep3Title => 'Your funds are safe on the Keystone.';
+
+ @override
+ String get keystoneRejectStep3Body => 'Keys never left the device. Move to a trusted phone before sending again.';
+
+ @override
+ String get keystoneRejectConfirm => 'I rejected it on Keystone';
+
+ @override
+ String get keystoneRejectContactSupport => 'Contact Support';
+
@override
String get sendLogicCantSelfTransfer => 'Can\'t Self Transfer';
diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart
index 7d48cc059..fc3f9b7c8 100644
--- a/mobile-app/lib/l10n/app_localizations_id.dart
+++ b/mobile-app/lib/l10n/app_localizations_id.dart
@@ -901,34 +901,47 @@ class AppLocalizationsId extends AppLocalizations {
String get accountDetailsTitle => 'Detail Alamat';
@override
- String get addHardwareAccountAddWallet => 'Tambah Hardware Wallet';
+ String get addKeystoneAppBarTitle => 'Add Keystone Wallet';
@override
- String get addHardwareAccountAddAccount => 'Tambah Akun Hardware';
+ String get addKeystoneIntroTitle => 'Keystone Hardware Wallet';
@override
- String get addHardwareAccountNameLabel => 'NAMA';
+ String get addKeystoneIntroSubtitle =>
+ 'Air-gapped signing for QUAN. Keys stay on the device, signing happens over QR.';
@override
- String get addHardwareAccountNameHintWallet => 'Hardware Wallet';
+ String get addKeystoneConnectButton => 'Connect Hardware Wallet';
@override
- String get addHardwareAccountNameHintAccount => 'Akun';
+ String get addKeystoneGetOneLink => 'Don\'t have one? Get a Keystone ↗';
@override
- String get addHardwareAccountAddressLabel => 'ALAMAT';
+ String get addKeystoneConnectTitle => 'Connect Hardware Wallet';
@override
- String get addHardwareAccountAddressHint => 'Alamat penandatangan';
+ String get addKeystoneConnectSubtitle => 'Scan your device\'s QR code to connect.';
@override
- String get addHardwareAccountDebugFill => 'Isi Debug';
+ String get addKeystoneBeforeYouStart => 'BEFORE YOU START';
@override
- String get addHardwareAccountNameRequired => 'Nama wajib diisi';
+ String get addKeystoneFirmwareTitle => 'Update to the latest Keystone firmware';
@override
- String get addHardwareAccountInvalidAddress => 'Alamat tidak valid';
+ String get addKeystoneFirmwareSubtitle => 'Required for QUAN signing';
+
+ @override
+ String get addKeystoneOnYourKeystone => 'ON YOUR KEYSTONE';
+
+ @override
+ String get addKeystoneStepUnlock => 'Unlock your Keystone';
+
+ @override
+ String get addKeystoneStepSelectQuantus => 'Select Quantus and scan the QR code';
+
+ @override
+ String get addKeystoneReadyToScan => 'Ready to Scan';
@override
String get invalidAddress => 'Alamat tidak valid';
@@ -1031,25 +1044,58 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get sendTxSubmittedDone => 'Selesai';
+ @override
+ String get keystoneSignScreenTitle => 'Sign with Keystone';
+
+ @override
+ String keystoneSignStep(int current, int total) {
+ return 'STEP $current/$total';
+ }
+
@override
String get keystoneSignTitle => 'Pindai dengan Keystone Anda';
@override
- String get keystoneSignInstruction =>
- 'Tunjukkan kode QR ini ke dompet perangkat keras Keystone Anda untuk menandatangani transaksi.';
+ String get keystoneSignInstruction => 'Open your Keystone and scan this QR code to load the transaction.';
+
+ @override
+ String get keystoneSignYouAreSigning => 'YOU ARE SIGNING';
@override
- String get keystoneSignNext => 'Pindai tanda tangan';
+ String get keystoneSignNext => 'Continue to Sign';
+
+ @override
+ String get keystoneSignCancel => 'Cancel Transaction';
@override
String get keystoneSignError => 'Gagal menyiapkan transaksi. Silakan coba lagi.';
@override
- String get keystoneScanInstruction => 'Pindai QR tanda tangan dari Keystone Anda';
+ String get keystoneVerifyTitle => 'Check your Keystone Screen';
+
+ @override
+ String get keystoneVerifyInstruction => 'Before approving on your Keystone, make sure its screen shows exactly this:';
+
+ @override
+ String get keystoneVerifyWarning =>
+ 'If the amount or address on your Keystone screen is different from what\'s shown here, reject the transaction on your device.';
+
+ @override
+ String get keystoneVerifyMismatch => 'It doesn\'t match';
+
+ @override
+ String get keystoneScanTitle => 'Scan the signature';
+
+ @override
+ String get keystoneScanInstruction =>
+ 'Your Keystone is now showing an animated QR. Hold your phone up to it, this takes a moment.';
+
+ @override
+ String get keystoneScanReceiving => 'RECEIVING SIGNATURE';
@override
String keystoneScanProgress(int scanned, int total) {
- return '$scanned / $total bingkai';
+ return '$scanned/$total';
}
@override
@@ -1063,6 +1109,33 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get keystoneScanError => 'Tidak dapat membaca tanda tangan. Silakan coba lagi.';
+ @override
+ String get keystoneRejectTitle => 'Don\'t approve this transaction';
+
+ @override
+ String get keystoneRejectStep1Title => 'Reject the transaction on your Keystone.';
+
+ @override
+ String get keystoneRejectStep1Body => 'Nothing is signed or sent until you approve there.';
+
+ @override
+ String get keystoneRejectStep2Title => 'Don\'t retry from this phone.';
+
+ @override
+ String get keystoneRejectStep2Body => 'If it\'s compromised, retrying gives the attacker another chance.';
+
+ @override
+ String get keystoneRejectStep3Title => 'Your funds are safe on the Keystone.';
+
+ @override
+ String get keystoneRejectStep3Body => 'Keys never left the device. Move to a trusted phone before sending again.';
+
+ @override
+ String get keystoneRejectConfirm => 'I rejected it on Keystone';
+
+ @override
+ String get keystoneRejectContactSupport => 'Contact Support';
+
@override
String get sendLogicCantSelfTransfer => 'Tidak Bisa Transfer ke Diri Sendiri';
diff --git a/mobile-app/lib/providers/account_providers.dart b/mobile-app/lib/providers/account_providers.dart
index fcdf2846f..4fa0d385a 100644
--- a/mobile-app/lib/providers/account_providers.dart
+++ b/mobile-app/lib/providers/account_providers.dart
@@ -96,3 +96,10 @@ final activeAccountProvider = StateNotifierProvider Function(List parts) onComplete;
final FutureOr Function(Object error)? onError;
final AnimatedQrSequence? Function(String part)? sequenceForPart;
+ final String appBarTitle;
+ final String stepLabel;
+ final String title;
final String instruction;
+ final String progressHeader;
final String submittingLabel;
final String Function(int scanned, int total) progressLabel;
final String Function(int scanned) scanningLabel;
@@ -39,7 +44,11 @@ class AnimatedQrScanner extends StatefulWidget {
required this.acceptsPart,
required this.isComplete,
required this.onComplete,
+ required this.appBarTitle,
+ required this.stepLabel,
+ required this.title,
required this.instruction,
+ required this.progressHeader,
required this.submittingLabel,
required this.progressLabel,
required this.scanningLabel,
@@ -130,107 +139,201 @@ class _AnimatedQrScannerState extends State {
Widget build(BuildContext context) {
final colors = context.colors;
final text = context.themeText;
- final size = MediaQuery.of(context).size;
- final frame = (size.width - 96).clamp(220.0, 300.0);
final progress = _expectedParts == null ? null : (_seenSequenceIndexes.length / _expectedParts!).clamp(0.0, 1.0);
return Scaffold(
- backgroundColor: Colors.black,
- body: Stack(
- children: [
- MobileScanner(controller: _controller, onDetect: _onDetect),
- Center(
- child: Container(
- width: frame,
- height: frame,
- decoration: BoxDecoration(
- border: Border.all(color: colors.accentOrange, width: 2),
- borderRadius: BorderRadius.circular(16),
- ),
- ),
- ),
- Positioned(
- top: 20,
- left: 16,
- right: 16,
- child: SafeArea(
- child: Row(
- children: [
- QuantusIconButton.rounded(
- icon: Icons.arrow_back,
- onTap: () => Navigator.pop(context),
- style: IconButtonStyle.glass,
+ backgroundColor: colors.background,
+ body: SafeArea(
+ child: Stack(
+ children: [
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24),
+ child: V2AppBar(title: widget.appBarTitle),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ widget.stepLabel,
+ style: text.detail?.copyWith(
+ fontFamily: AppTextTheme.fontFamilySecondary,
+ color: colors.accentOrange,
+ letterSpacing: 0.96,
+ ),
+ ),
+ const SizedBox(height: 12),
+ Text(widget.title, style: text.paragraph?.copyWith(color: colors.textPrimary, height: 1.0)),
+ const SizedBox(height: 8),
+ Text(widget.instruction, style: text.detail?.copyWith(color: colors.textSubtle, height: 1.35)),
+ ],
),
- const Spacer(),
- QuantusIconButton.rounded(
- icon: Icons.flash_on,
- onTap: _controller.toggleTorch,
- style: IconButtonStyle.glass,
+ ),
+ const SizedBox(height: 24),
+ Expanded(
+ child: Padding(padding: const EdgeInsets.symmetric(horizontal: 24), child: _cameraView(colors)),
+ ),
+ _bottomSection(colors, text, progress),
+ ],
+ ),
+ if (_submitting)
+ Positioned.fill(
+ child: ColoredBox(
+ color: colors.background.useOpacity(0.7),
+ child: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ CircularProgressIndicator(color: colors.accentOrange),
+ const SizedBox(height: 16),
+ Text(widget.submittingLabel, style: text.paragraph?.copyWith(color: colors.textPrimary)),
+ ],
+ ),
),
- ],
+ ),
),
- ),
- ),
- Positioned(
- left: 24,
- right: 24,
- bottom: 48,
- child: Column(
- mainAxisSize: MainAxisSize.min,
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _cameraView(AppColorsV2 colors) {
+ return Container(
+ decoration: BoxDecoration(
+ border: Border.all(color: colors.textLabel),
+ borderRadius: BorderRadius.circular(14),
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(14),
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ final frame = (constraints.maxWidth - 96).clamp(160.0, 300.0);
+ return Stack(
+ fit: StackFit.expand,
children: [
- Text(
- widget.errorText ?? widget.instruction,
- style: text.paragraph?.copyWith(color: widget.errorText != null ? colors.error : Colors.white),
- textAlign: TextAlign.center,
+ MobileScanner(controller: _controller, onDetect: _onDetect),
+ Center(
+ child: SizedBox(width: frame, height: frame, child: const _ScanBrackets()),
),
- const SizedBox(height: 16),
- if (progress != null) ...[
- ClipRRect(
- borderRadius: BorderRadius.circular(4),
- child: LinearProgressIndicator(
- value: progress,
- minHeight: 6,
- backgroundColor: colors.surface,
- color: colors.accentOrange,
- ),
+ ],
+ );
+ },
+ ),
+ ),
+ );
+ }
+
+ Widget _bottomSection(AppColorsV2 colors, AppTextTheme text, double? progress) {
+ final progressText = progress != null
+ ? widget.progressLabel(_seenSequenceIndexes.length, _expectedParts!)
+ : (_parts.isNotEmpty ? widget.scanningLabel(_parts.length) : null);
+
+ return Container(
+ padding: const EdgeInsets.fromLTRB(24, 25, 24, 24),
+ decoration: BoxDecoration(
+ border: Border(top: BorderSide(color: colors.surfaceDeep, width: 1)),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ if (widget.errorText != null)
+ Text(widget.errorText!, style: text.detail?.copyWith(color: colors.textError))
+ else ...[
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ widget.progressHeader,
+ style: text.detail?.copyWith(
+ fontFamily: AppTextTheme.fontFamilySecondary,
+ color: colors.textSubtle,
+ letterSpacing: 0.96,
),
- const SizedBox(height: 8),
+ ),
+ if (progressText != null)
Text(
- widget.progressLabel(_seenSequenceIndexes.length, _expectedParts!),
- style: text.detail?.copyWith(color: Colors.white70),
- ),
- ] else if (_parts.isNotEmpty)
- Text(widget.scanningLabel(_parts.length), style: text.detail?.copyWith(color: Colors.white70)),
- if (widget.debugPartsBuilder != null && !_submitting) ...[
- const SizedBox(height: 16),
- QuantusButton.simple(
- label: widget.debugActionLabel,
- onTap: _simulateScan,
- variant: ButtonVariant.danger,
- width: null,
+ progressText,
+ style: text.detail?.copyWith(
+ fontFamily: AppTextTheme.fontFamilySecondary,
+ color: colors.textPrimary,
+ letterSpacing: 0.96,
+ ),
),
- ],
],
),
- ),
- if (_submitting)
- Positioned.fill(
- child: ColoredBox(
- color: Colors.black.withValues(alpha: 0.7),
- child: Center(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- CircularProgressIndicator(color: colors.accentOrange),
- const SizedBox(height: 16),
- Text(widget.submittingLabel, style: text.paragraph?.copyWith(color: Colors.white)),
- ],
- ),
- ),
+ const SizedBox(height: 16),
+ ClipRRect(
+ borderRadius: BorderRadius.circular(2),
+ child: LinearProgressIndicator(
+ value: progress,
+ minHeight: 4,
+ backgroundColor: colors.progressStepPendingDot,
+ color: colors.accentOrange,
),
),
+ ],
+ if (widget.debugPartsBuilder != null && !_submitting) ...[
+ const SizedBox(height: 16),
+ QuantusButton.simple(label: widget.debugActionLabel, onTap: _simulateScan, variant: ButtonVariant.danger),
+ ],
],
),
);
}
}
+
+/// White corner brackets marking the scan region.
+class _ScanBrackets extends StatelessWidget {
+ const _ScanBrackets();
+
+ static const double _length = 28;
+ static const double _thickness = 2;
+
+ Widget _corner({required Color color, required Alignment alignment}) {
+ final horizontal = alignment.x < 0 ? Alignment.centerLeft : Alignment.centerRight;
+ return Align(
+ alignment: alignment,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: alignment.x < 0 ? CrossAxisAlignment.start : CrossAxisAlignment.end,
+ children: [
+ if (alignment.y < 0) ...[
+ _bar(color: color, vertical: true),
+ Align(
+ alignment: horizontal,
+ child: _bar(color: color, vertical: false),
+ ),
+ ] else ...[
+ Align(
+ alignment: horizontal,
+ child: _bar(color: color, vertical: false),
+ ),
+ _bar(color: color, vertical: true),
+ ],
+ ],
+ ),
+ );
+ }
+
+ Widget _bar({required Color color, required bool vertical}) {
+ return Container(width: vertical ? _thickness : _length, height: vertical ? _length : _thickness, color: color);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final color = context.colors.textPrimary;
+ return Stack(
+ children: [
+ _corner(color: color, alignment: Alignment.topLeft),
+ _corner(color: color, alignment: Alignment.topRight),
+ _corner(color: color, alignment: Alignment.bottomLeft),
+ _corner(color: color, alignment: Alignment.bottomRight),
+ ],
+ );
+ }
+}
diff --git a/mobile-app/lib/v2/components/quantus_button.dart b/mobile-app/lib/v2/components/quantus_button.dart
index c326af4a8..bd1e5b630 100644
--- a/mobile-app/lib/v2/components/quantus_button.dart
+++ b/mobile-app/lib/v2/components/quantus_button.dart
@@ -5,7 +5,7 @@ import 'package:resonance_network_wallet/v2/components/loader.dart';
import 'package:resonance_network_wallet/v2/theme/app_colors.dart';
import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart';
-enum ButtonVariant { transparent, primary, secondary, danger, success, outline, glass }
+enum ButtonVariant { transparent, primary, secondary, danger, success, outline, glass, underline }
enum IconPlacement { leading, trailing, top }
@@ -110,6 +110,20 @@ class QuantusButton extends StatelessWidget {
case ButtonVariant.glass:
buttonDecorationColor = context.colors.surfaceGlass;
break;
+
+ case ButtonVariant.underline:
+ buttonDecorationColor = Colors.transparent;
+ break;
+ }
+
+ if (variant == ButtonVariant.underline) {
+ return InkWell(
+ onTap: disabled ? null : onTap,
+ child: Opacity(
+ opacity: visibility,
+ child: Container(width: width, padding: padding, child: buttonContent),
+ ),
+ );
}
return InkWell(
@@ -139,6 +153,7 @@ class QuantusButton extends StatelessWidget {
ButtonVariant.success => context.colors.textPrimary,
ButtonVariant.outline => context.colors.textLabel,
ButtonVariant.glass => context.colors.textPrimary,
+ ButtonVariant.underline => context.colors.textMuted,
};
if (isLoading) {
@@ -148,13 +163,14 @@ class QuantusButton extends StatelessWidget {
if (child != null) return child!;
- final effectiveTextStyle =
- _textStyle ??
- context.themeText.paragraph!.copyWith(
- fontSize: buttonFontSize,
- color: disabled ? context.colors.textPrimary.useOpacity(0.5) : textColor,
- fontWeight: FontWeight.w500,
- );
+ final effectiveTextStyle = variant == ButtonVariant.underline
+ ? _underlineTextStyle(context, textColor)
+ : _textStyle ??
+ context.themeText.paragraph!.copyWith(
+ fontSize: buttonFontSize,
+ color: disabled ? context.colors.textPrimary.useOpacity(0.5) : textColor,
+ fontWeight: FontWeight.w500,
+ );
Widget content;
if (_iconPlacement == IconPlacement.top) {
@@ -180,4 +196,11 @@ class QuantusButton extends StatelessWidget {
return Center(child: content);
}
+
+ /// Underlined link label; a [textStyle] override supplies the color (e.g.
+ /// accent orange), everything else stays uniform across links.
+ TextStyle _underlineTextStyle(BuildContext context, Color defaultColor) {
+ final style = context.themeText.smallParagraph!.copyWith(color: defaultColor).merge(_textStyle);
+ return style.copyWith(decoration: TextDecoration.underline, decorationColor: style.color);
+ }
}
diff --git a/mobile-app/lib/v2/screens/accounts/account_menu_screen.dart b/mobile-app/lib/v2/screens/accounts/account_menu_screen.dart
index 7476f4d42..65aeab100 100644
--- a/mobile-app/lib/v2/screens/accounts/account_menu_screen.dart
+++ b/mobile-app/lib/v2/screens/accounts/account_menu_screen.dart
@@ -171,8 +171,7 @@ class AccountMenuScreen extends ConsumerWidget {
final l10n = ref.read(l10nProvider);
try {
await AccountsService().removeAccount(account);
- ref.invalidate(accountsProvider);
- ref.invalidate(activeAccountProvider);
+ invalidateAccountProviders(ref);
if (context.mounted) returnToAccountsSheet(context, ref);
} catch (e, st) {
quantusPrint('[AccountMenu] disconnect account error: $e\n$st');
@@ -184,8 +183,7 @@ class AccountMenuScreen extends ConsumerWidget {
final l10n = ref.read(l10nProvider);
try {
await AccountsService().removeWallet(walletIndex);
- ref.invalidate(accountsProvider);
- ref.invalidate(activeAccountProvider);
+ invalidateAccountProviders(ref);
if (context.mounted) returnToAccountsSheet(context, ref);
} catch (e, st) {
quantusPrint('[AccountMenu] disconnect wallet error: $e\n$st');
diff --git a/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart b/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart
index 4ace4dab2..0105a4e42 100644
--- a/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart
+++ b/mobile-app/lib/v2/screens/accounts/add_hardware_account_screen.dart
@@ -1,159 +1,89 @@
-import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:quantus_sdk/quantus_sdk.dart';
-import 'package:resonance_network_wallet/features/components/custom_text_field.dart';
+import 'package:resonance_network_wallet/providers/l10n_provider.dart';
import 'package:resonance_network_wallet/v2/components/quantus_button.dart';
import 'package:resonance_network_wallet/v2/components/scaffold_base.dart';
-import 'package:resonance_network_wallet/v2/components/qr_scanner_page.dart';
-import 'package:resonance_network_wallet/features/styles/app_size_theme.dart';
-import 'package:resonance_network_wallet/features/styles/app_text_theme.dart';
-import 'package:resonance_network_wallet/providers/account_providers.dart';
-import 'package:resonance_network_wallet/providers/l10n_provider.dart';
+import 'package:resonance_network_wallet/v2/components/scaffold_base_bottom_content.dart';
import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart';
-import 'package:resonance_network_wallet/v2/screens/accounts/account_menu_screen.dart';
+import 'package:resonance_network_wallet/v2/screens/accounts/connect_keystone_screen.dart';
+import 'package:resonance_network_wallet/v2/theme/app_colors.dart';
+import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart';
+import 'package:url_launcher/url_launcher.dart';
-class AddHardwareAccountScreen extends ConsumerStatefulWidget {
+/// Intro screen for adding a Keystone hardware wallet/account.
+///
+/// Explains what Keystone is, then hands off to [ConnectKeystoneScreen] for
+/// the on-device instructions and QR scan.
+class AddHardwareAccountScreen extends ConsumerWidget {
const AddHardwareAccountScreen({super.key, required this.walletIndex, this.isNewWallet = false});
final int walletIndex;
final bool isNewWallet;
- @override
- ConsumerState createState() => _AddHardwareAccountScreenState();
-}
-
-class _AddHardwareAccountScreenState extends ConsumerState {
- final _name = TextEditingController(text: 'Keystone Wallet');
- final _address = TextEditingController();
-
- final _accountsService = AccountsService();
- final _settingsService = SettingsService();
- final _substrateService = SubstrateService();
-
- bool _isSaving = false;
- String? _error;
-
- Future _scanQRCode() async {
- final result = await Navigator.push(
- context,
- MaterialPageRoute(builder: (context) => const QrScannerPage(), fullscreenDialog: true),
- );
- if (result != null && mounted) {
- _address.text = result.trim();
- if (_error != null) setState(() => _error = null);
- }
- }
-
- void _fillDebugAddress() {
- _address.text = AppConstants.debugTestAddress;
- if (_error != null) setState(() => _error = null);
- }
-
- Future _save() async {
- final name = _name.text.trim();
- final address = _address.text.trim();
-
- final l10n = ref.read(l10nProvider);
- if (name.isEmpty) {
- setState(() => _error = l10n.addHardwareAccountNameRequired);
- return;
- }
- if (!_substrateService.isValidSS58Address(address)) {
- setState(() => _error = l10n.addHardwareAccountInvalidAddress);
- return;
- }
-
- setState(() {
- _isSaving = true;
- _error = null;
- });
-
- try {
- final nextIndex = await _settingsService.getNextFreeAccountIndex(widget.walletIndex);
- final account = Account(
- walletIndex: widget.walletIndex,
- index: nextIndex,
- name: name,
- accountId: address,
- accountType: AccountType.keystone,
- );
- await _accountsService.addAccount(account);
- ref.invalidate(accountsProvider);
- ref.invalidate(activeAccountProvider);
- if (mounted) {
- Navigator.of(context).pushReplacement(
- MaterialPageRoute(builder: (_) => AccountMenuScreen(initialAccount: account, isPostCreation: true)),
- );
- }
- } catch (e) {
- if (mounted) setState(() => _error = e.toString());
- } finally {
- if (mounted) setState(() => _isSaving = false);
- }
- }
+ static final Uri _keystoneStoreUrl = Uri.parse('https://keyst.one');
@override
- void dispose() {
- _name.dispose();
- _address.dispose();
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
+ Widget build(BuildContext context, WidgetRef ref) {
final l10n = ref.watch(l10nProvider);
- final title = widget.isNewWallet ? l10n.addHardwareAccountAddWallet : l10n.addHardwareAccountAddAccount;
+ final colors = context.colors;
+ final text = context.themeText;
return ScaffoldBase(
- appBar: V2AppBar(title: title),
+ appBar: V2AppBar(title: l10n.addKeystoneAppBarTitle),
mainContent: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
- const SizedBox(height: 20),
- Text(title, style: context.themeText.smallTitle),
- const SizedBox(height: 16),
- CustomTextField(
- controller: _name,
- labelText: l10n.addHardwareAccountNameLabel,
- hintText: widget.isNewWallet
- ? l10n.addHardwareAccountNameHintWallet
- : l10n.addHardwareAccountNameHintAccount,
- onChanged: (_) {
- if (_error != null) setState(() => _error = null);
- },
+ Container(
+ height: 218,
+ decoration: BoxDecoration(
+ border: Border.all(color: colors.textPrimary.useOpacity(0.07)),
+ borderRadius: BorderRadius.circular(8),
+ gradient: LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [colors.sheetBackground, colors.surfaceHero],
+ ),
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(8),
+ child: Image.asset('assets/v2/keystone_hero.png', fit: BoxFit.cover, alignment: Alignment.topCenter),
+ ),
),
- const SizedBox(height: 16),
- CustomTextField(
- controller: _address,
- labelText: l10n.addHardwareAccountAddressLabel,
- hintText: l10n.addHardwareAccountAddressHint,
- onChanged: (_) {
- if (_error != null) setState(() => _error = null);
- },
+ const SizedBox(height: 24),
+ Text(
+ l10n.addKeystoneIntroTitle,
+ style: text.mediumTitle?.copyWith(color: colors.textPrimary),
+ textAlign: TextAlign.center,
),
- const SizedBox(height: 12),
- QuantusButton.simple(
- label: l10n.componentQrScannerTitle,
- variant: ButtonVariant.secondary,
- onTap: _scanQRCode,
+ const SizedBox(height: 8),
+ Text(
+ l10n.addKeystoneIntroSubtitle,
+ style: text.smallParagraph?.copyWith(color: colors.textSubtle),
+ textAlign: TextAlign.center,
),
- if (kDebugMode) ...[
- const SizedBox(height: 12),
+ ],
+ ),
+ bottomContent: ScaffoldBaseBottomContent(
+ child: Column(
+ children: [
QuantusButton.simple(
- label: l10n.addHardwareAccountDebugFill,
- variant: ButtonVariant.secondary,
- onTap: _fillDebugAddress,
+ label: l10n.addKeystoneConnectButton,
+ onTap: () => Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (_) => ConnectKeystoneScreen(walletIndex: walletIndex, isNewWallet: isNewWallet),
+ ),
+ ),
+ ),
+ const SizedBox(height: 4),
+ QuantusButton.simple(
+ label: l10n.addKeystoneGetOneLink,
+ variant: ButtonVariant.underline,
+ textStyle: TextStyle(color: colors.accentOrange),
+ onTap: () => launchUrl(_keystoneStoreUrl, mode: LaunchMode.externalApplication),
),
],
- if (_error != null) ...[
- const SizedBox(height: 10),
- Text(_error!, style: context.themeText.tiny?.copyWith(color: Colors.red)),
- ],
- const Spacer(),
- QuantusButton.simple(label: title, onTap: _isSaving ? null : _save, isLoading: _isSaving),
- SizedBox(height: context.themeSize.bottomButtonSpacing),
- ],
+ ),
),
);
}
diff --git a/mobile-app/lib/v2/screens/accounts/connect_keystone_screen.dart b/mobile-app/lib/v2/screens/accounts/connect_keystone_screen.dart
new file mode 100644
index 000000000..f9e19e3a1
--- /dev/null
+++ b/mobile-app/lib/v2/screens/accounts/connect_keystone_screen.dart
@@ -0,0 +1,197 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:quantus_sdk/quantus_sdk.dart';
+import 'package:resonance_network_wallet/providers/account_providers.dart';
+import 'package:resonance_network_wallet/providers/l10n_provider.dart';
+import 'package:resonance_network_wallet/v2/components/quantus_button.dart';
+import 'package:resonance_network_wallet/v2/components/qr_scanner_page.dart';
+import 'package:resonance_network_wallet/v2/components/scaffold_base.dart';
+import 'package:resonance_network_wallet/v2/components/scaffold_base_bottom_content.dart';
+import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart';
+import 'package:resonance_network_wallet/v2/screens/accounts/account_menu_screen.dart';
+import 'package:resonance_network_wallet/v2/theme/app_colors.dart';
+import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart';
+
+/// Second add-Keystone screen: on-device instructions, then scans the device's
+/// QR code to read the account address and saves the Keystone account.
+class ConnectKeystoneScreen extends ConsumerStatefulWidget {
+ const ConnectKeystoneScreen({super.key, required this.walletIndex, this.isNewWallet = false});
+
+ final int walletIndex;
+ final bool isNewWallet;
+
+ @override
+ ConsumerState createState() => _ConnectKeystoneScreenState();
+}
+
+class _ConnectKeystoneScreenState extends ConsumerState {
+ final _accountsService = AccountsService();
+ final _settingsService = SettingsService();
+ final _substrateService = SubstrateService();
+
+ bool _isSaving = false;
+ String? _error;
+
+ Future _scan() async {
+ final result = await Navigator.push(
+ context,
+ MaterialPageRoute(builder: (context) => const QrScannerPage(), fullscreenDialog: true),
+ );
+ if (result == null || !mounted) return;
+ final address = result.trim();
+ if (!_substrateService.isValidSS58Address(address)) {
+ setState(() => _error = ref.read(l10nProvider).invalidAddress);
+ return;
+ }
+ await _save(address);
+ }
+
+ Future _save(String address) async {
+ setState(() {
+ _isSaving = true;
+ _error = null;
+ });
+
+ try {
+ final nextIndex = await _settingsService.getNextFreeAccountIndex(widget.walletIndex);
+ final account = Account(
+ walletIndex: widget.walletIndex,
+ index: nextIndex,
+ name: 'Keystone Wallet',
+ accountId: address,
+ accountType: AccountType.keystone,
+ );
+ await _accountsService.addAccount(account);
+ invalidateAccountProviders(ref);
+ if (mounted) {
+ Navigator.of(context).pushReplacement(
+ MaterialPageRoute(builder: (_) => AccountMenuScreen(initialAccount: account, isPostCreation: true)),
+ );
+ }
+ } catch (e) {
+ if (mounted) setState(() => _error = e.toString());
+ } finally {
+ if (mounted) setState(() => _isSaving = false);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final l10n = ref.watch(l10nProvider);
+ final colors = context.colors;
+ final text = context.themeText;
+
+ final sectionLabelStyle = text.tiny?.copyWith(
+ fontFamily: AppTextTheme.fontFamilySecondary,
+ color: colors.textMuted,
+ letterSpacing: 0.88,
+ );
+
+ return ScaffoldBase(
+ appBar: V2AppBar(title: l10n.addKeystoneAppBarTitle),
+ mainContent: SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Image.asset('assets/v2/keystone_logo.png', width: 142, height: 32),
+ const SizedBox(height: 24),
+ Text(
+ l10n.addKeystoneConnectTitle,
+ style: text.mediumTitle?.copyWith(color: colors.textLightGray, letterSpacing: -0.48),
+ ),
+ const SizedBox(height: 8),
+ Text(l10n.addKeystoneConnectSubtitle, style: text.smallParagraph?.copyWith(color: colors.textSubtle)),
+ const SizedBox(height: 40),
+ Text(l10n.addKeystoneBeforeYouStart, style: sectionLabelStyle),
+ const SizedBox(height: 12),
+ _InstructionRow(
+ iconAsset: 'assets/v2/keystone_arrow_line_down.svg',
+ title: l10n.addKeystoneFirmwareTitle,
+ subtitle: l10n.addKeystoneFirmwareSubtitle,
+ ),
+ const SizedBox(height: 24),
+ Divider(height: 1, color: colors.separator),
+ const SizedBox(height: 24),
+ Text(l10n.addKeystoneOnYourKeystone, style: sectionLabelStyle),
+ const SizedBox(height: 12),
+ _InstructionRow(iconAsset: 'assets/v2/keystone_lock_simple_open.svg', title: l10n.addKeystoneStepUnlock),
+ _rowDivider(colors),
+ _InstructionRow(iconAsset: 'assets/v2/keystone_qr_code.svg', title: l10n.addKeystoneStepSelectQuantus),
+ if (_error != null) ...[
+ const SizedBox(height: 16),
+ Text(_error!, style: text.detail?.copyWith(color: colors.textError)),
+ ],
+ ],
+ ),
+ ),
+ bottomContent: ScaffoldBaseBottomContent(
+ child: Column(
+ children: [
+ QuantusButton.simple(
+ label: l10n.addKeystoneReadyToScan,
+ onTap: _isSaving ? null : _scan,
+ isLoading: _isSaving,
+ ),
+ if (kDebugMode) ...[
+ const SizedBox(height: 12),
+ QuantusButton.simple(
+ label: 'DEBUG: Use test address',
+ variant: ButtonVariant.secondary,
+ onTap: _isSaving ? null : () => _save(AppConstants.debugTestAddress),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _rowDivider(AppColorsV2 colors) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 12),
+ child: Divider(height: 1, color: colors.separator),
+ );
+ }
+}
+
+class _InstructionRow extends StatelessWidget {
+ final String iconAsset;
+ final String title;
+ final String? subtitle;
+
+ const _InstructionRow({required this.iconAsset, required this.title, this.subtitle});
+
+ @override
+ Widget build(BuildContext context) {
+ final colors = context.colors;
+ final text = context.themeText;
+
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Container(
+ width: 40,
+ height: 40,
+ alignment: Alignment.center,
+ decoration: BoxDecoration(color: colors.sheetBackground, borderRadius: BorderRadius.circular(20)),
+ child: SvgPicture.asset(iconAsset, width: 16, height: 16),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(title, style: text.paragraph?.copyWith(color: colors.textPrimary, height: 1.0)),
+ if (subtitle != null) ...[
+ const SizedBox(height: 8),
+ Text(subtitle!, style: text.detail?.copyWith(color: colors.textSubtle)),
+ ],
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/mobile-app/lib/v2/screens/accounts/create_account_screen.dart b/mobile-app/lib/v2/screens/accounts/create_account_screen.dart
index 0ffd95844..3a54cc4bd 100644
--- a/mobile-app/lib/v2/screens/accounts/create_account_screen.dart
+++ b/mobile-app/lib/v2/screens/accounts/create_account_screen.dart
@@ -51,8 +51,7 @@ class _CreateAccountScreenState extends ConsumerState {
final accountToSave = draft.copyWith(name: _accountName.text.trim());
await _accountsService.addAccount(accountToSave);
- ref.invalidate(accountsProvider);
- ref.invalidate(activeAccountProvider);
+ invalidateAccountProviders(ref);
ref.read(firebaseMessagingServiceProvider).insertNewAddress(accountToSave.accountId);
if (mounted) returnToAccountsSheet(context, ref, highlightAccountId: accountToSave.accountId);
diff --git a/mobile-app/lib/v2/screens/import/import_wallet_screen.dart b/mobile-app/lib/v2/screens/import/import_wallet_screen.dart
index 6f24a35e6..f84b6773b 100644
--- a/mobile-app/lib/v2/screens/import/import_wallet_screen.dart
+++ b/mobile-app/lib/v2/screens/import/import_wallet_screen.dart
@@ -113,8 +113,7 @@ class _ImportWalletScreenV2State extends ConsumerState {
if (!HdWalletService.isDevAccount(mnemonic)) {
await _discoverAccounts(mnemonic);
}
- ref.invalidate(accountsProvider);
- ref.invalidate(activeAccountProvider);
+ invalidateAccountProviders(ref);
_settingsService.setReferralCheckCompleted();
_settingsService.setExistingUserSeenPromoVideo();
_settingsService.setWalletOrigin(widget.walletIndex, WalletOrigin.imported);
@@ -148,8 +147,7 @@ class _ImportWalletScreenV2State extends ConsumerState {
await _accountsService.addAccount(account);
}
}
- ref.invalidate(accountsProvider);
- ref.invalidate(activeAccountProvider);
+ invalidateAccountProviders(ref);
unawaited(_discoverEncryptedAccount());
} catch (e) {
quantusPrint('error discovering accounts: $e');
diff --git a/mobile-app/lib/v2/screens/multisig/multisig_action_confirm_sheet.dart b/mobile-app/lib/v2/screens/multisig/multisig_action_confirm_sheet.dart
index 2f4f555eb..18b6ca009 100644
--- a/mobile-app/lib/v2/screens/multisig/multisig_action_confirm_sheet.dart
+++ b/mobile-app/lib/v2/screens/multisig/multisig_action_confirm_sheet.dart
@@ -303,7 +303,6 @@ class _MultisigActionConfirmSheetState extends ConsumerState widget.buildCall(signer, _callBytes),
- title: widget.labels.title(l10n),
primaryDetail: headline.primary,
secondaryDetail: headline.secondary,
cacheKey: KeystoneSignCacheKey.forExtrinsic(accountId: signer.accountId, identity: widget.hardwareCacheIdentity),
diff --git a/mobile-app/lib/v2/screens/send/keystone_reject_screen.dart b/mobile-app/lib/v2/screens/send/keystone_reject_screen.dart
new file mode 100644
index 000000000..9f0a5d413
--- /dev/null
+++ b/mobile-app/lib/v2/screens/send/keystone_reject_screen.dart
@@ -0,0 +1,102 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:quantus_sdk/quantus_sdk.dart';
+import 'package:resonance_network_wallet/providers/l10n_provider.dart';
+import 'package:resonance_network_wallet/v2/components/quantus_button.dart';
+import 'package:resonance_network_wallet/v2/components/scaffold_base.dart';
+import 'package:resonance_network_wallet/v2/components/scaffold_base_bottom_content.dart';
+import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart';
+import 'package:resonance_network_wallet/v2/screens/send/keystone_signing_widgets.dart';
+import 'package:resonance_network_wallet/v2/theme/app_colors.dart';
+import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+/// Shown when the Keystone screen does not match what the app asked it to
+/// sign: tells the user to reject on the device and not retry from this phone.
+/// Pops with `true` once the user confirms the rejection.
+class KeystoneRejectScreen extends ConsumerWidget {
+ const KeystoneRejectScreen({super.key});
+
+ static final Uri _supportUrl = Uri.parse('mailto:${AppConstants.emailSupport}');
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final l10n = ref.watch(l10nProvider);
+ final colors = context.colors;
+
+ final steps = [
+ (l10n.keystoneRejectStep1Title, l10n.keystoneRejectStep1Body),
+ (l10n.keystoneRejectStep2Title, l10n.keystoneRejectStep2Body),
+ (l10n.keystoneRejectStep3Title, l10n.keystoneRejectStep3Body),
+ ];
+
+ return ScaffoldBase(
+ appBar: V2AppBar(title: l10n.keystoneSignScreenTitle),
+ mainContent: SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ KeystoneWarningCard(title: l10n.keystoneRejectTitle, text: l10n.keystoneVerifyWarning),
+ const SizedBox(height: 40),
+ for (var i = 0; i < steps.length; i++) ...[
+ if (i > 0) ...[
+ const SizedBox(height: 16),
+ Divider(height: 1, color: colors.separator),
+ const SizedBox(height: 16),
+ ],
+ _RejectStep(number: '0${i + 1}', title: steps[i].$1, body: steps[i].$2),
+ ],
+ ],
+ ),
+ ),
+ bottomContent: ScaffoldBaseBottomContent(
+ child: Column(
+ children: [
+ QuantusButton.simple(label: l10n.keystoneRejectConfirm, onTap: () => Navigator.pop(context, true)),
+ const SizedBox(height: 4),
+ QuantusButton.simple(
+ label: l10n.keystoneRejectContactSupport,
+ variant: ButtonVariant.underline,
+ onTap: () => launchUrl(_supportUrl),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _RejectStep extends StatelessWidget {
+ final String number;
+ final String title;
+ final String body;
+
+ const _RejectStep({required this.number, required this.title, required this.body});
+
+ @override
+ Widget build(BuildContext context) {
+ final colors = context.colors;
+ final text = context.themeText;
+
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ number,
+ style: text.paragraph?.copyWith(fontFamily: AppTextTheme.fontFamilySecondary, color: colors.accentOrange),
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(title, style: text.smallParagraph?.copyWith(color: colors.textLightGray, height: 1.4)),
+ const SizedBox(height: 4),
+ Text(body, style: text.detail?.copyWith(color: colors.textSubtle, height: 1.35)),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/mobile-app/lib/v2/screens/send/keystone_sign_screen.dart b/mobile-app/lib/v2/screens/send/keystone_sign_screen.dart
index 3a4cb5f83..065055797 100644
--- a/mobile-app/lib/v2/screens/send/keystone_sign_screen.dart
+++ b/mobile-app/lib/v2/screens/send/keystone_sign_screen.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:quantus_sdk/quantus_sdk.dart';
+import 'package:resonance_network_wallet/l10n/app_localizations.dart';
import 'package:resonance_network_wallet/providers/l10n_provider.dart';
import 'package:resonance_network_wallet/providers/wallet_providers.dart';
import 'package:resonance_network_wallet/services/telemetry_service.dart';
@@ -12,12 +13,14 @@ import 'package:resonance_network_wallet/v2/components/scaffold_base.dart';
import 'package:resonance_network_wallet/v2/components/scaffold_base_bottom_content.dart';
import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart';
import 'package:resonance_network_wallet/v2/screens/send/keystone_sign_cache.dart';
-import 'package:resonance_network_wallet/v2/screens/send/keystone_signature_scan_screen.dart';
import 'package:resonance_network_wallet/v2/screens/send/keystone_signing_session.dart';
+import 'package:resonance_network_wallet/v2/screens/send/keystone_signing_widgets.dart';
+import 'package:resonance_network_wallet/v2/screens/send/keystone_verify_screen.dart';
import 'package:resonance_network_wallet/v2/theme/app_colors.dart';
import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart';
-/// Shows an unsigned transaction as a Keystone UR QR, then scans its signature.
+/// Step 1/3 of Keystone signing: shows the unsigned transaction as an
+/// animated UR QR for the device to scan.
///
/// The runtime call, display details, caching, and submission behavior come from
/// [session], so every Keystone-backed flow shares this screen.
@@ -81,16 +84,20 @@ class _KeystoneSignScreenState extends ConsumerState {
}
}
- Future _goToScan() async {
+ Future _goToVerify() async {
final unsignedData = _unsignedData;
if (unsignedData == null) return;
- final hash = await Navigator.push(
+ final result = await Navigator.push