Merge branch 'desktop-ui' into brightness-fix
What changed, and why it matters
This is a large merge commit that brings a new desktop user interface into the Skylight Wallet app. Most of the changes are UI layout, new desktop-specific screens, updated text strings, and build script tweaks. There is no obvious security bug in the diff, but the commit is a merge of two feature branches and the diff is huge, so a complete security review of every line is not practical from the supplied excerpt alone.
Treat this as a normal feature merge. Verify the new appimagetool SHA256 against the official go-appimage release page before relying on it. Because the diff is large and partially shown, a follow-up focused review of the new desktop screens and any platform-specific native code is prudent, but no immediate security patch is indicated.
Security signals we found
Large feature merge with 43 changed files and thousands of lines
Build script updates pinned appimagetool SHA256 and filename
New desktop UI screens added; no security-critical logic visible
Localization changes only; no new permissions or network code in excerpt
No vendor security disclosure or CVE references present
Evidence from the diff
The commit merges the ‘desktop-ui’ branch into ‘brightness-fix’. It adds desktop-specific Flutter screens (home shell, connection setup, wallet setup, password, fiat setup, receive), adjusts localization strings for the new onboarding flow, updates appimagetool version/hash and output filename matching in the build script, and tweaks theme/skeleton colors. No cryptographic, networking, or permission changes are visible in the supplied diff. The only potentially sensitive code is the build script downloading appimagetool with a pinned SHA256, which is updated to a newer release (build 951) with a new expected hash.
Changed components
appimage/build_appimage.shlib/main.dartlib/screens/address_book.dartlib/screens/connection_setup.dartlib/screens/create_wallet.dartlib/screens/create_wallet_password.dartlib/screens/desktop/*lib/l10n/*linux/runner/my_application.ccmacos/Runner/MainFlutterWindow.swiftwindows/runner/win32_window.cppInspect captured patch +3592 / −1438
### appimage/build_appimage.sh
@@ -111,8 +111,8 @@ chmod +x AppDir/AppRun
# Expected SHA256 hash - update this when updating appimagetool
# Verify from: https://github.com/probonopd/go-appimage/releases
# Run: sha256sum appimagetool-x86_64.AppImage
-EXPECTED_SHA256="376998aba63bb3a35a02ea3196f77268f8543a35a3b6b7db0dc2181365119b62"
-APPIMAGETOOL_FILENAME="appimagetool-947-x86_64.AppImage"
+EXPECTED_SHA256="7c974f525d5bcde2712dd080e0b079a6c3802113a337f0ab142522dcefbf452d"
+APPIMAGETOOL_FILENAME="appimagetool-951-x86_64.AppImage"
# Download appimagetool from go-appimage if not present
if [ ! -f "$APPIMAGETOOL_FILENAME" ]; then
@@ -149,7 +149,7 @@ export VERSION
export ARCH=x86_64
./squashfs-root/AppRun AppDir
-GENERATED_APPIMAGE=$(ls -1 Skylight_Wallet-${VERSION}-*.AppImage 2>/dev/null | head -n1)
+GENERATED_APPIMAGE=$(ls -1 ./*-"${VERSION}"-*.AppImage 2>/dev/null | grep -iv appimagetool | head -n1)
DESIRED_NAME="skylight-wallet-${FILE_VERSION}-x86_64.AppImage"
if [ -n "$GENERATED_APPIMAGE" ]; then
### lib/l10n/app_en.arb
@@ -4,6 +4,7 @@
"cancel": "Cancel",
"done": "Done",
"close": "Close",
+ "back": "Back",
"unknownError": "Unknown error.",
"warning": "Warning",
"amount": "Amount",
@@ -13,19 +14,14 @@
"copy": "Copy",
"addressCopied": "Address copied to clipboard",
"fieldEmptyError": "This field cannot be empty.",
- "welcomeTitle": "Welcome!",
"welcomeDescription": "Skylight Wallet is one of the simplest Monero wallets. We will help you set up a wallet and connect to a server.",
"welcomeGetStarted": "Get Started",
"welcomeAgreePrefix": "By continuing you agree to the ",
"welcomeTermsLink": "Terms of Service",
"welcomeAgreeMiddle": " and ",
"welcomePrivacyLink": "Privacy Policy",
- "restoreWarningTitle": "Restore Warning",
- "restoreWarningDescription": "Are you sure? The server that you connect to will be able to see your past and future Monero transaction history.",
- "restoreWarningContinueButton": "Continue",
"lwsSetupTitle": "Connection Setup",
"lwsSetupDescription": "Connect to a Monero light-wallet server (LWS) or your own full node. Only select a server you trust. Even if you use Tor, this server can learn information about you. With an LWS, your private view key and primary address will be shared with this server.",
- "lwsSetupAddressHint": "lws.example.com:18090",
"lwsSetupProxyPortLabel": "HTTP Proxy Port (Optional)",
"lwsSetupProxyPortHint": "4444 for I2P",
"lwsSetupUseTorLabel": "Use Tor",
@@ -34,7 +30,6 @@
"lwsSetupContinueButton": "Continue",
"connectionTypeLws": "Light Wallet Server",
"connectionTypeNode": "Monero Node",
- "connectionNodeAddressHint": "node.example.com:18081",
"connectionRemoteIpNotAllowed": "Connections to remote IP addresses aren't allowed. Use a domain name or a local IP address.",
"connectionProtocolHttps": "Removing protocol. Using HTTPS for domains.",
"connectionProtocolHttp": "Removing protocol. Using HTTP for local addresses.",
@@ -48,17 +43,6 @@
"connectionReachedOverTor": "Reached over Tor",
"connectionReachedViaProxy": "Reached through your proxy",
"connectionReachedDirect": "Reached directly",
- "connectionIndicatorHttps": "HTTPS",
- "connectionIndicatorLocal": "Local",
- "connectionIndicatorTorInternal": "Internal Tor",
- "connectionIndicatorTorExternal": "Using Port {port}",
- "@connectionIndicatorTorExternal": {
- "placeholders": {
- "port": {
- "type": "String"
- }
- }
- },
"settingsConnectionSettingsLabel": "Connection Settings",
"settingsConnectionServerLws": "LWS",
"settingsConnectionServerNode": "Node",
@@ -101,23 +85,39 @@
"passwordsDoNotMatchError": "Passwords do not match.",
"generateSeedTitle": "Write these down, in order",
"generateSeedTitleCovered": "Seed Phrase",
- "generateSeedDescription": "This is your polyseed. Write it down and keep it in a safe place.",
"generateSeedSubtitleCovered": "These words, in this order, are your wallet. Write them down and keep them in a physical safe. If you lose these words or if you share them with anyone else, you will lose your money permanently. Careful planning now avoids a potential disaster later.",
"generateSeedSubtitleRevealed": "Securely save these. Do not share them.",
"generateSeedScreenshotNote": "Screenshots are blocked on this screen. Make sure nobody is looking over your shoulder.",
"generateSeedReveal": "Tap to reveal",
"generateSeedConfirm": "I have written down all the words and stored them somewhere only I can reach.",
- "generateSeedContinueButton": "I Wrote It Down",
+ "generateSeedBirthdayLabel": "Wallet birthday",
+ "generateSeedBirthdayReason": "Where a future restore starts scanning",
+ "onboardingTorNotePrivacy": "Tor hides your address from the node you query — slower, and worth it.",
+ "onboardingTorNoteChangeable": "Changeable later under Settings → Connections.",
+ "onboardingPriceNoteRatesOnly": "The price service is asked for rates only — never for addresses or amounts.",
+ "onboardingPriceNoteTor": "Routed over Tor by default, separately from chain traffic.",
+ "onboardingConnectionNoteServer": "Point Skylight at a light-wallet server (LWS), or connect to your own Monero node.",
+ "onboardingConnectionNoteChangeable": "Changeable later under Settings → Connection.",
+ "onboardingWalletNoteGenerated": "A new wallet’s seed is generated here, offline, and shown to you once.",
+ "onboardingWalletNoteRestore": "Restoring asks roughly when the seed first held funds, to skip years of scanning.",
+ "onboardingWalletCreateBullet1": "A Monero seed phrase, shown once",
+ "onboardingWalletCreateBullet2": "Starts empty, syncs from today",
+ "onboardingWalletCreateBullet3": "Takes about two minutes",
+ "onboardingWalletRestoreBullet1": "A Polyseed, BIP39 or legacy Monero seed",
+ "onboardingWalletRestoreBullet2": "Optional scan-from date",
+ "onboardingWalletRestoreBullet3": "Recovers your existing balance and history",
+ "onboardingSeedNotePassword": "Skylight Wallet asks for your password before ever showing them again.",
+ "onboardingPasswordNoteLaunch": "Asked for at every launch, and before the seed is ever shown.",
+ "onboardingPasswordNoteNotCloud": "Not a cloud account. Losing it means restoring from your seed phrase.",
+ "onboardingPasswordStrong": "Strong",
+ "onboardingPasswordMatch": "Both entries match",
"lwsDetailsTitle": "Wallet Details",
"lwsDetailsDescription": "If your Monero light wallet server (LWS) requires registration, you can use these details to add this wallet to that server. Not all servers require registration.",
"lwsDetailsPrimaryAddressLabel": "Primary Address",
"lwsDetailsSecretViewKeyLabel": "Secret View Key",
"lwsDetailsRestoreHeightLabel": "Restore Height",
"restoreWalletTitle": "Restore Wallet",
"restoreWalletDescription": "Input your Monero seed below. We will check common formats.",
- "restoreWalletSeedLabel": "Seed",
- "restoreWalletRestoreHeightLabel": "Restore Height (optional)",
- "restoreWalletRestoreDateLabel": "Restore Date (optional)",
"restoreWalletScanFrom": "Scan from",
"restoreWalletScanFromReason": "Skip irrelevant history to save time",
"restoreWalletNotSet": "Not set",
@@ -160,20 +160,15 @@
"unlockWithFaceId": "Unlock with Face ID",
"unlockWithTouchId": "Unlock with Touch ID",
"unlockTitle": "Unlock Wallet",
- "unlockDescription": "Enter your wallet password to unlock",
- "unlockPasswordLabel": "Password",
"unlockPasswordHint": "Enter your password",
"unlockIncorrectPasswordError": "Incorrect password. Please try again.",
- "homeConnecting": "Connecting",
"homeSyncing": "Syncing",
- "homeHeight": "Height",
+ "homeSynced": "Synced",
+ "homeTorConnected": "Tor · connected",
+ "homeTorConnecting": "Tor · connecting",
+ "homeTorOff": "Tor · off",
"homeReceive": "Receive",
"homeSend": "Send",
- "homeBalanceLocked": "Locked",
- "homeTransactionsTitle": "Transactions",
- "homeOutgoingTxSemanticLabel": "Outgoing Transaction",
- "homeIncomingTxSemanticLabel": "Incoming Transaction",
- "homeTransactionConfirmed": "Confirmed",
"homeNoTransactions": "No Transactions",
"homeFiatApiError": "Error connecting to price server",
"homeDisconnected": "Disconnected",
@@ -182,14 +177,12 @@
"coinHomeSent": "Sent",
"receiveTitle": "Receive",
"receivePrimaryAddressWarn": "Warning: Unless you know what you're doing, please use subaddresses for better privacy.",
- "receiveShareButton": "Share",
- "receiveShowSubaddressButton": "Show Subaddress",
- "receiveShowPrimaryAddressButton": "Show Primary Address",
"receiveServerNoSubaddressesWarn": "Warning: This server does not support subaddresses. For better privacy, consider using a server that supports them. You are receiving to your primary address.",
"receiveMaxSubaddressesReachedWarn": "You have reached the maximum number of subaddresses supported by this server. This is a used address.",
"receiveSubaddressTab": "Subaddress",
"receivePrimaryTab": "Primary address",
"receiveCopyAddress": "Copy address",
+ "receiveQrHint": "Scan this code to send Monero to this wallet.",
"receiveEnlargeQr": "Tap to enlarge and brighten",
"receiveShrinkQr": "Tap to shrink",
"receiveShareError": "Could not open the share sheet",
@@ -201,14 +194,6 @@
}
}
},
- "receiveBlockchainSubtitle": "{coin} blockchain",
- "@receiveBlockchainSubtitle": {
- "placeholders": {
- "coin": {
- "type": "String"
- }
- }
- },
"sendTitle": "Send",
"sendSendButton": "Send",
"sendTransactionSuccessfullySent": "Transaction successfully sent!",
@@ -249,7 +234,6 @@
"settingsLwsViewKeysButton": "View",
"revealSeedAuthReason": "Confirm it's you to view your seed phrase",
"settingsSecretKeysLabel": "Secret Restore Keys",
- "settingsSecretKeysButton": "View",
"settingsViewLwsKeysDialogText": "Only share this information with your light-wallet server. These keys allow the holder to permanently see all transactions related to your wallets. Sharing these with an untrusted person will significantly harm your privacy.",
"settingsViewLwsKeysDialogRevealButton": "Reveal",
"settingsViewSecretKeysDialogText": "Do not share these keys with anyone, including anyone claiming to be support. If you receive a request to provide these, you are being scammed. If you provide this information to another person, you will lose your money and it cannot be recovered.",
@@ -311,23 +295,16 @@
"addressBookNoContacts": "No Contacts Yet",
"addressBookNoContactsDescription": "Add your first contact by tapping the + button",
"addressBookNoSearchResults": "No Contacts Found",
- "addressBookCopyAddress": "Copy Address",
"addressBookEdit": "Edit",
"addressBookContactName": "Contact Name",
"addressBookNameHint": "Name",
- "addressBookAddressHint": "Monero address",
"addressBookAddDescription": "A name and a Monero address to pay them on.",
"addressBookEditDescription": "Update this contact's name or address.",
"addressBookUpdate": "Update",
"addressBookSave": "Save",
- "sendSelectedContact": "Selected Contact",
- "sendClearSelectedContact": "Clear Selected Contact",
"sendPriorityLow": "Low",
"sendPriorityNormal": "Normal",
"sendPriorityHigh": "High",
- "sendPriorityLabel": "Priority",
- "sendTransactionPriority": "Transaction Priority",
- "sendFeeLabel": "Fee",
"sendContactsButton": "Contacts",
"sendToLabel": "To",
"sendPasteButton": "Paste",
@@ -337,13 +314,9 @@
"sendPriorityHeading": "Priority",
"sendPickContactTitle": "Send to a contact",
"sendAvailableSuffix": "available",
+ "sendIrreversibleNote": "Monero transactions are irreversible. Double-check the address and amount before sending.",
"sendFailedToGetFeesError": "Failed to get fees.",
- "torInfoTitle": "Tor Built-in",
- "torInfoDescription": "Skylight Wallet automatically uses built-in Tor to protect your internet connections.",
- "torInfoContinueButton": "Continue",
- "torInfoConfigureButton": "Configure",
"torSettingsTitle": "Tor Settings",
- "torSettingsModeLabel": "Tor Mode",
"torSettingsModeBuiltIn": "Built-in Tor",
"torSettingsModeExternal": "External Tor",
"torSettingsModeDisabled": "No Tor",
@@ -355,7 +328,6 @@
"torChoiceTitle": "Tor Connection Setup",
"torChoiceSubtitle": "Tor can hide your IP address from servers you connect to. This does not reduce the information that is stored on public blockchains. In general, how do you want Skylight Wallet to handle Tor connections?",
"torSettingsSocksPortLabel": "SOCKS Port",
- "torSettingsSocksPortHint": "9050",
"torSettingsUseOrbotLabel": "Use Orbot/InviZible",
"torSettingsUseOrbotLabelIos": "Use Orbot",
"torSettingsSaveButton": "Save",
@@ -364,15 +336,6 @@
"torDisabledWalletsWarningBody": "Some wallets are set to connect over Tor. Disabling Tor will disconnect them, and they will stay disconnected until you reconfigure their connection.",
"torDisabledWalletsWarningConfirm": "Disable Tor",
"settingsTorSettingsLabel": "Tor Settings",
- "lwsSetupUsingInternalTor": "Using Internal Tor",
- "lwsSetupUsingExternalTor": "Using External Tor Proxy at {address}",
- "@lwsSetupUsingExternalTor": {
- "placeholders": {
- "address": {
- "type": "String"
- }
- }
- },
"lwsSetupTorDisabledError": "Tor is disabled. Please go back and enable it.",
"lwsSetupInvalidQrCode": "Invalid connection address."
}
### lib/l10n/app_localizations.dart
@@ -117,6 +117,12 @@ abstract class AppLocalizations {
/// **'Close'**
String get close;
+ /// No description provided for @back.
+ ///
+ /// In en, this message translates to:
+ /// **'Back'**
+ String get back;
+
/// No description provided for @unknownError.
///
/// In en, this message translates to:
@@ -171,12 +177,6 @@ abstract class AppLocalizations {
/// **'This field cannot be empty.'**
String get fieldEmptyError;
- /// No description provided for @welcomeTitle.
- ///
- /// In en, this message translates to:
- /// **'Welcome!'**
- String get welcomeTitle;
-
/// No description provided for @welcomeDescription.
///
/// In en, this message translates to:
@@ -213,24 +213,6 @@ abstract class AppLocalizations {
/// **'Privacy Policy'**
String get welcomePrivacyLink;
- /// No description provided for @restoreWarningTitle.
- ///
- /// In en, this message translates to:
- /// **'Restore Warning'**
- String get restoreWarningTitle;
-
- /// No description provided for @restoreWarningDescription.
- ///
- /// In en, this message translates to:
- /// **'Are you sure? The server that you connect to will be able to see your past and future Monero transaction history.'**
- String get restoreWarningDescription;
-
- /// No description provided for @restoreWarningContinueButton.
- ///
- /// In en, this message translates to:
- /// **'Continue'**
- String get restoreWarningContinueButton;
-
/// No description provided for @lwsSetupTitle.
///
/// In en, this message translates to:
@@ -243,12 +225,6 @@ abstract class AppLocalizations {
/// **'Connect to a Monero light-wallet server (LWS) or your own full node. Only select a server you trust. Even if you use Tor, this server can learn information about you. With an LWS, your private view key and primary address will be shared with this server.'**
String get lwsSetupDescription;
- /// No description provided for @lwsSetupAddressHint.
- ///
- /// In en, this message translates to:
- /// **'lws.example.com:18090'**
- String get lwsSetupAddressHint;
-
/// No description provided for @lwsSetupProxyPortLabel.
///
/// In en, this message translates to:
@@ -297,12 +273,6 @@ abstract class AppLocalizations {
/// **'Monero Node'**
String get connectionTypeNode;
- /// No description provided for @connectionNodeAddressHint.
- ///
- /// In en, this message translates to:
- /// **'node.example.com:18081'**
- String get connectionNodeAddressHint;
-
/// No description provided for @connectionRemoteIpNotAllowed.
///
/// In en, this message translates to:
@@ -381,30 +351,6 @@ abstract class AppLocalizations {
/// **'Reached directly'**
String get connectionReachedDirect;
- /// No description provided for @connectionIndicatorHttps.
- ///
- /// In en, this message translates to:
- /// **'HTTPS'**
- String get connectionIndicatorHttps;
-
- /// No description provided for @connectionIndicatorLocal.
- ///
- /// In en, this message translates to:
- /// **'Local'**
- String get connectionIndicatorLocal;
-
- /// No description provided for @connectionIndicatorTorInternal.
- ///
- /// In en, this message translates to:
- /// **'Internal Tor'**
- String get connectionIndicatorTorInternal;
-
- /// No description provided for @connectionIndicatorTorExternal.
- ///
- /// In en, this message translates to:
- /// **'Using Port {port}'**
- String connectionIndicatorTorExternal(String port);
-
/// No description provided for @settingsConnectionSettingsLabel.
///
/// In en, this message translates to:
@@ -615,12 +561,6 @@ abstract class AppLocalizations {
/// **'Seed Phrase'**
String get generateSeedTitleCovered;
- /// No description provided for @generateSeedDescription.
- ///
- /// In en, this message translates to:
- /// **'This is your polyseed. Write it down and keep it in a safe place.'**
- String get generateSeedDescription;
-
/// No description provided for @generateSeedSubtitleCovered.
///
/// In en, this message translates to:
@@ -651,11 +591,131 @@ abstract class AppLocalizations {
/// **'I have written down all the words and stored them somewhere only I can reach.'**
String get generateSeedConfirm;
- /// No description provided for @generateSeedContinueButton.
+ /// No description provided for @generateSeedBirthdayLabel.
+ ///
+ /// In en, this message translates to:
+ /// **'Wallet birthday'**
+ String get generateSeedBirthdayLabel;
+
+ /// No description provided for @generateSeedBirthdayReason.
+ ///
+ /// In en, this message translates to:
+ /// **'Where a future restore starts scanning'**
+ String get generateSeedBirthdayReason;
+
+ /// No description provided for @onboardingTorNotePrivacy.
+ ///
+ /// In en, this message translates to:
+ /// **'Tor hides your address from the node you query — slower, and worth it.'**
+ String get onboardingTorNotePrivacy;
+
+ /// No description provided for @onboardingTorNoteChangeable.
+ ///
+ /// In en, this message translates to:
+ /// **'Changeable later under Settings → Connections.'**
+ String get onboardingTorNoteChangeable;
+
+ /// No description provided for @onboardingPriceNoteRatesOnly.
+ ///
+ /// In en, this message translates to:
+ /// **'The price service is asked for rates only — never for addresses or amounts.'**
+ String get onboardingPriceNoteRatesOnly;
+
+ /// No description provided for @onboardingPriceNoteTor.
+ ///
+ /// In en, this message translates to:
+ /// **'Routed over Tor by default, separately from chain traffic.'**
+ String get onboardingPriceNoteTor;
+
+ /// No description provided for @onboardingConnectionNoteServer.
+ ///
+ /// In en, this message translates to:
+ /// **'Point Skylight at a light-wallet server (LWS), or connect to your own Monero node.'**
+ String get onboardingConnectionNoteServer;
+
+ /// No description provided for @onboardingConnectionNoteChangeable.
+ ///
+ /// In en, this message translates to:
+ /// **'Changeable later under Settings → Connection.'**
+ String get onboardingConnectionNoteChangeable;
+
+ /// No description provided for @onboardingWalletNoteGenerated.
+ ///
+ /// In en, this message translates to:
+ /// **'A new wallet’s seed is generated here, offline, and shown to you once.'**
+ String get onboardingWalletNoteGenerated;
+
+ /// No description provided for @onboardingWalletNoteRestore.
///
/// In en, this message translates to:
- /// **'I Wrote It Down'**
- String get generateSeedContinueButton;
+ /// **'Restoring asks roughly when the seed first held funds, to skip years of scanning.'**
+ String get onboardingWalletNoteRestore;
+
+ /// No description provided for @onboardingWalletCreateBullet1.
+ ///
+ /// In en, this message translates to:
+ /// **'A Monero seed phrase, shown once'**
+ String get onboardingWalletCreateBullet1;
+
+ /// No description provided for @onboardingWalletCreateBullet2.
+ ///
+ /// In en, this message translates to:
+ /// **'Starts empty, syncs from today'**
+ String get onboardingWalletCreateBullet2;
+
+ /// No description provided for @onboardingWalletCreateBullet3.
+ ///
+ /// In en, this message translates to:
+ /// **'Takes about two minutes'**
+ String get onboardingWalletCreateBullet3;
+
+ /// No description provided for @onboardingWalletRestoreBullet1.
+ ///
+ /// In en, this message translates to:
+ /// **'A Polyseed, BIP39 or legacy Monero seed'**
+ String get onboardingWalletRestoreBullet1;
+
+ /// No description provided for @onboardingWalletRestoreBullet2.
+ ///
+ /// In en, this message translates to:
+ /// **'Optional scan-from date'**
+ String get onboardingWalletRestoreBullet2;
+
+ /// No description provided for @onboardingWalletRestoreBullet3.
+ ///
+ /// In en, this message translates to:
+ /// **'Recovers your existing balance and history'**
+ String get onboardingWalletRestoreBullet3;
+
+ /// No description provided for @onboardingSeedNotePassword.
+ ///
+ /// In en, this message translates to:
+ /// **'Skylight Wallet asks for your password before ever showing them again.'**
+ String get onboardingSeedNotePassword;
+
+ /// No description provided for @onboardingPasswordNoteLaunch.
+ ///
+ /// In en, this message translates to:
+ /// **'Asked for at every launch, and before the seed is ever shown.'**
+ String get onboardingPasswordNoteLaunch;
+
+ /// No description provided for @onboardingPasswordNoteNotCloud.
+ ///
+ /// In en, this message translates to:
+ /// **'Not a cloud account. Losing it means restoring from your seed phrase.'**
+ String get onboardingPasswordNoteNotCloud;
+
+ /// No description provided for @onboardingPasswordStrong.
+ ///
+ /// In en, this message translates to:
+ /// **'Strong'**
+ String get onboardingPasswordStrong;
+
+ /// No description provided for @onboardingPasswordMatch.
+ ///
+ /// In en, this message translates to:
+ /// **'Both entries match'**
+ String get onboardingPasswordMatch;
/// No description provided for @lwsDetailsTitle.
///
@@ -699,24 +759,6 @@ abstract class AppLocalizations {
/// **'Input your Monero seed below. We will check common formats.'**
String get restoreWalletDescription;
- /// No description provided for @restoreWalletSeedLabel.
- ///
- /// In en, this message translates to:
- /// **'Seed'**
- String get restoreWalletSeedLabel;
-
- /// No description provided for @restoreWalletRestoreHeightLabel.
- ///
- /// In en, this message translates to:
- /// **'Restore Height (optional)'**
- String get restoreWalletRestoreHeightLabel;
-
- /// No description provided for @restoreWalletRestoreDateLabel.
- ///
- /// In en, this message translates to:
- /// **'Restore Date (optional)'**
- String get restoreWalletRestoreDateLabel;
-
/// No description provided for @restoreWalletScanFrom.
///
/// In en, this message translates to:
@@ -885,18 +927,6 @@ abstract class AppLocalizations {
/// **'Unlock Wallet'**
String get unlockTitle;
- /// No description provided for @unlockDescription.
- ///
- /// In en, this message translates to:
- /// **'Enter your wallet password to unlock'**
- String get unlockDescription;
-
- /// No description provided for @unlockPasswordLabel.
- ///
- /// In en, this message translates to:
- /// **'Password'**
- String get unlockPasswordLabel;
-
/// No description provided for @unlockPasswordHint.
///
/// In en, this message translates to:
@@ -909,65 +939,47 @@ abstract class AppLocalizations {
/// **'Incorrect password. Please try again.'**
String get unlockIncorrectPasswordError;
- /// No description provided for @homeConnecting.
- ///
- /// In en, this message translates to:
- /// **'Connecting'**
- String get homeConnecting;
-
/// No description provided for @homeSyncing.
///
/// In en, this message translates to:
/// **'Syncing'**
String get homeSyncing;
- /// No description provided for @homeHeight.
+ /// No description provided for @homeSynced.
///
/// In en, this message translates to:
- /// **'Height'**
- String get homeHeight;
-
- /// No description provided for @homeReceive.
- ///
- /// In en, this message translates to:
- /// **'Receive'**
- String get homeReceive;
-
- /// No description provided for @homeSend.
- ///
- /// In en, this message translates to:
- /// **'Send'**
- String get homeSend;
+ /// **'Synced'**
+ String get homeSynced;
- /// No description provided for @homeBalanceLocked.
+ /// No description provided for @homeTorConnected.
///
/// In en, this message translates to:
- /// **'Locked'**
- String get homeBalanceLocked;
+ /// **'Tor · connected'**
+ String get homeTorConnected;
- /// No description provided for @homeTransactionsTitle.
+ /// No description provided for @homeTorConnecting.
///
/// In en, this message translates to:
- /// **'Transactions'**
- String get homeTransactionsTitle;
+ /// **'Tor · connecting'**
+ String get homeTorConnecting;
- /// No description provided for @homeOutgoingTxSemanticLabel.
+ /// No description provided for @homeTorOff.
///
/// In en, this message translates to:
- /// **'Outgoing Transaction'**
- String get homeOutgoingTxSemanticLabel;
+ /// **'Tor · off'**
+ String get homeTorOff;
- /// No description provided for @homeIncomingTxSemanticLabel.
+ /// No description provided for @homeReceive.
///
/// In en, this message translates to:
- /// **'Incoming Transaction'**
- String get homeIncomingTxSemanticLabel;
+ /// **'Receive'**
+ String get homeReceive;
- /// No description provided for @homeTransactionConfirmed.
+ /// No description provided for @homeSend.
///
/// In en, this message translates to:
- /// **'Confirmed'**
- String get homeTransactionConfirmed;
+ /// **'Send'**
+ String get homeSend;
/// No description provided for @homeNoTransactions.
///
@@ -1017,24 +1029,6 @@ abstract class AppLocalizations {
/// **'Warning: Unless you know what you\'re doing, please use subaddresses for better privacy.'**
String get receivePrimaryAddressWarn;
- /// No description provided for @receiveShareButton.
- ///
- /// In en, this message translates to:
- /// **'Share'**
- String get receiveShareButton;
-
- /// No description provided for @receiveShowSubaddressButton.
- ///
- /// In en, this message translates to:
- /// **'Show Subaddress'**
- String get receiveShowSubaddressButton;
-
- /// No description provided for @receiveShowPrimaryAddressButton.
- ///
- /// In en, this message translates to:
- /// **'Show Primary Address'**
- String get receiveShowPrimaryAddressButton;
-
/// No description provided for @receiveServerNoSubaddressesWarn.
///
/// In en, this message translates to:
@@ -1065,6 +1059,12 @@ abstract class AppLocalizations {
/// **'Copy address'**
String get receiveCopyAddress;
+ /// No description provided for @receiveQrHint.
+ ///
+ /// In en, this message translates to:
+ /// **'Scan this code to send Monero to this wallet.'**
+ String get receiveQrHint;
+
/// No description provided for @receiveEnlargeQr.
///
/// In en, this message translates to:
@@ -1089,12 +1089,6 @@ abstract class AppLocalizations {
/// **'Your {coin} address'**
String receiveAddressHeading(String coin);
- /// No description provided for @receiveBlockchainSubtitle.
- ///
- /// In en, this message translates to:
- /// **'{coin} blockchain'**
- String receiveBlockchainSubtitle(String coin);
-
/// No description provided for @sendTitle.
///
/// In en, this message translates to:
@@ -1335,12 +1329,6 @@ abstract class AppLocalizations {
/// **'Secret Restore Keys'**
String get settingsSecretKeysLabel;
- /// No description provided for @settingsSecretKeysButton.
- ///
- /// In en, this message translates to:
- /// **'View'**
- String get settingsSecretKeysButton;
-
/// No description provided for @settingsViewLwsKeysDialogText.
///
/// In en, this message translates to:
@@ -1623,12 +1611,6 @@ abstract class AppLocalizations {
/// **'No Contacts Found'**
String get addressBookNoSearchResults;
- /// No description provided for @addressBookCopyAddress.
- ///
- /// In en, this message translates to:
- /// **'Copy Address'**
- String get addressBookCopyAddress;
-
/// No description provided for @addressBookEdit.
///
/// In en, this message translates to:
@@ -1647,12 +1629,6 @@ abstract class AppLocalizations {
/// **'Name'**
String get addressBookNameHint;
- /// No description provided for @addressBookAddressHint.
- ///
- /// In en, this message translates to:
- /// **'Monero address'**
- String get addressBookAddressHint;
-
/// No description provided for @addressBookAddDescription.
///
/// In en, this message translates to:
@@ -1677,18 +1653,6 @@ abstract class AppLocalizations {
/// **'Save'**
String get addressBookSave;
- /// No description provided for @sendSelectedContact.
- ///
- /// In en, this message translates to:
- /// **'Selected Contact'**
- String get sendSelectedContact;
-
- /// No description provided for @sendClearSelectedContact.
- ///
- /// In en, this message translates to:
- /// **'Clear Selected Contact'**
- String get sendClearSelectedContact;
-
/// No description provided for @sendPriorityLow.
///
/// In en, this message translates to:
@@ -1707,24 +1671,6 @@ abstract class AppLocalizations {
/// **'High'**
String get sendPriorityHigh;
- /// No description provided for @sendPriorityLabel.
- ///
- /// In en, this message translates to:
- /// **'Priority'**
- String get sendPriorityLabel;
-
- /// No description provided for @sendTransactionPriority.
- ///
- /// In en, this message translates to:
- /// **'Transaction Priority'**
- String get sendTransactionPriority;
-
- /// No description provided for @sendFeeLabel.
- ///
- /// In en, this message translates to:
- /// **'Fee'**
- String get sendFeeLabel;
-
/// No description provided for @sendContactsButton.
///
/// In en, this message translates to:
@@ -1779,48 +1725,24 @@ abstract class AppLocalizations {
/// **'available'**
String get sendAvailableSuffix;
- /// No description provided for @sendFailedToGetFeesError.
- ///
- /// In en, this message translates to:
- /// **'Failed to get fees.'**
- String get sendFailedToGetFeesError;
-
- /// No description provided for @torInfoTitle.
+ /// No description provided for @sendIrreversibleNote.
///
/// In en, this message translates to:
- /// **'Tor Built-in'**
- String get torInfoTitle;
+ /// **'Monero transactions are irreversible. Double-check the address and amount before sending.'**
+ String get sendIrreversibleNote;
- /// No description provided for @torInfoDescription.
- ///
- /// In en, this message translates to:
- /// **'Skylight Wallet automatically uses built-in Tor to protect your internet connections.'**
- String get torInfoDescription;
-
- /// No description provided for @torInfoContinueButton.
- ///
- /// In en, this message translates to:
- /// **'Continue'**
- String get torInfoContinueButton;
-
- /// No description provided for @torInfoConfigureButton.
+ /// No description provided for @sendFailedToGetFeesError.
///
/// In en, this message translates to:
- /// **'Configure'**
- String get torInfoConfigureButton;
+ /// **'Failed to get fees.'**
+ String get sendFailedToGetFeesError;
/// No description provided for @torSettingsTitle.
///
/// In en, this message translates to:
/// **'Tor Settings'**
String get torSettingsTitle;
- /// No description provided for @torSettingsModeLabel.
- ///
- /// In en, this message translates to:
- /// **'Tor Mode'**
- String get torSettingsModeLabel;
-
/// No description provided for @torSettingsModeBuiltIn.
///
/// In en, this message translates to:
@@ -1887,12 +1809,6 @@ abstract class AppLocalizations {
/// **'SOCKS Port'**
String get torSettingsSocksPortLabel;
- /// No description provided for @torSettingsSocksPortHint.
- ///
- /// In en, this message translates to:
- /// **'9050'**
- String get torSettingsSocksPortHint;
-
/// No description provided for @torSettingsUseOrbotLabel.
///
/// In en, this message translates to:
@@ -1941,18 +1857,6 @@ abstract class AppLocalizations {
/// **'Tor Settings'**
String get settingsTorSettingsLabel;
- /// No description provided for @lwsSetupUsingInternalTor.
- ///
- /// In en, this message translates to:
- /// **'Using Internal Tor'**
- String get lwsSetupUsingInternalTor;
-
- /// No description provided for @lwsSetupUsingExternalTor.
- ///
- /// In en, this message translates to:
- /// **'Using External Tor Proxy at {address}'**
- String lwsSetupUsingExternalTor(String address);
-
/// No description provided for @lwsSetupTorDisabledError.
///
/// In en, this message translates to:
### lib/l10n/app_localizations_en.dart
@@ -20,6 +20,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get close => 'Close';
+ @override
+ String get back => 'Back';
+
@override
String get unknownError => 'Unknown error.';
@@ -47,9 +50,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get fieldEmptyError => 'This field cannot be empty.';
- @override
- String get welcomeTitle => 'Welcome!';
-
@override
String get welcomeDescription =>
'Skylight Wallet is one of the simplest Monero wallets. We will help you set up a wallet and connect to a server.';
@@ -69,26 +69,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get welcomePrivacyLink => 'Privacy Policy';
- @override
- String get restoreWarningTitle => 'Restore Warning';
-
- @override
- String get restoreWarningDescription =>
- 'Are you sure? The server that you connect to will be able to see your past and future Monero transaction history.';
-
- @override
- String get restoreWarningContinueButton => 'Continue';
-
@override
String get lwsSetupTitle => 'Connection Setup';
@override
String get lwsSetupDescription =>
'Connect to a Monero light-wallet server (LWS) or your own full node. Only select a server you trust. Even if you use Tor, this server can learn information about you. With an LWS, your private view key and primary address will be shared with this server.';
- @override
- String get lwsSetupAddressHint => 'lws.example.com:18090';
-
@override
String get lwsSetupProxyPortLabel => 'HTTP Proxy Port (Optional)';
@@ -113,9 +100,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get connectionTypeNode => 'Monero Node';
- @override
- String get connectionNodeAddressHint => 'node.example.com:18081';
-
@override
String get connectionRemoteIpNotAllowed =>
'Connections to remote IP addresses aren\'t allowed. Use a domain name or a local IP address.';
@@ -157,20 +141,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get connectionReachedDirect => 'Reached directly';
- @override
- String get connectionIndicatorHttps => 'HTTPS';
-
- @override
- String get connectionIndicatorLocal => 'Local';
-
- @override
- String get connectionIndicatorTorInternal => 'Internal Tor';
-
- @override
- String connectionIndicatorTorExternal(String port) {
- return 'Using Port $port';
- }
-
@override
String get settingsConnectionSettingsLabel => 'Connection Settings';
@@ -283,10 +253,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get generateSeedTitleCovered => 'Seed Phrase';
- @override
- String get generateSeedDescription =>
- 'This is your polyseed. Write it down and keep it in a safe place.';
-
@override
String get generateSeedSubtitleCovered =>
'These words, in this order, are your wallet. Write them down and keep them in a physical safe. If you lose these words or if you share them with anyone else, you will lose your money permanently. Careful planning now avoids a potential disaster later.';
@@ -306,7 +272,75 @@ class AppLocalizationsEn extends AppLocalizations {
'I have written down all the words and stored them somewhere only I can reach.';
@override
- String get generateSeedContinueButton => 'I Wrote It Down';
+ String get generateSeedBirthdayLabel => 'Wallet birthday';
+
+ @override
+ String get generateSeedBirthdayReason => 'Where a future restore starts scanning';
+
+ @override
+ String get onboardingTorNotePrivacy =>
+ 'Tor hides your address from the node you query — slower, and worth it.';
+
+ @override
+ String get onboardingTorNoteChangeable => 'Changeable later under Settings → Connections.';
+
+ @override
+ String get onboardingPriceNoteRatesOnly =>
+ 'The price service is asked for rates only — never for addresses or amounts.';
+
+ @override
+ String get onboardingPriceNoteTor => 'Routed over Tor by default, separately from chain traffic.';
+
+ @override
+ String get onboardingConnectionNoteServer =>
+ 'Point Skylight at a light-wallet server (LWS), or connect to your own Monero node.';
+
+ @override
+ String get onboardingConnectionNoteChangeable => 'Changeable later under Settings → Connection.';
+
+ @override
+ String get onboardingWalletNoteGenerated =>
+ 'A new wallet’s seed is generated here, offline, and shown to you once.';
+
+ @override
+ String get onboardingWalletNoteRestore =>
+ 'Restoring asks roughly when the seed first held funds, to skip years of scanning.';
+
+ @override
+ String get onboardingWalletCreateBullet1 => 'A Monero seed phrase, shown once';
+
+ @override
+ String get onboardingWalletCreateBullet2 => 'Starts empty, syncs from today';
+
+ @override
+ String get onboardingWalletCreateBullet3 => 'Takes about two minutes';
+
+ @override
+ String get onboardingWalletRestoreBullet1 => 'A Polyseed, BIP39 or legacy Monero seed';
+
+ @override
+ String get onboardingWalletRestoreBullet2 => 'Optional scan-from date';
+
+ @override
+ String get onboardingWalletRestoreBullet3 => 'Recovers your existing balance and history';
+
+ @override
+ String get onboardingSeedNotePassword =>
+ 'Skylight Wallet asks for your password before ever showing them again.';
+
+ @override
+ String get onboardingPasswordNoteLaunch =>
+ 'Asked for at every launch, and before the seed is ever shown.';
+
+ @override
+ String get onboardingPasswordNoteNotCloud =>
+ 'Not a cloud account. Losing it means restoring from your seed phrase.';
+
+ @override
+ String get onboardingPasswordStrong => 'Strong';
+
+ @override
+ String get onboardingPasswordMatch => 'Both entries match';
@override
String get lwsDetailsTitle => 'Wallet Details';
@@ -331,15 +365,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get restoreWalletDescription =>
'Input your Monero seed below. We will check common formats.';
- @override
- String get restoreWalletSeedLabel => 'Seed';
-
- @override
- String get restoreWalletRestoreHeightLabel => 'Restore Height (optional)';
-
- @override
- String get restoreWalletRestoreDateLabel => 'Restore Date (optional)';
-
@override
String get restoreWalletScanFrom => 'Scan from';
@@ -431,47 +456,32 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get unlockTitle => 'Unlock Wallet';
- @override
- String get unlockDescription => 'Enter your wallet password to unlock';
-
- @override
- String get unlockPasswordLabel => 'Password';
-
@override
String get unlockPasswordHint => 'Enter your password';
@override
String get unlockIncorrectPasswordError => 'Incorrect password. Please try again.';
- @override
- String get homeConnecting => 'Connecting';
-
@override
String get homeSyncing => 'Syncing';
@override
- String get homeHeight => 'Height';
-
- @override
- String get homeReceive => 'Receive';
-
- @override
- String get homeSend => 'Send';
+ String get homeSynced => 'Synced';
@override
- String get homeBalanceLocked => 'Locked';
+ String get homeTorConnected => 'Tor · connected';
@override
- String get homeTransactionsTitle => 'Transactions';
+ String get homeTorConnecting => 'Tor · connecting';
@override
- String get homeOutgoingTxSemanticLabel => 'Outgoing Transaction';
+ String get homeTorOff => 'Tor · off';
@override
- String get homeIncomingTxSemanticLabel => 'Incoming Transaction';
+ String get homeReceive => 'Receive';
@override
- String get homeTransactionConfirmed => 'Confirmed';
+ String get homeSend => 'Send';
@override
String get homeNoTransactions => 'No Transactions';
@@ -498,15 +508,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get receivePrimaryAddressWarn =>
'Warning: Unless you know what you\'re doing, please use subaddresses for better privacy.';
- @override
- String get receiveShareButton => 'Share';
-
- @override
- String get receiveShowSubaddressButton => 'Show Subaddress';
-
- @override
- String get receiveShowPrimaryAddressButton => 'Show Primary Address';
-
@override
String get receiveServerNoSubaddressesWarn =>
'Warning: This server does not support subaddresses. For better privacy, consider using a server that supports them. You are receiving to your primary address.';
@@ -524,6 +525,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get receiveCopyAddress => 'Copy address';
+ @override
+ String get receiveQrHint => 'Scan this code to send Monero to this wallet.';
+
@override
String get receiveEnlargeQr => 'Tap to enlarge and brighten';
@@ -538,11 +542,6 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Your $coin address';
}
- @override
- String receiveBlockchainSubtitle(String coin) {
- return '$coin blockchain';
- }
-
@override
String get sendTitle => 'Send';
@@ -669,9 +668,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settingsSecretKeysLabel => 'Secret Restore Keys';
- @override
- String get settingsSecretKeysButton => 'View';
-
@override
String get settingsViewLwsKeysDialogText =>
'Only share this information with your light-wallet server. These keys allow the holder to permanently see all transactions related to your wallets. Sharing these with an untrusted person will significantly harm your privacy.';
@@ -825,9 +821,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get addressBookNoSearchResults => 'No Contacts Found';
- @override
- String get addressBookCopyAddress => 'Copy Address';
-
@override
String get addressBookEdit => 'Edit';
@@ -837,9 +830,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get addressBookNameHint => 'Name';
- @override
- String get addressBookAddressHint => 'Monero address';
-
@override
String get addressBookAddDescription => 'A name and a Monero address to pay them on.';
@@ -852,12 +842,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get addressBookSave => 'Save';
- @override
- String get sendSelectedContact => 'Selected Contact';
-
- @override
- String get sendClearSelectedContact => 'Clear Selected Contact';
-
@override
String get sendPriorityLow => 'Low';
@@ -867,15 +851,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get sendPriorityHigh => 'High';
- @override
- String get sendPriorityLabel => 'Priority';
-
- @override
- String get sendTransactionPriority => 'Transaction Priority';
-
- @override
- String get sendFeeLabel => 'Fee';
-
@override
String get sendContactsButton => 'Contacts';
@@ -904,27 +879,15 @@ class AppLocalizationsEn extends AppLocalizations {
String get sendAvailableSuffix => 'available';
@override
- String get sendFailedToGetFeesError => 'Failed to get fees.';
-
- @override
- String get torInfoTitle => 'Tor Built-in';
-
- @override
- String get torInfoDescription =>
- 'Skylight Wallet automatically uses built-in Tor to protect your internet connections.';
+ String get sendIrreversibleNote =>
+ 'Monero transactions are irreversible. Double-check the address and amount before sending.';
@override
- String get torInfoContinueButton => 'Continue';
-
- @override
- String get torInfoConfigureButton => 'Configure';
+ String get sendFailedToGetFeesError => 'Failed to get fees.';
@override
String get torSettingsTitle => 'Tor Settings';
- @override
- String get torSettingsModeLabel => 'Tor Mode';
-
@override
String get torSettingsModeBuiltIn => 'Built-in Tor';
@@ -959,9 +922,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get torSettingsSocksPortLabel => 'SOCKS Port';
- @override
- String get torSettingsSocksPortHint => '9050';
-
@override
String get torSettingsUseOrbotLabel => 'Use Orbot/InviZible';
@@ -987,14 +947,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settingsTorSettingsLabel => 'Tor Settings';
- @override
- String get lwsSetupUsingInternalTor => 'Using Internal Tor';
-
- @override
- String lwsSetupUsingExternalTor(String address) {
- return 'Using External Tor Proxy at $address';
- }
-
@override
String get lwsSetupTorDisabledError => 'Tor is disabled. Please go back and enable it.';
### lib/l10n/app_localizations_pt.dart
@@ -20,6 +20,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get close => 'Fechar';
+ @override
+ String get back => 'Voltar';
+
@override
String get unknownError => 'Erro desconhecido.';
@@ -47,9 +50,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get fieldEmptyError => 'Este campo não pode ficar vazio.';
- @override
- String get welcomeTitle => 'Bem-vindo!';
-
@override
String get welcomeDescription =>
'A Skylight Wallet é uma das mais simples carteiras de Monero. Nós o ajudaremos a configurar uma carteira e se conectar à um servidor.';
@@ -69,26 +69,13 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get welcomePrivacyLink => 'Política de Privacidade';
- @override
- String get restoreWarningTitle => 'Aviso de Restauração';
-
- @override
- String get restoreWarningDescription =>
- 'Você tem certeza? O servidor ao qual você se conectar poderá ver seu histórico de transações Monero passadas e futuras.';
-
- @override
- String get restoreWarningContinueButton => 'Continuar';
-
@override
String get lwsSetupTitle => 'Configuração da conexão';
@override
String get lwsSetupDescription =>
'Conecte-se a um servidor light-wallet Monero (LWS) ou ao seu próprio nó completo. Selecione apenas um servidor em que você confia. Mesmo se você usar o Tor, este servidor pode obter informações sobre você. Com um LWS, sua chave privada de visualização e seu endereço primário serão compartilhados com este servidor.';
- @override
- String get lwsSetupAddressHint => 'lws.example.com:18090';
-
@override
String get lwsSetupProxyPortLabel => 'Porta do Proxy HTTP (opcional)';
@@ -113,9 +100,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get connectionTypeNode => 'Nó Monero';
- @override
- String get connectionNodeAddressHint => 'node.example.com:18081';
-
@override
String get connectionRemoteIpNotAllowed =>
'Conexões com endereços IP remotos não são permitidas. Use um nome de domínio ou um endereço IP local.';
@@ -157,20 +141,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get connectionReachedDirect => 'Acessado diretamente';
- @override
- String get connectionIndicatorHttps => 'HTTPS';
-
- @override
- String get connectionIndicatorLocal => 'Local';
-
- @override
- String get connectionIndicatorTorInternal => 'Tor Interno';
-
- @override
- String connectionIndicatorTorExternal(String port) {
- return 'Usando Porta $port';
- }
-
@override
String get settingsConnectionSettingsLabel => 'Configurações de conexão';
@@ -283,10 +253,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get generateSeedTitleCovered => 'Frase Seed';
- @override
- String get generateSeedDescription =>
- 'Esta é a sua polyseed. Anote-a e guarde-a em um lugar seguro.';
-
@override
String get generateSeedSubtitleCovered =>
'Estas palavras, nesta ordem, são a sua carteira. Anote-as e guarde-as em um cofre físico. Se você perder estas palavras ou compartilhá-las com outra pessoa, perderá seu dinheiro permanentemente. Um planejamento cuidadoso agora evita um possível desastre depois.';
@@ -306,7 +272,76 @@ class AppLocalizationsPt extends AppLocalizations {
'Anotei todas as palavras e as guardei em um lugar que só eu posso acessar.';
@override
- String get generateSeedContinueButton => 'Eu anotei';
+ String get generateSeedBirthdayLabel => 'Aniversário da carteira';
+
+ @override
+ String get generateSeedBirthdayReason => 'Onde uma futura restauração começa a escanear';
+
+ @override
+ String get onboardingTorNotePrivacy =>
+ 'O Tor esconde seu endereço do nó que você consulta — mais lento, e vale a pena.';
+
+ @override
+ String get onboardingTorNoteChangeable => 'Pode ser alterado depois em Configurações → Conexões.';
+
+ @override
+ String get onboardingPriceNoteRatesOnly =>
+ 'O serviço de preços recebe apenas pedidos de cotações — nunca endereços ou valores.';
+
+ @override
+ String get onboardingPriceNoteTor => 'Roteado pelo Tor por padrão, separado do tráfego da rede.';
+
+ @override
+ String get onboardingConnectionNoteServer =>
+ 'Aponte a Skylight para um servidor de carteira leve (LWS), ou conecte-se ao seu próprio nó Monero.';
+
+ @override
+ String get onboardingConnectionNoteChangeable =>
+ 'Pode ser alterado depois em Configurações → Conexão.';
+
+ @override
+ String get onboardingWalletNoteGenerated =>
+ 'A seed de uma nova carteira é gerada aqui, offline, e mostrada a você uma única vez.';
+
+ @override
+ String get onboardingWalletNoteRestore =>
+ 'A restauração pergunta aproximadamente quando a seed teve fundos pela primeira vez, para pular anos de escaneamento.';
+
+ @override
+ String get onboardingWalletCreateBullet1 => 'Uma frase seed Monero, mostrada uma vez';
+
+ @override
+ String get onboardingWalletCreateBullet2 => 'Começa vazia, sincroniza a partir de hoje';
+
+ @override
+ String get onboardingWalletCreateBullet3 => 'Leva cerca de dois minutos';
+
+ @override
+ String get onboardingWalletRestoreBullet1 => 'Uma seed Monero Polyseed, BIP39 ou legada';
+
+ @override
+ String get onboardingWalletRestoreBullet2 => 'Data de escaneamento opcional';
+
+ @override
+ String get onboardingWalletRestoreBullet3 => 'Recupera seu saldo e histórico existentes';
+
+ @override
+ String get onboardingSeedNotePassword =>
+ 'A Skylight Wallet pede sua senha antes de mostrá-las novamente.';
+
+ @override
+ String get onboardingPasswordNoteLaunch =>
+ 'Pedida a cada abertura, e antes de a seed ser mostrada.';
+
+ @override
+ String get onboardingPasswordNoteNotCloud =>
+ 'Não é uma conta na nuvem. Perdê-la significa restaurar a partir da sua frase seed.';
+
+ @override
+ String get onboardingPasswordStrong => 'Forte';
+
+ @override
+ String get onboardingPasswordMatch => 'As duas entradas coincidem';
@override
String get lwsDetailsTitle => 'Detalhes da Carteira';
@@ -331,15 +366,6 @@ class AppLocalizationsPt extends AppLocalizations {
String get restoreWalletDescription =>
'Insira sua semente Monero abaixo. Verificaremos os formatos comuns.';
- @override
- String get restoreWalletSeedLabel => 'Semente';
-
- @override
- String get restoreWalletRestoreHeightLabel => 'Bloco de Restauração (opcional)';
-
- @override
- String get restoreWalletRestoreDateLabel => 'Data de Restauração (opcional)';
-
@override
String get restoreWalletScanFrom => 'Escanear a partir de';
@@ -431,47 +457,32 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get unlockTitle => 'Desbloquear Carteira';
- @override
- String get unlockDescription => 'Digite a senha da sua carteira para desbloquear';
-
- @override
- String get unlockPasswordLabel => 'Senha';
-
@override
String get unlockPasswordHint => 'Digite sua senha';
@override
String get unlockIncorrectPasswordError => 'Senha incorreta. Tente novamente.';
- @override
- String get homeConnecting => 'Conectando';
-
@override
String get homeSyncing => 'Sincronizando';
@override
- String get homeHeight => 'Bloco';
-
- @override
- String get homeReceive => 'Receber';
-
- @override
- String get homeSend => 'Enviar';
+ String get homeSynced => 'Sincronizado';
@override
- String get homeBalanceLocked => 'travado';
+ String get homeTorConnected => 'Tor · conectado';
@override
- String get homeTransactionsTitle => 'Transações';
+ String get homeTorConnecting => 'Tor · conectando';
@override
- String get homeOutgoingTxSemanticLabel => 'Transação de Saída';
+ String get homeTorOff => 'Tor · desligado';
@override
- String get homeIncomingTxSemanticLabel => 'Transação de Entrada';
+ String get homeReceive => 'Receber';
@override
- String get homeTransactionConfirmed => 'Confirmado';
+ String get homeSend => 'Enviar';
@override
String get homeNoTransactions => 'Sem transações';
@@ -498,15 +509,6 @@ class AppLocalizationsPt extends AppLocalizations {
String get receivePrimaryAddressWarn =>
'Aviso: A menos que saiba o que está fazendo, por favor use subendereços para melhor privacidade.';
- @override
- String get receiveShareButton => 'Compartilhar';
-
- @override
- String get receiveShowSubaddressButton => 'Mostrar Subendereço';
-
- @override
- String get receiveShowPrimaryAddressButton => 'Mostrar Endereço Primário';
-
@override
String get receiveServerNoSubaddressesWarn =>
'Aviso: Este servidor não suporta subendereços. Para melhor privacidade, considere usar um servidor que os suporte. Você está recebendo no seu endereço primário.';
@@ -524,6 +526,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get receiveCopyAddress => 'Copiar endereço';
+ @override
+ String get receiveQrHint => 'Escaneie este código para enviar Monero para esta carteira.';
+
@override
String get receiveEnlargeQr => 'Toque para ampliar e aumentar o brilho';
@@ -538,11 +543,6 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Seu endereço $coin';
}
- @override
- String receiveBlockchainSubtitle(String coin) {
- return 'Blockchain $coin';
- }
-
@override
String get sendTitle => 'Enviar';
@@ -669,9 +669,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settingsSecretKeysLabel => 'Chaves Privadas de Restauração';
- @override
- String get settingsSecretKeysButton => 'Ver';
-
@override
String get settingsViewLwsKeysDialogText =>
'Somente compartilhe estas informações com o seu servidor light-wallet. Essas chaves permitem que o portador veja permanentemente todas as transações relacionadas às suas carteiras. Compartilhá-las com uma pessoa não confiável prejudicará significativamente sua privacidade.';
@@ -825,9 +822,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get addressBookNoSearchResults => 'Nenhum contato encontrado';
- @override
- String get addressBookCopyAddress => 'Copiar Endereço';
-
@override
String get addressBookEdit => 'Editar';
@@ -837,9 +831,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get addressBookNameHint => 'Nome';
- @override
- String get addressBookAddressHint => 'Endereço Monero';
-
@override
String get addressBookAddDescription => 'Um nome e um endereço Monero para pagá-lo.';
@@ -852,12 +843,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get addressBookSave => 'Salvar';
- @override
- String get sendSelectedContact => 'Contato selecionado';
-
- @override
- String get sendClearSelectedContact => 'Limpar contato selecionado';
-
@override
String get sendPriorityLow => 'Baixa';
@@ -867,15 +852,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get sendPriorityHigh => 'Alta';
- @override
- String get sendPriorityLabel => 'prioridade';
-
- @override
- String get sendTransactionPriority => 'Prioridade da Transação';
-
- @override
- String get sendFeeLabel => 'Taxa';
-
@override
String get sendContactsButton => 'Contatos';
@@ -904,27 +880,15 @@ class AppLocalizationsPt extends AppLocalizations {
String get sendAvailableSuffix => 'disponível';
@override
- String get sendFailedToGetFeesError => 'Não foi possível carregar taxas.';
-
- @override
- String get torInfoTitle => 'Tor Integrado';
-
- @override
- String get torInfoDescription =>
- 'A Skylight Wallet usa automaticamente Tor integrado para proteger suas conexões de internet.';
+ String get sendIrreversibleNote =>
+ 'Transações Monero são irreversíveis. Confira o endereço e o valor antes de enviar.';
@override
- String get torInfoContinueButton => 'Continuar';
-
- @override
- String get torInfoConfigureButton => 'Configurar';
+ String get sendFailedToGetFeesError => 'Não foi possível carregar taxas.';
@override
String get torSettingsTitle => 'Configurações do Tor';
- @override
- String get torSettingsModeLabel => 'Modo Tor';
-
@override
String get torSettingsModeBuiltIn => 'Tor Integrado';
@@ -960,9 +924,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get torSettingsSocksPortLabel => 'Porta SOCKS';
- @override
- String get torSettingsSocksPortHint => '9050';
-
@override
String get torSettingsUseOrbotLabel => 'Usar Orbot/InviZible';
@@ -988,14 +949,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settingsTorSettingsLabel => 'Configurações do Tor';
- @override
- String get lwsSetupUsingInternalTor => 'Usando Tor interno';
-
- @override
- String lwsSetupUsingExternalTor(String address) {
- return 'Usando proxy Tor externo em $address';
- }
-
@override
String get lwsSetupTorDisabledError => 'O Tor está desativado. Por favor, volte e ative-o.';
### lib/l10n/app_pt.arb
@@ -4,6 +4,7 @@
"cancel": "Cancelar",
"done": "Concluir",
"close": "Fechar",
+ "back": "Voltar",
"unknownError": "Erro desconhecido.",
"warning": "Atenção",
"amount": "Valor",
@@ -13,16 +14,12 @@
"copy": "Copiar",
"addressCopied": "Endereço copiado para a área de transferência",
"fieldEmptyError": "Este campo não pode ficar vazio.",
- "welcomeTitle": "Bem-vindo!",
"welcomeDescription": "A Skylight Wallet é uma das mais simples carteiras de Monero. Nós o ajudaremos a configurar uma carteira e se conectar à um servidor.",
"welcomeGetStarted": "Começar",
"welcomeAgreePrefix": "Ao continuar, você concorda com os ",
"welcomeTermsLink": "Termos de Serviço",
"welcomeAgreeMiddle": " e a ",
"welcomePrivacyLink": "Política de Privacidade",
- "restoreWarningTitle": "Aviso de Restauração",
- "restoreWarningDescription": "Você tem certeza? O servidor ao qual você se conectar poderá ver seu histórico de transações Monero passadas e futuras.",
- "restoreWarningContinueButton": "Continuar",
"lwsSetupTitle": "Configuração da conexão",
"lwsSetupDescription": "Conecte-se a um servidor light-wallet Monero (LWS) ou ao seu próprio nó completo. Selecione apenas um servidor em que você confia. Mesmo se você usar o Tor, este servidor pode obter informações sobre você. Com um LWS, sua chave privada de visualização e seu endereço primário serão compartilhados com este servidor.",
"lwsSetupProxyPortLabel": "Porta do Proxy HTTP (opcional)",
@@ -46,17 +43,6 @@
"connectionReachedOverTor": "Acessado via Tor",
"connectionReachedViaProxy": "Acessado pelo seu proxy",
"connectionReachedDirect": "Acessado diretamente",
- "connectionIndicatorHttps": "HTTPS",
- "connectionIndicatorLocal": "Local",
- "connectionIndicatorTorInternal": "Tor Interno",
- "connectionIndicatorTorExternal": "Usando Porta {port}",
- "@connectionIndicatorTorExternal": {
- "placeholders": {
- "port": {
- "type": "String"
- }
- }
- },
"settingsConnectionSettingsLabel": "Configurações de conexão",
"settingsConnectionServerLws": "LWS",
"settingsConnectionServerNode": "Nó",
@@ -99,23 +85,39 @@
"passwordsDoNotMatchError": "As senhas não coincidem.",
"generateSeedTitle": "Anote-as, em ordem",
"generateSeedTitleCovered": "Frase Seed",
- "generateSeedDescription": "Esta é a sua polyseed. Anote-a e guarde-a em um lugar seguro.",
"generateSeedSubtitleCovered": "Estas palavras, nesta ordem, são a sua carteira. Anote-as e guarde-as em um cofre físico. Se você perder estas palavras ou compartilhá-las com outra pessoa, perderá seu dinheiro permanentemente. Um planejamento cuidadoso agora evita um possível desastre depois.",
"generateSeedSubtitleRevealed": "Guarde-as em segurança. Não as compartilhe.",
"generateSeedScreenshotNote": "As capturas de tela estão bloqueadas nesta tela. Certifique-se de que ninguém está olhando por cima do seu ombro.",
"generateSeedReveal": "Toque para revelar",
"generateSeedConfirm": "Anotei todas as palavras e as guardei em um lugar que só eu posso acessar.",
- "generateSeedContinueButton": "Eu anotei",
+ "generateSeedBirthdayLabel": "Aniversário da carteira",
+ "generateSeedBirthdayReason": "Onde uma futura restauração começa a escanear",
+ "onboardingTorNotePrivacy": "O Tor esconde seu endereço do nó que você consulta — mais lento, e vale a pena.",
+ "onboardingTorNoteChangeable": "Pode ser alterado depois em Configurações → Conexões.",
+ "onboardingPriceNoteRatesOnly": "O serviço de preços recebe apenas pedidos de cotações — nunca endereços ou valores.",
+ "onboardingPriceNoteTor": "Roteado pelo Tor por padrão, separado do tráfego da rede.",
+ "onboardingConnectionNoteServer": "Aponte a Skylight para um servidor de carteira leve (LWS), ou conecte-se ao seu próprio nó Monero.",
+ "onboardingConnectionNoteChangeable": "Pode ser alterado depois em Configurações → Conexão.",
+ "onboardingWalletNoteGenerated": "A seed de uma nova carteira é gerada aqui, offline, e mostrada a você uma única vez.",
+ "onboardingWalletNoteRestore": "A restauração pergunta aproximadamente quando a seed teve fundos pela primeira vez, para pular anos de escaneamento.",
+ "onboardingWalletCreateBullet1": "Uma frase seed Monero, mostrada uma vez",
+ "onboardingWalletCreateBullet2": "Começa vazia, sincroniza a partir de hoje",
+ "onboardingWalletCreateBullet3": "Leva cerca de dois minutos",
+ "onboardingWalletRestoreBullet1": "Uma seed Monero Polyseed, BIP39 ou legada",
+ "onboardingWalletRestoreBullet2": "Data de escaneamento opcional",
+ "onboardingWalletRestoreBullet3": "Recupera seu saldo e histórico existentes",
+ "onboardingSeedNotePassword": "A Skylight Wallet pede sua senha antes de mostrá-las novamente.",
+ "onboardingPasswordNoteLaunch": "Pedida a cada abertura, e antes de a seed ser mostrada.",
+ "onboardingPasswordNoteNotCloud": "Não é uma conta na nuvem. Perdê-la significa restaurar a partir da sua frase seed.",
+ "onboardingPasswordStrong": "Forte",
+ "onboardingPasswordMatch": "As duas entradas coincidem",
"lwsDetailsTitle": "Detalhes da Carteira",
"lwsDetailsDescription": "Se o seu servidor Light Wallet Monero (LWS) exigir registro, você pode usar estes dados para adicionar esta carteira a esse servidor. Nem todos os servidores exigem registro.",
"lwsDetailsPrimaryAddressLabel": "Endereço Primário",
"lwsDetailsSecretViewKeyLabel": "Chave Privada de Visualização",
"lwsDetailsRestoreHeightLabel": "Bloco de Restauração",
"restoreWalletTitle": "Restaurar Carteira",
"restoreWalletDescription": "Insira sua semente Monero abaixo. Verificaremos os formatos comuns.",
- "restoreWalletSeedLabel": "Semente",
- "restoreWalletRestoreHeightLabel": "Bloco de Restauração (opcional)",
- "restoreWalletRestoreDateLabel": "Data de Restauração (opcional)",
"restoreWalletScanFrom": "Escanear a partir de",
"restoreWalletScanFromReason": "Ignore o histórico irrelevante para economizar tempo",
"restoreWalletNotSet": "Não definido",
@@ -158,20 +160,15 @@
"unlockWithFaceId": "Desbloquear com Face ID",
"unlockWithTouchId": "Desbloquear com Touch ID",
"unlockTitle": "Desbloquear Carteira",
- "unlockDescription": "Digite a senha da sua carteira para desbloquear",
- "unlockPasswordLabel": "Senha",
"unlockPasswordHint": "Digite sua senha",
"unlockIncorrectPasswordError": "Senha incorreta. Tente novamente.",
- "homeConnecting": "Conectando",
"homeSyncing": "Sincronizando",
- "homeHeight": "Bloco",
+ "homeSynced": "Sincronizado",
+ "homeTorConnected": "Tor · conectado",
+ "homeTorConnecting": "Tor · conectando",
+ "homeTorOff": "Tor · desligado",
"homeReceive": "Receber",
"homeSend": "Enviar",
- "homeBalanceLocked": "travado",
- "homeTransactionsTitle": "Transações",
- "homeOutgoingTxSemanticLabel": "Transação de Saída",
- "homeIncomingTxSemanticLabel": "Transação de Entrada",
- "homeTransactionConfirmed": "Confirmado",
"homeNoTransactions": "Sem transações",
"homeFiatApiError": "Erro ao conectar ao servidor de preços",
"homeDisconnected": "Desconectado",
@@ -180,14 +177,12 @@
"coinHomeSent": "Enviado",
"receiveTitle": "Receber",
"receivePrimaryAddressWarn": "Aviso: A menos que saiba o que está fazendo, por favor use subendereços para melhor privacidade.",
- "receiveShareButton": "Compartilhar",
- "receiveShowSubaddressButton": "Mostrar Subendereço",
- "receiveShowPrimaryAddressButton": "Mostrar Endereço Primário",
"receiveServerNoSubaddressesWarn": "Aviso: Este servidor não suporta subendereços. Para melhor privacidade, considere usar um servidor que os suporte. Você está recebendo no seu endereço primário.",
"receiveMaxSubaddressesReachedWarn": "Você atingiu o número máximo de subendereços suportados por este servidor. Este é um endereço já usado.",
"receiveSubaddressTab": "Subendereço",
"receivePrimaryTab": "Endereço primário",
"receiveCopyAddress": "Copiar endereço",
+ "receiveQrHint": "Escaneie este código para enviar Monero para esta carteira.",
"receiveEnlargeQr": "Toque para ampliar e aumentar o brilho",
"receiveShrinkQr": "Toque para reduzir",
"receiveShareError": "Não foi possível abrir a janela de compartilhamento",
@@ -199,14 +194,6 @@
}
}
},
- "receiveBlockchainSubtitle": "Blockchain {coin}",
- "@receiveBlockchainSubtitle": {
- "placeholders": {
- "coin": {
- "type": "String"
- }
- }
- },
"sendTitle": "Enviar",
"sendSendButton": "Enviar",
"sendTransactionSuccessfullySent": "Transação enviada com sucesso!",
@@ -247,7 +234,6 @@
"settingsLwsViewKeysButton": "Ver",
"revealSeedAuthReason": "Confirme sua identidade para ver a frase seed",
"settingsSecretKeysLabel": "Chaves Privadas de Restauração",
- "settingsSecretKeysButton": "Ver",
"settingsViewLwsKeysDialogText": "Somente compartilhe estas informações com o seu servidor light-wallet. Essas chaves permitem que o portador veja permanentemente todas as transações relacionadas às suas carteiras. Compartilhá-las com uma pessoa não confiável prejudicará significativamente sua privacidade.",
"settingsViewLwsKeysDialogRevealButton": "Revelar",
"settingsViewSecretKeysDialogText": "Não compartilhe essas chaves com ninguém, incluindo pessoas que aleguem ser suporte. Se você receber um pedido para fornecê-las, está sendo vítima de um golpe. Se você fornecer essas informações a outra pessoa, perderá seu dinheiro e ele não poderá ser recuperado.",
@@ -309,23 +295,16 @@
"addressBookNoContacts": "Nenhum contato ainda",
"addressBookNoContactsDescription": "Adicione seu primeiro contato tocando no botão +",
"addressBookNoSearchResults": "Nenhum contato encontrado",
- "addressBookCopyAddress": "Copiar Endereço",
"addressBookEdit": "Editar",
"addressBookContactName": "Nome do Contato",
"addressBookNameHint": "Nome",
- "addressBookAddressHint": "Endereço Monero",
"addressBookAddDescription": "Um nome e um endereço Monero para pagá-lo.",
"addressBookEditDescription": "Atualize o nome ou o endereço deste contato.",
"addressBookUpdate": "Atualizar",
"addressBookSave": "Salvar",
- "sendSelectedContact": "Contato selecionado",
- "sendClearSelectedContact": "Limpar contato selecionado",
"sendPriorityLow": "Baixa",
"sendPriorityNormal": "Normal",
"sendPriorityHigh": "Alta",
- "sendPriorityLabel": "prioridade",
- "sendTransactionPriority": "Prioridade da Transação",
- "sendFeeLabel": "Taxa",
"sendContactsButton": "Contatos",
"sendToLabel": "Para",
"sendPasteButton": "Colar",
@@ -335,13 +314,9 @@
"sendPriorityHeading": "Prioridade",
"sendPickContactTitle": "Enviar para um contato",
"sendAvailableSuffix": "disponível",
+ "sendIrreversibleNote": "Transações Monero são irreversíveis. Confira o endereço e o valor antes de enviar.",
"sendFailedToGetFeesError": "Não foi possível carregar taxas.",
- "torInfoTitle": "Tor Integrado",
- "torInfoDescription": "A Skylight Wallet usa automaticamente Tor integrado para proteger suas conexões de internet.",
- "torInfoContinueButton": "Continuar",
- "torInfoConfigureButton": "Configurar",
"torSettingsTitle": "Configurações do Tor",
- "torSettingsModeLabel": "Modo Tor",
"torSettingsModeBuiltIn": "Tor Integrado",
"torSettingsModeExternal": "Tor Externo",
"torSettingsModeDisabled": "Sem Tor",
@@ -353,7 +328,6 @@
"torChoiceTitle": "Configuração da Conexão Tor",
"torChoiceSubtitle": "O Tor pode ocultar seu endereço IP dos servidores aos quais você se conecta. Isso não reduz as informações que ficam armazenadas em blockchains públicas. De modo geral, como você quer que a Skylight Wallet lide com as conexões Tor?",
"torSettingsSocksPortLabel": "Porta SOCKS",
- "torSettingsSocksPortHint": "9050",
"torSettingsUseOrbotLabel": "Usar Orbot/InviZible",
"torSettingsUseOrbotLabelIos": "Usar Orbot",
"torSettingsSaveButton": "Salvar",
@@ -362,15 +336,6 @@
"torDisabledWalletsWarningBody": "Algumas carteiras estão configuradas para conectar via Tor. Desativar o Tor irá desconectá-las, e elas permanecerão desconectadas até que você reconfigure a conexão.",
"torDisabledWalletsWarningConfirm": "Desativar o Tor",
"settingsTorSettingsLabel": "Configurações do Tor",
- "lwsSetupUsingInternalTor": "Usando Tor interno",
- "lwsSetupUsingExternalTor": "Usando proxy Tor externo em {address}",
- "@lwsSetupUsingExternalTor": {
- "placeholders": {
- "address": {
- "type": "String"
- }
- }
- },
"lwsSetupTorDisabledError": "O Tor está desativado. Por favor, volte e ative-o.",
"lwsSetupInvalidQrCode": "Endereço de conexão inválido."
}
### lib/main.dart
@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
+import 'package:skeletonizer/skeletonizer.dart';
import 'package:timeago/timeago.dart' as timeago;
import 'package:skylight_wallet/models/fiat_rate_model.dart';
@@ -57,6 +58,9 @@ void main() async {
installWalletCore();
BrandColors.install(skylightPalette);
+ // Selected onboarding option cards stay white — only the accent border
+ // marks the selection (no tinted fill).
+ OnboardingRadioCard.selectedFill = () => BrandColors.card;
// Catch Flutter framework errors
FlutterError.onError = (FlutterErrorDetails details) {
@@ -330,7 +334,14 @@ class _AppRootState extends State<_AppRoot> with WidgetsBindingObserver {
});
}
_lastBrightness = brightness;
- return child ?? const SizedBox.shrink();
+ // Brand-tone skeletons (the default grey clashes with the
+ // navy/orange scheme); tokens resolve to the current theme.
+ return SkeletonizerConfig(
+ data: SkeletonizerConfigData(
+ effect: SoldColorEffect(color: BrandColors.surfaceMuted),
+ ),
+ child: child ?? const SizedBox.shrink(),
+ );
},
initialRoute: initialRoute,
locale: Locale.fromSubtags(languageCode: languageProvider.language),
### lib/screens/address_book.dart
@@ -6,7 +6,9 @@ import 'package:provider/provider.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
import 'package:skylight_wallet/models/contact_model.dart';
+import 'package:skylight_wallet/screens/desktop/home_shell.dart';
import 'package:skylight_wallet/screens/send.dart';
+import 'package:skylight_wallet/util/platform.dart';
import 'package:skylight_wallet/util/secure_clipboard.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/widgets/ui/ui.dart';
@@ -53,15 +55,18 @@ class _AddressBookScreenState extends State<AddressBookScreen> {
final i18n = AppLocalizations.of(context)!;
showBrandSheet<void>(
context: context,
- builder: (sheetContext) => SafeArea(
- top: false,
- child: Padding(
- padding: const EdgeInsets.fromLTRB(22, 8, 22, 12),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const SheetHandle(),
+ builder: (sheetContext) {
+ // Desktop modal: the card owns the edge padding (like every other sheet).
+ final hpad = isDesktopModal ? 0.0 : 22.0;
+ return SafeArea(
+ top: false,
+ child: Padding(
+ padding: EdgeInsets.fromLTRB(hpad, isDesktopModal ? 0 : 8, hpad, isDesktopModal ? 0 : 12),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SheetHandle(),
Row(
children: [
SheetIcon(
@@ -89,17 +94,82 @@ class _AddressBookScreenState extends State<AddressBookScreen> {
Navigator.pop(sheetContext);
},
),
- ],
+ ],
+ ),
),
- ),
- ),
+ );
+ },
+ );
+ }
+
+ /// The searchable contact list, shared by the mobile and desktop layouts; the
+ /// caller supplies the list padding for its gutters.
+ Widget _contactsList(EdgeInsets padding) {
+ return Consumer<ContactModel>(
+ builder: (context, contactModel, child) {
+ final contacts = contactModel.searchContacts(_searchQuery);
+ if (contacts.isEmpty) return _EmptyState(searching: _searchQuery.isNotEmpty);
+ return ListView.builder(
+ padding: padding,
+ itemCount: contacts.length,
+ itemBuilder: (context, index) {
+ final contact = contacts[index];
+ final expanded = contact.id == _expandedId;
+ return _ContactTile(
+ contact: contact,
+ expanded: expanded,
+ onToggle: () => setState(() => _expandedId = expanded ? null : contact.id),
+ onEdit: () => _showEditContactDialog(contact),
+ onDelete: () => _showDeleteContactDialog(contact),
+ isLast: index == contacts.length - 1,
+ );
+ },
+ );
+ },
);
}
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
+ if (isDesktop) {
+ return DesktopShell(
+ active: DesktopNav.addressBook,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(44, 30, 44, 0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Row(
+ children: [
+ Expanded(child: Text(i18n.addressBookTitle, style: desktopTitleStyle)),
+ BrandButton.secondary(
+ label: i18n.addressBookAddContact,
+ icon: Icons.add,
+ expand: false,
+ onPressed: _showAddContactDialog,
+ ),
+ ],
+ ),
+ const SizedBox(height: 22),
+ _SearchField(
+ controller: _searchController,
+ onChanged: (q) => setState(() => _searchQuery = q),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 12),
+ Expanded(child: _contactsList(const EdgeInsets.fromLTRB(44, 6, 44, 36))),
+ ],
+ ),
+ );
+ }
+
return Scaffold(
backgroundColor: BrandColors.paper,
bottomNavigationBar: const WalletNavigationBar(selectedIndex: 1),
@@ -131,7 +201,7 @@ class _AddressBookScreenState extends State<AddressBookScreen> {
),
),
const SizedBox(width: 10),
- GestureDetector(
+ Tappable(
behavior: HitTestBehavior.opaque,
onTap: _showAddContactDialog,
child: Container(
@@ -148,31 +218,7 @@ class _AddressBookScreenState extends State<AddressBookScreen> {
],
),
),
- Expanded(
- child: Consumer<ContactModel>(
- builder: (context, contactModel, child) {
- final contacts = contactModel.searchContacts(_searchQuery);
- if (contacts.isEmpty) return _EmptyState(searching: _searchQuery.isNotEmpty);
- return ListView.builder(
- padding: const EdgeInsets.fromLTRB(16, 6, 16, 24),
- itemCount: contacts.length,
- itemBuilder: (context, index) {
- final contact = contacts[index];
- final expanded = contact.id == _expandedId;
- return _ContactTile(
- contact: contact,
- expanded: expanded,
- onToggle: () =>
- setState(() => _expandedId = expanded ? null : contact.id),
- onEdit: () => _showEditContactDialog(contact),
- onDelete: () => _showDeleteContactDialog(contact),
- isLast: index == contacts.length - 1,
- );
- },
- );
- },
- ),
- ),
+ Expanded(child: _contactsList(const EdgeInsets.fromLTRB(16, 6, 16, 24))),
],
),
),
@@ -477,7 +523,7 @@ class _IconSquare extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return GestureDetector(
+ return Tappable(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Container(
@@ -595,6 +641,8 @@ class _ContactSheetState extends State<_ContactSheet> {
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
final name = _nameController.text.trim();
+ // Desktop modal: the card owns the edge padding (like every other sheet).
+ final hpad = isDesktopModal ? 0.0 : 22.0;
// No keyboard padding here: showBrandSheet applies it once for the
// whole sheet, and a second one lifts this clear off the keyboard.
@@ -606,9 +654,12 @@ class _ContactSheetState extends State<_ContactSheet> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- const Padding(padding: EdgeInsets.only(top: 8), child: SheetHandle()),
Padding(
- padding: const EdgeInsets.fromLTRB(22, 0, 22, 16),
+ padding: EdgeInsets.only(top: isDesktopModal ? 0 : 8),
+ child: const SheetHandle(),
+ ),
+ Padding(
+ padding: EdgeInsets.fromLTRB(hpad, 0, hpad, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -636,7 +687,7 @@ class _ContactSheetState extends State<_ContactSheet> {
),
Flexible(
child: SingleChildScrollView(
- padding: const EdgeInsets.fromLTRB(22, 0, 22, 0),
+ padding: EdgeInsets.fromLTRB(hpad, 0, hpad, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -660,7 +711,7 @@ class _ContactSheetState extends State<_ContactSheet> {
),
),
Padding(
- padding: const EdgeInsets.fromLTRB(22, 18, 22, 8),
+ padding: EdgeInsets.fromLTRB(hpad, 18, hpad, isDesktopModal ? 0 : 8),
child: Column(
children: [
BrandButton(
@@ -684,9 +735,7 @@ class _ContactSheetState extends State<_ContactSheet> {
}
String _addressHeaderLabel(AppLocalizations i18n) {
- final coinName = xmrWallet(context)?.blockchainName ?? 'Monero';
- final label = '$coinName ${i18n.address}';
- return _address == null ? '$label · ${i18n.restoreWalletNotSet}' : label;
+ return _address == null ? '${i18n.address} · ${i18n.restoreWalletNotSet}' : i18n.address;
}
Widget _nameField(String name) {
@@ -738,11 +787,6 @@ class _ContactSheetState extends State<_ContactSheet> {
Widget _addressEntry() {
final i18n = AppLocalizations.of(context)!;
- final wallet = xmrWallet(context);
- final coinName = wallet?.blockchainName ?? 'Monero';
- final tile = wallet != null
- ? CoinMark(coinSymbol: wallet.coinSymbol, iconAsset: wallet.iconAsset, size: 30)
- : const SizedBox(width: 30, height: 30);
final address = _address;
if (address != null) {
@@ -751,33 +795,18 @@ class _ContactSheetState extends State<_ContactSheet> {
padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 11),
child: Row(
children: [
- tile,
- const SizedBox(width: 11),
Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- coinName,
- style: TextStyle(
- fontSize: 12.5,
- fontWeight: FontWeight.w500,
- color: BrandColors.ink,
- ),
- ),
- const SizedBox(height: 3),
- Text(
- shortenMiddle(address, head: 9, tail: 9),
- style: TextStyle(
- fontFamily: 'Ubuntu Mono',
- fontSize: 11,
- color: BrandColors.inkMuted,
- ),
- ),
- ],
+ child: Text(
+ shortenMiddle(address, head: 12, tail: 12),
+ style: TextStyle(
+ fontFamily: 'Ubuntu Mono',
+ fontSize: 12.5,
+ color: BrandColors.ink,
+ ),
),
),
- GestureDetector(
+ const SizedBox(width: 11),
+ Tappable(
behavior: HitTestBehavior.opaque,
onTap: () => setState(() {
_address = null;
@@ -812,18 +841,7 @@ class _ContactSheetState extends State<_ContactSheet> {
children: [
Row(
children: [
- tile,
- const SizedBox(width: 11),
- Expanded(
- child: Text(
- coinName,
- style: TextStyle(
- fontSize: 12.5,
- fontWeight: FontWeight.w500,
- color: BrandColors.inkFaint,
- ),
- ),
- ),
+ const Spacer(),
MiniActionButton(
bordered: true,
color: BrandColors.card,
### lib/screens/connection_setup.dart
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/screens/desktop/connection_view.dart';
+import 'package:skylight_wallet/util/platform.dart';
import 'package:skylight_wallet/widgets/connection_settings_form.dart';
import 'package:skylight_wallet/widgets/ui/ui.dart';
@@ -12,7 +14,19 @@ class ConnectionSetupScreen extends StatelessWidget {
final i18n = AppLocalizations.of(context)!;
void onSaved() {
- Navigator.pushNamed(context, '/fiat_api_setup');
+ Navigator.pushNamed(context, '/create_wallet');
+ }
+
+ if (isDesktop) {
+ return DesktopConnectionView(
+ title: i18n.lwsSetupTitle,
+ description: i18n.lwsSetupDescription,
+ noteServer: i18n.onboardingConnectionNoteServer,
+ noteChangeable: i18n.onboardingConnectionNoteChangeable,
+ saveButtonLabel: i18n.lwsSetupContinueButton,
+ onSaved: onSaved,
+ onBack: () => Navigator.pop(context),
+ );
}
return Scaffold(
@@ -29,7 +43,7 @@ class ConnectionSetupScreen extends StatelessWidget {
const SizedBox(height: BrandSpacing.sm),
BrandScreenHeader(
onBack: () => Navigator.of(context).pop(),
- center: const StepDots(count: 5, index: 1),
+ center: const StepDots(count: 6, index: 2),
),
const SizedBox(height: BrandSpacing.lg),
Text(i18n.lwsSetupTitle, style: BrandText.title),
### lib/screens/create_wallet.dart
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/screens/desktop/wallet_setup_view.dart';
+import 'package:skylight_wallet/util/platform.dart';
import 'package:skylight_wallet/widgets/ui/ui.dart';
class CreateWalletScreenArgs {
@@ -41,18 +43,43 @@ class _CreateWalletScreenState extends State<CreateWalletScreen> {
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
+ final labels = CreateWalletLabels(
+ title: i18n.createWalletTitle,
+ subtitle: i18n.createWalletDescription,
+ createNew: i18n.createWalletCreateNewButton,
+ createNewDesc: i18n.createWalletCreateNewDesc,
+ restore: i18n.createWalletRestoreExistingButton,
+ restoreDesc: i18n.createWalletRestoreExistingDesc,
+ );
+ void createNew() => Navigator.pushNamed(context, '/generate_seed');
+
+ if (isDesktop) {
+ return DesktopWalletSetupView(
+ labels: labels,
+ continueText: i18n.continueText,
+ noteGenerated: i18n.onboardingWalletNoteGenerated,
+ noteRestore: i18n.onboardingWalletNoteRestore,
+ createBullets: [
+ i18n.onboardingWalletCreateBullet1,
+ i18n.onboardingWalletCreateBullet2,
+ i18n.onboardingWalletCreateBullet3,
+ ],
+ restoreBullets: [
+ i18n.onboardingWalletRestoreBullet1,
+ i18n.onboardingWalletRestoreBullet2,
+ i18n.onboardingWalletRestoreBullet3,
+ ],
+ onCreateNew: createNew,
+ onRestore: _restoreExisting,
+ onBack: () => Navigator.pop(context),
+ );
+ }
+
return CreateWalletView(
- stepCount: 5,
+ stepCount: 6,
stepIndex: 3,
- labels: CreateWalletLabels(
- title: i18n.createWalletTitle,
- subtitle: i18n.createWalletDescription,
- createNew: i18n.createWalletCreateNewButton,
- createNewDesc: i18n.createWalletCreateNewDesc,
- restore: i18n.createWalletRestoreExistingButton,
- restoreDesc: i18n.createWalletRestoreExistingDesc,
- ),
- onCreateNew: () => Navigator.pushNamed(context, '/generate_seed'),
+ labels: labels,
+ onCreateNew: createNew,
onRestore: _restoreExisting,
);
}
### lib/screens/create_wallet_password.dart
@@ -1,8 +1,23 @@
import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/models/fiat_rate_model.dart';
+import 'package:skylight_wallet/screens/desktop/create_password_view.dart';
+import 'package:skylight_wallet/util/logging.dart';
+import 'package:skylight_wallet/util/platform.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/widgets/ui/ui.dart';
+/// Carries the wallet commit (create or restore) to the final password step,
+/// which sets the password then writes the wallet (the password-last flow).
+class CreateWalletPasswordArgs {
+ /// Commits the wallet using [ctx] (the password is already set); returns the
+ /// wallet's restore height, for the LWS-details step.
+ final Future<int> Function(BuildContext ctx) commit;
+ const CreateWalletPasswordArgs({required this.commit});
+}
+
class CreateWalletPasswordScreen extends StatefulWidget {
const CreateWalletPasswordScreen({super.key});
@@ -13,42 +28,72 @@ class CreateWalletPasswordScreen extends StatefulWidget {
class _CreateWalletPasswordScreenState extends State<CreateWalletPasswordScreen> {
bool _isLoading = false;
- Future<void> _savePassword(String password) async {
+ /// Sets the password, then commits the wallet from the seed/restore carried in
+ /// [args]. Node mode goes straight home; LWS mode shows its whitelisting info.
+ Future<void> _createWallet(String password, CreateWalletPasswordArgs args) async {
setState(() => _isLoading = true);
-
try {
- if (mounted) {
- setWalletPassword(context, password);
- Navigator.pushNamed(context, '/create_wallet');
- }
- } catch (e) {
- if (mounted) {
- showBrandToast(context, 'Failed to save password: $e');
+ setWalletPassword(context, password);
+ final restoreHeight = await args.commit(context);
+ if (!mounted) return;
+ Provider.of<FiatRateModel>(context, listen: false).startService();
+ if (appWalletOf(context).isNodeMode) {
+ Navigator.pushNamedAndRemoveUntil(context, '/wallet_home', (Route<dynamic> route) => false);
+ } else {
+ Navigator.pushNamedAndRemoveUntil(
+ context,
+ '/lws_details',
+ (Route<dynamic> route) => false,
+ arguments: restoreHeight,
+ );
}
- } finally {
- if (mounted) {
- setState(() => _isLoading = false);
+ } catch (error) {
+ if (!mounted) return;
+ setState(() => _isLoading = false);
+ var errorMsg = 'Sorry, something went wrong.';
+ if (error.toString().contains('failedToLoadHeight')) {
+ errorMsg = 'Check your internet connection.';
+ } else {
+ log(LogLevel.error, error.toString());
}
+ showBrandToast(context, errorMsg);
}
}
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
+ final args = ModalRoute.of(context)?.settings.arguments as CreateWalletPasswordArgs?;
- return CreatePasswordView(
- loading: _isLoading,
- onSubmit: _savePassword,
- labels: CreatePasswordLabels(
- title: i18n.createWalletPasswordTitle,
- description: i18n.createWalletPasswordDescription,
- passwordHint: i18n.createWalletPasswordHint,
- confirmPasswordHint: i18n.createWalletConfirmPasswordHint,
- submit: i18n.continueText,
- fieldEmptyError: i18n.fieldEmptyError,
- tooShortError: i18n.passwordTooShortError,
- doNotMatchError: i18n.passwordsDoNotMatchError,
- ),
+ final labels = CreatePasswordLabels(
+ title: i18n.createWalletPasswordTitle,
+ description: i18n.createWalletPasswordDescription,
+ passwordHint: i18n.createWalletPasswordHint,
+ confirmPasswordHint: i18n.createWalletConfirmPasswordHint,
+ submit: i18n.continueText,
+ fieldEmptyError: i18n.fieldEmptyError,
+ tooShortError: i18n.passwordTooShortError,
+ doNotMatchError: i18n.passwordsDoNotMatchError,
);
+
+ void submit(String password) {
+ if (args != null) _createWallet(password, args);
+ }
+
+ if (isDesktop) {
+ return DesktopCreatePasswordView(
+ labels: labels,
+ continueText: i18n.continueText,
+ noteLaunch: i18n.onboardingPasswordNoteLaunch,
+ noteNotCloud: i18n.onboardingPasswordNoteNotCloud,
+ strongLabel: i18n.onboardingPasswordStrong,
+ matchLabel: i18n.onboardingPasswordMatch,
+ loading: _isLoading,
+ onSubmit: submit,
+ onBack: () => Navigator.pop(context),
+ );
+ }
+
+ return CreatePasswordView(loading: _isLoading, onSubmit: submit, labels: labels);
}
}
### lib/screens/desktop/connection_view.dart
@@ -0,0 +1,73 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+
+import 'package:skylight_wallet/widgets/connection_settings_form.dart';
+import 'package:skylight_wallet/widgets/ui/ui.dart';
+
+/// Desktop Step 3 of 6 — the LWS/node connection (skylight is LWS-first, so this
+/// is part of onboarding). The form runs embedded so its Save moves into the
+/// scaffold footer's Continue, on the same row as Back; a [ConnectionFormController]
+/// mirrors the form's save gating and triggers the save.
+class DesktopConnectionView extends StatefulWidget {
+ final String title;
+ final String description;
+ final String noteServer;
+ final String noteChangeable;
+ final String saveButtonLabel;
+ final VoidCallback onSaved;
+ final VoidCallback? onBack;
+
+ const DesktopConnectionView({
+ super.key,
+ required this.title,
+ required this.description,
+ required this.noteServer,
+ required this.noteChangeable,
+ required this.saveButtonLabel,
+ required this.onSaved,
+ this.onBack,
+ });
+
+ @override
+ State<DesktopConnectionView> createState() => _DesktopConnectionViewState();
+}
+
+class _DesktopConnectionViewState extends State<DesktopConnectionView> {
+ final _controller = ConnectionFormController();
+
+ @override
+ void dispose() {
+ _controller.canSave.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ValueListenableBuilder<bool>(
+ valueListenable: _controller.canSave,
+ builder: (context, canSave, _) => DesktopOnboardingScaffold(
+ logo: SvgPicture.asset('assets/logo_nobg.svg', height: 52),
+ title: widget.title,
+ description: widget.description,
+ step: 3,
+ totalSteps: 6,
+ continueLabel: widget.saveButtonLabel,
+ continueEnabled: canSave,
+ onBack: widget.onBack,
+ onContinue: _controller.save,
+ notes: [
+ OnboardingNote(Icons.dns_outlined, widget.noteServer),
+ OnboardingNote(Icons.tune, widget.noteChangeable),
+ ],
+ content: SingleChildScrollView(
+ child: ConnectionSettingsForm(
+ embedded: true,
+ controller: _controller,
+ saveButtonLabel: widget.saveButtonLabel,
+ onSaved: widget.onSaved,
+ ),
+ ),
+ ),
+ );
+ }
+}
### lib/screens/desktop/create_password_view.dart
@@ -0,0 +1,181 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+
+import 'package:skylight_wallet/widgets/ui/ui.dart';
+
+/// Desktop Step 6 of 6 — set the wallet password (last step; the wallet is
+/// created + encrypted here). Reuses the mobile [CreatePasswordLabels]; the
+/// strength/match hints and the acknowledgement are desktop-only copy.
+class DesktopCreatePasswordView extends StatefulWidget {
+ final CreatePasswordLabels labels;
+ final String continueText;
+ final String noteLaunch;
+ final String noteNotCloud;
+ final String strongLabel;
+ final String matchLabel;
+ final bool loading;
+ final ValueChanged<String> onSubmit;
+ final VoidCallback? onBack;
+
+ const DesktopCreatePasswordView({
+ super.key,
+ required this.labels,
+ required this.continueText,
+ required this.noteLaunch,
+ required this.noteNotCloud,
+ required this.strongLabel,
+ required this.matchLabel,
+ required this.onSubmit,
+ this.loading = false,
+ this.onBack,
+ });
+
+ @override
+ State<DesktopCreatePasswordView> createState() => _DesktopCreatePasswordViewState();
+}
+
+class _DesktopCreatePasswordViewState extends State<DesktopCreatePasswordView> {
+ static const _minLength = 8;
+
+ final _password = TextEditingController();
+ final _confirm = TextEditingController();
+ bool _obscurePassword = true;
+ bool _obscureConfirm = true;
+
+ @override
+ void initState() {
+ super.initState();
+ _password.addListener(_onChanged);
+ _confirm.addListener(_onChanged);
+ }
+
+ void _onChanged() => setState(() {});
+
+ @override
+ void dispose() {
+ _password.dispose();
+ _confirm.dispose();
+ super.dispose();
+ }
+
+ bool get _longEnough => _password.text.length >= _minLength;
+ bool get _matches => _confirm.text.isNotEmpty && _confirm.text == _password.text;
+ bool get _valid => _longEnough && _matches;
+
+ @override
+ Widget build(BuildContext context) {
+ final l = widget.labels;
+ return DesktopOnboardingScaffold(
+ logo: SvgPicture.asset('assets/logo_nobg.svg', height: 52),
+ title: l.title,
+ description: l.description,
+ step: 6,
+ totalSteps: 6,
+ continueLabel: widget.continueText,
+ continueEnabled: _valid,
+ loading: widget.loading,
+ onBack: widget.onBack,
+ onContinue: () => widget.onSubmit(_password.text),
+ notes: [
+ OnboardingNote(Icons.lock_outline, widget.noteLaunch),
+ OnboardingNote(Icons.cloud_off_outlined, widget.noteNotCloud),
+ ],
+ content: ListView(
+ padding: EdgeInsets.zero,
+ children: [
+ _passwordField(
+ controller: _password,
+ label: l.passwordHint,
+ obscured: _obscurePassword,
+ onToggle: () => setState(() => _obscurePassword = !_obscurePassword),
+ ),
+ if (_password.text.isNotEmpty)
+ _hint(_longEnough ? widget.strongLabel : l.tooShortError, ok: _longEnough),
+ const SizedBox(height: 18),
+ _passwordField(
+ controller: _confirm,
+ label: l.confirmPasswordHint,
+ obscured: _obscureConfirm,
+ onToggle: () => setState(() => _obscureConfirm = !_obscureConfirm),
+ ),
+ if (_confirm.text.isNotEmpty)
+ _hint(_matches ? widget.matchLabel : l.doNotMatchError, ok: _matches),
+ ],
+ ),
+ );
+ }
+
+ /// A labeled inset field — small-caps mono label above a white bordered box —
+ /// matching the design and the app's other inputs (not a Material text field).
+ Widget _passwordField({
+ required TextEditingController controller,
+ required String label,
+ required bool obscured,
+ required VoidCallback onToggle,
+ }) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ label.toUpperCase(),
+ style: TextStyle(
+ fontFamily: 'Ubuntu Mono',
+ fontSize: 10,
+ height: 1,
+ fontWeight: FontWeight.w700,
+ letterSpacing: 1.2,
+ color: BrandColors.inkMuted,
+ ),
+ ),
+ const SizedBox(height: 9),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
+ decoration: BoxDecoration(
+ color: BrandColors.card,
+ borderRadius: BorderRadius.circular(16),
+ border: Border.all(color: BrandColors.border),
+ ),
+ child: Row(
+ children: [
+ Expanded(
+ child: TextField(
+ controller: controller,
+ obscureText: obscured,
+ style: TextStyle(
+ fontFamily: 'Ubuntu Mono',
+ fontSize: 16,
+ letterSpacing: obscured ? 3.5 : 0.5,
+ color: BrandColors.ink,
+ ),
+ cursorColor: BrandColors.primary,
+ decoration: const InputDecoration.collapsed(hintText: ''),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Tappable(
+ onTap: onToggle,
+ child: Icon(
+ obscured ? Icons.visibility_outlined : Icons.visibility_off_outlined,
+ size: 20,
+ color: BrandColors.inkMuted,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _hint(String text, {required bool ok}) => Padding(
+ padding: const EdgeInsets.only(top: 8, left: 4),
+ child: Text(
+ text,
+ style: TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 12,
+ color: ok ? BrandColors.success : BrandColors.error,
+ ),
+ ),
+ );
+}
### lib/screens/desktop/fiat_setup_view.dart
@@ -0,0 +1,162 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+
+import 'package:skylight_wallet/widgets/ui/ui.dart';
+
+/// Desktop Step 2 of 6 — price-display choice (Tor-Only / Clearnet / Disabled)
+/// plus the display currency. Reuses the mobile [FiatSetupLabels] and callbacks.
+class DesktopFiatSetupView extends StatelessWidget {
+ final FiatSetupLabels labels;
+ final String noteRatesOnly;
+ final String noteTor;
+ final List<FiatCurrencyOption> currencies;
+ final int modeIndex;
+ final String currency;
+ final bool offerTorOnly;
+ final ValueChanged<int> onModeChanged;
+ final ValueChanged<String> onCurrencyChanged;
+ final VoidCallback onContinue;
+ final VoidCallback? onBack;
+
+ const DesktopFiatSetupView({
+ super.key,
+ required this.labels,
+ required this.noteRatesOnly,
+ required this.noteTor,
+ required this.currencies,
+ required this.modeIndex,
+ required this.currency,
+ required this.offerTorOnly,
+ required this.onModeChanged,
+ required this.onCurrencyChanged,
+ required this.onContinue,
+ this.onBack,
+ });
+
+ static const _torOnly = 0, _clearnet = 1, _disabled = 2;
+
+ @override
+ Widget build(BuildContext context) {
+ return DesktopOnboardingScaffold(
+ logo: SvgPicture.asset('assets/logo_nobg.svg', height: 52),
+ title: labels.title,
+ description: labels.subtitle,
+ step: 2,
+ totalSteps: 6,
+ continueLabel: labels.continueText,
+ onBack: onBack,
+ onContinue: onContinue,
+ notes: [
+ OnboardingNote(Icons.price_change_outlined, noteRatesOnly),
+ OnboardingNote(Icons.lock_outline, noteTor),
+ ],
+ content: ListView(
+ padding: EdgeInsets.zero,
+ children: [
+ if (offerTorOnly) ...[
+ OnboardingRadioCard(
+ selected: modeIndex == _torOnly,
+ icon: Icons.lock_outline,
+ title: labels.torOnly,
+ description: labels.torOnlyDesc,
+ onTap: () => onModeChanged(_torOnly),
+ ),
+ const SizedBox(height: 10),
+ ],
+ OnboardingRadioCard(
+ selected: modeIndex == _clearnet,
+ icon: Icons.public,
+ title: labels.clearnet,
+ description: labels.clearnetDesc,
+ onTap: () => onModeChanged(_clearnet),
+ ),
+ const SizedBox(height: 10),
+ OnboardingRadioCard(
+ selected: modeIndex == _disabled,
+ icon: Icons.money_off,
+ title: labels.disabled,
+ description: labels.disabledDesc,
+ onTap: () => onModeChanged(_disabled),
+ ),
+ const SizedBox(height: 26),
+ Text(
+ labels.currencyLabel,
+ style: TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 13,
+ fontWeight: FontWeight.w500,
+ color: BrandColors.inkMuted,
+ ),
+ ),
+ const SizedBox(height: 12),
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ for (final c in currencies)
+ _CurrencyChip(
+ code: c.code,
+ symbol: c.symbol,
+ selected: c.code == currency,
+ onTap: () => onCurrencyChanged(c.code),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _CurrencyChip extends StatelessWidget {
+ final String code;
+ final String symbol;
+ final bool selected;
+ final VoidCallback onTap;
+
+ const _CurrencyChip({
+ required this.code,
+ required this.symbol,
+ required this.selected,
+ required this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Tappable(
+ onTap: onTap,
+ behavior: HitTestBehavior.opaque,
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 9),
+ decoration: BoxDecoration(
+ // Selected chip stays white — only the accent border marks it.
+ color: BrandColors.card,
+ border: Border.all(
+ color: selected ? BrandColors.primary : BrandColors.border,
+ width: selected ? 1.5 : 1,
+ ),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ code,
+ style: TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 13,
+ fontWeight: FontWeight.w500,
+ color: selected ? BrandColors.primary : BrandColors.ink,
+ ),
+ ),
+ const SizedBox(width: 6),
+ Text(
+ symbol,
+ style: TextStyle(fontFamily: 'Ubuntu', fontSize: 13, color: BrandColors.inkFaint),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
### lib/screens/desktop/home_shell.dart
@@ -0,0 +1,319 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:package_info_plus/package_info_plus.dart';
+
+import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/screens/settings.dart';
+import 'package:skylight_wallet/services/tor_service.dart';
+import 'package:skylight_wallet/widgets/ui/ui.dart';
+
+/// The top-level desktop destinations, mirroring the mobile navigation bar
+/// (Skylight has no separate History screen).
+enum DesktopNav { home, addressBook, settings }
+
+/// Shared style for a desktop screen's large title (Settings, Contacts, …).
+TextStyle get desktopTitleStyle => TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 26,
+ height: 1.2,
+ fontWeight: FontWeight.w700,
+ color: BrandColors.ink,
+);
+
+/// The "‹ label" back link at the top of a desktop sub-screen (receive, send,
+/// ToS, …) — the shared back affordance in place of a mobile app bar.
+class DesktopBackLink extends StatelessWidget {
+ final String label;
+ final VoidCallback onTap;
+
+ const DesktopBackLink({super.key, required this.label, required this.onTap});
+
+ @override
+ Widget build(BuildContext context) {
+ // Sized to its content (left-aligned) so the hover/ripple doesn't span the
+ // full parent width; a little right padding keeps the ripple off the label.
+ return Align(
+ alignment: Alignment.centerLeft,
+ child: InkWell(
+ mouseCursor: WidgetStateMouseCursor.clickable,
+ onTap: onTap,
+ borderRadius: BorderRadius.circular(8),
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(0, 4, 4, 4),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(Icons.chevron_left, size: 18, color: BrandColors.primary),
+ const SizedBox(width: 2),
+ Text(
+ label,
+ style: TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 12.5,
+ height: 1,
+ fontWeight: FontWeight.w500,
+ color: BrandColors.primary,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+/// Two-pane desktop chrome: a fixed left sidebar (brand, nav, Tor status,
+/// version) with the screen's content on the right. Shared by the desktop
+/// screens so the sidebar stays put.
+///
+/// The sidebar is a [Hero] (like the mobile nav bar) so it flies in place across
+/// route transitions instead of sliding with the page — the content animates,
+/// the sidebar looks fixed. Modals stay on the root navigator, so they still
+/// cover (and dim) the whole window, sidebar included.
+class DesktopShell extends StatefulWidget {
+ final DesktopNav active;
+ final Widget child;
+
+ const DesktopShell({super.key, required this.active, required this.child});
+
+ @override
+ State<DesktopShell> createState() => _DesktopShellState();
+}
+
+class _DesktopShellState extends State<DesktopShell> {
+ // Cached across shells: each nav click mounts a fresh shell, and a per-instance
+ // reload would blank the version line for a frame (a flicker) every time.
+ static String _cachedVersion = '';
+ String _version = _cachedVersion;
+
+ // TorService exposes no change notification, so poll its status and rebuild
+ // when it flips (e.g. connecting → connected after launch).
+ Timer? _torPoll;
+ TorConnectionStatus _torState = TorService.sharedInstance.status;
+
+ @override
+ void initState() {
+ super.initState();
+ if (_cachedVersion.isEmpty) {
+ PackageInfo.fromPlatform().then((info) {
+ _cachedVersion = 'v${info.version} · build ${info.buildNumber}';
+ if (mounted) setState(() => _version = _cachedVersion);
+ });
+ }
+ _torPoll = Timer.periodic(const Duration(seconds: 1), (_) {
+ final status = TorService.sharedInstance.status;
+ if (mounted && status != _torState) setState(() => _torState = status);
+ });
+ }
+
+ @override
+ void dispose() {
+ _torPoll?.cancel();
+ super.dispose();
+ }
+
+ void _go(String route) {
+ if (ModalRoute.of(context)?.settings.name == route) return;
+ Navigator.pushNamed(context, route);
+ }
+
+ /// Readable cap for a screen's content so it doesn't span an ultra-wide window
+ /// (the child adds its own gutters, so this includes ~44px each side).
+ static const contentMaxWidth = 948.0;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: BrandColors.paper,
+ body: Row(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ // Flies in place across transitions, so the sidebar doesn't slide with
+ // the page. Kept out of the page's own transition, like the mobile nav.
+ // placeholderBuilder keeps it painted in place during the flight, so
+ // instant (zero-duration) tab switches don't blink an empty gap.
+ Hero(
+ tag: 'desktop-sidebar',
+ placeholderBuilder: (context, heroSize, child) => child,
+ child: _sidebar(context),
+ ),
+ Expanded(
+ child: Align(
+ alignment: Alignment.topCenter,
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: contentMaxWidth),
+ child: widget.child,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _sidebar(BuildContext context) {
+ final i18n = AppLocalizations.of(context)!;
+ // The Hero lifts this into the overlay mid-flight, outside any Scaffold, so
+ // give it a Material for text styling (transparent to keep the fill).
+ return Material(
+ type: MaterialType.transparency,
+ child: Container(
+ width: 280,
+ decoration: BoxDecoration(
+ color: BrandColors.surfaceSunken,
+ border: Border(right: BorderSide(color: BrandColors.border)),
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 26),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 7),
+ child: Row(
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(6),
+ child: SvgPicture.asset('assets/logo.svg', width: 28, height: 28),
+ ),
+ const SizedBox(width: 10),
+ Text(
+ 'Skylight Wallet',
+ style: TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 16,
+ height: 1,
+ fontWeight: FontWeight.w700,
+ color: BrandColors.ink,
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 28),
+ _navItem(
+ DesktopNav.home,
+ Icons.account_balance_wallet_outlined,
+ i18n.navigationBarWallet,
+ '/wallet_home',
+ ),
+ const SizedBox(height: 2),
+ _navItem(
+ DesktopNav.addressBook,
+ Icons.people_outline,
+ i18n.addressBookTitle,
+ '/address_book',
+ ),
+ const Spacer(),
+ // Settings opens as a modal over the current screen (not a page).
+ _navTile(
+ icon: Icons.tune,
+ label: i18n.navigationBarSettings,
+ active: false,
+ onTap: () => showSettingsSheet(context),
+ ),
+ const SizedBox(height: 6),
+ _torStatus(),
+ if (_version.isNotEmpty)
+ Padding(
+ padding: const EdgeInsets.only(left: 14, top: 1),
+ child: Text(
+ 'Skylight Wallet $_version',
+ style: TextStyle(
+ fontFamily: 'Ubuntu Mono',
+ fontSize: 10.5,
+ height: 1.5,
+ color: BrandColors.inkFaint,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _navItem(DesktopNav nav, IconData icon, String label, String route) {
+ // Always clickable (cursor + ripple), even when this tab is the active one;
+ // _go no-ops when you're already on that exact route.
+ return _navTile(
+ icon: icon,
+ label: label,
+ active: widget.active == nav,
+ onTap: () => _go(route),
+ );
+ }
+
+ Widget _navTile({
+ required IconData icon,
+ required String label,
+ required bool active,
+ required VoidCallback? onTap,
+ }) {
+ final fg = active ? BrandColors.primaryDeep : BrandColors.inkMuted;
+ return Material(
+ color: active ? BrandColors.paper : Colors.transparent,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ side: BorderSide(color: active ? BrandColors.border : Colors.transparent),
+ ),
+ child: InkWell(
+ mouseCursor: WidgetStateMouseCursor.clickable,
+ borderRadius: BorderRadius.circular(12),
+ onTap: onTap,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 11),
+ child: Row(
+ children: [
+ Icon(icon, size: 18, color: fg),
+ const SizedBox(width: 12),
+ Text(
+ label,
+ style: TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 14,
+ height: 1,
+ fontWeight: active ? FontWeight.w500 : FontWeight.w400,
+ color: fg,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _torStatus() {
+ final i18n = AppLocalizations.of(context)!;
+ final (Color color, String text) = switch (_torState) {
+ TorConnectionStatus.connected => (BrandColors.purple, i18n.homeTorConnected),
+ TorConnectionStatus.connecting => (BrandColors.warning, i18n.homeTorConnecting),
+ TorConnectionStatus.disconnected => (BrandColors.inkFaint, i18n.homeTorOff),
+ };
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4),
+ child: Row(
+ children: [
+ Container(
+ width: 7,
+ height: 7,
+ decoration: BoxDecoration(color: color, shape: BoxShape.circle),
+ ),
+ const SizedBox(width: 9),
+ Text(
+ text,
+ style: TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 12,
+ height: 1,
+ color: BrandColors.inkMuted,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
### lib/screens/desktop/receive_view.dart
@@ -0,0 +1,175 @@
+import 'package:flutter/material.dart';
+import 'package:qr_flutter/qr_flutter.dart';
+
+import 'package:skylight_wallet/screens/desktop/home_shell.dart';
+import 'package:skylight_wallet/widgets/ui/ui.dart';
+
+/// Desktop receive (inside [DesktopShell]): a QR panel on the left, and the
+/// subaddress/primary toggle, address and Copy on the right. Monero-only, so
+/// there is no coin card. Presentational — the screen computes the address /
+/// heading / warning and supplies callbacks.
+class DesktopReceiveView extends StatelessWidget {
+ final String title;
+ final String backLabel;
+ final String copyLabel;
+ final String qrHint;
+ final bool ready;
+ final String address;
+ final String qrHeading;
+ final List<String>? tabLabels;
+ final int selectedTab;
+ final ValueChanged<int>? onSelectTab;
+ final String? warning;
+ final VoidCallback onCopy;
+ final VoidCallback onBack;
+
+ const DesktopReceiveView({
+ super.key,
+ required this.title,
+ required this.backLabel,
+ required this.copyLabel,
+ required this.qrHint,
+ required this.ready,
+ required this.address,
+ required this.qrHeading,
+ required this.onCopy,
+ required this.onBack,
+ this.tabLabels,
+ this.selectedTab = 0,
+ this.onSelectTab,
+ this.warning,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ if (!ready) {
+ return Center(child: CircularProgressIndicator(color: BrandColors.primary));
+ }
+ return ListView(
+ padding: const EdgeInsets.fromLTRB(44, 30, 44, 36),
+ children: [
+ DesktopBackLink(label: backLabel, onTap: onBack),
+ const SizedBox(height: 16),
+ Text(title, style: desktopTitleStyle),
+ const SizedBox(height: 24),
+ LayoutBuilder(
+ builder: (context, constraints) {
+ // Two columns (QR beside details) only when there's room; below that,
+ // stack the QR above the details.
+ const twoColMinWidth = 640.0;
+ final qr = SizedBox(width: 300, child: _qrCard());
+ if (constraints.maxWidth < twoColMinWidth) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Center(child: qr),
+ const SizedBox(height: 24),
+ _details(context),
+ ],
+ );
+ }
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ qr,
+ const SizedBox(width: 28),
+ Expanded(child: _details(context)),
+ ],
+ );
+ },
+ ),
+ ],
+ );
+ }
+
+ Widget _qrCard() {
+ return Container(
+ decoration: BoxDecoration(
+ color: BrandColors.card,
+ border: Border.all(color: BrandColors.border),
+ borderRadius: BorderRadius.circular(20),
+ ),
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ padding: const EdgeInsets.all(10),
+ decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
+ child: QrImageView(
+ data: address,
+ size: 232,
+ backgroundColor: Colors.white,
+ padding: EdgeInsets.zero,
+ ),
+ ),
+ const SizedBox(height: 14),
+ Text(
+ qrHint,
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 12,
+ height: 1.5,
+ color: BrandColors.inkMuted,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _details(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ if (tabLabels != null) ...[
+ BrandSegmented(
+ dense: true,
+ labels: tabLabels!,
+ selectedIndex: selectedTab,
+ onSelect: onSelectTab ?? (_) {},
+ ),
+ const SizedBox(height: 18),
+ ],
+ SectionHeader(label: qrHeading, padding: EdgeInsets.zero),
+ const SizedBox(height: 8),
+ Container(
+ decoration: BoxDecoration(
+ color: BrandColors.card,
+ border: Border.all(color: BrandColors.border),
+ borderRadius: BorderRadius.circular(16),
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
+ child: SelectableText(
+ address,
+ style: TextStyle(
+ fontFamily: 'Ubuntu Mono',
+ fontSize: 13.5,
+ height: 1.7,
+ color: BrandColors.ink,
+ ),
+ ),
+ ),
+ if (warning != null) ...[
+ const SizedBox(height: 12),
+ Text(
+ warning!,
+ style: BrandText.caption.copyWith(color: BrandColors.warning, height: 1.4),
+ ),
+ ],
+ const SizedBox(height: 16),
+ Align(
+ alignment: Alignment.centerLeft,
+ child: BrandButton(
+ label: copyLabel,
+ icon: Icons.copy_outlined,
+ expand: false,
+ dense: true,
+ onPressed: onCopy,
+ ),
+ ),
+ ],
+ );
+ }
+}
### lib/screens/desktop/tor_choice_view.dart
@@ -0,0 +1,151 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+
+import 'package:skylight_wallet/widgets/tor_settings_form.dart'
+ show TorPortField, TorTestChip, TorTestStatus;
+import 'package:skylight_wallet/widgets/ui/ui.dart';
+
+/// Desktop Step 1 of 5 — Tor connection choice (Built-in / External / No Tor).
+/// Reuses the mobile [TorChoiceLabels] (all i18n) and callbacks; only the dark
+/// pane's footnotes are desktop-only copy.
+class DesktopTorChoiceView extends StatefulWidget {
+ final TorChoiceLabels labels;
+ final String notePrivacy;
+ final String noteChangeable;
+ final Future<bool> Function(String port) onTest;
+ final void Function({required int modeIndex, required String port, required bool useOrbot})
+ onContinue;
+ final VoidCallback? onBack;
+
+ const DesktopTorChoiceView({
+ super.key,
+ required this.labels,
+ required this.notePrivacy,
+ required this.noteChangeable,
+ required this.onTest,
+ required this.onContinue,
+ this.onBack,
+ });
+
+ @override
+ State<DesktopTorChoiceView> createState() => _DesktopTorChoiceViewState();
+}
+
+class _DesktopTorChoiceViewState extends State<DesktopTorChoiceView> {
+ static const _builtIn = 0, _external = 1, _noTor = 2;
+
+ int? _selected;
+ final _portController = TextEditingController(text: '9050');
+ final bool _useOrbot = false;
+ bool _testing = false;
+ bool? _testOk;
+
+ @override
+ void dispose() {
+ _portController.dispose();
+ super.dispose();
+ }
+
+ bool get _canContinue => _selected != null && (_selected != _external || _testOk == true);
+
+ Future<void> _runTest() async {
+ setState(() {
+ _testing = true;
+ _testOk = null;
+ });
+ final ok = await widget.onTest(_portController.text.trim());
+ if (!mounted) return;
+ setState(() {
+ _testing = false;
+ _testOk = ok;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final l = widget.labels;
+ return DesktopOnboardingScaffold(
+ logo: SvgPicture.asset('assets/logo_nobg.svg', height: 52),
+ title: l.title,
+ description: l.subtitle,
+ step: 1,
+ totalSteps: 6,
+ continueLabel: l.continueText,
+ continueEnabled: _canContinue,
+ onBack: widget.onBack,
+ onContinue: _selected == null
+ ? null
+ : () => widget.onContinue(
+ modeIndex: _selected!,
+ port: _portController.text.trim(),
+ useOrbot: _useOrbot,
+ ),
+ notes: [
+ OnboardingNote(Icons.lock_outline, widget.notePrivacy),
+ OnboardingNote(Icons.tune, widget.noteChangeable),
+ ],
+ content: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ OnboardingRadioCard(
+ selected: _selected == _builtIn,
+ icon: Icons.lock_outline,
+ title: l.builtIn,
+ description: l.builtInDesc,
+ onTap: () => setState(() => _selected = _builtIn),
+ ),
+ const SizedBox(height: 10),
+ OnboardingRadioCard(
+ selected: _selected == _external,
+ icon: Icons.shield_outlined,
+ title: l.external,
+ description: l.externalDesc,
+ onTap: () => setState(() => _selected = _external),
+ trailing: _selected == _external ? _externalForm(l) : null,
+ ),
+ const SizedBox(height: 10),
+ OnboardingRadioCard(
+ selected: _selected == _noTor,
+ icon: Icons.visibility_outlined,
+ title: l.noTor,
+ description: l.noTorDesc,
+ onTap: () => setState(() => _selected = _noTor),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _externalForm(TorChoiceLabels l) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ // Same labeled port field + compact test chip / inline status as the
+ // settings Tor sheet (shared widgets from tor_settings_form).
+ TorPortField(
+ controller: _portController,
+ label: l.socksPortLabel,
+ onChanged: () {
+ if (_testOk != null) setState(() => _testOk = null);
+ },
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ Expanded(
+ child: TorTestStatus(
+ testing: _testing,
+ tested: _testOk != null,
+ ok: _testOk == true,
+ connectedLabel: l.connected,
+ failedLabel: l.testFailed,
+ ),
+ ),
+ const SizedBox(width: 12),
+ TorTestChip(label: l.testButton, onTap: _testing ? null : _runTest),
+ ],
+ ),
+ ],
+ );
+ }
+}
### lib/screens/desktop/wallet_setup_view.dart
@@ -0,0 +1,120 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+
+import 'package:skylight_wallet/widgets/ui/ui.dart';
+
+/// Desktop Step 4 of 6 — create-new vs restore. Selection then Continue (rather
+/// than the mobile immediate-navigate). Card title/description reuse the mobile
+/// [CreateWalletLabels]; the feature bullets are desktop-only copy.
+class DesktopWalletSetupView extends StatefulWidget {
+ final CreateWalletLabels labels;
+ final String continueText;
+ final String noteGenerated;
+ final String noteRestore;
+ final List<String> createBullets;
+ final List<String> restoreBullets;
+ final VoidCallback onCreateNew;
+ final VoidCallback onRestore;
+ final VoidCallback? onBack;
+
+ const DesktopWalletSetupView({
+ super.key,
+ required this.labels,
+ required this.continueText,
+ required this.noteGenerated,
+ required this.noteRestore,
+ required this.createBullets,
+ required this.restoreBullets,
+ required this.onCreateNew,
+ required this.onRestore,
+ this.onBack,
+ });
+
+ @override
+ State<DesktopWalletSetupView> createState() => _DesktopWalletSetupViewState();
+}
+
+class _DesktopWalletSetupViewState extends State<DesktopWalletSetupView> {
+ static const _new = 0, _restore = 1;
+ int? _selected;
+
+ @override
+ Widget build(BuildContext context) {
+ final l = widget.labels;
+ return DesktopOnboardingScaffold(
+ logo: SvgPicture.asset('assets/logo_nobg.svg', height: 52),
+ title: l.title,
+ description: l.subtitle,
+ step: 4,
+ totalSteps: 6,
+ continueLabel: widget.continueText,
+ continueEnabled: _selected != null,
+ onBack: widget.onBack,
+ onContinue: _selected == null
+ ? null
+ : (_selected == _new ? widget.onCreateNew : widget.onRestore),
+ notes: [
+ OnboardingNote(Icons.key_outlined, widget.noteGenerated),
+ OnboardingNote(Icons.history, widget.noteRestore),
+ ],
+ content: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ OnboardingRadioCard(
+ selected: _selected == _new,
+ icon: Icons.auto_awesome_outlined,
+ title: l.createNew,
+ description: l.createNewDesc,
+ onTap: () => setState(() => _selected = _new),
+ trailing: _Bullets(widget.createBullets),
+ ),
+ const SizedBox(height: 10),
+ OnboardingRadioCard(
+ selected: _selected == _restore,
+ icon: Icons.download_outlined,
+ title: l.restore,
+ description: l.restoreDesc,
+ onTap: () => setState(() => _selected = _restore),
+ trailing: _Bullets(widget.restoreBullets),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _Bullets extends StatelessWidget {
+ final List<String> items;
+ const _Bullets(this.items);
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ for (final item in items)
+ Padding(
+ padding: const EdgeInsets.only(top: 6),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(Icons.check, size: 14, color: BrandColors.primary),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ item,
+ style: TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 12.5,
+ height: 1.5,
+ color: BrandColors.inkMuted,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}
### lib/screens/desktop/welcome_view.dart
@@ -0,0 +1,121 @@
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+
+import 'package:skylight_wallet/widgets/ui/ui.dart';
+
+/// Desktop welcome screen: a centred 540px column (logo, name, description,
+/// Get Started, ToS) — the same content as the mobile [WelcomeView], laid out
+/// for a desktop window. Every colour is an existing skylightPalette token.
+class DesktopWelcomeView extends StatelessWidget {
+ final Widget logo;
+ final String appName;
+ final String description;
+ final WelcomeLabels labels;
+ final VoidCallback onGetStarted;
+ final VoidCallback onTerms;
+ final VoidCallback onPrivacy;
+
+ const DesktopWelcomeView({
+ super.key,
+ required this.logo,
+ required this.appName,
+ required this.description,
+ required this.labels,
+ required this.onGetStarted,
+ required this.onTerms,
+ required this.onPrivacy,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: BrandColors.paper,
+ body: Center(
+ child: SizedBox(
+ width: 540,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 40),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ logo,
+ const SizedBox(height: 26),
+ Text(
+ appName,
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 34,
+ height: 1.15,
+ fontWeight: FontWeight.w700,
+ letterSpacing: -0.5,
+ color: BrandColors.ink,
+ ),
+ ),
+ const SizedBox(height: 14),
+ Text(
+ description,
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 15,
+ height: 1.65,
+ color: BrandColors.inkMuted,
+ ),
+ ),
+ const SizedBox(height: 34),
+ SizedBox(
+ width: 320,
+ child: BrandButton(label: labels.getStarted, onPressed: onGetStarted),
+ ),
+ const SizedBox(height: 16),
+ _TermsLine(labels: labels, onTerms: onTerms, onPrivacy: onPrivacy),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _TermsLine extends StatelessWidget {
+ final WelcomeLabels labels;
+ final VoidCallback onTerms;
+ final VoidCallback onPrivacy;
+
+ const _TermsLine({required this.labels, required this.onTerms, required this.onPrivacy});
+
+ @override
+ Widget build(BuildContext context) {
+ final base = TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 12,
+ height: 1.6,
+ color: BrandColors.inkFaint,
+ );
+ final link = base.copyWith(color: BrandColors.primary, fontWeight: FontWeight.w500);
+ return Text.rich(
+ TextSpan(
+ style: base,
+ children: [
+ TextSpan(text: labels.agreePrefix),
+ TextSpan(
+ // Non-breaking spaces keep the link phrase whole — the line wraps
+ // between phrases, never mid-"Terms of Service".
+ text: labels.termsLink.replaceAll(' ', ' '),
+ style: link,
+ recognizer: TapGestureRecognizer()..onTap = onTerms,
+ ),
+ TextSpan(text: labels.agreeMiddle),
+ TextSpan(
+ text: labels.privacyLink.replaceAll(' ', ' '),
+ style: link,
+ recognizer: TapGestureRecognizer()..onTap = onPrivacy,
+ ),
+ ],
+ ),
+ textAlign: TextAlign.center,
+ );
+ }
+}
### lib/screens/fiat_api_setup_screen.dart
@@ -1,12 +1,12 @@
-import 'dart:io';
-
import 'package:flutter/material.dart';
import 'package:skylight_wallet/consts.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
import 'package:skylight_wallet/models/fiat_rate_model.dart';
+import 'package:skylight_wallet/screens/desktop/fiat_setup_view.dart';
import 'package:skylight_wallet/services/shared_preferences_service.dart';
import 'package:skylight_wallet/services/tor_settings_service.dart';
+import 'package:skylight_wallet/util/platform.dart';
import 'package:skylight_wallet/widgets/ui/ui.dart';
class FiatApiSetupScreen extends StatefulWidget {
@@ -37,42 +37,57 @@ class _FiatApiSetupScreenState extends State<FiatApiSetupScreen> {
await SharedPreferencesService.remove(SharedPreferencesKeys.fiatRate);
if (!mounted) return;
- if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
- Navigator.pushNamed(context, '/create_wallet_password');
- } else {
- Navigator.pushNamed(context, '/create_wallet');
- }
+ Navigator.pushNamed(context, '/connection_setup');
}
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
+ final labels = FiatSetupLabels(
+ title: i18n.fiatApiSetupTitle,
+ subtitle: i18n.fiatApiSetupDescription,
+ torOnly: i18n.fiatApiSettingsModeTorOnly,
+ torOnlyDesc: i18n.fiatModeTorOnlyDesc,
+ clearnet: i18n.fiatApiSettingsModeClearnet,
+ clearnetDesc: i18n.fiatModeClearnetDesc,
+ disabled: i18n.fiatApiSettingsModeDisabled,
+ disabledDesc: i18n.fiatModeDisabledDesc,
+ currencyLabel: i18n.fiatApiSettingsDisplayCurrencyLabel,
+ continueText: i18n.lwsSetupContinueButton,
+ );
+ final currencies = [
+ for (final code in supportedFiatCurrencies)
+ FiatCurrencyOption(code: code, symbol: currencySymbols[code] ?? ''),
+ ];
+
+ if (isDesktop) {
+ return DesktopFiatSetupView(
+ labels: labels,
+ noteRatesOnly: i18n.onboardingPriceNoteRatesOnly,
+ noteTor: i18n.onboardingPriceNoteTor,
+ currencies: currencies,
+ modeIndex: _fiatMode.index,
+ currency: _fiatCurrency,
+ offerTorOnly: !_globalTorDisabled,
+ onModeChanged: (i) => setState(() => _fiatMode = FiatApiMode.values[i]),
+ onCurrencyChanged: (code) => setState(() => _fiatCurrency = code),
+ onContinue: _onContinue,
+ onBack: () => Navigator.pop(context),
+ );
+ }
+
return FiatSetupView(
- labels: FiatSetupLabels(
- title: i18n.fiatApiSetupTitle,
- subtitle: i18n.fiatApiSetupDescription,
- torOnly: i18n.fiatApiSettingsModeTorOnly,
- torOnlyDesc: i18n.fiatModeTorOnlyDesc,
- clearnet: i18n.fiatApiSettingsModeClearnet,
- clearnetDesc: i18n.fiatModeClearnetDesc,
- disabled: i18n.fiatApiSettingsModeDisabled,
- disabledDesc: i18n.fiatModeDisabledDesc,
- currencyLabel: i18n.fiatApiSettingsDisplayCurrencyLabel,
- continueText: i18n.lwsSetupContinueButton,
- ),
- currencies: [
- for (final code in supportedFiatCurrencies)
- FiatCurrencyOption(code: code, symbol: currencySymbols[code] ?? ''),
- ],
+ labels: labels,
+ currencies: currencies,
modeIndex: _fiatMode.index,
currency: _fiatCurrency,
offerTorOnly: !_globalTorDisabled,
onModeChanged: (i) => setState(() => _fiatMode = FiatApiMode.values[i]),
onCurrencyChanged: (code) => setState(() => _fiatCurrency = code),
onContinue: _onContinue,
- stepCount: 5,
- stepIndex: 2,
+ stepCount: 6,
+ stepIndex: 1,
);
}
}
### lib/screens/generate_seed.dart
@@ -1,12 +1,13 @@
import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:intl/intl.dart';
import 'package:wallet_domain/wallet_domain.dart' show SeedSource;
import 'package:skylight_wallet/l10n/app_localizations.dart';
-import 'package:skylight_wallet/models/fiat_rate_model.dart';
-import 'package:skylight_wallet/wallet_core_glue.dart';
-import 'package:skylight_wallet/util/logging.dart';
+import 'package:skylight_wallet/screens/create_wallet_password.dart';
+import 'package:skylight_wallet/util/platform.dart';
import 'package:skylight_wallet/util/secure_screen.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/widgets/ui/ui.dart';
class GenerateSeedScreen extends StatefulWidget {
@@ -19,65 +20,65 @@ class GenerateSeedScreen extends StatefulWidget {
class _GenerateSeedScreenState extends State<GenerateSeedScreen> with SecureScreenMixin {
List<String>? _seed;
({SeedSource seed, DateTime restoreDate})? _generated;
- bool _committing = false;
@override
void initState() {
super.initState();
// Generate the seed in memory only — nothing is written to disk until the
- // user confirms their backup and taps Continue (_commit).
+ // password step commits the wallet (password-last flow).
final generated = generateWalletSeed(context);
_generated = generated;
_seed = generated.seed.mnemonic.split(' ');
}
- Future<void> _continue() async {
- if (_committing || _generated == null) return;
- setState(() => _committing = true);
-
- try {
- // This is where the wallet file is actually written to disk.
- final restoreHeight = await commitGeneratedWallet(
- context,
- seed: _generated!.seed,
- restoreDate: _generated!.restoreDate,
- );
- if (!mounted) return;
- Provider.of<FiatRateModel>(context, listen: false).startService();
- // Wallet Details is LWS whitelisting info; a full node needs none of it.
- if (appWalletOf(context).isNodeMode) {
- Navigator.pushNamedAndRemoveUntil(context, '/wallet_home', (Route<dynamic> route) => false);
- } else {
- Navigator.pushNamedAndRemoveUntil(
- context,
- '/lws_details',
- (Route<dynamic> route) => false,
- arguments: restoreHeight,
- );
- }
- } catch (error) {
- if (!mounted) return;
- setState(() => _committing = false);
- var errorMsg = 'Sorry, something went wrong.';
- if (error.toString().contains('failedToLoadHeight')) {
- errorMsg = 'Check your internet connection.';
- } else {
- log(LogLevel.error, error.toString());
- }
- showBrandToast(context, errorMsg);
- }
+ /// Carry the generated seed to the password step, which sets the password and
+ /// writes the wallet.
+ void _continue() {
+ final generated = _generated;
+ if (generated == null) return;
+ Navigator.pushNamed(
+ context,
+ '/create_wallet_password',
+ arguments: CreateWalletPasswordArgs(
+ commit: (ctx) =>
+ commitGeneratedWallet(ctx, seed: generated.seed, restoreDate: generated.restoreDate),
+ ),
+ );
}
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
+ if (isDesktop) {
+ return DesktopGenerateSeedView(
+ logo: SvgPicture.asset('assets/logo_nobg.svg', height: 52),
+ step: 5,
+ totalSteps: 6,
+ title: i18n.generateSeedTitle,
+ description: i18n.generateSeedSubtitleRevealed,
+ seedWords: _seed ?? const [],
+ birthdayLabel: i18n.generateSeedBirthdayLabel,
+ birthdayReason: i18n.generateSeedBirthdayReason,
+ birthdayValue: _generated != null
+ ? DateFormat.yMMM(
+ Localizations.localeOf(context).toString(),
+ ).format(_generated!.restoreDate)
+ : null,
+ confirmLabel: i18n.generateSeedConfirm,
+ passwordNote: i18n.onboardingSeedNotePassword,
+ revealLabel: i18n.generateSeedReveal,
+ continueText: i18n.continueText,
+ onContinue: _continue,
+ onBack: () => Navigator.pop(context),
+ );
+ }
+
return GenerateSeedView(
- stepCount: 5,
+ stepCount: 6,
stepIndex: 4,
seedWords: _seed,
birthdayCard: null,
- continueLoading: _committing,
onContinue: _continue,
labels: GenerateSeedLabels(
titleCovered: i18n.generateSeedTitleCovered,
### lib/screens/lws_details.dart
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/util/platform.dart';
import 'package:skylight_wallet/util/secure_clipboard.dart';
import 'package:skylight_wallet/util/secure_screen.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
@@ -41,29 +43,54 @@ class _LwsDetailsScreenState extends State<LwsDetailsScreen> with SecureScreenMi
final primaryAddress = appWalletOf(context, listen: true).getPrimaryAddress();
final restoreHeight = ModalRoute.of(context)!.settings.arguments as int;
- return LwsKeysView(
- largeTitle: true,
- labels: LwsKeysLabels(
+ final labels = LwsKeysLabels(
+ title: i18n.lwsDetailsTitle,
+ description: i18n.lwsDetailsDescription,
+ primaryAddressLabel: i18n.lwsDetailsPrimaryAddressLabel,
+ viewKeyLabel: i18n.lwsDetailsSecretViewKeyLabel,
+ restoreHeightLabel: i18n.lwsDetailsRestoreHeightLabel,
+ reveal: i18n.generateSeedReveal,
+ warning: i18n.lwsKeysWarning,
+ );
+
+ void goHome() => Navigator.pushNamedAndRemoveUntil(
+ context,
+ '/wallet_home',
+ (Route<dynamic> route) => false,
+ );
+
+ // Desktop: an unnumbered onboarding step — the two-pane chrome carries the
+ // title/description/warning, the content slot shows just the value cards.
+ if (isDesktop) {
+ return DesktopOnboardingScaffold(
+ showSteps: false,
+ logo: SvgPicture.asset('assets/logo_nobg.svg', height: 52),
title: i18n.lwsDetailsTitle,
description: i18n.lwsDetailsDescription,
- primaryAddressLabel: i18n.lwsDetailsPrimaryAddressLabel,
- viewKeyLabel: i18n.lwsDetailsSecretViewKeyLabel,
- restoreHeightLabel: i18n.lwsDetailsRestoreHeightLabel,
- reveal: i18n.generateSeedReveal,
- warning: i18n.lwsKeysWarning,
- ),
+ step: 0,
+ totalSteps: 0,
+ continueLabel: i18n.continueText,
+ onContinue: goHome,
+ notes: [OnboardingNote(Icons.visibility_off_outlined, i18n.lwsKeysWarning)],
+ content: LwsKeysView(
+ fieldsOnly: true,
+ labels: labels,
+ primaryAddress: primaryAddress,
+ secretViewKey: _secretViewKey,
+ restoreHeight: restoreHeight.toString(),
+ onCopy: _copy,
+ ),
+ );
+ }
+
+ return LwsKeysView(
+ largeTitle: true,
+ labels: labels,
primaryAddress: primaryAddress,
secretViewKey: _secretViewKey,
restoreHeight: restoreHeight.toString(),
onCopy: _copy,
- footer: BrandButton(
- label: i18n.continueText,
- onPressed: () => Navigator.pushNamedAndRemoveUntil(
- context,
- '/wallet_home',
- (Route<dynamic> route) => false,
- ),
- ),
+ footer: BrandButton(label: i18n.continueText, onPressed: goHome),
);
}
}
### lib/screens/lws_keys.dart
@@ -6,8 +6,23 @@ import 'package:skylight_wallet/util/secure_screen.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/widgets/ui/ui.dart';
+/// Desktop: LWS keys as a centered modal (opened from Settings); mobile keeps
+/// the full-screen route.
+Future<void> showLwsKeysSheet(BuildContext context) {
+ return showBrandSheet<void>(
+ context: context,
+ isScrollControlled: true,
+ builder: (ctx) => ConstrainedBox(
+ constraints: BoxConstraints(maxHeight: maxSheetHeight(ctx)),
+ child: const LwsKeysScreen(asModal: true),
+ ),
+ );
+}
+
class LwsKeysScreen extends StatefulWidget {
- const LwsKeysScreen({super.key});
+ final bool asModal;
+
+ const LwsKeysScreen({super.key, this.asModal = false});
@override
State<LwsKeysScreen> createState() => _LwsKeysScreenState();
@@ -60,7 +75,8 @@ class _LwsKeysScreenState extends State<LwsKeysScreen> with SecureScreenMixin {
secretViewKey: _secretViewKey,
restoreHeight: _restoreHeight.toString(),
onCopy: _copy,
- onBack: () => Navigator.pop(context),
+ asModal: widget.asModal,
+ onBack: widget.asModal ? null : () => Navigator.pop(context),
);
}
}
### lib/screens/privacy_policy.dart
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/screens/desktop/home_shell.dart';
+import 'package:skylight_wallet/util/platform.dart';
import 'package:skylight_wallet/widgets/ui/ui.dart';
class BulletList extends StatelessWidget {
@@ -59,103 +61,117 @@ class PrivacyPolicy extends StatelessWidget {
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
- return Scaffold(
- backgroundColor: BrandColors.paper,
- body: SafeArea(
- child: Center(
- child: ConstrainedBox(
- constraints: const BoxConstraints(maxWidth: 520),
+ final scroll = SingleChildScrollView(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Updated 2026-09-03', style: bodyStyle),
+ SizedBox(height: 32),
+
+ paragraph(
+ 'MAGIC Grants (“we”) respect your privacy and are committed to protecting it through our compliance with this policy.',
+ ),
+ paragraph(
+ 'This policy applies to information we collect through your direct use of the Skylight Wallet and/or Spice Wallet app (the "App"). It does not apply to information we collect by any other means. This policy is subject to change, so make sure to periodically review this policy.',
+ ),
+
+ sectionHeading('Information We Do Not Collect'),
+ paragraph(
+ 'We do not collect any usage data. The App does not send any private keys, including private spend keys, mnemonic seeds, and private view keys to MAGIC Grants. The App does not connect to any servers run by MAGIC Grants.',
+ ),
+ paragraph('You do not need to make an account with MAGIC Grants to use the app.'),
+ paragraph(
+ 'MAGIC Grants may be able to review information provided by the App Store that you are using, if you download this app from an App Store.',
+ ),
+
+ sectionHeading('Information You May Share with Third Parties'),
+ paragraph(
+ 'When using the App, you need to specify a server (or servers) to connect to. The information shared with a server will depend on the cryptocurrency network that you are configuring and connecting to, but it could include sensitive information including your cryptocurrency address(es), your private view key(s), and your transaction data. This is necessary for the core functionality of the App.',
+ ),
+ paragraph(
+ 'You can choose to only connect to the cryptocurrency networks that you choose. It is not necessary to connect to any particular cryptocurrency network to use the App.',
+ ),
+ paragraph(
+ 'MAGIC Grants does not provide default servers. You must research the servers that you want to use and any terms of service or privacy policies associated with those servers before you connect to those servers.',
+ ),
+ paragraph(
+ 'In many cases, you may choose to avoid using a third party by connecting to servers that you operate. For example, instead of connecting to a node operated by a third party, you can connect to a node operated by you.',
+ ),
+
+ sectionHeading('Other Connections to Third Parties'),
+ paragraph(
+ 'The App optionally connects to services provided by third parties for your convenience. These include:',
+ ),
+ BulletList(
+ items: [
+ 'Block explorers, to display network and transaction information.',
+ 'Nodes, to display network and transaction information.',
+ 'Price quotes, to display richer information about your transactions.',
+ ],
+ ),
+ paragraph(
+ 'Within the App, connections to these services can be configured to be routed over the Tor network, which helps improve your privacy. However, MAGIC Grants does not warrant that these connections are bug-free, or that these protections are fit for a particular purpose. Please be mindful of the third party connections that you make when using the App. The App may contain links to websites that, when clicked, open outside of Tor in your default web browser.',
+ ),
+ paragraph(
+ 'Some third party connections, such as the price quotes, can be disabled in App settings.',
+ ),
+ paragraph(
+ 'When you connect to third parties, you must abide by their terms of service and privacy policies.',
+ ),
+
+ sectionHeading('When You Contact Us'),
+ paragraph(
+ 'There is no mechanism for contacting us within the App. If you contact us through a different means, then we may learn and retain information about you, such as your email address, account identifier, and the contents of your communications.',
+ ),
+ paragraph(
+ 'Before sending any app diagnostic logs, make sure to review their contents for any potentially sensitive information. Remove this information before sending any information to us.',
+ ),
+ ],
+ ),
+ );
+
+ final inShell = ModalRoute.of(context)?.settings.arguments == true;
+ final header = isDesktop
+ ? Padding(
+ padding: const EdgeInsets.fromLTRB(16, 10, 16, 0),
child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
+ crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
- child: BrandScreenHeader(
- onBack: () => Navigator.pop(context),
- center: Text(
- i18n.welcomePrivacyLink,
- style: BrandText.appBar.copyWith(fontSize: 16),
- ),
- ),
- ),
- Expanded(
- child: SingleChildScrollView(
- padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text('Updated 2026-09-03', style: bodyStyle),
- SizedBox(height: 32),
-
- paragraph(
- 'MAGIC Grants (“we”) respect your privacy and are committed to protecting it through our compliance with this policy.',
- ),
- paragraph(
- 'This policy applies to information we collect through your direct use of the Skylight Wallet and/or Spice Wallet app (the "App"). It does not apply to information we collect by any other means. This policy is subject to change, so make sure to periodically review this policy.',
- ),
-
- sectionHeading('Information We Do Not Collect'),
- paragraph(
- 'We do not collect any usage data. The App does not send any private keys, including private spend keys, mnemonic seeds, and private view keys to MAGIC Grants. The App does not connect to any servers run by MAGIC Grants.',
- ),
- paragraph(
- 'You do not need to make an account with MAGIC Grants to use the app.',
- ),
- paragraph(
- 'MAGIC Grants may be able to review information provided by the App Store that you are using, if you download this app from an App Store.',
- ),
-
- sectionHeading('Information You May Share with Third Parties'),
- paragraph(
- 'When using the App, you need to specify a server (or servers) to connect to. The information shared with a server will depend on the cryptocurrency network that you are configuring and connecting to, but it could include sensitive information including your cryptocurrency address(es), your private view key(s), and your transaction data. This is necessary for the core functionality of the App.',
- ),
- paragraph(
- 'You can choose to only connect to the cryptocurrency networks that you choose. It is not necessary to connect to any particular cryptocurrency network to use the App.',
- ),
- paragraph(
- 'MAGIC Grants does not provide default servers. You must research the servers that you want to use and any terms of service or privacy policies associated with those servers before you connect to those servers.',
- ),
- paragraph(
- 'In many cases, you may choose to avoid using a third party by connecting to servers that you operate. For example, instead of connecting to a node operated by a third party, you can connect to a node operated by you.',
- ),
-
- sectionHeading('Other Connections to Third Parties'),
- paragraph(
- 'The App optionally connects to services provided by third parties for your convenience. These include:',
- ),
- BulletList(
- items: [
- 'Block explorers, to display network and transaction information.',
- 'Nodes, to display network and transaction information.',
- 'Price quotes, to display richer information about your transactions.',
- ],
- ),
- paragraph(
- 'Within the App, connections to these services can be configured to be routed over the Tor network, which helps improve your privacy. However, MAGIC Grants does not warrant that these connections are bug-free, or that these protections are fit for a particular purpose. Please be mindful of the third party connections that you make when using the App. The App may contain links to websites that, when clicked, open outside of Tor in your default web browser.',
- ),
- paragraph(
- 'Some third party connections, such as the price quotes, can be disabled in App settings.',
- ),
- paragraph(
- 'When you connect to third parties, you must abide by their terms of service and privacy policies.',
- ),
-
- sectionHeading('When You Contact Us'),
- paragraph(
- 'There is no mechanism for contacting us within the App. If you contact us through a different means, then we may learn and retain information about you, such as your email address, account identifier, and the contents of your communications.',
- ),
- paragraph(
- 'Before sending any app diagnostic logs, make sure to review their contents for any potentially sensitive information. Remove this information before sending any information to us.',
- ),
- ],
- ),
- ),
+ DesktopBackLink(
+ label: inShell ? i18n.navigationBarSettings : i18n.back,
+ onTap: () => Navigator.pop(context),
),
+ const SizedBox(height: 12),
+ Text(i18n.welcomePrivacyLink, style: desktopTitleStyle),
],
),
+ )
+ : Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+ child: BrandScreenHeader(
+ onBack: () => Navigator.pop(context),
+ center: Text(i18n.welcomePrivacyLink, style: BrandText.appBar.copyWith(fontSize: 16)),
+ ),
+ );
+ final body = SafeArea(
+ child: Center(
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 520),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ header,
+ Expanded(child: scroll),
+ ],
),
),
),
);
+
+ if (isDesktop && inShell) {
+ return DesktopShell(active: DesktopNav.settings, child: body);
+ }
+ return Scaffold(backgroundColor: BrandColors.paper, body: body);
}
}
### lib/screens/receive.dart
@@ -6,7 +6,10 @@ import 'package:screen_brightness/screen_brightness.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
import 'package:skylight_wallet/models/app_wallet.dart';
+import 'package:skylight_wallet/screens/desktop/home_shell.dart';
+import 'package:skylight_wallet/screens/desktop/receive_view.dart';
import 'package:skylight_wallet/util/logging.dart';
+import 'package:skylight_wallet/util/platform.dart';
import 'package:skylight_wallet/util/secure_clipboard.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/widgets/ui/ui.dart';
@@ -116,6 +119,30 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
final ready = (subSupported != null || isDemoMode) && address != null;
final warning = _warning(i18n, wallet, subSupported);
+ final qrHeading = canToggle && _showSubaddress
+ ? (sub != null ? '${i18n.receiveSubaddressTab} #${sub.index}' : i18n.receiveSubaddressTab)
+ : i18n.receiveAddressHeading('Monero');
+
+ if (isDesktop) {
+ return DesktopShell(
+ active: DesktopNav.home,
+ child: DesktopReceiveView(
+ title: i18n.receiveTitle,
+ backLabel: i18n.navigationBarWallet,
+ copyLabel: i18n.receiveCopyAddress,
+ qrHint: i18n.receiveQrHint,
+ ready: ready,
+ address: address ?? '',
+ qrHeading: ready ? qrHeading : '',
+ tabLabels: canToggle ? [i18n.receiveSubaddressTab, i18n.receivePrimaryTab] : null,
+ selectedTab: _showSubaddress ? 0 : 1,
+ onSelectTab: (index) => setState(() => _showSubaddress = index == 0),
+ warning: warning,
+ onCopy: () => _copyAddressToClipboard(address!),
+ onBack: () => Navigator.of(context).pop(),
+ ),
+ );
+ }
return ReceiveView(
labels: ReceiveLabels(
@@ -136,9 +163,7 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
selectedTab: _showSubaddress ? 0 : 1,
onSelectTab: (index) => setState(() => _showSubaddress = index == 0),
address: address ?? '',
- qrHeading: canToggle && _showSubaddress
- ? (sub != null ? '${i18n.receiveSubaddressTab} #${sub.index}' : i18n.receiveSubaddressTab)
- : i18n.receiveAddressHeading('Monero'),
+ qrHeading: qrHeading,
warning: warning,
onCopy: () => _copyAddressToClipboard(address!),
// Mobile only: the hint promises a brighter screen, and brightness is
### lib/screens/restore_wallet.dart
@@ -1,21 +1,19 @@
-import 'dart:io';
-
import 'package:bip39/bip39.dart' as bip39;
// ignore: implementation_imports — the BIP39 English wordlist for per-word checks.
import 'package:bip39/src/wordlists/english.dart' show WORDLIST;
import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import 'package:intl/intl.dart';
-import 'package:provider/provider.dart';
import 'package:polyseed/polyseed.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
-import 'package:skylight_wallet/models/fiat_rate_model.dart';
+import 'package:skylight_wallet/screens/create_wallet_password.dart';
import 'package:skylight_wallet/util/get_height_by_date.dart';
+import 'package:skylight_wallet/util/platform.dart';
import 'package:skylight_wallet/util/secure_screen.dart';
-import 'package:skylight_wallet/util/logging.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/widgets/ui/ui.dart';
-import 'package:wallet_domain/wallet_domain.dart' show parseRestoreQr;
+import 'package:wallet_domain/wallet_domain.dart' show SeedSource, parseRestoreQr;
class RestoreWalletScreen extends StatefulWidget {
const RestoreWalletScreen({super.key});
@@ -34,7 +32,6 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> with SecureSc
int _restoreHeight = 0;
bool _scanChosen = false;
bool _heightManuallySet = false; // date picked / QR height
- bool _isLoading = false;
String _seedTypeId = 'polyseed'; // the view's default (first seed type)
bool _validWord(String w) => _wordSet.contains(w);
@@ -105,63 +102,38 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> with SecureSc
});
}
- Future<void> _restore(String mnemonic, String seedTypeId) async {
- if (_isLoading) return;
-
+ /// Password-last flow: validate the seed, then carry it (with its restore
+ /// height) to the password step, which sets the password and writes the wallet.
+ void _restore(String mnemonic, String seedTypeId) {
final i18n = AppLocalizations.of(context)!;
+ // Reject an unrecognized seed before advancing to the password step.
+ if (SeedSource.detect(mnemonic) == null) {
+ showBrandToast(context, i18n.restoreWalletInvalidMnemonic);
+ return;
+ }
+
// Polyseed with no user-chosen restore point: derive height from its birthday.
if (seedTypeId == 'polyseed' && !_heightManuallySet) {
_applyPolyseedHeight(mnemonic);
}
final restoreHeight = _restoreHeight;
- setState(() => _isLoading = true);
-
- try {
- await restoreWallet(context, mnemonic: mnemonic, restoreHeight: restoreHeight);
- } on Exception catch (error) {
- final errorMsg = error.toString().replaceFirst('Exception: ', '');
- setState(() => _isLoading = false);
-
- if (mounted) {
- final message = errorMsg == 'Invalid mnemonic.'
- ? i18n.restoreWalletInvalidMnemonic
- : i18n.unknownError;
- showBrandToast(context, message);
- }
- return;
- } catch (error) {
- log(LogLevel.error, error.toString());
- setState(() => _isLoading = false);
- if (mounted) {
- showBrandToast(context, i18n.unknownError);
- }
- return;
- }
-
- setState(() => _isLoading = false);
-
- if (mounted) {
- Provider.of<FiatRateModel>(context, listen: false).startService();
- // Wallet Details is LWS whitelisting info; a full node needs none of it.
- if (appWalletOf(context).isNodeMode) {
- Navigator.pushNamedAndRemoveUntil(context, '/wallet_home', (Route<dynamic> route) => false);
- } else {
- Navigator.pushNamedAndRemoveUntil(
- context,
- '/lws_details',
- (Route<dynamic> route) => false,
- arguments: restoreHeight,
- );
- }
- }
+ Navigator.pushNamed(
+ context,
+ '/create_wallet_password',
+ arguments: CreateWalletPasswordArgs(
+ commit: (ctx) async {
+ await restoreWallet(ctx, mnemonic: mnemonic, restoreHeight: restoreHeight);
+ return restoreHeight;
+ },
+ ),
+ );
}
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
- final isMobile = Platform.isAndroid || Platform.isIOS;
final String scanValue;
final TextStyle scanStyle;
@@ -176,9 +148,11 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> with SecureSc
scanStyle = BrandText.body;
}
- return RestoreWalletView(
+ final view = RestoreWalletView(
controller: _restoreWalletController,
- restoring: _isLoading,
+ embedded: isDesktop,
+ stepCount: isDesktop ? null : 6,
+ stepIndex: isDesktop ? null : 4,
onScan: isMobile ? _scanQrCode : null,
onSeedTypeChanged: (id) => setState(() {
_seedTypeId = id;
@@ -222,5 +196,25 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> with SecureSc
onTap: _openScanFrom,
),
);
+
+ if (!isDesktop) return view;
+
+ // Desktop: the shared onboarding shell supplies the header, step dots and
+ // Continue; the controller drives its enabled state and triggers restore.
+ return ValueListenableBuilder<bool>(
+ valueListenable: _restoreWalletController.canRestore,
+ builder: (context, valid, _) => DesktopOnboardingScaffold(
+ logo: SvgPicture.asset('assets/logo_nobg.svg', height: 52),
+ title: i18n.restoreWalletTitle,
+ description: i18n.restoreWalletDescription,
+ step: 5,
+ totalSteps: 6,
+ continueLabel: i18n.restoreWalletRestoreButton,
+ continueEnabled: valid,
+ onBack: () => Navigator.pop(context),
+ onContinue: _restoreWalletController.restore,
+ content: view,
+ ),
+ );
}
}
### lib/screens/secret_keys.dart
@@ -6,8 +6,23 @@ import 'package:skylight_wallet/util/secure_screen.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/widgets/ui/ui.dart';
+/// Desktop: secret keys as a centered modal (opened from Settings); mobile keeps
+/// the full-screen route.
+Future<void> showSecretKeysSheet(BuildContext context) {
+ return showBrandSheet<void>(
+ context: context,
+ isScrollControlled: true,
+ builder: (ctx) => ConstrainedBox(
+ constraints: BoxConstraints(maxHeight: maxSheetHeight(ctx)),
+ child: const SecretKeysScreen(asModal: true),
+ ),
+ );
+}
+
class SecretKeysScreen extends StatefulWidget {
- const SecretKeysScreen({super.key});
+ final bool asModal;
+
+ const SecretKeysScreen({super.key, this.asModal = false});
@override
State<SecretKeysScreen> createState() => _SecretKeysScreenState();
@@ -67,6 +82,11 @@ class _SecretKeysScreenState extends State<SecretKeysScreen> with SecureScreenMi
final data = _data;
if (data == null) {
+ if (widget.asModal) {
+ // Bounded height so the modal stays compact while loading, then grows to
+ // fit the keys once they're read (an unbounded Center balloons the card).
+ return const SizedBox(height: 160, child: Center(child: CircularProgressIndicator()));
+ }
return Scaffold(
backgroundColor: BrandColors.paper,
body: const SafeArea(child: Center(child: CircularProgressIndicator())),
@@ -81,7 +101,8 @@ class _SecretKeysScreenState extends State<SecretKeysScreen> with SecureScreenMi
warning: i18n.secretKeysWarning,
revealLabel: i18n.generateSeedReveal,
onCopy: _copy,
- onBack: () => Navigator.pop(context),
+ asModal: widget.asModal,
+ onBack: widget.asModal ? null : () => Navigator.pop(context),
fields: [
if (data.bip39 != null)
KeyRevealField(
### lib/screens/send.dart
@@ -10,8 +10,10 @@ import 'package:skylight_wallet/models/app_wallet.dart';
import 'package:skylight_wallet/models/contact_model.dart';
import 'package:skylight_wallet/models/fiat_rate_model.dart';
import 'package:skylight_wallet/models/wallet_types.dart';
+import 'package:skylight_wallet/screens/desktop/home_shell.dart';
import 'package:skylight_wallet/util/formatting.dart';
import 'package:skylight_wallet/util/logging.dart';
+import 'package:skylight_wallet/util/platform.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/widgets/ui/ui.dart';
@@ -648,6 +650,13 @@ class _SendScreenState extends State<SendScreen> {
final available = wallet.unlockedBalance ?? 0;
final xmr = xmrWallet(context);
+ if (isDesktop) {
+ return DesktopShell(
+ active: DesktopNav.home,
+ child: _desktopBody(context, i18n, fiatSymbol, coinRate, amountFiat, available),
+ );
+ }
+
return SendView(
labels: SendLabels(
title: i18n.sendTitle,
@@ -695,6 +704,347 @@ class _SendScreenState extends State<SendScreen> {
);
}
+ // ----- Desktop two-column form (Monero-only: no from-card / asset picker) -----
+
+ Widget _desktopBody(
+ BuildContext context,
+ AppLocalizations i18n,
+ String fiatSymbol,
+ double? coinRate,
+ double amountFiat,
+ double available,
+ ) {
+ final canSend = _formValid && _openAliasResolving == 0 && !_isLoading;
+ return ListView(
+ padding: const EdgeInsets.fromLTRB(44, 30, 44, 36),
+ children: [
+ DesktopBackLink(label: i18n.navigationBarWallet, onTap: () => Navigator.pop(context)),
+ const SizedBox(height: 16),
+ Text(i18n.sendTitle, style: desktopTitleStyle),
+ const SizedBox(height: 24),
+ _sendForm(context, i18n, fiatSymbol, coinRate, amountFiat, available, canSend),
+ ],
+ );
+ }
+
+ Widget _sendForm(
+ BuildContext context,
+ AppLocalizations i18n,
+ String fiatSymbol,
+ double? coinRate,
+ double amountFiat,
+ double available,
+ bool canSend,
+ ) {
+ final formColumn = Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ _dField(i18n.sendToLabel, _dToCard(i18n)),
+ if (_destinationAddressError.isNotEmpty) _dError(_destinationAddressError),
+ const SizedBox(height: 16),
+ _dField(i18n.amount, _dAmountCard(i18n, fiatSymbol, amountFiat, available)),
+ if (_amountError.isNotEmpty) _dError(_amountError),
+ ],
+ );
+ final sideColumn = Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ _dField(
+ i18n.sendPriorityHeading,
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ BrandSegmented(
+ labels: [i18n.sendPriorityLow, i18n.sendPriorityNormal, i18n.sendPriorityHigh],
+ selectedIndex: _selectedPriority,
+ onSelect: _setPriority,
+ ),
+ const SizedBox(height: 12),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ i18n.sendNetworkFee,
+ style: TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 12.5,
+ color: BrandColors.inkMuted,
+ ),
+ ),
+ _feeValue(fiatSymbol, coinRate),
+ ],
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 18),
+ BrandButton(label: i18n.sendSendButton, loading: _isLoading, onPressed: canSend ? _send : null),
+ const SizedBox(height: 9),
+ BrandButton.ghost(label: i18n.cancel, onPressed: () => Navigator.pop(context)),
+ const SizedBox(height: 16),
+ Text(
+ i18n.sendIrreversibleNote,
+ style: TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 12,
+ height: 1.6,
+ color: BrandColors.inkMuted,
+ ),
+ ),
+ ],
+ );
+
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ // Two columns only when the form + fixed side column both fit; below
+ // that, stack them into a single column.
+ const twoColMinWidth = 640.0;
+ if (constraints.maxWidth < twoColMinWidth) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [formColumn, const SizedBox(height: 24), sideColumn],
+ );
+ }
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Expanded(child: formColumn),
+ const SizedBox(width: 28),
+ SizedBox(width: 316, child: sideColumn),
+ ],
+ );
+ },
+ );
+ }
+
+ Widget _dField(String caption, Widget child) => Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Align(
+ alignment: Alignment.centerLeft,
+ child: Text(caption.toUpperCase(), style: BrandText.section),
+ ),
+ const SizedBox(height: 8),
+ child,
+ ],
+ );
+
+ Widget _dError(String text) => Padding(
+ padding: const EdgeInsets.only(top: 8, left: 2),
+ child: Text(text, style: BrandText.caption.copyWith(color: BrandColors.error)),
+ );
+
+ Widget _dCard({required Widget child}) => Container(
+ decoration: BoxDecoration(
+ color: BrandColors.card,
+ border: Border.all(color: BrandColors.border),
+ borderRadius: BorderRadius.circular(16),
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
+ child: child,
+ );
+
+ Widget _dToCard(AppLocalizations i18n) {
+ final contact = _selectedContact;
+ return _dCard(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ if (contact != null)
+ Row(
+ children: [
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ contact.name,
+ style: TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
+ color: BrandColors.ink,
+ ),
+ ),
+ const SizedBox(height: 2),
+ Text(
+ _shortenMiddle(_destinationAddressController.text, head: 10, tail: 12),
+ style: TextStyle(
+ fontFamily: 'Ubuntu Mono',
+ fontSize: 12,
+ color: BrandColors.inkMuted,
+ ),
+ ),
+ ],
+ ),
+ ),
+ IconButton(
+ icon: Icon(Icons.close, size: 18, color: BrandColors.inkMuted),
+ onPressed: _clearSelectedContact,
+ ),
+ ],
+ )
+ else
+ TextField(
+ controller: _destinationAddressController,
+ minLines: 1,
+ maxLines: 2,
+ style: TextStyle(
+ fontFamily: 'Ubuntu Mono',
+ fontSize: 13.5,
+ height: 1.6,
+ color: BrandColors.ink,
+ ),
+ decoration: InputDecoration.collapsed(
+ hintText: i18n.address,
+ hintStyle: TextStyle(
+ fontFamily: 'Ubuntu Mono',
+ fontSize: 13.5,
+ height: 1.6,
+ color: BrandColors.inkFaint,
+ ),
+ ),
+ ),
+ const SizedBox(height: 14),
+ Row(
+ children: [
+ _dPill(Icons.content_paste, i18n.sendPasteButton, _pasteAddressFromClipboard),
+ const SizedBox(width: 8),
+ _dPill(Icons.people_outline, i18n.sendContactsButton, _showContactPicker),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _dAmountCard(
+ AppLocalizations i18n,
+ String fiatSymbol,
+ double amountFiat,
+ double available,
+ ) {
+ return _dCard(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Expanded(
+ child: TextField(
+ controller: _amountController,
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
+ style: TextStyle(
+ fontFamily: 'Ubuntu Mono',
+ fontSize: 28,
+ height: 1,
+ fontWeight: FontWeight.w700,
+ color: BrandColors.ink,
+ ),
+ decoration: InputDecoration.collapsed(
+ hintText: '0',
+ hintStyle: TextStyle(
+ fontFamily: 'Ubuntu Mono',
+ fontSize: 28,
+ height: 1,
+ fontWeight: FontWeight.w700,
+ color: BrandColors.inkDisabled,
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: 10),
+ Text(
+ 'XMR',
+ style: TextStyle(
+ fontFamily: 'Ubuntu Mono',
+ fontSize: 14,
+ fontWeight: FontWeight.w700,
+ color: BrandColors.ink,
+ ),
+ ),
+ const SizedBox(width: 12),
+ Material(
+ color: BrandColors.surfaceTinted,
+ borderRadius: BorderRadius.circular(8),
+ child: InkWell(
+ mouseCursor: WidgetStateMouseCursor.clickable,
+ onTap: _setBalanceAsSendAmount,
+ borderRadius: BorderRadius.circular(8),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 11),
+ child: Text(
+ i18n.sendMaxButton,
+ style: TextStyle(
+ fontFamily: 'Ubuntu Mono',
+ fontSize: 10.5,
+ height: 1,
+ fontWeight: FontWeight.w700,
+ color: BrandColors.primaryDeep,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 13),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ '≈ ${formatFiat(amountFiat, fiatSymbol)}',
+ style: TextStyle(
+ fontFamily: 'Ubuntu Mono',
+ fontSize: 12.5,
+ color: BrandColors.inkMuted,
+ ),
+ ),
+ Text(
+ '${_amountText(available)} ${i18n.sendAvailableSuffix}',
+ style: TextStyle(
+ fontFamily: 'Ubuntu Mono',
+ fontSize: 12.5,
+ color: BrandColors.inkMuted,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _dPill(IconData icon, String label, VoidCallback onTap) => Material(
+ color: BrandColors.surfaceSunken,
+ borderRadius: BorderRadius.circular(11),
+ child: InkWell(
+ mouseCursor: WidgetStateMouseCursor.clickable,
+ onTap: onTap,
+ borderRadius: BorderRadius.circular(11),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 11),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(icon, size: 16, color: BrandColors.primaryDeep),
+ const SizedBox(width: 7),
+ Text(
+ label,
+ style: TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 12.5,
+ height: 1,
+ fontWeight: FontWeight.w500,
+ color: BrandColors.primaryDeep,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+
Widget _feeValue(String fiatSymbol, double? coinRate) {
final feePiconero = (_fees != null && _fees!.length > _selectedPriority)
? _fees![_selectedPriority]
### lib/screens/settings.dart
@@ -12,7 +12,10 @@ import 'package:skylight_wallet/widgets/fiat_api_settings_form.dart';
import 'package:skylight_wallet/widgets/tor_settings_form.dart';
import 'package:skylight_wallet/models/language_model.dart';
import 'package:skylight_wallet/models/theme_model.dart';
+import 'package:skylight_wallet/screens/lws_keys.dart';
+import 'package:skylight_wallet/screens/secret_keys.dart';
import 'package:skylight_wallet/util/logging.dart';
+import 'package:skylight_wallet/util/platform.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/periodic_tasks.dart';
import 'package:skylight_wallet/services/notifications_service.dart';
@@ -23,8 +26,25 @@ import 'package:skylight_wallet/widgets/ui/ui.dart';
import 'package:skylight_wallet/widgets/wallet_navigation_bar.dart';
import 'package:wallet_infra/wallet_infra.dart' show BiometricAuth, BiometricAuthResult;
+/// Desktop: settings as a wide centered modal (opened from the sidebar); mobile
+/// keeps the full-screen tab.
+Future<void> showSettingsSheet(BuildContext context) {
+ return showBrandSheet<void>(
+ context: context,
+ isScrollControlled: true,
+ maxWidth: 720,
+ builder: (ctx) => ConstrainedBox(
+ constraints: const BoxConstraints(maxHeight: 640),
+ child: const SettingsScreen(asModal: true),
+ ),
+ );
+}
+
class SettingsScreen extends StatefulWidget {
- const SettingsScreen({super.key});
+ /// Content-only render for the desktop settings modal (2-column, no shell).
+ final bool asModal;
+
+ const SettingsScreen({super.key, this.asModal = false});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
@@ -183,9 +203,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
- void _showViewLwsKeysDialog() {
+ Future<void> _showViewLwsKeysDialog() async {
final i18n = AppLocalizations.of(context)!;
- showConfirmSheet(
+ final ok = await showConfirmSheet(
context: context,
icon: Icons.warning_amber_rounded,
iconBg: BrandColors.warningBg,
@@ -195,8 +215,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
confirmLabel: i18n.settingsViewLwsKeysDialogRevealButton,
cancelLabel: i18n.cancel,
confirmColor: BrandColors.warning,
- onConfirm: () => Navigator.pushNamed(context, '/lws_keys'),
);
+ if (ok != true || !mounted) return;
+ if (isDesktop) {
+ await showLwsKeysSheet(context);
+ } else {
+ await Navigator.pushNamed(context, '/lws_keys');
+ }
}
/// Gate the seed behind a device auth even though the app is already
@@ -223,7 +248,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (!mounted) return;
}
- showConfirmSheet(
+ final ok = await showConfirmSheet(
context: context,
icon: Icons.warning_amber_rounded,
iconBg: BrandColors.errorBg,
@@ -232,8 +257,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
body: i18n.settingsViewSecretKeysDialogText,
confirmLabel: i18n.settingsViewSecretKeysDialogRevealButton,
cancelLabel: i18n.cancel,
- onConfirm: () => Navigator.pushNamed(context, '/secret_keys'),
);
+ if (ok != true || !mounted) return;
+ if (isDesktop) {
+ await showSecretKeysSheet(context);
+ } else {
+ await Navigator.pushNamed(context, '/secret_keys');
+ }
}
Future<void> _deleteWallet() async {
@@ -259,42 +289,46 @@ class _SettingsScreenState extends State<SettingsScreen> {
return showBrandSheet<void>(
context: context,
isScrollControlled: true,
- builder: (sheetContext) => SafeArea(
- top: false,
- child: ConstrainedBox(
- constraints: BoxConstraints(maxHeight: _sheetMaxHeight(sheetContext)),
- child: Padding(
- padding: const EdgeInsets.only(top: 10),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const SheetHandle(),
- Padding(
- padding: const EdgeInsets.fromLTRB(22, 0, 22, 16),
- child: Row(
- children: [
- SheetIcon(
- icon: icon,
- bg: BrandColors.surfaceTinted,
- color: BrandColors.primaryDeep,
- ),
- const SizedBox(width: 11),
- Expanded(child: Text(title, style: BrandText.sheetTitle)),
- ],
+ builder: (sheetContext) {
+ // Desktop modal: the card owns the edge padding and drops the drag handle.
+ final hpad = isDesktopModal ? 0.0 : 22.0;
+ return SafeArea(
+ top: false,
+ child: ConstrainedBox(
+ constraints: BoxConstraints(maxHeight: _sheetMaxHeight(sheetContext)),
+ child: Padding(
+ padding: EdgeInsets.only(top: isDesktopModal ? 0 : 10),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SheetHandle(),
+ Padding(
+ padding: EdgeInsets.fromLTRB(hpad, 0, hpad, 16),
+ child: Row(
+ children: [
+ SheetIcon(
+ icon: icon,
+ bg: BrandColors.surfaceTinted,
+ color: BrandColors.primaryDeep,
+ ),
+ const SizedBox(width: 11),
+ Expanded(child: Text(title, style: BrandText.sheetTitle)),
+ ],
+ ),
),
- ),
- Flexible(
- child: SingleChildScrollView(
- padding: const EdgeInsets.fromLTRB(22, 0, 22, 16),
- child: form,
+ Flexible(
+ child: SingleChildScrollView(
+ padding: EdgeInsets.fromLTRB(hpad, 0, hpad, isDesktopModal ? 0 : 16),
+ child: form,
+ ),
),
- ),
- ],
+ ],
+ ),
),
),
- ),
- ),
+ );
+ },
);
}
@@ -463,6 +497,132 @@ class _SettingsScreenState extends State<SettingsScreen> {
? _fiatModeLabel(i18n)
: '${_fiatModeLabel(i18n)} · ${fiatRate.fiatCode}';
+ final groups = <Widget>[
+ SettingsGroup(
+ label: i18n.settingsSectionGeneral,
+ tiles: [
+ SettingsNavTile(
+ title: i18n.settingsThemeLabel,
+ value: _themeLabel(i18n, theme.theme),
+ onTap: _showThemePicker,
+ ),
+ SettingsNavTile(
+ title: i18n.settingsLanguageLabel,
+ value: languageNames[language.language]?.$1 ?? language.language.toUpperCase(),
+ onTap: () => showLanguageSheet(context),
+ ),
+ if (isMobile)
+ SettingsToggleTile(
+ title: i18n.settingsAppLockLabel,
+ value: _appLockEnabled,
+ onChanged: _setAppLockEnabled,
+ animate: _animateToggles,
+ ),
+ SettingsLinkTile(
+ title: i18n.settingsConnectionSettingsLabel,
+ subtitle: connectionSubtitle,
+ linkLabel: i18n.settingsLwsViewKeysButton,
+ onTap: _showConnectionSettings,
+ ),
+ SettingsLinkTile(
+ title: i18n.settingsTorSettingsLabel,
+ subtitle: _torModeLabel(i18n),
+ linkLabel: i18n.settingsLwsViewKeysButton,
+ onTap: _showTorSettings,
+ ),
+ SettingsLinkTile(
+ title: i18n.settingsFiatApiSettingsLabel,
+ subtitle: fiatSubtitle,
+ linkLabel: i18n.settingsLwsViewKeysButton,
+ onTap: _showFiatApiSettings,
+ ),
+ ],
+ ),
+ SettingsGroup(
+ label: i18n.settingsSectionBehaviour,
+ tiles: [
+ if (showNotifyToggle)
+ SettingsToggleTile(
+ title: i18n.settingsNotifyNewTxsLabel,
+ description: Platform.isIOS
+ ? i18n.settingsNotifyNewTxsDescriptionIos
+ : i18n.settingsNotifyNewTxsDescription,
+ value: _newTxNotificationsEnabled,
+ onChanged: _setTxNotificationsEnabled,
+ animate: _animateToggles,
+ ),
+ SettingsToggleTile(
+ title: i18n.settingsVerboseLoggingLabel,
+ description: Platform.isIOS
+ ? i18n.settingsVerboseLoggingDescriptionIos
+ : i18n.settingsVerboseLoggingDescription,
+ value: _verboseLoggingEnabled,
+ onChanged: _setVerboseLoggingEnabled,
+ animate: _animateToggles,
+ ),
+ // Only meaningful with logs to export, so hide it when
+ // verbose logging is off rather than disabling it.
+ if (Platform.isIOS && _verboseLoggingEnabled)
+ SettingsLinkTile(
+ title: i18n.settingsExportLogsLabel,
+ linkLabel: i18n.settingsExportLogsButton,
+ onTap: _exportLogs,
+ ),
+ ],
+ ),
+ SettingsGroup(
+ label: i18n.settingsSectionWallet,
+ tiles: [
+ SettingsLinkTile(
+ title: i18n.settingsLwsViewKeysLabel,
+ titleColor: BrandColors.warning,
+ onTap: _showViewLwsKeysDialog,
+ ),
+ SettingsLinkTile(
+ title: i18n.settingsSecretKeysLabel,
+ titleColor: BrandColors.error,
+ onTap: _showViewSecretKeysDialog,
+ ),
+ SettingsLinkTile(
+ title: i18n.settingsDeleteWalletButton,
+ titleColor: BrandColors.error,
+ onTap: _showDeleteWalletDialog,
+ ),
+ ],
+ ),
+ SettingsGroup(
+ label: i18n.settingsSectionAbout,
+ tiles: [
+ SettingsNavTile(
+ title: i18n.welcomeTermsLink,
+ onTap: () => Navigator.pushNamed(context, '/terms_of_service', arguments: true),
+ ),
+ SettingsNavTile(
+ title: i18n.welcomePrivacyLink,
+ onTap: () => Navigator.pushNamed(context, '/privacy_policy', arguments: true),
+ ),
+ ],
+ ),
+ ];
+
+ final versionFooter = Center(
+ child: Text(
+ 'Skylight Wallet v$_appVersion (build $_buildNumber)',
+ style: BrandText.caption.copyWith(color: BrandColors.inkFaint),
+ ),
+ );
+
+ if (widget.asModal) return _modalBody(i18n, groups, versionFooter);
+
+ final tiles = <Widget>[
+ for (var i = 0; i < groups.length; i++) ...[
+ if (i > 0) const SizedBox(height: 18),
+ groups[i],
+ ],
+ const SizedBox(height: 16),
+ versionFooter,
+ ];
+
return Scaffold(
backgroundColor: BrandColors.paper,
bottomNavigationBar: const WalletNavigationBar(selectedIndex: 2),
@@ -486,128 +646,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- SettingsGroup(
- label: i18n.settingsSectionGeneral,
- tiles: [
- SettingsNavTile(
- title: i18n.settingsThemeLabel,
- value: _themeLabel(i18n, theme.theme),
- onTap: _showThemePicker,
- ),
- SettingsNavTile(
- title: i18n.settingsLanguageLabel,
- value:
- languageNames[language.language]?.$1 ??
- language.language.toUpperCase(),
- onTap: () => showLanguageSheet(context),
- ),
- if (isMobile)
- SettingsToggleTile(
- title: i18n.settingsAppLockLabel,
- value: _appLockEnabled,
- onChanged: _setAppLockEnabled,
- animate: _animateToggles,
- ),
- SettingsLinkTile(
- title: i18n.settingsConnectionSettingsLabel,
- subtitle: connectionSubtitle,
- linkLabel: i18n.settingsLwsViewKeysButton,
- onTap: _showConnectionSettings,
- ),
- SettingsLinkTile(
- title: i18n.settingsTorSettingsLabel,
- subtitle: _torModeLabel(i18n),
- linkLabel: i18n.settingsLwsViewKeysButton,
- onTap: _showTorSettings,
- ),
- SettingsLinkTile(
- title: i18n.settingsFiatApiSettingsLabel,
- subtitle: fiatSubtitle,
- linkLabel: i18n.settingsLwsViewKeysButton,
- onTap: _showFiatApiSettings,
- ),
- ],
- ),
- const SizedBox(height: 18),
- SettingsGroup(
- label: i18n.settingsSectionBehaviour,
- tiles: [
- if (showNotifyToggle)
- SettingsToggleTile(
- title: i18n.settingsNotifyNewTxsLabel,
- description: Platform.isIOS
- ? i18n.settingsNotifyNewTxsDescriptionIos
- : i18n.settingsNotifyNewTxsDescription,
- value: _newTxNotificationsEnabled,
- onChanged: _setTxNotificationsEnabled,
- animate: _animateToggles,
- ),
- SettingsToggleTile(
- title: i18n.settingsVerboseLoggingLabel,
- description: Platform.isIOS
- ? i18n.settingsVerboseLoggingDescriptionIos
- : i18n.settingsVerboseLoggingDescription,
- value: _verboseLoggingEnabled,
- onChanged: _setVerboseLoggingEnabled,
- animate: _animateToggles,
- ),
- // Only meaningful with logs to export, so hide it when
- // verbose logging is off rather than disabling it.
- if (Platform.isIOS && _verboseLoggingEnabled)
- SettingsLinkTile(
- title: i18n.settingsExportLogsLabel,
- linkLabel: i18n.settingsExportLogsButton,
- onTap: _exportLogs,
- ),
- ],
- ),
- const SizedBox(height: 18),
- SettingsGroup(
- label: i18n.settingsSectionWallet,
- tiles: [
- SettingsLinkTile(
- title: i18n.settingsLwsViewKeysLabel,
- titleColor: BrandColors.warning,
- onTap: _showViewLwsKeysDialog,
- ),
- SettingsLinkTile(
- title: i18n.settingsSecretKeysLabel,
- titleColor: BrandColors.error,
- onTap: _showViewSecretKeysDialog,
- ),
- SettingsLinkTile(
- title: i18n.settingsDeleteWalletButton,
- titleColor: BrandColors.error,
- onTap: _showDeleteWalletDialog,
- ),
- ],
- ),
- const SizedBox(height: 18),
- SettingsGroup(
- label: i18n.settingsSectionAbout,
- tiles: [
- SettingsNavTile(
- title: i18n.welcomeTermsLink,
- onTap: () => Navigator.pushNamed(context, '/terms_of_service'),
- ),
- SettingsNavTile(
- title: i18n.welcomePrivacyLink,
- onTap: () => Navigator.pushNamed(context, '/privacy_policy'),
- ),
- ],
- ),
- const SizedBox(height: 16),
- Center(
- child: Text(
- 'Skylight Wallet v$_appVersion (build $_buildNumber)',
- style: BrandText.caption.copyWith(color: BrandColors.inkFaint),
- ),
- ),
- ],
- ),
+ child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: tiles),
),
),
],
@@ -617,4 +656,76 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
);
}
+
+ /// The desktop settings modal: a header + a two-column body of the groups that
+ /// wraps to one column when the modal is narrow (design: 720px, 2 columns).
+ Widget _modalBody(AppLocalizations i18n, List<Widget> groups, Widget versionFooter) {
+ final hpad = isDesktopModal ? 0.0 : 20.0;
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Padding(
+ padding: EdgeInsets.fromLTRB(hpad, 2, isDesktopModal ? 34 : hpad, 18),
+ child: Row(
+ children: [
+ Container(
+ width: 38,
+ height: 38,
+ alignment: Alignment.center,
+ decoration: BoxDecoration(
+ color: BrandColors.surfaceTinted,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Icon(Icons.tune, size: 19, color: BrandColors.primary),
+ ),
+ const SizedBox(width: 13),
+ Text(
+ i18n.settingsTitle,
+ style: TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 21,
+ height: 1.25,
+ fontWeight: FontWeight.w700,
+ color: BrandColors.ink,
+ ),
+ ),
+ ],
+ ),
+ ),
+ Flexible(
+ child: SingleChildScrollView(
+ padding: EdgeInsets.fromLTRB(hpad, 0, hpad, 2),
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ const gap = 20.0;
+ Widget stack(List<Widget> gs, {Widget? tail}) => Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ for (var i = 0; i < gs.length; i++) ...[
+ if (i > 0) const SizedBox(height: gap),
+ gs[i],
+ ],
+ if (tail != null) ...[const SizedBox(height: gap), tail],
+ ],
+ );
+ if (constraints.maxWidth >= 520) {
+ final half = (groups.length / 2).ceil();
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Expanded(child: stack(groups.sublist(0, half))),
+ const SizedBox(width: 22),
+ Expanded(child: stack(groups.sublist(half), tail: versionFooter)),
+ ],
+ );
+ }
+ return stack(groups, tail: versionFooter);
+ },
+ ),
+ ),
+ ),
+ ],
+ );
+ }
}
### lib/screens/terms_of_service.dart
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/screens/desktop/home_shell.dart';
+import 'package:skylight_wallet/util/platform.dart';
import 'package:skylight_wallet/widgets/ui/ui.dart';
class TermsOfService extends StatelessWidget {
@@ -27,152 +29,168 @@ class TermsOfService extends StatelessWidget {
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
- return Scaffold(
- backgroundColor: BrandColors.paper,
- body: SafeArea(
- child: Center(
- child: ConstrainedBox(
- constraints: const BoxConstraints(maxWidth: 520),
+ final scroll = SingleChildScrollView(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Updated 2026-09-03', style: bodyStyle),
+ SizedBox(height: 12),
+
+ sectionHeading('Acceptance'),
+ paragraph(
+ 'These Terms of Service (ToS) are entered into by and between You and MAGIC Grants, a Colorado nonprofit corporation (“we” or “us”). These terms govern your access to and use of Skylight Wallet and Spice Wallet (the “App”).',
+ ),
+ paragraph(
+ 'Please read the Terms of Service carefully before you start to use the App. By using the App you accept and agree to be bound and abide by these Terms of Service and our Privacy Policy. If you do not agree to these Terms of Service or the Privacy Policy, then you must not access or use the App.',
+ ),
+
+ sectionHeading('Updates to the Terms of Service'),
+ paragraph(
+ 'We may revise and update these Terms of Service from time to time in our sole discretion. All changes are effective immediately when we post them, and apply to all access to and use of the App thereafter.',
+ ),
+ paragraph(
+ 'Your continued use of the App following the posting of revised Terms of Service means that you accept and agree to the changes. You are expected to check this page from time to time so you are aware of any changes, as they are binding on you.',
+ ),
+
+ sectionHeading('All MIT License Terms Apply'),
+ paragraph(
+ 'The App is released under the MIT open source license permission notice, incorporated below:',
+ ),
+
+ Container(
+ width: double.infinity,
+ margin: EdgeInsets.only(bottom: 10.0),
+ decoration: BoxDecoration(borderRadius: BorderRadius.circular(6.0)),
+ child: Text(
+ 'THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.',
+ style: monospaceStyle,
+ ),
+ ),
+
+ sectionHeading('No Financial or Legal Advice'),
+ paragraph(
+ 'MAGIC Grants does not provide any financial or legal advice in connection with the App. You are responsible for all of your actions when using the App.',
+ ),
+
+ sectionHeading('Security and Network Risks'),
+ paragraph(
+ 'You are fully responsible for the security of your funds when using the App. You agree that MAGIC Grants is not liable for your loss of funds, including due to any bugs that are present in the application. You agree that MAGIC Grants has not warranted that the App is secure for any particular purpose, including your desired purpose. You acknowledge that MAGIC Grants has not claimed that the App is bug-free.',
+ ),
+ paragraph(
+ 'You acknowledge that MAGIC Grants is not responsible for any loss of funds or other harms that could arise from related networks and software, including but not limited to the cryptocurrency networks that you connect to. These networks may have bugs (including severe ones which could lead to loss of funds or dilution of value), and infrastructure that you connect to can be malicious. Cryptocurrency networks may charge you fees, and you can lose value through value fluctuations and volatility, impermanent loss, slippage, and other market factors which may be very complex. You agree that MAGIC Grants is not liable for any of these issues. By interacting with these cryptocurrency networks, you agree that you are accepting all of the risks of these networks and accepting full responsibility for understanding them.',
+ ),
+ paragraph(
+ 'You acknowledge that MAGIC Grants offers no warranty of any kind, without limitation, for performance or security of the App or these cryptocurrency networks, even cryptocurrency networks that MAGIC Grants has commissioned security reviews for and cryptocurrency networks that MAGIC Grants runs our own nodes for.',
+ ),
+
+ sectionHeading('No Custody by MAGIC Grants'),
+ paragraph(
+ 'MAGIC Grants does not have custody of your funds while you are using the App. The App is software that intends to allow you to manage your self-custody wallets. MAGIC Grants does not transmit funds for you. MAGIC Grants is not an exchange or money services business (MSB).',
+ ),
+ paragraph(
+ 'If you use any feature in the App named swap, trade, exchange, add liquidity, remove liquidity, bridge, or similar, you acknowledge that MAGIC Grants is not involved with nor performing these activities/actions whatsoever. The App may include features that you may use to interact with decentralized exchanges; however, MAGIC Grants is not a party nor counterparty to these transactions. The App is solely software that allows you to connect to relevant cryptocurrency networks through mechanisms that you specify, and you have full, independent control over how you decide to connect.',
+ ),
+ paragraph(
+ 'MAGIC Grants may run our own nodes for various cryptocurrency networks, but MAGIC Grants does not include our nodes or any default nodes in the App. Even in cases where you create transactions in the App that are processed or reviewed in any way by MAGIC Grants nodes (which is a standard function of nodes on a cryptocurrency network), you agree that MAGIC Grants is not a party to your transaction, this action does not create any binding liability for MAGIC Grants, and MAGIC Grants is not responsible for performing any service to you.',
+ ),
+ paragraph(
+ 'Cryptocurrencies exist only by virtue of the ownership record maintained in their respective networks. Any transfer of title in cryptocurrency occurs within a decentralized cryptocurrency network.',
+ ),
+
+ sectionHeading('Restricted Use'),
+ paragraph(
+ 'You may not use the App if it is prohibited in your jurisdiction. You may not use the App in connection with an illegal purpose. You are responsible for compliance with local laws and regulations.',
+ ),
+
+ sectionHeading('Third Party Services'),
+ paragraph(
+ 'The App includes links to other sites and resources provided by third parties, and the App incorporates information provided by third parties for convenience. MAGIC Grants does not guarantee that these sites, resources, and information are accurate or fit for Your desired purpose. We have no control over the contents of those sites or resources, and you agree that we have no responsibility for them or for any loss or damage that may arise from your use of them. If you decide to access any of the third-party services linked in this App, you do so entirely at your own risk and subject to the terms and conditions of use for such Apps.',
+ ),
+
+ sectionHeading('Tax Matters'),
+ paragraph(
+ 'The users of the App are solely responsible in determining what, if any, taxes apply to their cryptocurrency transactions. We are not responsible for determining any taxes that apply to such transactions.',
+ ),
+ paragraph(
+ 'If you donate to MAGIC Grants, a 501(c)(3) public charity, your donation may qualify for a tax deduction. Additional paperwork, especially for cryptocurrency donations, may be required. If you choose to donate to MAGIC Grants as a part of any other activity in the App, such as any transaction you make that includes a donation to MAGIC Grants, you acknowledge that the sole involvement by MAGIC Grants for such transactions is to receive your donation, and that MAGIC Grants is not otherwise party to nor responsible for your transaction in any other way.',
+ ),
+
+ sectionHeading('Indemnification'),
+ paragraph(
+ "You agree to defend, indemnify, and hold harmless MAGIC Grants, its affiliates, licensors, and service providers, and its and their respective officers, directors, employees, contractors, agents, licensors, suppliers, successors, and assigns from and against any claims, liabilities, damages, judgments, awards, losses, costs, expenses, or fees (including reasonable attorneys' fees) arising out of or relating to your violation of these Terms of Service or your use of the App, including, but not limited to, any use of the App's content, services, and products other than as expressly authorized in these Terms of Service, or your use of any information obtained from the App.",
+ ),
+
+ sectionHeading('Governing Law and Jurisdiction'),
+ paragraph(
+ 'All matters relating to the App and these Terms of Service, and any dispute or claim arising therefrom or related thereto (in each case, including non-contractual disputes or claims), shall be governed by and construed in accordance with the internal laws of the State of Colorado without giving effect to any choice or conflict of law provision or rule (whether of the State of Colorado or any other jurisdiction).',
+ ),
+ paragraph(
+ 'Any legal suit, action, or proceeding arising out of, or related to, these Terms of Service or the App shall be instituted exclusively in the federal courts of the United States or the courts of the State of Colorado, although we retain the right to bring any suit, action, or proceeding against you for breach of these Terms of Service in your country of residence or any other relevant country. You waive any and all objections to the exercise of jurisdiction over you by such courts and to venue in such courts.',
+ ),
+
+ sectionHeading('Arbitration'),
+ paragraph(
+ 'At our sole discretion, we may require You to submit any disputes arising from these Terms of Service or use of the App, including disputes arising from or concerning their interpretation, violation, invalidity, non-performance, or termination, to final and binding arbitration under the Rules of Arbitration of the American Arbitration Association applying Colorado law.',
+ ),
+
+ sectionHeading('Waiver and Severability'),
+ paragraph(
+ 'No waiver by MAGIC Grants of any term or condition set out in these Terms of Service shall be deemed a further or continuing waiver of such term or condition or a waiver of any other term or condition, and any failure of MAGIC Grants to assert a right or provision under these Terms of Service shall not constitute a waiver of such right or provision.',
+ ),
+ paragraph(
+ 'If any provision of these Terms of Service is held by a court or other tribunal of competent jurisdiction to be invalid, illegal, or unenforceable for any reason, such provision shall be eliminated or limited to the minimum extent such that the remaining provisions of the Terms of Service will continue in full force and effect.',
+ ),
+
+ sectionHeading('Entire Agreement'),
+ paragraph(
+ 'The Terms of Service and our Privacy Policy constitute the sole and entire agreement between You and MAGIC Grants regarding the App and supersede all prior and contemporaneous understandings, agreements, representations, and warranties, both written and oral, regarding the App.',
+ ),
+ ],
+ ),
+ );
+
+ final inShell = ModalRoute.of(context)?.settings.arguments == true;
+ final header = isDesktop
+ ? Padding(
+ padding: const EdgeInsets.fromLTRB(16, 10, 16, 0),
child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
+ crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
- child: BrandScreenHeader(
- onBack: () => Navigator.pop(context),
- center: Text(
- i18n.welcomeTermsLink,
- style: BrandText.appBar.copyWith(fontSize: 16),
- ),
- ),
- ),
- Expanded(
- child: SingleChildScrollView(
- padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text('Updated 2026-09-03', style: bodyStyle),
- SizedBox(height: 12),
-
- sectionHeading('Acceptance'),
- paragraph(
- 'These Terms of Service (ToS) are entered into by and between You and MAGIC Grants, a Colorado nonprofit corporation (“we” or “us”). These terms govern your access to and use of Skylight Wallet and Spice Wallet (the “App”).',
- ),
- paragraph(
- 'Please read the Terms of Service carefully before you start to use the App. By using the App you accept and agree to be bound and abide by these Terms of Service and our Privacy Policy. If you do not agree to these Terms of Service or the Privacy Policy, then you must not access or use the App.',
- ),
-
- sectionHeading('Updates to the Terms of Service'),
- paragraph(
- 'We may revise and update these Terms of Service from time to time in our sole discretion. All changes are effective immediately when we post them, and apply to all access to and use of the App thereafter.',
- ),
- paragraph(
- 'Your continued use of the App following the posting of revised Terms of Service means that you accept and agree to the changes. You are expected to check this page from time to time so you are aware of any changes, as they are binding on you.',
- ),
-
- sectionHeading('All MIT License Terms Apply'),
- paragraph(
- 'The App is released under the MIT open source license permission notice, incorporated below:',
- ),
-
- Container(
- width: double.infinity,
- margin: EdgeInsets.only(bottom: 10.0),
- decoration: BoxDecoration(borderRadius: BorderRadius.circular(6.0)),
- child: Text(
- 'THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.',
- style: monospaceStyle,
- ),
- ),
-
- sectionHeading('No Financial or Legal Advice'),
- paragraph(
- 'MAGIC Grants does not provide any financial or legal advice in connection with the App. You are responsible for all of your actions when using the App.',
- ),
-
- sectionHeading('Security and Network Risks'),
- paragraph(
- 'You are fully responsible for the security of your funds when using the App. You agree that MAGIC Grants is not liable for your loss of funds, including due to any bugs that are present in the application. You agree that MAGIC Grants has not warranted that the App is secure for any particular purpose, including your desired purpose. You acknowledge that MAGIC Grants has not claimed that the App is bug-free.',
- ),
- paragraph(
- 'You acknowledge that MAGIC Grants is not responsible for any loss of funds or other harms that could arise from related networks and software, including but not limited to the cryptocurrency networks that you connect to. These networks may have bugs (including severe ones which could lead to loss of funds or dilution of value), and infrastructure that you connect to can be malicious. Cryptocurrency networks may charge you fees, and you can lose value through value fluctuations and volatility, impermanent loss, slippage, and other market factors which may be very complex. You agree that MAGIC Grants is not liable for any of these issues. By interacting with these cryptocurrency networks, you agree that you are accepting all of the risks of these networks and accepting full responsibility for understanding them.',
- ),
- paragraph(
- 'You acknowledge that MAGIC Grants offers no warranty of any kind, without limitation, for performance or security of the App or these cryptocurrency networks, even cryptocurrency networks that MAGIC Grants has commissioned security reviews for and cryptocurrency networks that MAGIC Grants runs our own nodes for.',
- ),
-
- sectionHeading('No Custody by MAGIC Grants'),
- paragraph(
- 'MAGIC Grants does not have custody of your funds while you are using the App. The App is software that intends to allow you to manage your self-custody wallets. MAGIC Grants does not transmit funds for you. MAGIC Grants is not an exchange or money services business (MSB).',
- ),
- paragraph(
- 'If you use any feature in the App named swap, trade, exchange, add liquidity, remove liquidity, bridge, or similar, you acknowledge that MAGIC Grants is not involved with nor performing these activities/actions whatsoever. The App may include features that you may use to interact with decentralized exchanges; however, MAGIC Grants is not a party nor counterparty to these transactions. The App is solely software that allows you to connect to relevant cryptocurrency networks through mechanisms that you specify, and you have full, independent control over how you decide to connect.',
- ),
- paragraph(
- 'MAGIC Grants may run our own nodes for various cryptocurrency networks, but MAGIC Grants does not include our nodes or any default nodes in the App. Even in cases where you create transactions in the App that are processed or reviewed in any way by MAGIC Grants nodes (which is a standard function of nodes on a cryptocurrency network), you agree that MAGIC Grants is not a party to your transaction, this action does not create any binding liability for MAGIC Grants, and MAGIC Grants is not responsible for performing any service to you.',
- ),
- paragraph(
- 'Cryptocurrencies exist only by virtue of the ownership record maintained in their respective networks. Any transfer of title in cryptocurrency occurs within a decentralized cryptocurrency network.',
- ),
-
- sectionHeading('Restricted Use'),
- paragraph(
- 'You may not use the App if it is prohibited in your jurisdiction. You may not use the App in connection with an illegal purpose. You are responsible for compliance with local laws and regulations.',
- ),
-
- sectionHeading('Third Party Services'),
- paragraph(
- 'The App includes links to other sites and resources provided by third parties, and the App incorporates information provided by third parties for convenience. MAGIC Grants does not guarantee that these sites, resources, and information are accurate or fit for Your desired purpose. We have no control over the contents of those sites or resources, and you agree that we have no responsibility for them or for any loss or damage that may arise from your use of them. If you decide to access any of the third-party services linked in this App, you do so entirely at your own risk and subject to the terms and conditions of use for such Apps.',
- ),
-
- sectionHeading('Tax Matters'),
- paragraph(
- 'The users of the App are solely responsible in determining what, if any, taxes apply to their cryptocurrency transactions. We are not responsible for determining any taxes that apply to such transactions.',
- ),
- paragraph(
- 'If you donate to MAGIC Grants, a 501(c)(3) public charity, your donation may qualify for a tax deduction. Additional paperwork, especially for cryptocurrency donations, may be required. If you choose to donate to MAGIC Grants as a part of any other activity in the App, such as any transaction you make that includes a donation to MAGIC Grants, you acknowledge that the sole involvement by MAGIC Grants for such transactions is to receive your donation, and that MAGIC Grants is not otherwise party to nor responsible for your transaction in any other way.',
- ),
-
- sectionHeading('Indemnification'),
- paragraph(
- "You agree to defend, indemnify, and hold harmless MAGIC Grants, its affiliates, licensors, and service providers, and its and their respective officers, directors, employees, contractors, agents, licensors, suppliers, successors, and assigns from and against any claims, liabilities, damages, judgments, awards, losses, costs, expenses, or fees (including reasonable attorneys' fees) arising out of or relating to your violation of these Terms of Service or your use of the App, including, but not limited to, any use of the App's content, services, and products other than as expressly authorized in these Terms of Service, or your use of any information obtained from the App.",
- ),
-
- sectionHeading('Governing Law and Jurisdiction'),
- paragraph(
- 'All matters relating to the App and these Terms of Service, and any dispute or claim arising therefrom or related thereto (in each case, including non-contractual disputes or claims), shall be governed by and construed in accordance with the internal laws of the State of Colorado without giving effect to any choice or conflict of law provision or rule (whether of the State of Colorado or any other jurisdiction).',
- ),
- paragraph(
- 'Any legal suit, action, or proceeding arising out of, or related to, these Terms of Service or the App shall be instituted exclusively in the federal courts of the United States or the courts of the State of Colorado, although we retain the right to bring any suit, action, or proceeding against you for breach of these Terms of Service in your country of residence or any other relevant country. You waive any and all objections to the exercise of jurisdiction over you by such courts and to venue in such courts.',
- ),
-
- sectionHeading('Arbitration'),
- paragraph(
- 'At our sole discretion, we may require You to submit any disputes arising from these Terms of Service or use of the App, including disputes arising from or concerning their interpretation, violation, invalidity, non-performance, or termination, to final and binding arbitration under the Rules of Arbitration of the American Arbitration Association applying Colorado law.',
- ),
-
- sectionHeading('Waiver and Severability'),
- paragraph(
- 'No waiver by MAGIC Grants of any term or condition set out in these Terms of Service shall be deemed a further or continuing waiver of such term or condition or a waiver of any other term or condition, and any failure of MAGIC Grants to assert a right or provision under these Terms of Service shall not constitute a waiver of such right or provision.',
- ),
- paragraph(
- 'If any provision of these Terms of Service is held by a court or other tribunal of competent jurisdiction to be invalid, illegal, or unenforceable for any reason, such provision shall be eliminated or limited to the minimum extent such that the remaining provisions of the Terms of Service will continue in full force and effect.',
- ),
-
- sectionHeading('Entire Agreement'),
- paragraph(
- 'The Terms of Service and our Privacy Policy constitute the sole and entire agreement between You and MAGIC Grants regarding the App and supersede all prior and contemporaneous understandings, agreements, representations, and warranties, both written and oral, regarding the App.',
- ),
- ],
- ),
- ),
+ DesktopBackLink(
+ label: inShell ? i18n.navigationBarSettings : i18n.back,
+ onTap: () => Navigator.pop(context),
),
+ const SizedBox(height: 12),
+ Text(i18n.welcomeTermsLink, style: desktopTitleStyle),
],
),
+ )
+ : Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+ child: BrandScreenHeader(
+ onBack: () => Navigator.pop(context),
+ center: Text(i18n.welcomeTermsLink, style: BrandText.appBar.copyWith(fontSize: 16)),
+ ),
+ );
+ final body = SafeArea(
+ child: Center(
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 520),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ header,
+ Expanded(child: scroll),
+ ],
),
),
),
);
+
+ if (isDesktop && inShell) {
+ return DesktopShell(active: DesktopNav.settings, child: body);
+ }
+ return Scaffold(backgroundColor: BrandColors.paper, body: body);
}
}
### lib/screens/tor_settings.dart
@@ -3,7 +3,9 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/screens/desktop/tor_choice_view.dart';
import 'package:skylight_wallet/services/tor_settings_service.dart';
+import 'package:skylight_wallet/util/platform.dart';
import 'package:skylight_wallet/util/socks_http.dart';
import 'package:skylight_wallet/widgets/ui/ui.dart';
@@ -42,39 +44,52 @@ class TorSettingsScreen extends StatelessWidget {
useOrbot: useOrbot,
);
if (!context.mounted) return;
- Navigator.pushNamed(context, '/connection_setup');
+ Navigator.pushNamed(context, '/fiat_api_setup');
}
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
- final isMobile = Platform.isAndroid || Platform.isIOS;
+
+ final labels = TorChoiceLabels(
+ title: i18n.torChoiceTitle,
+ subtitle: i18n.torChoiceSubtitle,
+ builtIn: i18n.torSettingsModeBuiltIn,
+ builtInDesc: i18n.torChoiceBuiltInDesc,
+ external: i18n.torSettingsModeExternal,
+ externalDesc: i18n.torChoiceExternalDesc,
+ noTor: i18n.torSettingsModeDisabled,
+ noTorDesc: i18n.torChoiceNoTorDesc,
+ socksPortLabel: i18n.torSettingsSocksPortLabel,
+ orbotLabel: Platform.isIOS
+ ? i18n.torSettingsUseOrbotLabelIos
+ : i18n.torSettingsUseOrbotLabel,
+ testButton: i18n.torSettingsTestConnectionButton,
+ connected: i18n.torChoiceConnected,
+ testFailed: i18n.torChoiceTestFailed,
+ continueText: i18n.continueText,
+ );
+
+ void onContinue({required int modeIndex, required String port, required bool useOrbot}) =>
+ _continue(context, modeIndex: modeIndex, port: port, useOrbot: useOrbot);
+
+ if (isDesktop) {
+ return DesktopTorChoiceView(
+ labels: labels,
+ notePrivacy: i18n.onboardingTorNotePrivacy,
+ noteChangeable: i18n.onboardingTorNoteChangeable,
+ onTest: _test,
+ onContinue: onContinue,
+ );
+ }
return TorChoiceView(
- labels: TorChoiceLabels(
- title: i18n.torChoiceTitle,
- subtitle: i18n.torChoiceSubtitle,
- builtIn: i18n.torSettingsModeBuiltIn,
- builtInDesc: i18n.torChoiceBuiltInDesc,
- external: i18n.torSettingsModeExternal,
- externalDesc: i18n.torChoiceExternalDesc,
- noTor: i18n.torSettingsModeDisabled,
- noTorDesc: i18n.torChoiceNoTorDesc,
- socksPortLabel: i18n.torSettingsSocksPortLabel,
- orbotLabel: Platform.isIOS
- ? i18n.torSettingsUseOrbotLabelIos
- : i18n.torSettingsUseOrbotLabel,
- testButton: i18n.torSettingsTestConnectionButton,
- connected: i18n.torChoiceConnected,
- testFailed: i18n.torChoiceTestFailed,
- continueText: i18n.continueText,
- ),
+ labels: labels,
initialModeIndex: 0,
isMobile: isMobile,
onTest: _test,
- onContinue: ({required modeIndex, required port, required useOrbot}) =>
- _continue(context, modeIndex: modeIndex, port: port, useOrbot: useOrbot),
- stepCount: 5,
+ onContinue: onContinue,
+ stepCount: 6,
stepIndex: 0,
);
}
### lib/screens/unlock.dart
@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:local_auth/local_auth.dart';
+import 'package:package_info_plus/package_info_plus.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
@@ -25,6 +26,17 @@ class _UnlockScreenState extends State<UnlockScreen> {
String? _error;
String? _biometricLabel; // resolved per device on iOS (Face ID vs Touch ID)
bool _started = false;
+ String _version = ''; // desktop footer status bar
+
+ @override
+ void initState() {
+ super.initState();
+ PackageInfo.fromPlatform().then((info) {
+ if (mounted) {
+ setState(() => _version = 'Skylight Wallet v${info.version} · build ${info.buildNumber}');
+ }
+ });
+ }
@override
void didChangeDependencies() {
@@ -114,6 +126,7 @@ class _UnlockScreenState extends State<UnlockScreen> {
unlockButton: i18n.unlockButton,
),
isDesktop: _isDesktop,
+ version: _version.isEmpty ? null : _version,
passwordController: _passwordController,
obscure: _obscure,
onToggleObscure: () => setState(() => _obscure = !_obscure),
### lib/screens/wallet_home.dart
@@ -8,7 +8,9 @@ import 'package:skylight_wallet/consts.dart' as consts;
import 'package:skylight_wallet/l10n/app_localizations.dart';
import 'package:skylight_wallet/models/app_wallet.dart';
import 'package:skylight_wallet/models/fiat_rate_model.dart';
+import 'package:skylight_wallet/screens/desktop/home_shell.dart';
import 'package:skylight_wallet/services/tor_service.dart';
+import 'package:skylight_wallet/util/platform.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/widgets/ui/ui.dart';
import 'package:skylight_wallet/widgets/wallet_navigation_bar.dart';
@@ -34,6 +36,46 @@ String _amountText(double amount) => amount.toStringAsFixed(5);
/// The resolved LWS connection state, driving the disconnected indicator.
enum StatusIconStatus { loading, complete, fail }
+/// The balance/connection values the home renders, derived once and shared by
+/// the mobile and desktop layouts.
+typedef _HomeValues = ({
+ double unlockedBalance,
+ double? unlockedBalanceFiat,
+ double lockedBalance,
+ String fiatSymbol,
+ StatusIconStatus status,
+ bool disconnected,
+});
+
+_HomeValues _deriveHome(AppWallet wallet, FiatRateModel fiatRate) {
+ final unlockedBalance = wallet.unlockedBalance ?? 0;
+ final unlockedBalanceFiat = fiatRate.rateFor('XMR') is double && wallet.unlockedBalance is double
+ ? wallet.unlockedBalance! * fiatRate.rateFor('XMR')!
+ : null;
+ final lockedBalance = (wallet.totalBalance ?? 0) - (wallet.unlockedBalance ?? 0);
+ final fiatSymbol = consts.currencySymbols[fiatRate.fiatCode] ?? '\$';
+
+ var status = StatusIconStatus.fail;
+ if (wallet.isFullySynced) {
+ status = StatusIconStatus.complete;
+ } else if (wallet.usingTor &&
+ TorService.sharedInstance.status == TorConnectionStatus.connecting ||
+ !wallet.hasAttemptedConnection ||
+ wallet.isConnected && !wallet.isSynced ||
+ wallet.isConnected && wallet.isSynced && (wallet.syncedHeight ?? 0) == 0) {
+ status = StatusIconStatus.loading;
+ }
+
+ return (
+ unlockedBalance: unlockedBalance,
+ unlockedBalanceFiat: unlockedBalanceFiat,
+ lockedBalance: lockedBalance,
+ fiatSymbol: fiatSymbol,
+ status: status,
+ disconnected: wallet.torRequirementBroken || status == StatusIconStatus.fail,
+ );
+}
+
class WalletHomeScreen extends StatefulWidget {
const WalletHomeScreen({super.key});
@@ -71,24 +113,10 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
final i18n = AppLocalizations.of(context)!;
final wallet = appWalletOf(context, listen: true);
final fiatRate = context.watch<FiatRateModel>();
+ final v = _deriveHome(wallet, fiatRate);
- final unlockedBalance = wallet.unlockedBalance ?? 0;
- final unlockedBalanceFiat =
- fiatRate.rateFor('XMR') is double && wallet.unlockedBalance is double
- ? wallet.unlockedBalance! * fiatRate.rateFor('XMR')!
- : null;
- final lockedBalance = (wallet.totalBalance ?? 0) - (wallet.unlockedBalance ?? 0);
- final fiatSymbol = consts.currencySymbols[fiatRate.fiatCode] ?? '\$';
-
- var lwsConnectionIconStatus = StatusIconStatus.fail;
- if (wallet.isFullySynced) {
- lwsConnectionIconStatus = StatusIconStatus.complete;
- } else if (wallet.usingTor &&
- TorService.sharedInstance.status == TorConnectionStatus.connecting ||
- !wallet.hasAttemptedConnection ||
- wallet.isConnected && !wallet.isSynced ||
- wallet.isConnected && wallet.isSynced && (wallet.syncedHeight ?? 0) == 0) {
- lwsConnectionIconStatus = StatusIconStatus.loading;
+ if (isDesktop) {
+ return const DesktopShell(active: DesktopNav.home, child: DesktopHomeView());
}
return Scaffold(
@@ -111,14 +139,12 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
children: [
_BalanceHero(
wallet: wallet,
- unlockedBalance: unlockedBalance,
- unlockedBalanceFiat: unlockedBalanceFiat,
- lockedBalance: lockedBalance,
- fiatSymbol: fiatSymbol,
+ unlockedBalance: v.unlockedBalance,
+ unlockedBalanceFiat: v.unlockedBalanceFiat,
+ lockedBalance: v.lockedBalance,
+ fiatSymbol: v.fiatSymbol,
fiatRate: fiatRate,
- disconnected:
- wallet.torRequirementBroken ||
- lwsConnectionIconStatus == StatusIconStatus.fail,
+ disconnected: v.disconnected,
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 22),
@@ -141,7 +167,7 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
wallet: wallet,
i18n: i18n,
fiatRate: fiatRate,
- fiatSymbol: fiatSymbol,
+ fiatSymbol: v.fiatSymbol,
onTapTx: _showTxDetails,
),
const SliverToBoxAdapter(child: SizedBox(height: 24)),
@@ -253,16 +279,26 @@ class _BalanceHero extends StatelessWidget {
],
),
),
- _connectionRow(context, i18n),
+ _ConnectionRow(wallet: wallet, disconnected: disconnected),
],
),
);
}
+}
- /// The connection info row: transport/kind pills. While syncing the pills
- /// compact to icons with "x blocks left" beside them; when the connection has
- /// failed, the same slot shows "Disconnected" in red.
- Widget _connectionRow(BuildContext context, AppLocalizations i18n) {
+/// The connection info row: transport/kind pills. While syncing the pills
+/// compact to icons with "x blocks left" beside them; when the connection has
+/// failed, the same slot shows "Disconnected" in red. Shared by the mobile
+/// balance hero and the desktop home.
+class _ConnectionRow extends StatelessWidget {
+ final AppWallet wallet;
+ final bool disconnected;
+
+ const _ConnectionRow({required this.wallet, required this.disconnected});
+
+ @override
+ Widget build(BuildContext context) {
+ final i18n = AppLocalizations.of(context)!;
final cw = xmrWallet(context);
if (cw == null || cw.connectionAddress.isEmpty) return const SizedBox.shrink();
final blocks = wallet.syncBlocksRemaining;
@@ -331,6 +367,7 @@ class _ActivitySliver extends StatelessWidget {
final AppLocalizations i18n;
final FiatRateModel fiatRate;
final String fiatSymbol;
+ final double horizontalPadding;
final void Function(TxDetails tx) onTapTx;
const _ActivitySliver({
@@ -339,6 +376,7 @@ class _ActivitySliver extends StatelessWidget {
required this.fiatRate,
required this.fiatSymbol,
required this.onTapTx,
+ this.horizontalPadding = 20,
});
@override
@@ -352,7 +390,7 @@ class _ActivitySliver extends StatelessWidget {
if (txs.isEmpty) {
return SliverToBoxAdapter(
child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
+ padding: EdgeInsets.symmetric(horizontal: horizontalPadding, vertical: 12),
child: Text(
i18n.homeNoTransactions,
textAlign: TextAlign.center,
@@ -376,7 +414,7 @@ class _ActivitySliver extends StatelessWidget {
}
return SliverPadding(
- padding: const EdgeInsets.symmetric(horizontal: 20),
+ padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
sliver: SliverList.builder(
itemCount: rows.length,
itemBuilder: (context, index) {
@@ -406,3 +444,212 @@ class _ActivitySliver extends StatelessWidget {
);
}
}
+
+/// Desktop main home body (rendered inside [DesktopShell]): the balance and
+/// Receive/Send on one row, then the full day-grouped activity. Mirrors the
+/// mobile [WalletHomeScreen] logic (shared via [_deriveHome]); only the layout
+/// is desktop-specific. Skylight is single-coin, so there is no coin grid.
+class DesktopHomeView extends StatelessWidget {
+ const DesktopHomeView({super.key});
+
+ // Getter, not const: BrandColors.ink is a runtime (palette/brightness) value.
+ static TextStyle get _bigBalance => TextStyle(
+ fontFamily: 'Ubuntu Mono',
+ fontSize: 44,
+ height: 1,
+ fontWeight: FontWeight.w700,
+ letterSpacing: -0.88,
+ color: BrandColors.ink,
+ fontFeatures: const [FontFeature.tabularFigures()],
+ );
+
+ @override
+ Widget build(BuildContext context) {
+ final i18n = AppLocalizations.of(context)!;
+ final wallet = appWalletOf(context, listen: true);
+ final fiatRate = context.watch<FiatRateModel>();
+ final v = _deriveHome(wallet, fiatRate);
+
+ final showFiat = !fiatRate.isDisabled && v.unlockedBalanceFiat != null;
+ final coinText = '${_amountText(v.unlockedBalance)} XMR';
+ final cw = xmrWallet(context);
+ final configured = cw != null && cw.connectionAddress.isNotEmpty;
+
+ return CustomScrollView(
+ slivers: [
+ SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(44, 40, 44, 0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ if (configured)
+ Align(
+ alignment: Alignment.centerRight,
+ child: Padding(
+ padding: const EdgeInsets.only(bottom: 18),
+ child: _SyncStatusPill(
+ status: v.status,
+ blocksRemaining: wallet.syncBlocksRemaining,
+ ),
+ ),
+ ),
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (showFiat)
+ BalanceText.split(
+ formatFiat(v.unlockedBalanceFiat!, v.fiatSymbol),
+ style: _bigBalance,
+ )
+ else if (!fiatRate.isDisabled && !fiatRate.hasFailed)
+ Skeletonizer(child: Text('0.0000', style: _bigBalance))
+ else
+ Text(coinText, style: _bigBalance),
+ const SizedBox(height: 12),
+ if (showFiat) Text(coinText, style: _balanceSubStyle),
+ if (v.lockedBalance > 0)
+ Padding(
+ padding: const EdgeInsets.only(top: 6),
+ child: Text(
+ '${i18n.pending}: +${_amountText(v.lockedBalance)} XMR',
+ style: _balanceSubStyle,
+ ),
+ ),
+ if (fiatRate.hasFailed && !fiatRate.isDisabled)
+ Padding(
+ padding: const EdgeInsets.only(top: 6),
+ child: Row(
+ children: [
+ Icon(Icons.warning_rounded, size: 15, color: BrandColors.error),
+ const SizedBox(width: 6),
+ Flexible(
+ child: Text(
+ i18n.homeFiatApiError,
+ overflow: TextOverflow.ellipsis,
+ style: BrandText.caption.copyWith(color: BrandColors.error),
+ ),
+ ),
+ ],
+ ),
+ ),
+ if (configured) ...[
+ const SizedBox(height: 14),
+ ConnectionPills(wallet: cw),
+ ],
+ ],
+ ),
+ ),
+ const SizedBox(width: 24),
+ _DesktopActions(
+ onReceive: () => Navigator.pushNamed(context, '/receive'),
+ onSend: () => Navigator.pushNamed(context, '/send'),
+ ),
+ ],
+ ),
+ const SizedBox(height: 34),
+ SectionHeader(label: i18n.coinHomeActivityTitle, padding: EdgeInsets.zero),
+ const SizedBox(height: 4),
+ ],
+ ),
+ ),
+ ),
+ _ActivitySliver(
+ wallet: wallet,
+ i18n: i18n,
+ fiatRate: fiatRate,
+ fiatSymbol: v.fiatSymbol,
+ horizontalPadding: 44,
+ onTapTx: (tx) => showTxDetailsDialog(context, tx),
+ ),
+ const SliverToBoxAdapter(child: SizedBox(height: 28)),
+ ],
+ );
+ }
+}
+
+/// Desktop sync-status pill: a coloured dot + Synced / Syncing (blocks left) /
+/// Disconnected, mirroring Spice's coin-home status pill.
+class _SyncStatusPill extends StatelessWidget {
+ final StatusIconStatus status;
+ final int? blocksRemaining;
+
+ const _SyncStatusPill({required this.status, required this.blocksRemaining});
+
+ @override
+ Widget build(BuildContext context) {
+ final i18n = AppLocalizations.of(context)!;
+ final (Color dot, String text) = switch (status) {
+ StatusIconStatus.complete => (BrandColors.success, i18n.homeSynced),
+ StatusIconStatus.loading => (
+ BrandColors.warning,
+ blocksRemaining != null
+ ? i18n.homeBlocksRemaining(NumberFormat.decimalPattern().format(blocksRemaining))
+ : i18n.homeSyncing,
+ ),
+ StatusIconStatus.fail => (BrandColors.error, i18n.homeDisconnected),
+ };
+ return Container(
+ decoration: BoxDecoration(
+ color: BrandColors.surfaceSunken,
+ border: Border.all(color: BrandColors.border),
+ borderRadius: BorderRadius.circular(999),
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 7,
+ height: 7,
+ decoration: BoxDecoration(color: dot, shape: BoxShape.circle),
+ ),
+ const SizedBox(width: 8),
+ Text(
+ text,
+ style: TextStyle(
+ fontFamily: 'Ubuntu',
+ fontSize: 12.5,
+ height: 1,
+ color: BrandColors.inkMuted,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _DesktopActions extends StatelessWidget {
+ final VoidCallback onReceive;
+ final VoidCallback onSend;
+
+ const _DesktopActions({required this.onReceive, required this.onSend});
+
+ @override
+ Widget build(BuildContext context) {
+ final i18n = AppLocalizations.of(context)!;
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ BrandButton.secondary(
+ label: i18n.homeReceive,
+ icon: Icons.south_west,
+ expand: false,
+ onPressed: onReceive,
+ ),
+ const SizedBox(width: 10),
+ BrandButton.secondary(
+ label: i18n.homeSend,
+ icon: Icons.north_east,
+ expand: false,
+ onPressed: onSend,
+ ),
+ ],
+ );
+ }
+}
### lib/screens/welcome.dart
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/screens/desktop/welcome_view.dart';
+import 'package:skylight_wallet/util/platform.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/widgets/floating_bob.dart';
import 'package:skylight_wallet/widgets/language_sheet.dart';
@@ -31,19 +33,33 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
+ final labels = WelcomeLabels(
+ getStarted: i18n.welcomeGetStarted,
+ agreePrefix: i18n.welcomeAgreePrefix,
+ termsLink: i18n.welcomeTermsLink,
+ agreeMiddle: i18n.welcomeAgreeMiddle,
+ privacyLink: i18n.welcomePrivacyLink,
+ );
+
+ if (isDesktop) {
+ return DesktopWelcomeView(
+ logo: FloatingBob(child: SvgPicture.asset('assets/logo_nobg.svg', width: 120, height: 120)),
+ appName: 'Skylight Wallet',
+ description: i18n.welcomeDescription,
+ labels: labels,
+ onGetStarted: () => Navigator.pushNamed(context, '/tor_settings'),
+ onTerms: () => Navigator.pushNamed(context, '/terms_of_service'),
+ onPrivacy: () => Navigator.pushNamed(context, '/privacy_policy'),
+ );
+ }
+
return WelcomeView(
logo: FloatingBob(child: SvgPicture.asset('assets/logo_nobg.svg', width: 132, height: 132)),
appName: 'Skylight Wallet',
appNameColor: BrandColors.ink,
logoBottomGap: BrandSpacing.md,
description: i18n.welcomeDescription,
- labels: WelcomeLabels(
- getStarted: i18n.welcomeGetStarted,
- agreePrefix: i18n.welcomeAgreePrefix,
- termsLink: i18n.welcomeTermsLink,
- agreeMiddle: i18n.welcomeAgreeMiddle,
- privacyLink: i18n.welcomePrivacyLink,
- ),
+ labels: labels,
onGetStarted: () => Navigator.pushNamed(context, '/tor_settings'),
onTerms: () => Navigator.pushNamed(context, '/terms_of_service'),
onPrivacy: () => Navigator.pushNamed(context, '/privacy_policy'),
### lib/util/platform.dart
@@ -0,0 +1,7 @@
+import 'dart:io';
+
+/// True on the desktop platforms (Linux/Windows/macOS).
+final bool isDesktop = Platform.isLinux || Platform.isWindows || Platform.isMacOS;
+
+/// True on the mobile platforms (Android/iOS).
+final bool isMobile = Platform.isAndroid || Platform.isIOS;
### lib/widgets/connection_settings_form.dart
@@ -17,19 +17,43 @@ const isDemoMode = String.fromEnvironment('DEMO_MODE') == 'true';
const connectionTypeOptions = ['lws', 'node'];
+/// Drives the form's Save from outside — for the desktop onboarding footer,
+/// whose Continue button sits beside Back rather than inside the form. Attach
+/// with [ConnectionSettingsForm.controller]; [canSave] mirrors the internal
+/// save gating and [save] triggers it.
+class ConnectionFormController {
+ _ConnectionSettingsFormState? _state;
+
+ final ValueNotifier<bool> canSave = ValueNotifier(false);
+
+ void save() => _state?._saveConnection();
+
+ void _attach(_ConnectionSettingsFormState s) => _state = s;
+ void _detach(_ConnectionSettingsFormState s) {
+ if (_state == s) _state = null;
+ }
+}
+
/// Shared form widget used by both ConnectionSetupScreen and the connection settings dialog
class ConnectionSettingsForm extends StatefulWidget {
final String saveButtonLabel;
final VoidCallback onSaved;
final bool isInDialog;
final Future<void> Function()? onBeforeSave;
+ /// Hide the built-in Save button and publish save state to [controller] — the
+ /// host renders its own Save/Continue (desktop onboarding footer).
+ final bool embedded;
+ final ConnectionFormController? controller;
+
const ConnectionSettingsForm({
super.key,
required this.saveButtonLabel,
required this.onSaved,
this.isInDialog = false,
this.onBeforeSave,
+ this.embedded = false,
+ this.controller,
});
@override
@@ -119,11 +143,13 @@ class _ConnectionSettingsFormState extends State<ConnectionSettingsForm> {
@override
void initState() {
super.initState();
+ widget.controller?._attach(this);
_loadPersistedConnection();
}
@override
void dispose() {
+ widget.controller?._detach(this);
_torStatusTimer?.cancel();
_addressController.dispose();
_customProxyPortController.dispose();
@@ -450,6 +476,14 @@ class _ConnectionSettingsFormState extends State<ConnectionSettingsForm> {
final disablingSyncOnly = syncChanged && !enablingSync && !connectionChanged;
final canSave = !_connectionTestIsLoading && (hasWorkingConnection || disablingSyncOnly);
+ // Publish save state for an external Continue button (embedded mode).
+ final controller = widget.controller;
+ if (controller != null) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (mounted) controller.canSave.value = canSave;
+ });
+ }
+
return ConnectionFormView(
labels: ConnectionFormLabels(
proxyPortLabel: i18n.lwsSetupProxyPortLabel,
@@ -507,6 +541,7 @@ class _ConnectionSettingsFormState extends State<ConnectionSettingsForm> {
saveButtonLabel: widget.saveButtonLabel,
canSave: canSave,
onSave: _saveConnection,
+ showSave: !widget.embedded,
);
}
}
### lib/widgets/language_sheet.dart
@@ -11,9 +11,9 @@ const languageNames = {'en': ('English', 'English'), 'pt': ('Português', 'Portu
/// Opens the language picker — reachable from Settings and the welcome screen
/// (welcome has no route to Settings, so a reader who can't read it can switch
/// here before anything else).
-void showLanguageSheet(BuildContext context) {
+Future<void> showLanguageSheet(BuildContext context) {
final language = context.read<LanguageModel>();
- showLanguagePickerSheet(
+ return showLanguagePickerSheet(
context,
// Resolved per build from the sheet's own context rather than captured
// here: picking a language re-localizes the app while this sheet is still
### lib/widgets/tor_settings_form.dart
@@ -225,7 +225,7 @@ class _TorSettingsFormState extends State<TorSettingsForm> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
- _PortField(
+ TorPortField(
controller: _socksPortController,
label: i18n.torSettingsSocksPortLabel,
enabled: portEnabled,
@@ -250,9 +250,17 @@ class _TorSettingsFormState extends State<TorSettingsForm> {
const SizedBox(height: BrandSpacing.md),
Row(
children: [
- Expanded(child: _testStatus(i18n)),
+ Expanded(
+ child: TorTestStatus(
+ testing: _isTestingConnection,
+ tested: _hasTested,
+ ok: _connectionSuccess,
+ connectedLabel: i18n.torChoiceConnected,
+ failedLabel: i18n.torChoiceTestFailed,
+ ),
+ ),
const SizedBox(width: BrandSpacing.md),
- _TestChip(
+ TorTestChip(
label: i18n.torSettingsTestConnectionButton,
onTap: _isTestingConnection ? null : _testConnection,
),
@@ -261,9 +269,30 @@ class _TorSettingsFormState extends State<TorSettingsForm> {
],
);
}
+}
- Widget _testStatus(AppLocalizations i18n) {
- if (_isTestingConnection) {
+/// The inline connection-test status for the external-Tor controls: a spinner
+/// while [testing], then a success dot / error icon with label once [tested].
+/// Shared by the settings Tor sheet and the desktop onboarding Tor step.
+class TorTestStatus extends StatelessWidget {
+ final bool testing;
+ final bool tested;
+ final bool ok;
+ final String connectedLabel;
+ final String failedLabel;
+
+ const TorTestStatus({
+ super.key,
+ required this.testing,
+ required this.tested,
+ required this.ok,
+ required this.connectedLabel,
+ required this.failedLabel,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ if (testing) {
return Align(
alignment: Alignment.centerLeft,
child: SizedBox(
@@ -273,8 +302,8 @@ class _TorSettingsFormState extends State<TorSettingsForm> {
),
);
}
- if (!_hasTested) return const SizedBox.shrink();
- if (_connectionSuccess) {
+ if (!tested) return const SizedBox.shrink();
+ if (ok) {
return Row(
children: [
Container(
@@ -284,7 +313,7 @@ class _TorSettingsFormState extends State<TorSettingsForm> {
),
const SizedBox(width: BrandSpacing.sm),
Text(
- i18n.torChoiceConnected,
+ connectedLabel,
style: BrandText.caption.copyWith(
color: BrandColors.success,
fontWeight: FontWeight.w500,
@@ -298,7 +327,7 @@ class _TorSettingsFormState extends State<TorSettingsForm> {
Icon(Icons.error_outline, color: BrandColors.error, size: 18),
const SizedBox(width: BrandSpacing.sm),
Text(
- i18n.torChoiceTestFailed,
+ failedLabel,
style: BrandText.caption.copyWith(color: BrandColors.error, fontWeight: FontWeight.w500),
),
],
@@ -307,65 +336,89 @@ class _TorSettingsFormState extends State<TorSettingsForm> {
}
/// Labeled inset field — a small-caps mono label above the value, per the
-/// design (not a Material floating-label box).
-class _PortField extends StatelessWidget {
+/// design (not a Material floating-label box). The whole box is a tap target for
+/// the field, so a tap on the padding (not just the thin text line) still puts
+/// the caret in — otherwise, nested in a card's tap handler, it reads as unfocusable.
+class TorPortField extends StatefulWidget {
final TextEditingController controller;
final String label;
final bool enabled;
final VoidCallback onChanged;
- const _PortField({
+ const TorPortField({
+ super.key,
required this.controller,
required this.label,
- required this.enabled,
+ this.enabled = true,
required this.onChanged,
});
+ @override
+ State<TorPortField> createState() => _TorPortFieldState();
+}
+
+class _TorPortFieldState extends State<TorPortField> {
+ final _focusNode = FocusNode();
+
+ @override
+ void dispose() {
+ _focusNode.dispose();
+ super.dispose();
+ }
+
@override
Widget build(BuildContext context) {
- return Container(
- padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 13),
- decoration: BoxDecoration(
- color: BrandColors.paper,
- borderRadius: BorderRadius.circular(BrandRadii.tile),
- border: Border.all(color: BrandColors.border),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- label.toUpperCase(),
- style: TextStyle(
- fontFamily: 'Ubuntu Mono',
- fontSize: 10,
- fontWeight: FontWeight.w700,
- letterSpacing: 1.4,
- color: BrandColors.inkFaint,
- ),
+ return GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: widget.enabled ? _focusNode.requestFocus : null,
+ child: MouseRegion(
+ cursor: widget.enabled ? SystemMouseCursors.text : MouseCursor.defer,
+ child: Container(
+ padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 13),
+ decoration: BoxDecoration(
+ color: BrandColors.paper,
+ borderRadius: BorderRadius.circular(BrandRadii.tile),
+ border: Border.all(color: BrandColors.border),
),
- const SizedBox(height: 6),
- TextField(
- controller: controller,
- enabled: enabled,
- keyboardType: TextInputType.number,
- inputFormatters: [FilteringTextInputFormatter.digitsOnly],
- style: TextStyle(fontFamily: 'Ubuntu Mono', fontSize: 14, color: BrandColors.ink),
- cursorColor: BrandColors.primary,
- decoration: const InputDecoration.collapsed(hintText: '9050'),
- onChanged: (_) => onChanged(),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ widget.label.toUpperCase(),
+ style: TextStyle(
+ fontFamily: 'Ubuntu Mono',
+ fontSize: 10,
+ fontWeight: FontWeight.w700,
+ letterSpacing: 1.4,
+ color: BrandColors.inkFaint,
+ ),
+ ),
+ const SizedBox(height: 6),
+ TextField(
+ controller: widget.controller,
+ focusNode: _focusNode,
+ enabled: widget.enabled,
+ keyboardType: TextInputType.number,
+ inputFormatters: [FilteringTextInputFormatter.digitsOnly],
+ style: TextStyle(fontFamily: 'Ubuntu Mono', fontSize: 14, color: BrandColors.ink),
+ cursorColor: BrandColors.primary,
+ decoration: const InputDecoration.collapsed(hintText: '9050'),
+ onChanged: (_) => widget.onChanged(),
+ ),
+ ],
),
- ],
+ ),
),
);
}
}
/// Small compact chip for the connection test action.
-class _TestChip extends StatelessWidget {
+class TorTestChip extends StatelessWidget {
final String label;
final VoidCallback? onTap;
- const _TestChip({required this.label, required this.onTap});
+ const TorTestChip({super.key, required this.label, required this.onTap});
@override
Widget build(BuildContext context) {
### linux/runner/my_application.cc
@@ -48,7 +48,13 @@ static void my_application_activate(GApplication* application) {
}
gtk_window_set_default_size(window, 1280, 720);
-
+
+ // Minimum window size so the two-pane layouts and sidebar stay usable.
+ GdkGeometry min_size;
+ min_size.min_width = 900;
+ min_size.min_height = 640;
+ gtk_window_set_geometry_hints(window, nullptr, &min_size, GDK_HINT_MIN_SIZE);
+
// Set window icon - construct path relative to executable
g_autofree gchar* exe_path = g_file_read_link("/proc/self/exe", nullptr);
if (exe_path != nullptr) {
### macos/Runner/MainFlutterWindow.swift
@@ -8,6 +8,9 @@ class MainFlutterWindow: NSWindow {
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
+ // Minimum window size so the two-pane layouts and sidebar stay usable.
+ self.minSize = NSSize(width: 900, height: 640)
+
RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib()
### pubspec.lock
@@ -1233,62 +1233,62 @@ packages:
dependency: "direct main"
description:
path: "packages/wallet_background"
- ref: "3c8ce7f8088ffcd5d39c393654309ff01112cf93"
- resolved-ref: "3c8ce7f8088ffcd5d39c393654309ff01112cf93"
+ ref: "77c6e66f32b10090c4b06198211f2f54b63f2136"
+ resolved-ref: "77c6e66f32b10090c4b06198211f2f54b63f2136"
url: "https://github.com/MAGICGrants/wallet-core"
source: git
version: "0.1.0"
wallet_domain:
dependency: "direct main"
description:
path: "packages/wallet_domain"
- ref: "3c8ce7f8088ffcd5d39c393654309ff01112cf93"
- resolved-ref: "3c8ce7f8088ffcd5d39c393654309ff01112cf93"
+ ref: "77c6e66f32b10090c4b06198211f2f54b63f2136"
+ resolved-ref: "77c6e66f32b10090c4b06198211f2f54b63f2136"
url: "https://github.com/MAGICGrants/wallet-core"
source: git
version: "0.1.0"
wallet_fiat:
dependency: "direct main"
description:
path: "packages/wallet_fiat"
- ref: "3c8ce7f8088ffcd5d39c393654309ff01112cf93"
- resolved-ref: "3c8ce7f8088ffcd5d39c393654309ff01112cf93"
+ ref: "77c6e66f32b10090c4b06198211f2f54b63f2136"
+ resolved-ref: "77c6e66f32b10090c4b06198211f2f54b63f2136"
url: "https://github.com/MAGICGrants/wallet-core"
source: git
version: "0.1.0"
wallet_infra:
dependency: "direct main"
description:
path: "packages/wallet_infra"
- ref: "3c8ce7f8088ffcd5d39c393654309ff01112cf93"
- resolved-ref: "3c8ce7f8088ffcd5d39c393654309ff01112cf93"
+ ref: "77c6e66f32b10090c4b06198211f2f54b63f2136"
+ resolved-ref: "77c6e66f32b10090c4b06198211f2f54b63f2136"
url: "https://github.com/MAGICGrants/wallet-core"
source: git
version: "0.1.0"
wallet_monero:
dependency: "direct main"
description:
path: "packages/wallet_monero"
- ref: "3c8ce7f8088ffcd5d39c393654309ff01112cf93"
- resolved-ref: "3c8ce7f8088ffcd5d39c393654309ff01112cf93"
+ ref: "77c6e66f32b10090c4b06198211f2f54b63f2136"
+ resolved-ref: "77c6e66f32b10090c4b06198211f2f54b63f2136"
url: "https://github.com/MAGICGrants/wallet-core"
source: git
version: "0.1.0"
wallet_openalias:
dependency: "direct main"
description:
path: "packages/wallet_openalias"
- ref: "3c8ce7f8088ffcd5d39c393654309ff01112cf93"
- resolved-ref: "3c8ce7f8088ffcd5d39c393654309ff01112cf93"
+ ref: "77c6e66f32b10090c4b06198211f2f54b63f2136"
+ resolved-ref: "77c6e66f32b10090c4b06198211f2f54b63f2136"
url: "https://github.com/MAGICGrants/wallet-core"
source: git
version: "0.1.0"
wallet_ui:
dependency: "direct main"
description:
path: "packages/wallet_ui"
- ref: "3c8ce7f8088ffcd5d39c393654309ff01112cf93"
- resolved-ref: "3c8ce7f8088ffcd5d39c393654309ff01112cf93"
+ ref: "77c6e66f32b10090c4b06198211f2f54b63f2136"
+ resolved-ref: "77c6e66f32b10090c4b06198211f2f54b63f2136"
url: "https://github.com/MAGICGrants/wallet-core"
source: git
version: "0.1.0"
### pubspec.yaml
@@ -65,37 +65,37 @@ dependencies:
wallet_infra:
git:
url: https://github.com/MAGICGrants/wallet-core
- ref: 3c8ce7f8088ffcd5d39c393654309ff01112cf93
+ ref: 77c6e66f32b10090c4b06198211f2f54b63f2136
path: packages/wallet_infra
wallet_domain:
git:
url: https://github.com/MAGICGrants/wallet-core
- ref: 3c8ce7f8088ffcd5d39c393654309ff01112cf93
+ ref: 77c6e66f32b10090c4b06198211f2f54b63f2136
path: packages/wallet_domain
wallet_monero:
git:
url: https://github.com/MAGICGrants/wallet-core
- ref: 3c8ce7f8088ffcd5d39c393654309ff01112cf93
+ ref: 77c6e66f32b10090c4b06198211f2f54b63f2136
path: packages/wallet_monero
wallet_openalias:
git:
url: https://github.com/MAGICGrants/wallet-core
- ref: 3c8ce7f8088ffcd5d39c393654309ff01112cf93
+ ref: 77c6e66f32b10090c4b06198211f2f54b63f2136
path: packages/wallet_openalias
wallet_background:
git:
url: https://github.com/MAGICGrants/wallet-core
- ref: 3c8ce7f8088ffcd5d39c393654309ff01112cf93
+ ref: 77c6e66f32b10090c4b06198211f2f54b63f2136
path: packages/wallet_background
wallet_fiat:
git:
url: https://github.com/MAGICGrants/wallet-core
- ref: 3c8ce7f8088ffcd5d39c393654309ff01112cf93
+ ref: 77c6e66f32b10090c4b06198211f2f54b63f2136
path: packages/wallet_fiat
wallet_ui:
git:
url: https://github.com/MAGICGrants/wallet-core
- ref: 3c8ce7f8088ffcd5d39c393654309ff01112cf93
+ ref: 77c6e66f32b10090c4b06198211f2f54b63f2136
path: packages/wallet_ui
bip39:
### windows/runner/win32_window.cpp
@@ -216,6 +216,17 @@ Win32Window::MessageHandler(HWND hwnd,
case WM_DWMCOLORIZATIONCOLORCHANGED:
UpdateTheme(hwnd);
return 0;
+
+ case WM_GETMINMAXINFO: {
+ // Minimum window size (DPI-scaled) so the two-pane layouts and sidebar
+ // stay usable.
+ auto* info = reinterpret_cast<MINMAXINFO*>(lparam);
+ UINT dpi = GetDpiForWindow(hwnd);
+ double scale = dpi > 0 ? dpi / 96.0 : 1.0;
+ info->ptMinTrackSize.x = static_cast<LONG>(900 * scale);
+ info->ptMinTrackSize.y = static_cast<LONG>(640 * scale);
+ return 0;
+ }
}
return DefWindowProc(window_handle_, message, wparam, lparam);Why this scored 17/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.