What changed, and why it matters
This is a large user-interface refresh for the Skylight Monero wallet. Most changes are cosmetic: new logo, fonts, colors, labels, and screen flows. The only functional security-relevant change visible in the diff is that the app now derives whether a server connection uses HTTPS automatically from the address type (routable vs. onion/local), instead of letting the user toggle an 'useSsl' switch. This is a hardening move, not a vulnerability. There is no evidence in the commit of an exploit, backdoor, or data leak.
No immediate security action required. Treat as a normal UI release. Review the full wallet-core changes that accompany this commit (not supplied) to confirm the requiresSecureTransport() derivation is correct and that dropping useSsl does not break existing custom server configurations.
Security signals we found
Removal of explicit useSsl toggle in favor of automatic transport derivation
Shared method-channel rename for secure clipboard (namespace change only)
Large UI-only refactor with no visible crypto/storage/network protocol changes
New user-facing warnings about seed storage, view keys, and Tor privacy
Evidence from the diff
Commit 447bb2d is a broad UI refactor (+4851/-4720 across 63 files). It replaces the old blue Material theme with a branded ‘wallet_ui’ theme/palette, adds new onboarding and settings copy, and moves LanguageModel/ThemeModel into a shared wallet-core package. The wallet adapter drops an explicit useSsl parameter and instead calls wallet-core’s requiresSecureTransport() to derive TLS for display purposes. The Android/iOS secure-clipboard method channel is renamed to a shared, app-neutral namespace. No cryptographic, network, or storage logic changes are visible in the supplied diff that would introduce a security flaw.
Changed components
lib/main.dartlib/models/monero_wallet_adapter.dartlib/models/app_wallet.dartlib/models/language_model.dartlib/models/theme_model.dartandroid/app/src/main/kotlin/.../MainActivity.ktios/Runner/AppDelegate.swiftlib/l10n/*Inspect captured patch +4851 / −4720
diff --git a/android/app/src/main/kotlin/com/example/monero_light_wallet/MainActivity.kt b/android/app/src/main/kotlin/com/example/monero_light_wallet/MainActivity.kt
index 4f76235..d17cb4f 100644
--- a/android/app/src/main/kotlin/com/example/monero_light_wallet/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/example/monero_light_wallet/MainActivity.kt
@@ -9,7 +9,8 @@ import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterFragmentActivity() {
- private val secureClipboardChannel = "org.magicgrants.skylight/secure_clipboard"
+ // App-neutral name shared with wallet-core's SecureClipboard (D10).
+ private val secureClipboardChannel = "org.magicgrants.wallet/secure_clipboard"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
diff --git a/assets/logo.svg b/assets/logo.svg
new file mode 100644
index 0000000..567aace
--- /dev/null
+++ b/assets/logo.svg
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 1500 1500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+ <g transform="matrix(0.967742,0,0,0.982962,48.3871,25.557)">
+ <rect x="-50" y="-26" width="1550" height="1526" style="fill:rgb(38,63,95);"/>
+ </g>
+ <g id="Layer2" transform="matrix(1,0,0,1,0,59)">
+ <clipPath id="_clip1">
+ <path d="M308,952.027C226.868,952.027 161,877.869 161,786.527C161,711.666 205.244,648.347 265.873,627.935C264.639,619.389 264,610.656 264,601.777C264,499.811 348.24,417.027 452,417.027C455.439,417.027 458.856,417.118 462.249,417.298C483.392,293.266 605.153,198.027 752,198.027C899.114,198.027 1021.05,293.612 1041.87,417.975C1048.16,417.348 1054.54,417.027 1061,417.027C1164.76,417.027 1249,499.811 1249,601.777C1249,612.325 1248.1,622.667 1246.37,632.733C1300.62,657.091 1339,716.803 1339,786.527C1339,877.869 1273.13,952.027 1192,952.027L308,952.027Z"/>
+ </clipPath>
+ <g clip-path="url(#_clip1)">
+ <g transform="matrix(1,0,0,1,2,18)">
+ <path d="M306,954.027C224.868,954.027 159,879.869 159,788.527C159,713.666 203.244,650.347 263.873,629.935C262.639,621.389 262,612.656 262,603.777C262,501.811 346.24,419.027 450,419.027C453.439,419.027 456.856,419.118 460.249,419.298C481.392,295.266 603.153,200.027 750,200.027C897.114,200.027 1019.05,295.612 1039.87,419.975C1046.16,419.348 1052.54,419.027 1059,419.027C1162.76,419.027 1247,501.811 1247,603.777C1247,614.325 1246.1,624.667 1244.37,634.733C1298.62,659.091 1337,718.803 1337,788.527C1337,879.869 1271.13,954.027 1190,954.027L306,954.027Z" style="fill:rgb(228,238,247);"/>
+ </g>
+ <g transform="matrix(0.997878,-0.0651119,0.0651119,0.997878,-101.353,79.3356)">
+ <path d="M562.533,841.809C542.065,850.325 519.792,855 496.5,855C396.327,855 315,768.52 315,662C315,555.48 396.327,469 496.5,469C531.725,469 564.619,479.693 592.467,498.191C612.935,489.675 635.208,485 658.5,485C675.281,485 691.533,487.427 706.962,491.968C723.287,401.995 805.126,333.5 903.5,333.5C993.972,333.5 1070.46,391.433 1094.86,470.727C1183.53,483.398 1252,564.264 1252,662C1252,768.52 1170.67,855 1070.5,855C1053.01,855 1036.09,852.363 1020.08,847.443C986.859,886.468 938.852,911 885.5,911C831.155,911 782.356,885.547 749.081,845.25C722.417,861.626 691.48,871 658.5,871C623.275,871 590.381,860.307 562.533,841.809Z" style="fill:rgb(213,230,240);"/>
+ </g>
+ <g transform="matrix(0.999897,0.0143316,-0.0143316,0.999897,9.76827,4.26014)">
+ <path d="M479.418,982.636C442.574,1006.19 397.783,1020 349.5,1020C223.939,1020 122,926.574 122,811.5C122,696.426 223.939,603 349.5,603C429.57,603 500.034,640.993 540.582,698.364C577.426,674.815 622.217,661 670.5,661C766.156,661 848.102,715.223 881.711,791.932C919.793,765.612 967.173,750 1018.5,750C1040.37,750 1061.52,752.833 1081.54,758.121C1107.29,668.872 1196.07,603 1301.5,603C1427.06,603 1529,696.426 1529,811.5C1529,926.574 1427.06,1020 1301.5,1020C1279.63,1020 1258.48,1017.17 1238.46,1011.88C1212.71,1101.13 1123.93,1167 1018.5,1167C922.844,1167 840.898,1112.78 807.289,1036.07C769.207,1062.39 721.827,1078 670.5,1078C590.43,1078 519.966,1040.01 479.418,982.636Z" style="fill:rgb(203,220,231);"/>
+ </g>
+ </g>
+ </g>
+ <g transform="matrix(1,0,0,1,0,59)">
+ <g transform="matrix(1.01695,0,0,0.925183,-13.2203,102.953)">
+ <path d="M957,510.221L957,1185.76L544,1185.76L544,511.221L750,717.221L957,510.221Z" style="fill:url(#_Linear2);"/>
+ </g>
+ <g transform="matrix(1.02364,0,0,1.02998,-18.9259,-19.1621)">
+ <path d="M458.095,741.304L545,741.304L545,507.051L750,712.128L958,504.05L958,741L1044.24,741L1013.05,795.012L894,795.012L894,650.114L750.031,794.137L750,794.168L609,653.115L609,795.317L489.279,795.317L458.095,741.304Z"/>
+ </g>
+ </g>
+ <defs>
+ <linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1,0,0,1,0,0.5)"><stop offset="0" style="stop-color:rgb(255,102,0);stop-opacity:0.66"/><stop offset="1" style="stop-color:rgb(255,102,0);stop-opacity:0.66"/></linearGradient>
+ </defs>
+</svg>
diff --git a/assets/logo_nobg.svg b/assets/logo_nobg.svg
new file mode 100644
index 0000000..0775a5e
--- /dev/null
+++ b/assets/logo_nobg.svg
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 1500 1500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+ <g id="Layer2" transform="matrix(1,0,0,1,0,59)">
+ <clipPath id="_clip1">
+ <path d="M308,952.027C226.868,952.027 161,877.869 161,786.527C161,711.666 205.244,648.347 265.873,627.935C264.639,619.389 264,610.656 264,601.777C264,499.811 348.24,417.027 452,417.027C455.439,417.027 458.856,417.118 462.249,417.298C483.392,293.266 605.153,198.027 752,198.027C899.114,198.027 1021.05,293.612 1041.87,417.975C1048.16,417.348 1054.54,417.027 1061,417.027C1164.76,417.027 1249,499.811 1249,601.777C1249,612.325 1248.1,622.667 1246.37,632.733C1300.62,657.091 1339,716.803 1339,786.527C1339,877.869 1273.13,952.027 1192,952.027L308,952.027Z"/>
+ </clipPath>
+ <g clip-path="url(#_clip1)">
+ <g transform="matrix(1,0,0,1,2,18)">
+ <path d="M306,954.027C224.868,954.027 159,879.869 159,788.527C159,713.666 203.244,650.347 263.873,629.935C262.639,621.389 262,612.656 262,603.777C262,501.811 346.24,419.027 450,419.027C453.439,419.027 456.856,419.118 460.249,419.298C481.392,295.266 603.153,200.027 750,200.027C897.114,200.027 1019.05,295.612 1039.87,419.975C1046.16,419.348 1052.54,419.027 1059,419.027C1162.76,419.027 1247,501.811 1247,603.777C1247,614.325 1246.1,624.667 1244.37,634.733C1298.62,659.091 1337,718.803 1337,788.527C1337,879.869 1271.13,954.027 1190,954.027L306,954.027Z" style="fill:rgb(228,238,247);"/>
+ </g>
+ <g transform="matrix(0.997878,-0.0651119,0.0651119,0.997878,-101.353,79.3356)">
+ <path d="M562.533,841.809C542.065,850.325 519.792,855 496.5,855C396.327,855 315,768.52 315,662C315,555.48 396.327,469 496.5,469C531.725,469 564.619,479.693 592.467,498.191C612.935,489.675 635.208,485 658.5,485C675.281,485 691.533,487.427 706.962,491.968C723.287,401.995 805.126,333.5 903.5,333.5C993.972,333.5 1070.46,391.433 1094.86,470.727C1183.53,483.398 1252,564.264 1252,662C1252,768.52 1170.67,855 1070.5,855C1053.01,855 1036.09,852.363 1020.08,847.443C986.859,886.468 938.852,911 885.5,911C831.155,911 782.356,885.547 749.081,845.25C722.417,861.626 691.48,871 658.5,871C623.275,871 590.381,860.307 562.533,841.809Z" style="fill:rgb(213,230,240);"/>
+ </g>
+ <g transform="matrix(0.999897,0.0143316,-0.0143316,0.999897,9.76827,4.26014)">
+ <path d="M479.418,982.636C442.574,1006.19 397.783,1020 349.5,1020C223.939,1020 122,926.574 122,811.5C122,696.426 223.939,603 349.5,603C429.57,603 500.034,640.993 540.582,698.364C577.426,674.815 622.217,661 670.5,661C766.156,661 848.102,715.223 881.711,791.932C919.793,765.612 967.173,750 1018.5,750C1040.37,750 1061.52,752.833 1081.54,758.121C1107.29,668.872 1196.07,603 1301.5,603C1427.06,603 1529,696.426 1529,811.5C1529,926.574 1427.06,1020 1301.5,1020C1279.63,1020 1258.48,1017.17 1238.46,1011.88C1212.71,1101.13 1123.93,1167 1018.5,1167C922.844,1167 840.898,1112.78 807.289,1036.07C769.207,1062.39 721.827,1078 670.5,1078C590.43,1078 519.966,1040.01 479.418,982.636Z" style="fill:rgb(203,220,231);"/>
+ </g>
+ </g>
+ </g>
+ <g transform="matrix(1,0,0,1,0,59)">
+ <g transform="matrix(1.01695,0,0,0.925183,-13.2203,102.953)">
+ <path d="M957,510.221L957,1185.76L544,1185.76L544,511.221L750,717.221L957,510.221Z" style="fill:url(#_Linear2);"/>
+ </g>
+ <g transform="matrix(1.02364,0,0,1.02998,-18.9259,-19.1621)">
+ <path d="M458.095,741.304L545,741.304L545,507.051L750,712.128L958,504.05L958,741L1044.24,741L1013.05,795.012L894,795.012L894,650.114L750.031,794.137L750,794.168L609,653.115L609,795.317L489.279,795.317L458.095,741.304Z"/>
+ </g>
+ </g>
+ <defs>
+ <linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1,0,0,1,0,0.5)"><stop offset="0" style="stop-color:rgb(255,102,0);stop-opacity:0.66"/><stop offset="1" style="stop-color:rgb(255,102,0);stop-opacity:0.66"/></linearGradient>
+ </defs>
+</svg>
diff --git a/fonts/UFL.txt b/fonts/UFL.txt
new file mode 100644
index 0000000..ae78a8f
--- /dev/null
+++ b/fonts/UFL.txt
@@ -0,0 +1,96 @@
+-------------------------------
+UBUNTU FONT LICENCE Version 1.0
+-------------------------------
+
+PREAMBLE
+This licence allows the licensed fonts to be used, studied, modified and
+redistributed freely. The fonts, including any derivative works, can be
+bundled, embedded, and redistributed provided the terms of this licence
+are met. The fonts and derivatives, however, cannot be released under
+any other licence. The requirement for fonts to remain under this
+licence does not require any document created using the fonts or their
+derivatives to be published under this licence, as long as the primary
+purpose of the document is not to be a vehicle for the distribution of
+the fonts.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this licence and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Original Version" refers to the collection of Font Software components
+as received under this licence.
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to
+a new environment.
+
+"Copyright Holder(s)" refers to all individuals and companies who have a
+copyright ownership of the Font Software.
+
+"Substantially Changed" refers to Modified Versions which can be easily
+identified as dissimilar to the Font Software by users of the Font
+Software comparing the Original Version with the Modified Version.
+
+To "Propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification and with or without charging
+a redistribution fee), making available to the public, and in some
+countries other activities as well.
+
+PERMISSION & CONDITIONS
+This licence does not grant any rights under trademark law and all such
+rights are reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of the Font Software, to propagate the Font Software, subject to
+the below conditions:
+
+1) Each copy of the Font Software must contain the above copyright
+notice and this licence. These can be included either as stand-alone
+text files, human-readable headers or in the appropriate machine-
+readable metadata fields within text or binary files as long as those
+fields can be easily viewed by the user.
+
+2) The font name complies with the following:
+(a) The Original Version must retain its name, unmodified.
+(b) Modified Versions which are Substantially Changed must be renamed to
+avoid use of the name of the Original Version or similar names entirely.
+(c) Modified Versions which are not Substantially Changed must be
+renamed to both (i) retain the name of the Original Version and (ii) add
+additional naming elements to distinguish the Modified Version from the
+Original Version. The name of such Modified Versions must be the name of
+the Original Version, with "derivative X" where X represents the name of
+the new work, appended to that name.
+
+3) The name(s) of the Copyright Holder(s) and any contributor to the
+Font Software shall not be used to promote, endorse or advertise any
+Modified Version, except (i) as required by this licence, (ii) to
+acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
+their explicit written permission.
+
+4) The Font Software, modified or unmodified, in part or in whole, must
+be distributed entirely under this licence, and must not be distributed
+under any other licence. The requirement for fonts to remain under this
+licence does not affect any document created using the Font Software,
+except any version of the Font Software extracted from a document
+created using the Font Software may only be distributed under this
+licence.
+
+TERMINATION
+This licence becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
+COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
+DEALINGS IN THE FONT SOFTWARE.
diff --git a/fonts/Ubuntu-Bold.ttf b/fonts/Ubuntu-Bold.ttf
new file mode 100644
index 0000000..7b85515
Binary files /dev/null and b/fonts/Ubuntu-Bold.ttf differ
diff --git a/fonts/Ubuntu-Medium.ttf b/fonts/Ubuntu-Medium.ttf
new file mode 100644
index 0000000..c6f048e
Binary files /dev/null and b/fonts/Ubuntu-Medium.ttf differ
diff --git a/fonts/Ubuntu-Regular.ttf b/fonts/Ubuntu-Regular.ttf
new file mode 100644
index 0000000..2001d6e
Binary files /dev/null and b/fonts/Ubuntu-Regular.ttf differ
diff --git a/fonts/UbuntuMono-Bold.ttf b/fonts/UbuntuMono-Bold.ttf
new file mode 100644
index 0000000..7bd6665
Binary files /dev/null and b/fonts/UbuntuMono-Bold.ttf differ
diff --git a/fonts/UbuntuMono-Regular.ttf b/fonts/UbuntuMono-Regular.ttf
new file mode 100644
index 0000000..fdd309d
Binary files /dev/null and b/fonts/UbuntuMono-Regular.ttf differ
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index 50a8a71..e87ddda 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -42,8 +42,9 @@ import workmanager_apple
let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "SecureClipboard")
if let messenger = registrar?.messenger() {
+ // App-neutral name shared with wallet-core's SecureClipboard (D10).
let channel = FlutterMethodChannel(
- name: "org.magicgrants.skylight/secure_clipboard",
+ name: "org.magicgrants.wallet/secure_clipboard",
binaryMessenger: messenger
)
channel.setMethodCallHandler { call, reply in
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 0d3791e..1e057aa 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -2,6 +2,7 @@
"@@locale": "en",
"continueText": "Continue",
"cancel": "Cancel",
+ "done": "Done",
"close": "Close",
"unknownError": "Unknown error.",
"warning": "Warning",
@@ -13,8 +14,12 @@
"addressCopied": "Address copied to clipboard",
"fieldEmptyError": "This field cannot be empty.",
"welcomeTitle": "Welcome!",
- "welcomeDescription": "Skylight is one of the simplest Monero wallets. We will help you set up a wallet and connect to a server.",
+ "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",
@@ -33,6 +38,16 @@
"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.",
+ "connectionTestStop": "Stop",
+ "connectionTestingTitle": "Testing connection",
+ "connectionTestingDetail": "Checking whether the server answers.",
+ "connectionTestAgain": "Test again",
+ "connectionResultWorksTitle": "Connection works",
+ "connectionResultFailedTitle": "Could not reach this server",
+ "connectionResultFailedDetail": "Nothing answered. Check the address and port, and whether the server accepts your connection.",
+ "connectionReachedOverTor": "Reached over Tor",
+ "connectionReachedViaProxy": "Reached through your proxy",
+ "connectionReachedDirect": "Reached directly",
"connectionIndicatorHttps": "HTTPS",
"connectionIndicatorLocal": "Local",
"connectionIndicatorTorInternal": "Internal Tor",
@@ -45,6 +60,11 @@
}
},
"settingsConnectionSettingsLabel": "Connection Settings",
+ "settingsConnectionServerLws": "LWS",
+ "settingsConnectionServerNode": "Node",
+ "settingsConnectionOverTor": "over Tor",
+ "settingsConnectionOverClearnet": "over Clearnet",
+ "settingsConnectionInLocalNetwork": "in Local Network",
"settingsBackgroundSyncLabel": "Background Sync",
"settingsBackgroundSyncDescription": "Periodically sync Monero in the background so it's up to date when you open the app. Only runs while charging and on WiFi.",
"settingsForegroundSyncLabel": "Continuous Sync",
@@ -63,13 +83,30 @@
"fiatApiSettingsModeTorOnly": "Tor-Only",
"fiatApiSettingsModeClearnet": "Clearnet-Only (Not Private)",
"fiatApiSettingsModeDisabled": "Disabled",
+ "fiatModeTorOnlyDesc": "Rates fetched over Tor · recommended",
+ "fiatModeClearnetDesc": "Not private — the rate server sees your IP address",
+ "fiatModeDisabledDesc": "No rates fetched, balances shown in crypto only",
"fiatApiSettingsDisplayCurrencyLabel": "Display Currency",
"createWalletTitle": "Create Wallet",
"createWalletDescription": "Do you already have a Monero wallet seed, or do you need to make a new one?",
"createWalletRestoreExistingButton": "Restore Existing",
+ "createWalletRestoreExistingDesc": "Enter an existing Monero seed",
"createWalletCreateNewButton": "Create New",
- "generateSeedTitle": "New Wallet",
+ "createWalletCreateNewDesc": "Skylight Wallet generates a new polyseed",
+ "createWalletPasswordTitle": "Create Wallet Password",
+ "createWalletPasswordDescription": "Create a password to protect your wallet. This password will be required to unlock your wallet.",
+ "createWalletPasswordHint": "Enter your password",
+ "createWalletConfirmPasswordHint": "Confirm your password",
+ "passwordTooShortError": "Password must be at least 8 characters long.",
+ "passwordsDoNotMatchError": "Passwords do not match.",
+ "generateSeedTitle": "Write these down, in order",
+ "generateSeedTitleCovered": "Your 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 on paper — not in a photo or a notes app.",
+ "generateSeedSubtitleRevealed": "Anyone with these words has your funds.",
+ "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",
"lwsDetailsTitle": "Wallet Details",
"lwsDetailsDescription": "You can use these details to whitelist this wallet on the light wallet server if needed.",
@@ -81,8 +118,40 @@
"restoreWalletSeedLabel": "Seed",
"restoreWalletRestoreHeightLabel": "Restore Height (optional)",
"restoreWalletRestoreDateLabel": "Restore Date (optional)",
+ "restoreWalletScanFrom": "Scan from",
+ "restoreWalletScanFromReason": "Earlier is slower but never misses funds.",
+ "restoreWalletNotSet": "Not set",
+ "restoreScanTitle": "When did this seed first hold funds?",
+ "restoreScanDescription": "Skylight Wallet only scans forward from this point. Guess early — a wrong-but-earlier answer costs sync time, a wrong-but-later one hides transactions.",
+ "restoreScanPickMonth": "Pick a month",
+ "restoreScanNotSure": "I'm not sure",
+ "restoreScanNotSureDesc": "Scan everything. Slower this first setup but not any slower later. Always complete.",
+ "restoreScanFromStart": "Genesis",
+ "restoreScanDone": "Done",
"restoreWalletRestoreButton": "Restore",
"restoreWalletInvalidMnemonic": "Invalid seed.",
+ "restoreWalletSeedLength": "Seed length",
+ "restoreWalletPaste": "Paste",
+ "restoreWalletSeedTypePolyseed": "Polyseed",
+ "restoreWalletSeedTypeBip39": "BIP39",
+ "restoreWalletSeedTypeLegacy": "Legacy",
+ "restoreWalletBadWord": "Word {position} isn't a BIP39 word.",
+ "@restoreWalletBadWord": {
+ "placeholders": {
+ "position": {
+ "type": "int"
+ }
+ }
+ },
+ "restoreWalletDidYouMean": "Did you mean {word}?",
+ "@restoreWalletDidYouMean": {
+ "placeholders": {
+ "word": {
+ "type": "String"
+ }
+ }
+ },
+ "restoreWalletChecksumError": "This isn't a valid seed phrase — check the words and their order.",
"navigationBarWallet": "Wallet",
"navigationBarSettings": "Settings",
"unlockButton": "Unlock",
@@ -105,6 +174,10 @@
"homeTransactionConfirmed": "Confirmed",
"homeNoTransactions": "No Transactions",
"homeFiatApiError": "Error connecting to fiat API",
+ "homeDisconnected": "Disconnected",
+ "coinHomeActivityTitle": "Activity",
+ "coinHomeReceived": "Received",
+ "coinHomeSent": "Sent",
"receiveTitle": "Receive",
"receivePrimaryAddressWarn": "Warning: Unless you know what you're doing, please consider using subaddresses for better privacy.",
"receiveShareButton": "Share",
@@ -112,6 +185,25 @@
"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",
+ "receiveAddressHeading": "Your {coin} address",
+ "@receiveAddressHeading": {
+ "placeholders": {
+ "coin": {
+ "type": "String"
+ }
+ }
+ },
+ "receiveBlockchainSubtitle": "{coin} blockchain",
+ "@receiveBlockchainSubtitle": {
+ "placeholders": {
+ "coin": {
+ "type": "String"
+ }
+ }
+ },
"sendTitle": "Send",
"sendSendButton": "Send",
"sendTransactionSuccessfullySent": "Transaction successfully sent!",
@@ -120,7 +212,12 @@
"sendInsufficientBalanceError": "Insufficient balance.",
"sendInsufficientBalanceToCoverFeeError": "Insufficient balance to cover the network fee.",
"settingsTitle": "Settings",
+ "settingsSectionGeneral": "General",
+ "settingsSectionBehaviour": "Behaviour",
+ "settingsSectionWallet": "Wallet",
+ "settingsSectionAbout": "About",
"settingsNotifyNewTxsLabel": "Notify New Transactions",
+ "settingsNotifyNewTxsDescription": "Shows a notification when you receive a transaction. When connected to a Monero node, Background Sync must also be enabled.",
"settingsNotifyNewTxsDescriptionIos": "Notifications will be delayed for Tor LWS connections.",
"settingsAppLockLabel": "App Lock",
"settingsAppLockUnlockReason": "Unlock Wallet",
@@ -135,8 +232,13 @@
"settingsThemeSystem": "System",
"settingsThemeLight": "Light",
"settingsThemeDark": "Dark",
+ "settingsThemeLightDesc": "Light and clean",
+ "settingsThemeDarkDesc": "Dark ground, easier at night",
+ "settingsThemeSystemDesc": "Follows your phone",
+ "settingsThemeSheetSubtitle": "Changes take effect straight away. Amount colours stay the same in both.",
"settingsLanguageLabel": "Language",
- "settingsFiatApiSettingsLabel": "Fiat API Settings",
+ "settingsLanguageSheetSubtitle": "Amounts and dates follow the language you pick.",
+ "settingsFiatApiSettingsLabel": "Fiat Display Settings",
"settingsLwsViewKeysLabel": "LWS View Keys",
"settingsLwsViewKeysButton": "View",
"settingsSecretKeysLabel": "Secret Restore Keys",
@@ -148,7 +250,7 @@
"settingsDeleteWalletButton": "Delete Wallet",
"settingsDeleteWalletDialogText": "Are you sure you want to delete your wallet? You will lose access to your funds unless you have backed up your seed phrase.",
"settingsDeleteWalletDialogDeleteButton": "Delete",
- "txDetailsTitle": "Transaction",
+ "txDetailsTitle": "Transaction Details",
"txDetailsHashLabel": "Hash",
"txDetailsTimeAndDateLabel": "Time and Date",
"txDetailsConfirmationHeightLabel": "Confirmation Height",
@@ -156,19 +258,33 @@
"txDetailsViewKeyLabel": "View Key",
"txDetailsRecipientsLabel": "Recipients",
"txDetailsChangeRecipientLabel": "Change",
+ "txDetailsCopyHint": "tap any value to copy",
+ "txDetailsFailed": "This transaction failed. The funds were not sent.",
+ "txDetailsUnknownStatus": "This transaction was not confirmed as sent. Check before sending again.",
"copiedToClipboard": "Copied to clipboard",
"lwsKeysTitle": "LWS Keys",
"lwsKeysPrimaryAddress": "Primary Address",
"lwsKeysRestoreHeight": "Restore Height",
"lwsKeysSecretViewKey": "Secret View Key",
+ "lwsKeysWarning": "Screenshots are blocked on this screen. The view key lets a server see everything you receive — share it only with a server you run or trust.",
"secretKeysTitle": "Secret Restore Keys",
+ "secretKeysDescription": "These seeds and keys restore full control of your wallet. Anyone who sees them can spend your funds.",
"secretKeysMnemonic": "Seed",
"secretKeysPublicSpendKey": "Public Spend Key",
"secretKeysSecretSpendKey": "Secret Spend Key",
"secretKeysPublicViewKey": "Public View Key",
+ "secretKeysWarning": "Never share these or enter them into any website. Store them offline.",
"scanQrTitle": "Scan QR Code",
"confirmSendTitle": "Confirm Send",
"confirmSendDescription": "Transactions are irreversible, so make sure that these details match exactly.",
+ "confirmSendHighFeeWarning": "The network fee is {percent} of the amount you are sending.",
+ "@confirmSendHighFeeWarning": {
+ "placeholders": {
+ "percent": {
+ "type": "String"
+ }
+ }
+ },
"addressBookTitle": "Address Book",
"addressBookAddContact": "Add Contact",
"addressBookEditContact": "Edit Contact",
@@ -189,6 +305,10 @@
"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",
@@ -200,6 +320,14 @@
"sendTransactionPriority": "Transaction Priority",
"sendFeeLabel": "Fee",
"sendContactsButton": "Contacts",
+ "sendToLabel": "To",
+ "sendPasteButton": "Paste",
+ "sendScanButton": "Scan",
+ "sendMaxButton": "MAX",
+ "sendNetworkFee": "Network fee",
+ "sendPriorityHeading": "Priority",
+ "sendPickContactTitle": "Send to a contact",
+ "sendAvailableSuffix": "available",
"sendFailedToGetFeesError": "Failed to get fees.",
"torInfoTitle": "Tor Built-in",
"torInfoDescription": "Skylight Wallet automatically uses built-in Tor to protect your internet connections.",
@@ -210,6 +338,13 @@
"torSettingsModeBuiltIn": "Built-in Tor",
"torSettingsModeExternal": "External Tor",
"torSettingsModeDisabled": "No Tor",
+ "torChoiceBuiltInDesc": "Bundled with Skylight Wallet · recommended",
+ "torChoiceExternalDesc": "Orbot, or a daemon you run yourself",
+ "torChoiceNoTorDesc": "Servers you connect to can see your IP address",
+ "torChoiceConnected": "Connected to Tor",
+ "torChoiceTestFailed": "Test failed",
+ "torChoiceTitle": "How should Skylight Wallet reach the network?",
+ "torChoiceSubtitle": "Nothing connects until you choose. Tor hides your IP address from the servers Skylight Wallet talks to.",
"torSettingsSocksPortLabel": "SOCKS Port",
"torSettingsSocksPortHint": "e.g. 9050",
"torSettingsUseOrbotLabel": "Use Orbot/InviZible",
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index 6d78804..b0cce47 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -105,6 +105,12 @@ abstract class AppLocalizations {
/// **'Cancel'**
String get cancel;
+ /// No description provided for @done.
+ ///
+ /// In en, this message translates to:
+ /// **'Done'**
+ String get done;
+
/// No description provided for @close.
///
/// In en, this message translates to:
@@ -174,7 +180,7 @@ abstract class AppLocalizations {
/// No description provided for @welcomeDescription.
///
/// In en, this message translates to:
- /// **'Skylight is one of the simplest Monero wallets. We will help you set up a wallet and connect to a server.'**
+ /// **'Skylight Wallet is one of the simplest Monero wallets. We will help you set up a wallet and connect to a server.'**
String get welcomeDescription;
/// No description provided for @welcomeGetStarted.
@@ -183,6 +189,30 @@ abstract class AppLocalizations {
/// **'Get Started'**
String get welcomeGetStarted;
+ /// No description provided for @welcomeAgreePrefix.
+ ///
+ /// In en, this message translates to:
+ /// **'By continuing you agree to the '**
+ String get welcomeAgreePrefix;
+
+ /// No description provided for @welcomeTermsLink.
+ ///
+ /// In en, this message translates to:
+ /// **'Terms of Service'**
+ String get welcomeTermsLink;
+
+ /// No description provided for @welcomeAgreeMiddle.
+ ///
+ /// In en, this message translates to:
+ /// **' and '**
+ String get welcomeAgreeMiddle;
+
+ /// No description provided for @welcomePrivacyLink.
+ ///
+ /// In en, this message translates to:
+ /// **'Privacy Policy'**
+ String get welcomePrivacyLink;
+
/// No description provided for @restoreWarningTitle.
///
/// In en, this message translates to:
@@ -291,6 +321,66 @@ abstract class AppLocalizations {
/// **'Removing protocol. Using HTTP for local addresses.'**
String get connectionProtocolHttp;
+ /// No description provided for @connectionTestStop.
+ ///
+ /// In en, this message translates to:
+ /// **'Stop'**
+ String get connectionTestStop;
+
+ /// No description provided for @connectionTestingTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Testing connection'**
+ String get connectionTestingTitle;
+
+ /// No description provided for @connectionTestingDetail.
+ ///
+ /// In en, this message translates to:
+ /// **'Checking whether the server answers.'**
+ String get connectionTestingDetail;
+
+ /// No description provided for @connectionTestAgain.
+ ///
+ /// In en, this message translates to:
+ /// **'Test again'**
+ String get connectionTestAgain;
+
+ /// No description provided for @connectionResultWorksTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Connection works'**
+ String get connectionResultWorksTitle;
+
+ /// No description provided for @connectionResultFailedTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Could not reach this server'**
+ String get connectionResultFailedTitle;
+
+ /// No description provided for @connectionResultFailedDetail.
+ ///
+ /// In en, this message translates to:
+ /// **'Nothing answered. Check the address and port, and whether the server accepts your connection.'**
+ String get connectionResultFailedDetail;
+
+ /// No description provided for @connectionReachedOverTor.
+ ///
+ /// In en, this message translates to:
+ /// **'Reached over Tor'**
+ String get connectionReachedOverTor;
+
+ /// No description provided for @connectionReachedViaProxy.
+ ///
+ /// In en, this message translates to:
+ /// **'Reached through your proxy'**
+ String get connectionReachedViaProxy;
+
+ /// No description provided for @connectionReachedDirect.
+ ///
+ /// In en, this message translates to:
+ /// **'Reached directly'**
+ String get connectionReachedDirect;
+
/// No description provided for @connectionIndicatorHttps.
///
/// In en, this message translates to:
@@ -321,6 +411,36 @@ abstract class AppLocalizations {
/// **'Connection Settings'**
String get settingsConnectionSettingsLabel;
+ /// No description provided for @settingsConnectionServerLws.
+ ///
+ /// In en, this message translates to:
+ /// **'LWS'**
+ String get settingsConnectionServerLws;
+
+ /// No description provided for @settingsConnectionServerNode.
+ ///
+ /// In en, this message translates to:
+ /// **'Node'**
+ String get settingsConnectionServerNode;
+
+ /// No description provided for @settingsConnectionOverTor.
+ ///
+ /// In en, this message translates to:
+ /// **'over Tor'**
+ String get settingsConnectionOverTor;
+
+ /// No description provided for @settingsConnectionOverClearnet.
+ ///
+ /// In en, this message translates to:
+ /// **'over Clearnet'**
+ String get settingsConnectionOverClearnet;
+
+ /// No description provided for @settingsConnectionInLocalNetwork.
+ ///
+ /// In en, this message translates to:
+ /// **'in Local Network'**
+ String get settingsConnectionInLocalNetwork;
+
/// No description provided for @settingsBackgroundSyncLabel.
///
/// In en, this message translates to:
@@ -387,6 +507,24 @@ abstract class AppLocalizations {
/// **'Disabled'**
String get fiatApiSettingsModeDisabled;
+ /// No description provided for @fiatModeTorOnlyDesc.
+ ///
+ /// In en, this message translates to:
+ /// **'Rates fetched over Tor · recommended'**
+ String get fiatModeTorOnlyDesc;
+
+ /// No description provided for @fiatModeClearnetDesc.
+ ///
+ /// In en, this message translates to:
+ /// **'Not private — the rate server sees your IP address'**
+ String get fiatModeClearnetDesc;
+
+ /// No description provided for @fiatModeDisabledDesc.
+ ///
+ /// In en, this message translates to:
+ /// **'No rates fetched, balances shown in crypto only'**
+ String get fiatModeDisabledDesc;
+
/// No description provided for @fiatApiSettingsDisplayCurrencyLabel.
///
/// In en, this message translates to:
@@ -411,24 +549,108 @@ abstract class AppLocalizations {
/// **'Restore Existing'**
String get createWalletRestoreExistingButton;
+ /// No description provided for @createWalletRestoreExistingDesc.
+ ///
+ /// In en, this message translates to:
+ /// **'Enter an existing Monero seed'**
+ String get createWalletRestoreExistingDesc;
+
/// No description provided for @createWalletCreateNewButton.
///
/// In en, this message translates to:
/// **'Create New'**
String get createWalletCreateNewButton;
+ /// No description provided for @createWalletCreateNewDesc.
+ ///
+ /// In en, this message translates to:
+ /// **'Skylight Wallet generates a new polyseed'**
+ String get createWalletCreateNewDesc;
+
+ /// No description provided for @createWalletPasswordTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Create Wallet Password'**
+ String get createWalletPasswordTitle;
+
+ /// No description provided for @createWalletPasswordDescription.
+ ///
+ /// In en, this message translates to:
+ /// **'Create a password to protect your wallet. This password will be required to unlock your wallet.'**
+ String get createWalletPasswordDescription;
+
+ /// No description provided for @createWalletPasswordHint.
+ ///
+ /// In en, this message translates to:
+ /// **'Enter your password'**
+ String get createWalletPasswordHint;
+
+ /// No description provided for @createWalletConfirmPasswordHint.
+ ///
+ /// In en, this message translates to:
+ /// **'Confirm your password'**
+ String get createWalletConfirmPasswordHint;
+
+ /// No description provided for @passwordTooShortError.
+ ///
+ /// In en, this message translates to:
+ /// **'Password must be at least 8 characters long.'**
+ String get passwordTooShortError;
+
+ /// No description provided for @passwordsDoNotMatchError.
+ ///
+ /// In en, this message translates to:
+ /// **'Passwords do not match.'**
+ String get passwordsDoNotMatchError;
+
/// No description provided for @generateSeedTitle.
///
/// In en, this message translates to:
- /// **'New Wallet'**
+ /// **'Write these down, in order'**
String get generateSeedTitle;
+ /// No description provided for @generateSeedTitleCovered.
+ ///
+ /// In en, this message translates to:
+ /// **'Your 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:
+ /// **'These words, in this order, are your wallet. Write them on paper — not in a photo or a notes app.'**
+ String get generateSeedSubtitleCovered;
+
+ /// No description provided for @generateSeedSubtitleRevealed.
+ ///
+ /// In en, this message translates to:
+ /// **'Anyone with these words has your funds.'**
+ String get generateSeedSubtitleRevealed;
+
+ /// No description provided for @generateSeedScreenshotNote.
+ ///
+ /// In en, this message translates to:
+ /// **'Screenshots are blocked on this screen. Make sure nobody is looking over your shoulder.'**
+ String get generateSeedScreenshotNote;
+
+ /// No description provided for @generateSeedReveal.
+ ///
+ /// In en, this message translates to:
+ /// **'Tap to reveal'**
+ String get generateSeedReveal;
+
+ /// No description provided for @generateSeedConfirm.
+ ///
+ /// In en, this message translates to:
+ /// **'I have written down all the words and stored them somewhere only I can reach.'**
+ String get generateSeedConfirm;
+
/// No description provided for @generateSeedContinueButton.
///
/// In en, this message translates to:
@@ -495,6 +717,66 @@ abstract class AppLocalizations {
/// **'Restore Date (optional)'**
String get restoreWalletRestoreDateLabel;
+ /// No description provided for @restoreWalletScanFrom.
+ ///
+ /// In en, this message translates to:
+ /// **'Scan from'**
+ String get restoreWalletScanFrom;
+
+ /// No description provided for @restoreWalletScanFromReason.
+ ///
+ /// In en, this message translates to:
+ /// **'Earlier is slower but never misses funds.'**
+ String get restoreWalletScanFromReason;
+
+ /// No description provided for @restoreWalletNotSet.
+ ///
+ /// In en, this message translates to:
+ /// **'Not set'**
+ String get restoreWalletNotSet;
+
+ /// No description provided for @restoreScanTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'When did this seed first hold funds?'**
+ String get restoreScanTitle;
+
+ /// No description provided for @restoreScanDescription.
+ ///
+ /// In en, this message translates to:
+ /// **'Skylight Wallet only scans forward from this point. Guess early — a wrong-but-earlier answer costs sync time, a wrong-but-later one hides transactions.'**
+ String get restoreScanDescription;
+
+ /// No description provided for @restoreScanPickMonth.
+ ///
+ /// In en, this message translates to:
+ /// **'Pick a month'**
+ String get restoreScanPickMonth;
+
+ /// No description provided for @restoreScanNotSure.
+ ///
+ /// In en, this message translates to:
+ /// **'I\'m not sure'**
+ String get restoreScanNotSure;
+
+ /// No description provided for @restoreScanNotSureDesc.
+ ///
+ /// In en, this message translates to:
+ /// **'Scan everything. Slower this first setup but not any slower later. Always complete.'**
+ String get restoreScanNotSureDesc;
+
+ /// No description provided for @restoreScanFromStart.
+ ///
+ /// In en, this message translates to:
+ /// **'Genesis'**
+ String get restoreScanFromStart;
+
+ /// No description provided for @restoreScanDone.
+ ///
+ /// In en, this message translates to:
+ /// **'Done'**
+ String get restoreScanDone;
+
/// No description provided for @restoreWalletRestoreButton.
///
/// In en, this message translates to:
@@ -507,6 +789,54 @@ abstract class AppLocalizations {
/// **'Invalid seed.'**
String get restoreWalletInvalidMnemonic;
+ /// No description provided for @restoreWalletSeedLength.
+ ///
+ /// In en, this message translates to:
+ /// **'Seed length'**
+ String get restoreWalletSeedLength;
+
+ /// No description provided for @restoreWalletPaste.
+ ///
+ /// In en, this message translates to:
+ /// **'Paste'**
+ String get restoreWalletPaste;
+
+ /// No description provided for @restoreWalletSeedTypePolyseed.
+ ///
+ /// In en, this message translates to:
+ /// **'Polyseed'**
+ String get restoreWalletSeedTypePolyseed;
+
+ /// No description provided for @restoreWalletSeedTypeBip39.
+ ///
+ /// In en, this message translates to:
+ /// **'BIP39'**
+ String get restoreWalletSeedTypeBip39;
+
+ /// No description provided for @restoreWalletSeedTypeLegacy.
+ ///
+ /// In en, this message translates to:
+ /// **'Legacy'**
+ String get restoreWalletSeedTypeLegacy;
+
+ /// No description provided for @restoreWalletBadWord.
+ ///
+ /// In en, this message translates to:
+ /// **'Word {position} isn\'t a BIP39 word.'**
+ String restoreWalletBadWord(int position);
+
+ /// No description provided for @restoreWalletDidYouMean.
+ ///
+ /// In en, this message translates to:
+ /// **'Did you mean {word}?'**
+ String restoreWalletDidYouMean(String word);
+
+ /// No description provided for @restoreWalletChecksumError.
+ ///
+ /// In en, this message translates to:
+ /// **'This isn\'t a valid seed phrase — check the words and their order.'**
+ String get restoreWalletChecksumError;
+
/// No description provided for @navigationBarWallet.
///
/// In en, this message translates to:
@@ -639,6 +969,30 @@ abstract class AppLocalizations {
/// **'Error connecting to fiat API'**
String get homeFiatApiError;
+ /// No description provided for @homeDisconnected.
+ ///
+ /// In en, this message translates to:
+ /// **'Disconnected'**
+ String get homeDisconnected;
+
+ /// No description provided for @coinHomeActivityTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Activity'**
+ String get coinHomeActivityTitle;
+
+ /// No description provided for @coinHomeReceived.
+ ///
+ /// In en, this message translates to:
+ /// **'Received'**
+ String get coinHomeReceived;
+
+ /// No description provided for @coinHomeSent.
+ ///
+ /// In en, this message translates to:
+ /// **'Sent'**
+ String get coinHomeSent;
+
/// No description provided for @receiveTitle.
///
/// In en, this message translates to:
@@ -681,6 +1035,36 @@ abstract class AppLocalizations {
/// **'You have reached the maximum number of subaddresses supported by this server. This is a used address.'**
String get receiveMaxSubaddressesReachedWarn;
+ /// No description provided for @receiveSubaddressTab.
+ ///
+ /// In en, this message translates to:
+ /// **'Subaddress'**
+ String get receiveSubaddressTab;
+
+ /// No description provided for @receivePrimaryTab.
+ ///
+ /// In en, this message translates to:
+ /// **'Primary address'**
+ String get receivePrimaryTab;
+
+ /// No description provided for @receiveCopyAddress.
+ ///
+ /// In en, this message translates to:
+ /// **'Copy address'**
+ String get receiveCopyAddress;
+
+ /// No description provided for @receiveAddressHeading.
+ ///
+ /// In en, this message translates to:
+ /// **'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:
@@ -729,12 +1113,42 @@ abstract class AppLocalizations {
/// **'Settings'**
String get settingsTitle;
+ /// No description provided for @settingsSectionGeneral.
+ ///
+ /// In en, this message translates to:
+ /// **'General'**
+ String get settingsSectionGeneral;
+
+ /// No description provided for @settingsSectionBehaviour.
+ ///
+ /// In en, this message translates to:
+ /// **'Behaviour'**
+ String get settingsSectionBehaviour;
+
+ /// No description provided for @settingsSectionWallet.
+ ///
+ /// In en, this message translates to:
+ /// **'Wallet'**
+ String get settingsSectionWallet;
+
+ /// No description provided for @settingsSectionAbout.
+ ///
+ /// In en, this message translates to:
+ /// **'About'**
+ String get settingsSectionAbout;
+
/// No description provided for @settingsNotifyNewTxsLabel.
///
/// In en, this message translates to:
/// **'Notify New Transactions'**
String get settingsNotifyNewTxsLabel;
+ /// No description provided for @settingsNotifyNewTxsDescription.
+ ///
+ /// In en, this message translates to:
+ /// **'Shows a notification when you receive a transaction. When connected to a Monero node, Background Sync must also be enabled.'**
+ String get settingsNotifyNewTxsDescription;
+
/// No description provided for @settingsNotifyNewTxsDescriptionIos.
///
/// In en, this message translates to:
@@ -819,16 +1233,46 @@ abstract class AppLocalizations {
/// **'Dark'**
String get settingsThemeDark;
+ /// No description provided for @settingsThemeLightDesc.
+ ///
+ /// In en, this message translates to:
+ /// **'Light and clean'**
+ String get settingsThemeLightDesc;
+
+ /// No description provided for @settingsThemeDarkDesc.
+ ///
+ /// In en, this message translates to:
+ /// **'Dark ground, easier at night'**
+ String get settingsThemeDarkDesc;
+
+ /// No description provided for @settingsThemeSystemDesc.
+ ///
+ /// In en, this message translates to:
+ /// **'Follows your phone'**
+ String get settingsThemeSystemDesc;
+
+ /// No description provided for @settingsThemeSheetSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Changes take effect straight away. Amount colours stay the same in both.'**
+ String get settingsThemeSheetSubtitle;
+
/// No description provided for @settingsLanguageLabel.
///
/// In en, this message translates to:
/// **'Language'**
String get settingsLanguageLabel;
+ /// No description provided for @settingsLanguageSheetSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Amounts and dates follow the language you pick.'**
+ String get settingsLanguageSheetSubtitle;
+
/// No description provided for @settingsFiatApiSettingsLabel.
///
/// In en, this message translates to:
- /// **'Fiat API Settings'**
+ /// **'Fiat Display Settings'**
String get settingsFiatApiSettingsLabel;
/// No description provided for @settingsLwsViewKeysLabel.
@@ -900,7 +1344,7 @@ abstract class AppLocalizations {
/// No description provided for @txDetailsTitle.
///
/// In en, this message translates to:
- /// **'Transaction'**
+ /// **'Transaction Details'**
String get txDetailsTitle;
/// No description provided for @txDetailsHashLabel.
@@ -945,6 +1389,24 @@ abstract class AppLocalizations {
/// **'Change'**
String get txDetailsChangeRecipientLabel;
+ /// No description provided for @txDetailsCopyHint.
+ ///
+ /// In en, this message translates to:
+ /// **'tap any value to copy'**
+ String get txDetailsCopyHint;
+
+ /// No description provided for @txDetailsFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'This transaction failed. The funds were not sent.'**
+ String get txDetailsFailed;
+
+ /// No description provided for @txDetailsUnknownStatus.
+ ///
+ /// In en, this message translates to:
+ /// **'This transaction was not confirmed as sent. Check before sending again.'**
+ String get txDetailsUnknownStatus;
+
/// No description provided for @copiedToClipboard.
///
/// In en, this message translates to:
@@ -975,12 +1437,24 @@ abstract class AppLocalizations {
/// **'Secret View Key'**
String get lwsKeysSecretViewKey;
+ /// No description provided for @lwsKeysWarning.
+ ///
+ /// In en, this message translates to:
+ /// **'Screenshots are blocked on this screen. The view key lets a server see everything you receive — share it only with a server you run or trust.'**
+ String get lwsKeysWarning;
+
/// No description provided for @secretKeysTitle.
///
/// In en, this message translates to:
/// **'Secret Restore Keys'**
String get secretKeysTitle;
+ /// No description provided for @secretKeysDescription.
+ ///
+ /// In en, this message translates to:
+ /// **'These seeds and keys restore full control of your wallet. Anyone who sees them can spend your funds.'**
+ String get secretKeysDescription;
+
/// No description provided for @secretKeysMnemonic.
///
/// In en, this message translates to:
@@ -1005,6 +1479,12 @@ abstract class AppLocalizations {
/// **'Public View Key'**
String get secretKeysPublicViewKey;
+ /// No description provided for @secretKeysWarning.
+ ///
+ /// In en, this message translates to:
+ /// **'Never share these or enter them into any website. Store them offline.'**
+ String get secretKeysWarning;
+
/// No description provided for @scanQrTitle.
///
/// In en, this message translates to:
@@ -1023,6 +1503,12 @@ abstract class AppLocalizations {
/// **'Transactions are irreversible, so make sure that these details match exactly.'**
String get confirmSendDescription;
+ /// No description provided for @confirmSendHighFeeWarning.
+ ///
+ /// In en, this message translates to:
+ /// **'The network fee is {percent} of the amount you are sending.'**
+ String confirmSendHighFeeWarning(String percent);
+
/// No description provided for @addressBookTitle.
///
/// In en, this message translates to:
@@ -1101,6 +1587,30 @@ abstract class AppLocalizations {
/// **'Contact Name'**
String get addressBookContactName;
+ /// No description provided for @addressBookNameHint.
+ ///
+ /// In en, this message translates to:
+ /// **'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:
+ /// **'A name and a Monero address to pay them on.'**
+ String get addressBookAddDescription;
+
+ /// No description provided for @addressBookEditDescription.
+ ///
+ /// In en, this message translates to:
+ /// **'Update this contact\'s name or address.'**
+ String get addressBookEditDescription;
+
/// No description provided for @addressBookUpdate.
///
/// In en, this message translates to:
@@ -1167,6 +1677,54 @@ abstract class AppLocalizations {
/// **'Contacts'**
String get sendContactsButton;
+ /// No description provided for @sendToLabel.
+ ///
+ /// In en, this message translates to:
+ /// **'To'**
+ String get sendToLabel;
+
+ /// No description provided for @sendPasteButton.
+ ///
+ /// In en, this message translates to:
+ /// **'Paste'**
+ String get sendPasteButton;
+
+ /// No description provided for @sendScanButton.
+ ///
+ /// In en, this message translates to:
+ /// **'Scan'**
+ String get sendScanButton;
+
+ /// No description provided for @sendMaxButton.
+ ///
+ /// In en, this message translates to:
+ /// **'MAX'**
+ String get sendMaxButton;
+
+ /// No description provided for @sendNetworkFee.
+ ///
+ /// In en, this message translates to:
+ /// **'Network fee'**
+ String get sendNetworkFee;
+
+ /// No description provided for @sendPriorityHeading.
+ ///
+ /// In en, this message translates to:
+ /// **'Priority'**
+ String get sendPriorityHeading;
+
+ /// No description provided for @sendPickContactTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Send to a contact'**
+ String get sendPickContactTitle;
+
+ /// No description provided for @sendAvailableSuffix.
+ ///
+ /// In en, this message translates to:
+ /// **'available'**
+ String get sendAvailableSuffix;
+
/// No description provided for @sendFailedToGetFeesError.
///
/// In en, this message translates to:
@@ -1227,6 +1785,48 @@ abstract class AppLocalizations {
/// **'No Tor'**
String get torSettingsModeDisabled;
+ /// No description provided for @torChoiceBuiltInDesc.
+ ///
+ /// In en, this message translates to:
+ /// **'Bundled with Skylight Wallet · recommended'**
+ String get torChoiceBuiltInDesc;
+
+ /// No description provided for @torChoiceExternalDesc.
+ ///
+ /// In en, this message translates to:
+ /// **'Orbot, or a daemon you run yourself'**
+ String get torChoiceExternalDesc;
+
+ /// No description provided for @torChoiceNoTorDesc.
+ ///
+ /// In en, this message translates to:
+ /// **'Servers you connect to can see your IP address'**
+ String get torChoiceNoTorDesc;
+
+ /// No description provided for @torChoiceConnected.
+ ///
+ /// In en, this message translates to:
+ /// **'Connected to Tor'**
+ String get torChoiceConnected;
+
+ /// No description provided for @torChoiceTestFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'Test failed'**
+ String get torChoiceTestFailed;
+
+ /// No description provided for @torChoiceTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'How should Skylight Wallet reach the network?'**
+ String get torChoiceTitle;
+
+ /// No description provided for @torChoiceSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Nothing connects until you choose. Tor hides your IP address from the servers Skylight Wallet talks to.'**
+ String get torChoiceSubtitle;
+
/// No description provided for @torSettingsSocksPortLabel.
///
/// In en, this message translates to:
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
index c8ddd3b..98d7bcb 100644
--- a/lib/l10n/app_localizations_en.dart
+++ b/lib/l10n/app_localizations_en.dart
@@ -14,6 +14,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get cancel => 'Cancel';
+ @override
+ String get done => 'Done';
+
@override
String get close => 'Close';
@@ -49,11 +52,23 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get welcomeDescription =>
- 'Skylight is one of the simplest Monero wallets. We will help you set up a wallet and connect to a server.';
+ 'Skylight Wallet is one of the simplest Monero wallets. We will help you set up a wallet and connect to a server.';
@override
String get welcomeGetStarted => 'Get Started';
+ @override
+ String get welcomeAgreePrefix => 'By continuing you agree to the ';
+
+ @override
+ String get welcomeTermsLink => 'Terms of Service';
+
+ @override
+ String get welcomeAgreeMiddle => ' and ';
+
+ @override
+ String get welcomePrivacyLink => 'Privacy Policy';
+
@override
String get restoreWarningTitle => 'Restore Warning';
@@ -111,6 +126,37 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get connectionProtocolHttp => 'Removing protocol. Using HTTP for local addresses.';
+ @override
+ String get connectionTestStop => 'Stop';
+
+ @override
+ String get connectionTestingTitle => 'Testing connection';
+
+ @override
+ String get connectionTestingDetail => 'Checking whether the server answers.';
+
+ @override
+ String get connectionTestAgain => 'Test again';
+
+ @override
+ String get connectionResultWorksTitle => 'Connection works';
+
+ @override
+ String get connectionResultFailedTitle => 'Could not reach this server';
+
+ @override
+ String get connectionResultFailedDetail =>
+ 'Nothing answered. Check the address and port, and whether the server accepts your connection.';
+
+ @override
+ String get connectionReachedOverTor => 'Reached over Tor';
+
+ @override
+ String get connectionReachedViaProxy => 'Reached through your proxy';
+
+ @override
+ String get connectionReachedDirect => 'Reached directly';
+
@override
String get connectionIndicatorHttps => 'HTTPS';
@@ -128,6 +174,21 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settingsConnectionSettingsLabel => 'Connection Settings';
+ @override
+ String get settingsConnectionServerLws => 'LWS';
+
+ @override
+ String get settingsConnectionServerNode => 'Node';
+
+ @override
+ String get settingsConnectionOverTor => 'over Tor';
+
+ @override
+ String get settingsConnectionOverClearnet => 'over Clearnet';
+
+ @override
+ String get settingsConnectionInLocalNetwork => 'in Local Network';
+
@override
String get settingsBackgroundSyncLabel => 'Background Sync';
@@ -165,6 +226,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get fiatApiSettingsModeDisabled => 'Disabled';
+ @override
+ String get fiatModeTorOnlyDesc => 'Rates fetched over Tor · recommended';
+
+ @override
+ String get fiatModeClearnetDesc => 'Not private — the rate server sees your IP address';
+
+ @override
+ String get fiatModeDisabledDesc => 'No rates fetched, balances shown in crypto only';
+
@override
String get fiatApiSettingsDisplayCurrencyLabel => 'Display Currency';
@@ -178,16 +248,62 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get createWalletRestoreExistingButton => 'Restore Existing';
+ @override
+ String get createWalletRestoreExistingDesc => 'Enter an existing Monero seed';
+
@override
String get createWalletCreateNewButton => 'Create New';
@override
- String get generateSeedTitle => 'New Wallet';
+ String get createWalletCreateNewDesc => 'Skylight Wallet generates a new polyseed';
+
+ @override
+ String get createWalletPasswordTitle => 'Create Wallet Password';
+
+ @override
+ String get createWalletPasswordDescription =>
+ 'Create a password to protect your wallet. This password will be required to unlock your wallet.';
+
+ @override
+ String get createWalletPasswordHint => 'Enter your password';
+
+ @override
+ String get createWalletConfirmPasswordHint => 'Confirm your password';
+
+ @override
+ String get passwordTooShortError => 'Password must be at least 8 characters long.';
+
+ @override
+ String get passwordsDoNotMatchError => 'Passwords do not match.';
+
+ @override
+ String get generateSeedTitle => 'Write these down, in order';
+
+ @override
+ String get generateSeedTitleCovered => 'Your 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 on paper — not in a photo or a notes app.';
+
+ @override
+ String get generateSeedSubtitleRevealed => 'Anyone with these words has your funds.';
+
+ @override
+ String get generateSeedScreenshotNote =>
+ 'Screenshots are blocked on this screen. Make sure nobody is looking over your shoulder.';
+
+ @override
+ String get generateSeedReveal => 'Tap to reveal';
+
+ @override
+ String get generateSeedConfirm =>
+ 'I have written down all the words and stored them somewhere only I can reach.';
+
@override
String get generateSeedContinueButton => 'I Wrote It Down';
@@ -223,12 +339,73 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get restoreWalletRestoreDateLabel => 'Restore Date (optional)';
+ @override
+ String get restoreWalletScanFrom => 'Scan from';
+
+ @override
+ String get restoreWalletScanFromReason => 'Earlier is slower but never misses funds.';
+
+ @override
+ String get restoreWalletNotSet => 'Not set';
+
+ @override
+ String get restoreScanTitle => 'When did this seed first hold funds?';
+
+ @override
+ String get restoreScanDescription =>
+ 'Skylight Wallet only scans forward from this point. Guess early — a wrong-but-earlier answer costs sync time, a wrong-but-later one hides transactions.';
+
+ @override
+ String get restoreScanPickMonth => 'Pick a month';
+
+ @override
+ String get restoreScanNotSure => 'I\'m not sure';
+
+ @override
+ String get restoreScanNotSureDesc =>
+ 'Scan everything. Slower this first setup but not any slower later. Always complete.';
+
+ @override
+ String get restoreScanFromStart => 'Genesis';
+
+ @override
+ String get restoreScanDone => 'Done';
+
@override
String get restoreWalletRestoreButton => 'Restore';
@override
String get restoreWalletInvalidMnemonic => 'Invalid seed.';
+ @override
+ String get restoreWalletSeedLength => 'Seed length';
+
+ @override
+ String get restoreWalletPaste => 'Paste';
+
+ @override
+ String get restoreWalletSeedTypePolyseed => 'Polyseed';
+
+ @override
+ String get restoreWalletSeedTypeBip39 => 'BIP39';
+
+ @override
+ String get restoreWalletSeedTypeLegacy => 'Legacy';
+
+ @override
+ String restoreWalletBadWord(int position) {
+ return 'Word $position isn\'t a BIP39 word.';
+ }
+
+ @override
+ String restoreWalletDidYouMean(String word) {
+ return 'Did you mean $word?';
+ }
+
+ @override
+ String get restoreWalletChecksumError =>
+ 'This isn\'t a valid seed phrase — check the words and their order.';
+
@override
String get navigationBarWallet => 'Wallet';
@@ -295,6 +472,18 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get homeFiatApiError => 'Error connecting to fiat API';
+ @override
+ String get homeDisconnected => 'Disconnected';
+
+ @override
+ String get coinHomeActivityTitle => 'Activity';
+
+ @override
+ String get coinHomeReceived => 'Received';
+
+ @override
+ String get coinHomeSent => 'Sent';
+
@override
String get receiveTitle => 'Receive';
@@ -319,6 +508,25 @@ class AppLocalizationsEn extends AppLocalizations {
String get receiveMaxSubaddressesReachedWarn =>
'You have reached the maximum number of subaddresses supported by this server. This is a used address.';
+ @override
+ String get receiveSubaddressTab => 'Subaddress';
+
+ @override
+ String get receivePrimaryTab => 'Primary address';
+
+ @override
+ String get receiveCopyAddress => 'Copy address';
+
+ @override
+ String receiveAddressHeading(String coin) {
+ return 'Your $coin address';
+ }
+
+ @override
+ String receiveBlockchainSubtitle(String coin) {
+ return '$coin blockchain';
+ }
+
@override
String get sendTitle => 'Send';
@@ -344,9 +552,25 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settingsTitle => 'Settings';
+ @override
+ String get settingsSectionGeneral => 'General';
+
+ @override
+ String get settingsSectionBehaviour => 'Behaviour';
+
+ @override
+ String get settingsSectionWallet => 'Wallet';
+
+ @override
+ String get settingsSectionAbout => 'About';
+
@override
String get settingsNotifyNewTxsLabel => 'Notify New Transactions';
+ @override
+ String get settingsNotifyNewTxsDescription =>
+ 'Shows a notification when you receive a transaction. When connected to a Monero node, Background Sync must also be enabled.';
+
@override
String get settingsNotifyNewTxsDescriptionIos =>
'Notifications will be delayed for Tor LWS connections.';
@@ -393,11 +617,27 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settingsThemeDark => 'Dark';
+ @override
+ String get settingsThemeLightDesc => 'Light and clean';
+
+ @override
+ String get settingsThemeDarkDesc => 'Dark ground, easier at night';
+
+ @override
+ String get settingsThemeSystemDesc => 'Follows your phone';
+
+ @override
+ String get settingsThemeSheetSubtitle =>
+ 'Changes take effect straight away. Amount colours stay the same in both.';
+
@override
String get settingsLanguageLabel => 'Language';
@override
- String get settingsFiatApiSettingsLabel => 'Fiat API Settings';
+ String get settingsLanguageSheetSubtitle => 'Amounts and dates follow the language you pick.';
+
+ @override
+ String get settingsFiatApiSettingsLabel => 'Fiat Display Settings';
@override
String get settingsLwsViewKeysLabel => 'LWS View Keys';
@@ -436,7 +676,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get settingsDeleteWalletDialogDeleteButton => 'Delete';
@override
- String get txDetailsTitle => 'Transaction';
+ String get txDetailsTitle => 'Transaction Details';
@override
String get txDetailsHashLabel => 'Hash';
@@ -459,6 +699,16 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get txDetailsChangeRecipientLabel => 'Change';
+ @override
+ String get txDetailsCopyHint => 'tap any value to copy';
+
+ @override
+ String get txDetailsFailed => 'This transaction failed. The funds were not sent.';
+
+ @override
+ String get txDetailsUnknownStatus =>
+ 'This transaction was not confirmed as sent. Check before sending again.';
+
@override
String get copiedToClipboard => 'Copied to clipboard';
@@ -474,9 +724,17 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get lwsKeysSecretViewKey => 'Secret View Key';
+ @override
+ String get lwsKeysWarning =>
+ 'Screenshots are blocked on this screen. The view key lets a server see everything you receive — share it only with a server you run or trust.';
+
@override
String get secretKeysTitle => 'Secret Restore Keys';
+ @override
+ String get secretKeysDescription =>
+ 'These seeds and keys restore full control of your wallet. Anyone who sees them can spend your funds.';
+
@override
String get secretKeysMnemonic => 'Seed';
@@ -489,6 +747,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get secretKeysPublicViewKey => 'Public View Key';
+ @override
+ String get secretKeysWarning =>
+ 'Never share these or enter them into any website. Store them offline.';
+
@override
String get scanQrTitle => 'Scan QR Code';
@@ -499,6 +761,11 @@ class AppLocalizationsEn extends AppLocalizations {
String get confirmSendDescription =>
'Transactions are irreversible, so make sure that these details match exactly.';
+ @override
+ String confirmSendHighFeeWarning(String percent) {
+ return 'The network fee is $percent of the amount you are sending.';
+ }
+
@override
String get addressBookTitle => 'Address Book';
@@ -540,6 +807,18 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get addressBookContactName => 'Contact Name';
+ @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.';
+
+ @override
+ String get addressBookEditDescription => 'Update this contact\'s name or address.';
+
@override
String get addressBookUpdate => 'Update';
@@ -573,6 +852,30 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get sendContactsButton => 'Contacts';
+ @override
+ String get sendToLabel => 'To';
+
+ @override
+ String get sendPasteButton => 'Paste';
+
+ @override
+ String get sendScanButton => 'Scan';
+
+ @override
+ String get sendMaxButton => 'MAX';
+
+ @override
+ String get sendNetworkFee => 'Network fee';
+
+ @override
+ String get sendPriorityHeading => 'Priority';
+
+ @override
+ String get sendPickContactTitle => 'Send to a contact';
+
+ @override
+ String get sendAvailableSuffix => 'available';
+
@override
String get sendFailedToGetFeesError => 'Failed to get fees.';
@@ -604,6 +907,28 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get torSettingsModeDisabled => 'No Tor';
+ @override
+ String get torChoiceBuiltInDesc => 'Bundled with Skylight Wallet · recommended';
+
+ @override
+ String get torChoiceExternalDesc => 'Orbot, or a daemon you run yourself';
+
+ @override
+ String get torChoiceNoTorDesc => 'Servers you connect to can see your IP address';
+
+ @override
+ String get torChoiceConnected => 'Connected to Tor';
+
+ @override
+ String get torChoiceTestFailed => 'Test failed';
+
+ @override
+ String get torChoiceTitle => 'How should Skylight Wallet reach the network?';
+
+ @override
+ String get torChoiceSubtitle =>
+ 'Nothing connects until you choose. Tor hides your IP address from the servers Skylight Wallet talks to.';
+
@override
String get torSettingsSocksPortLabel => 'SOCKS Port';
diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart
index 7b60b7a..52b2ecd 100644
--- a/lib/l10n/app_localizations_pt.dart
+++ b/lib/l10n/app_localizations_pt.dart
@@ -14,6 +14,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get cancel => 'Cancelar';
+ @override
+ String get done => 'Concluir';
+
@override
String get close => 'Fechar';
@@ -49,11 +52,23 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get welcomeDescription =>
- 'A Skylight é uma das mais simples carteiras de Monero. Nós o ajudaremos a configurar uma carteira e se conectar à um servidor.';
+ 'A Skylight Wallet é uma das mais simples carteiras de Monero. Nós o ajudaremos a configurar uma carteira e se conectar à um servidor.';
@override
String get welcomeGetStarted => 'Começar';
+ @override
+ String get welcomeAgreePrefix => 'Ao continuar, você concorda com os ';
+
+ @override
+ String get welcomeTermsLink => 'Termos de Serviço';
+
+ @override
+ String get welcomeAgreeMiddle => ' e a ';
+
+ @override
+ String get welcomePrivacyLink => 'Política de Privacidade';
+
@override
String get restoreWarningTitle => 'Aviso de Restauração';
@@ -111,6 +126,37 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get connectionProtocolHttp => 'Removendo protocolo. Usando HTTP para endereços locais.';
+ @override
+ String get connectionTestStop => 'Parar';
+
+ @override
+ String get connectionTestingTitle => 'Testando conexão';
+
+ @override
+ String get connectionTestingDetail => 'Verificando se o servidor responde.';
+
+ @override
+ String get connectionTestAgain => 'Testar novamente';
+
+ @override
+ String get connectionResultWorksTitle => 'A conexão funciona';
+
+ @override
+ String get connectionResultFailedTitle => 'Não foi possível acessar este servidor';
+
+ @override
+ String get connectionResultFailedDetail =>
+ 'Nada respondeu. Verifique o endereço e a porta, e se o servidor aceita sua conexão.';
+
+ @override
+ String get connectionReachedOverTor => 'Acessado via Tor';
+
+ @override
+ String get connectionReachedViaProxy => 'Acessado pelo seu proxy';
+
+ @override
+ String get connectionReachedDirect => 'Acessado diretamente';
+
@override
String get connectionIndicatorHttps => 'HTTPS';
@@ -128,6 +174,21 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settingsConnectionSettingsLabel => 'Configurações de conexão';
+ @override
+ String get settingsConnectionServerLws => 'LWS';
+
+ @override
+ String get settingsConnectionServerNode => 'Nó';
+
+ @override
+ String get settingsConnectionOverTor => 'via Tor';
+
+ @override
+ String get settingsConnectionOverClearnet => 'via Clearnet';
+
+ @override
+ String get settingsConnectionInLocalNetwork => 'na rede local';
+
@override
String get settingsBackgroundSyncLabel => 'Sincronização em segundo plano';
@@ -165,6 +226,15 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get fiatApiSettingsModeDisabled => 'Desativado';
+ @override
+ String get fiatModeTorOnlyDesc => 'Cotações buscadas pelo Tor · recomendado';
+
+ @override
+ String get fiatModeClearnetDesc => 'Não privado — o servidor de cotações vê seu endereço IP';
+
+ @override
+ String get fiatModeDisabledDesc => 'Sem cotações; saldos exibidos apenas em cripto';
+
@override
String get fiatApiSettingsDisplayCurrencyLabel => 'Moeda de Exibição';
@@ -178,16 +248,62 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get createWalletRestoreExistingButton => 'Restaurar Existente';
+ @override
+ String get createWalletRestoreExistingDesc => 'Insira uma semente Monero existente';
+
@override
String get createWalletCreateNewButton => 'Criar Nova';
@override
- String get generateSeedTitle => 'Nova Carteira';
+ String get createWalletCreateNewDesc => 'A Skylight Wallet gera uma nova polyseed';
+
+ @override
+ String get createWalletPasswordTitle => 'Criar Senha da Carteira';
+
+ @override
+ String get createWalletPasswordDescription =>
+ 'Crie uma senha para proteger sua carteira. Esta senha será necessária para desbloquear sua carteira.';
+
+ @override
+ String get createWalletPasswordHint => 'Digite sua senha';
+
+ @override
+ String get createWalletConfirmPasswordHint => 'Confirme sua senha';
+
+ @override
+ String get passwordTooShortError => 'A senha deve ter pelo menos 8 caracteres.';
+
+ @override
+ String get passwordsDoNotMatchError => 'As senhas não coincidem.';
+
+ @override
+ String get generateSeedTitle => 'Anote-as, em ordem';
+
+ @override
+ String get generateSeedTitleCovered => 'Sua 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 no papel — não em uma foto ou app de notas.';
+
+ @override
+ String get generateSeedSubtitleRevealed => 'Qualquer pessoa com estas palavras tem seus fundos.';
+
+ @override
+ String get generateSeedScreenshotNote =>
+ 'As capturas de tela estão bloqueadas nesta tela. Certifique-se de que ninguém está olhando por cima do seu ombro.';
+
+ @override
+ String get generateSeedReveal => 'Toque para revelar';
+
+ @override
+ String get generateSeedConfirm =>
+ 'Anotei todas as palavras e as guardei em um lugar que só eu posso acessar.';
+
@override
String get generateSeedContinueButton => 'Eu anotei';
@@ -223,12 +339,73 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get restoreWalletRestoreDateLabel => 'Data de Restauração (opcional)';
+ @override
+ String get restoreWalletScanFrom => 'Escanear a partir de';
+
+ @override
+ String get restoreWalletScanFromReason => 'Mais cedo é mais lento, mas nunca perde fundos.';
+
+ @override
+ String get restoreWalletNotSet => 'Não definido';
+
+ @override
+ String get restoreScanTitle => 'Quando esta seed teve fundos pela primeira vez?';
+
+ @override
+ String get restoreScanDescription =>
+ 'A Skylight Wallet só escaneia a partir deste ponto. Chute cedo — uma resposta mais antiga custa tempo de sincronização, uma mais recente esconde transações.';
+
+ @override
+ String get restoreScanPickMonth => 'Escolher um mês';
+
+ @override
+ String get restoreScanNotSure => 'Não tenho certeza';
+
+ @override
+ String get restoreScanNotSureDesc =>
+ 'Escanear tudo. Mais lento nesta configuração inicial, mas não depois. Sempre completo.';
+
+ @override
+ String get restoreScanFromStart => 'Genesis';
+
+ @override
+ String get restoreScanDone => 'Concluir';
+
@override
String get restoreWalletRestoreButton => 'Restaurar';
@override
String get restoreWalletInvalidMnemonic => 'Semente inválida.';
+ @override
+ String get restoreWalletSeedLength => 'Tamanho da semente';
+
+ @override
+ String get restoreWalletPaste => 'Colar';
+
+ @override
+ String get restoreWalletSeedTypePolyseed => 'Polyseed';
+
+ @override
+ String get restoreWalletSeedTypeBip39 => 'BIP39';
+
+ @override
+ String get restoreWalletSeedTypeLegacy => 'Legado';
+
+ @override
+ String restoreWalletBadWord(int position) {
+ return 'A palavra $position não é uma palavra BIP39.';
+ }
+
+ @override
+ String restoreWalletDidYouMean(String word) {
+ return 'Você quis dizer $word?';
+ }
+
+ @override
+ String get restoreWalletChecksumError =>
+ 'Esta não é uma frase-semente válida — verifique as palavras e sua ordem.';
+
@override
String get navigationBarWallet => 'Carteira';
@@ -295,6 +472,18 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get homeFiatApiError => 'Erro ao conectar à API de cotação';
+ @override
+ String get homeDisconnected => 'Desconectado';
+
+ @override
+ String get coinHomeActivityTitle => 'Atividade';
+
+ @override
+ String get coinHomeReceived => 'Recebido';
+
+ @override
+ String get coinHomeSent => 'Enviado';
+
@override
String get receiveTitle => 'Receber';
@@ -319,6 +508,25 @@ class AppLocalizationsPt extends AppLocalizations {
String get receiveMaxSubaddressesReachedWarn =>
'Você atingiu o número máximo de subendereços suportados por este servidor. Este é um endereço já usado.';
+ @override
+ String get receiveSubaddressTab => 'Subendereço';
+
+ @override
+ String get receivePrimaryTab => 'Endereço primário';
+
+ @override
+ String get receiveCopyAddress => 'Copiar endereço';
+
+ @override
+ String receiveAddressHeading(String coin) {
+ return 'Seu endereço $coin';
+ }
+
+ @override
+ String receiveBlockchainSubtitle(String coin) {
+ return 'Blockchain $coin';
+ }
+
@override
String get sendTitle => 'Enviar';
@@ -344,9 +552,25 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settingsTitle => 'Configurações';
+ @override
+ String get settingsSectionGeneral => 'Geral';
+
+ @override
+ String get settingsSectionBehaviour => 'Comportamento';
+
+ @override
+ String get settingsSectionWallet => 'Carteira';
+
+ @override
+ String get settingsSectionAbout => 'Sobre';
+
@override
String get settingsNotifyNewTxsLabel => 'Notificar Novas Transações';
+ @override
+ String get settingsNotifyNewTxsDescription =>
+ 'Mostra uma notificação quando você recebe uma transação. Ao conectar-se a um nó Monero, a Sincronização em Segundo Plano também precisa estar ativada.';
+
@override
String get settingsNotifyNewTxsDescriptionIos =>
'As notificações serão atrasadas para conexões LWS via Tor.';
@@ -393,11 +617,27 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settingsThemeDark => 'Escuro';
+ @override
+ String get settingsThemeLightDesc => 'Claro e limpo';
+
+ @override
+ String get settingsThemeDarkDesc => 'Fundo escuro, melhor à noite';
+
+ @override
+ String get settingsThemeSystemDesc => 'Segue o seu telefone';
+
+ @override
+ String get settingsThemeSheetSubtitle =>
+ 'As mudanças têm efeito imediato. As cores dos valores permanecem iguais nos dois.';
+
@override
String get settingsLanguageLabel => 'Idioma';
@override
- String get settingsFiatApiSettingsLabel => 'Configs. da API de Moeda Local';
+ String get settingsLanguageSheetSubtitle => 'Valores e datas seguem o idioma escolhido.';
+
+ @override
+ String get settingsFiatApiSettingsLabel => 'Exibição de Moeda Local';
@override
String get settingsLwsViewKeysLabel => 'Chaves de Visualização do LWS';
@@ -436,7 +676,7 @@ class AppLocalizationsPt extends AppLocalizations {
String get settingsDeleteWalletDialogDeleteButton => 'Excluir';
@override
- String get txDetailsTitle => 'Transação';
+ String get txDetailsTitle => 'Detalhes da transação';
@override
String get txDetailsHashLabel => 'Hash';
@@ -459,6 +699,16 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get txDetailsChangeRecipientLabel => 'Troco';
+ @override
+ String get txDetailsCopyHint => 'toque em qualquer valor para copiar';
+
+ @override
+ String get txDetailsFailed => 'Esta transação falhou. Os fundos não foram enviados.';
+
+ @override
+ String get txDetailsUnknownStatus =>
+ 'Esta transação não foi confirmada como enviada. Verifique antes de enviar novamente.';
+
@override
String get copiedToClipboard => 'Copiado para a área de transferência';
@@ -474,9 +724,17 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get lwsKeysSecretViewKey => 'Chave Privada de Visualização';
+ @override
+ String get lwsKeysWarning =>
+ 'Capturas de tela estão bloqueadas nesta tela. A chave de visualização permite que um servidor veja tudo o que você recebe — compartilhe-a apenas com um servidor que você opera ou confia.';
+
@override
String get secretKeysTitle => 'Chaves Privadas de Restauração';
+ @override
+ String get secretKeysDescription =>
+ 'Estas sementes e chaves restauram o controle total da sua carteira. Qualquer pessoa que as veja pode gastar seus fundos.';
+
@override
String get secretKeysMnemonic => 'Semente';
@@ -489,6 +747,10 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get secretKeysPublicViewKey => 'Chave Pública de Visualização';
+ @override
+ String get secretKeysWarning =>
+ 'Nunca as compartilhe ou insira em qualquer site. Guarde-as offline.';
+
@override
String get scanQrTitle => 'Escanear QR Code';
@@ -499,6 +761,11 @@ class AppLocalizationsPt extends AppLocalizations {
String get confirmSendDescription =>
'As transações são irreversíveis, então verifique se estes detalhes correspondem exatamente.';
+ @override
+ String confirmSendHighFeeWarning(String percent) {
+ return 'A taxa de rede é $percent do valor que você está enviando.';
+ }
+
@override
String get addressBookTitle => 'Lista de Contatos';
@@ -540,6 +807,18 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get addressBookContactName => 'Nome do Contato';
+ @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.';
+
+ @override
+ String get addressBookEditDescription => 'Atualize o nome ou o endereço deste contato.';
+
@override
String get addressBookUpdate => 'Atualizar';
@@ -573,6 +852,30 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get sendContactsButton => 'Contatos';
+ @override
+ String get sendToLabel => 'Para';
+
+ @override
+ String get sendPasteButton => 'Colar';
+
+ @override
+ String get sendScanButton => 'Escanear';
+
+ @override
+ String get sendMaxButton => 'MÁX';
+
+ @override
+ String get sendNetworkFee => 'Taxa de rede';
+
+ @override
+ String get sendPriorityHeading => 'Prioridade';
+
+ @override
+ String get sendPickContactTitle => 'Enviar para um contato';
+
+ @override
+ String get sendAvailableSuffix => 'disponível';
+
@override
String get sendFailedToGetFeesError => 'Não foi possível carregar taxas.';
@@ -581,7 +884,7 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get torInfoDescription =>
- 'A Carteira Skylight usa automaticamente Tor integrado para proteger suas conexões de internet.';
+ 'A Skylight Wallet usa automaticamente Tor integrado para proteger suas conexões de internet.';
@override
String get torInfoContinueButton => 'Continuar';
@@ -604,6 +907,29 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get torSettingsModeDisabled => 'Sem Tor';
+ @override
+ String get torChoiceBuiltInDesc => 'Incluído na Skylight Wallet · recomendado';
+
+ @override
+ String get torChoiceExternalDesc => 'Orbot, ou um daemon que você mesmo executa';
+
+ @override
+ String get torChoiceNoTorDesc =>
+ 'Os servidores aos quais você se conecta podem ver seu endereço IP';
+
+ @override
+ String get torChoiceConnected => 'Conectado ao Tor';
+
+ @override
+ String get torChoiceTestFailed => 'Falha no teste';
+
+ @override
+ String get torChoiceTitle => 'Como a Skylight Wallet deve acessar a rede?';
+
+ @override
+ String get torChoiceSubtitle =>
+ 'Nada se conecta até você escolher. O Tor oculta seu endereço IP dos servidores com os quais a Skylight Wallet se comunica.';
+
@override
String get torSettingsSocksPortLabel => 'Porta SOCKS';
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index 830b699..e75d480 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -2,6 +2,7 @@
"@@locale": "pt",
"continueText": "Continuar",
"cancel": "Cancelar",
+ "done": "Concluir",
"close": "Fechar",
"unknownError": "Erro desconhecido.",
"warning": "Atenção",
@@ -13,8 +14,12 @@
"addressCopied": "Endereço copiado para a área de transferência",
"fieldEmptyError": "Este campo não pode ficar vazio.",
"welcomeTitle": "Bem-vindo!",
- "welcomeDescription": "A Skylight é uma das mais simples carteiras de Monero. Nós o ajudaremos a configurar uma carteira e se conectar à um servidor.",
+ "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",
@@ -33,6 +38,16 @@
"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.",
"connectionProtocolHttps": "Removendo protocolo. Usando HTTPS para domínios.",
"connectionProtocolHttp": "Removendo protocolo. Usando HTTP para endereços locais.",
+ "connectionTestStop": "Parar",
+ "connectionTestingTitle": "Testando conexão",
+ "connectionTestingDetail": "Verificando se o servidor responde.",
+ "connectionTestAgain": "Testar novamente",
+ "connectionResultWorksTitle": "A conexão funciona",
+ "connectionResultFailedTitle": "Não foi possível acessar este servidor",
+ "connectionResultFailedDetail": "Nada respondeu. Verifique o endereço e a porta, e se o servidor aceita sua conexão.",
+ "connectionReachedOverTor": "Acessado via Tor",
+ "connectionReachedViaProxy": "Acessado pelo seu proxy",
+ "connectionReachedDirect": "Acessado diretamente",
"connectionIndicatorHttps": "HTTPS",
"connectionIndicatorLocal": "Local",
"connectionIndicatorTorInternal": "Tor Interno",
@@ -45,6 +60,11 @@
}
},
"settingsConnectionSettingsLabel": "Configurações de conexão",
+ "settingsConnectionServerLws": "LWS",
+ "settingsConnectionServerNode": "Nó",
+ "settingsConnectionOverTor": "via Tor",
+ "settingsConnectionOverClearnet": "via Clearnet",
+ "settingsConnectionInLocalNetwork": "na rede local",
"settingsBackgroundSyncLabel": "Sincronização em segundo plano",
"settingsBackgroundSyncDescription": "Sincroniza o Monero periodicamente em segundo plano para que esteja atualizado ao abrir o app. Só roda enquanto carrega e no WiFi.",
"settingsForegroundSyncLabel": "Sincronização contínua",
@@ -63,13 +83,30 @@
"fiatApiSettingsModeTorOnly": "Somente Tor",
"fiatApiSettingsModeClearnet": "Somente Clearnet (não privado)",
"fiatApiSettingsModeDisabled": "Desativado",
+ "fiatModeTorOnlyDesc": "Cotações buscadas pelo Tor · recomendado",
+ "fiatModeClearnetDesc": "Não privado — o servidor de cotações vê seu endereço IP",
+ "fiatModeDisabledDesc": "Sem cotações; saldos exibidos apenas em cripto",
"fiatApiSettingsDisplayCurrencyLabel": "Moeda de Exibição",
"createWalletTitle": "Criar Carteira",
"createWalletDescription": "Você já possui uma semente de carteira Monero ou precisa criar uma nova?",
"createWalletRestoreExistingButton": "Restaurar Existente",
+ "createWalletRestoreExistingDesc": "Insira uma semente Monero existente",
"createWalletCreateNewButton": "Criar Nova",
- "generateSeedTitle": "Nova Carteira",
+ "createWalletCreateNewDesc": "A Skylight Wallet gera uma nova polyseed",
+ "createWalletPasswordTitle": "Criar Senha da Carteira",
+ "createWalletPasswordDescription": "Crie uma senha para proteger sua carteira. Esta senha será necessária para desbloquear sua carteira.",
+ "createWalletPasswordHint": "Digite sua senha",
+ "createWalletConfirmPasswordHint": "Confirme sua senha",
+ "passwordTooShortError": "A senha deve ter pelo menos 8 caracteres.",
+ "passwordsDoNotMatchError": "As senhas não coincidem.",
+ "generateSeedTitle": "Anote-as, em ordem",
+ "generateSeedTitleCovered": "Sua 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 no papel — não em uma foto ou app de notas.",
+ "generateSeedSubtitleRevealed": "Qualquer pessoa com estas palavras tem seus fundos.",
+ "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",
"lwsDetailsTitle": "Detalhes da Carteira",
"lwsDetailsDescription": "Você pode usar esses detalhes para permitir essa carteira no seu servidor de light wallet caso necessário.",
@@ -81,8 +118,40 @@
"restoreWalletSeedLabel": "Semente",
"restoreWalletRestoreHeightLabel": "Bloco de Restauração (opcional)",
"restoreWalletRestoreDateLabel": "Data de Restauração (opcional)",
+ "restoreWalletScanFrom": "Escanear a partir de",
+ "restoreWalletScanFromReason": "Mais cedo é mais lento, mas nunca perde fundos.",
+ "restoreWalletNotSet": "Não definido",
+ "restoreScanTitle": "Quando esta seed teve fundos pela primeira vez?",
+ "restoreScanDescription": "A Skylight Wallet só escaneia a partir deste ponto. Chute cedo — uma resposta mais antiga custa tempo de sincronização, uma mais recente esconde transações.",
+ "restoreScanPickMonth": "Escolher um mês",
+ "restoreScanNotSure": "Não tenho certeza",
+ "restoreScanNotSureDesc": "Escanear tudo. Mais lento nesta configuração inicial, mas não depois. Sempre completo.",
+ "restoreScanFromStart": "Genesis",
+ "restoreScanDone": "Concluir",
"restoreWalletRestoreButton": "Restaurar",
"restoreWalletInvalidMnemonic": "Semente inválida.",
+ "restoreWalletSeedLength": "Tamanho da semente",
+ "restoreWalletPaste": "Colar",
+ "restoreWalletSeedTypePolyseed": "Polyseed",
+ "restoreWalletSeedTypeBip39": "BIP39",
+ "restoreWalletSeedTypeLegacy": "Legado",
+ "restoreWalletBadWord": "A palavra {position} não é uma palavra BIP39.",
+ "@restoreWalletBadWord": {
+ "placeholders": {
+ "position": {
+ "type": "int"
+ }
+ }
+ },
+ "restoreWalletDidYouMean": "Você quis dizer {word}?",
+ "@restoreWalletDidYouMean": {
+ "placeholders": {
+ "word": {
+ "type": "String"
+ }
+ }
+ },
+ "restoreWalletChecksumError": "Esta não é uma frase-semente válida — verifique as palavras e sua ordem.",
"navigationBarWallet": "Carteira",
"navigationBarSettings": "Configurações",
"unlockButton": "Desbloquear",
@@ -105,6 +174,10 @@
"homeTransactionConfirmed": "Confirmado",
"homeNoTransactions": "Sem transações",
"homeFiatApiError": "Erro ao conectar à API de cotação",
+ "homeDisconnected": "Desconectado",
+ "coinHomeActivityTitle": "Atividade",
+ "coinHomeReceived": "Recebido",
+ "coinHomeSent": "Enviado",
"receiveTitle": "Receber",
"receivePrimaryAddressWarn": "Aviso: A menos que saiba o que está fazendo, por favor considere usar subendereços para melhor privacidade.",
"receiveShareButton": "Compartilhar",
@@ -112,6 +185,25 @@
"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",
+ "receiveAddressHeading": "Seu endereço {coin}",
+ "@receiveAddressHeading": {
+ "placeholders": {
+ "coin": {
+ "type": "String"
+ }
+ }
+ },
+ "receiveBlockchainSubtitle": "Blockchain {coin}",
+ "@receiveBlockchainSubtitle": {
+ "placeholders": {
+ "coin": {
+ "type": "String"
+ }
+ }
+ },
"sendTitle": "Enviar",
"sendSendButton": "Enviar",
"sendTransactionSuccessfullySent": "Transação enviada com sucesso!",
@@ -120,7 +212,12 @@
"sendInsufficientBalanceError": "Saldo insuficiente.",
"sendInsufficientBalanceToCoverFeeError": "Saldo insuficiente para cobrir a taxa da rede.",
"settingsTitle": "Configurações",
+ "settingsSectionGeneral": "Geral",
+ "settingsSectionBehaviour": "Comportamento",
+ "settingsSectionWallet": "Carteira",
+ "settingsSectionAbout": "Sobre",
"settingsNotifyNewTxsLabel": "Notificar Novas Transações",
+ "settingsNotifyNewTxsDescription": "Mostra uma notificação quando você recebe uma transação. Ao conectar-se a um nó Monero, a Sincronização em Segundo Plano também precisa estar ativada.",
"settingsNotifyNewTxsDescriptionIos": "As notificações serão atrasadas para conexões LWS via Tor.",
"settingsAppLockLabel": "Desbloqueio com PIN/Biometria",
"settingsAppLockUnlockReason": "Desbloquear carteira",
@@ -135,8 +232,13 @@
"settingsThemeSystem": "Sistema",
"settingsThemeLight": "Claro",
"settingsThemeDark": "Escuro",
+ "settingsThemeLightDesc": "Claro e limpo",
+ "settingsThemeDarkDesc": "Fundo escuro, melhor à noite",
+ "settingsThemeSystemDesc": "Segue o seu telefone",
+ "settingsThemeSheetSubtitle": "As mudanças têm efeito imediato. As cores dos valores permanecem iguais nos dois.",
"settingsLanguageLabel": "Idioma",
- "settingsFiatApiSettingsLabel": "Configs. da API de Moeda Local",
+ "settingsLanguageSheetSubtitle": "Valores e datas seguem o idioma escolhido.",
+ "settingsFiatApiSettingsLabel": "Exibição de Moeda Local",
"settingsLwsViewKeysLabel": "Chaves de Visualização do LWS",
"settingsLwsViewKeysButton": "Ver",
"settingsSecretKeysLabel": "Chaves Privadas de Restauração",
@@ -148,7 +250,7 @@
"settingsDeleteWalletButton": "Excluir Carteira",
"settingsDeleteWalletDialogText": "Tem certeza que deseja excluir sua carteira? Você perderá acesso à seus fundos, a menos que tenha anotado sua semente.",
"settingsDeleteWalletDialogDeleteButton": "Excluir",
- "txDetailsTitle": "Transação",
+ "txDetailsTitle": "Detalhes da transação",
"txDetailsHashLabel": "Hash",
"txDetailsTimeAndDateLabel": "Data e Hora",
"txDetailsConfirmationHeightLabel": "Bloco de Confirmação",
@@ -156,19 +258,33 @@
"txDetailsViewKeyLabel": "Chave de Visualização",
"txDetailsRecipientsLabel": "Destinatários",
"txDetailsChangeRecipientLabel": "Troco",
+ "txDetailsCopyHint": "toque em qualquer valor para copiar",
+ "txDetailsFailed": "Esta transação falhou. Os fundos não foram enviados.",
+ "txDetailsUnknownStatus": "Esta transação não foi confirmada como enviada. Verifique antes de enviar novamente.",
"copiedToClipboard": "Copiado para a área de transferência",
"lwsKeysTitle": "Chaves do LWS",
"lwsKeysPrimaryAddress": "Endereço Primário",
"lwsKeysRestoreHeight": "Bloco de Restauração",
"lwsKeysSecretViewKey": "Chave Privada de Visualização",
+ "lwsKeysWarning": "Capturas de tela estão bloqueadas nesta tela. A chave de visualização permite que um servidor veja tudo o que você recebe — compartilhe-a apenas com um servidor que você opera ou confia.",
"secretKeysTitle": "Chaves Privadas de Restauração",
+ "secretKeysDescription": "Estas sementes e chaves restauram o controle total da sua carteira. Qualquer pessoa que as veja pode gastar seus fundos.",
"secretKeysMnemonic": "Semente",
"secretKeysPublicSpendKey": "Chave Pública de Gasto",
"secretKeysSecretSpendKey": "Chave Privada de Gasto",
"secretKeysPublicViewKey": "Chave Pública de Visualização",
+ "secretKeysWarning": "Nunca as compartilhe ou insira em qualquer site. Guarde-as offline.",
"scanQrTitle": "Escanear QR Code",
"confirmSendTitle": "Confirmar Envio",
"confirmSendDescription": "As transações são irreversíveis, então verifique se estes detalhes correspondem exatamente.",
+ "confirmSendHighFeeWarning": "A taxa de rede é {percent} do valor que você está enviando.",
+ "@confirmSendHighFeeWarning": {
+ "placeholders": {
+ "percent": {
+ "type": "String"
+ }
+ }
+ },
"addressBookTitle": "Lista de Contatos",
"addressBookAddContact": "Adicionar Contato",
"addressBookEditContact": "Editar Contato",
@@ -189,6 +305,10 @@
"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",
@@ -200,9 +320,17 @@
"sendTransactionPriority": "Prioridade da Transação",
"sendFeeLabel": "Taxa",
"sendContactsButton": "Contatos",
+ "sendToLabel": "Para",
+ "sendPasteButton": "Colar",
+ "sendScanButton": "Escanear",
+ "sendMaxButton": "MÁX",
+ "sendNetworkFee": "Taxa de rede",
+ "sendPriorityHeading": "Prioridade",
+ "sendPickContactTitle": "Enviar para um contato",
+ "sendAvailableSuffix": "disponível",
"sendFailedToGetFeesError": "Não foi possível carregar taxas.",
"torInfoTitle": "Tor Integrado",
- "torInfoDescription": "A Carteira Skylight usa automaticamente Tor integrado para proteger suas conexões de internet.",
+ "torInfoDescription": "A Skylight Wallet usa automaticamente Tor integrado para proteger suas conexões de internet.",
"torInfoContinueButton": "Continuar",
"torInfoConfigureButton": "Configurar",
"torSettingsTitle": "Configurações do Tor",
@@ -210,6 +338,13 @@
"torSettingsModeBuiltIn": "Tor Integrado",
"torSettingsModeExternal": "Tor Externo",
"torSettingsModeDisabled": "Sem Tor",
+ "torChoiceBuiltInDesc": "Incluído na Skylight Wallet · recomendado",
+ "torChoiceExternalDesc": "Orbot, ou um daemon que você mesmo executa",
+ "torChoiceNoTorDesc": "Os servidores aos quais você se conecta podem ver seu endereço IP",
+ "torChoiceConnected": "Conectado ao Tor",
+ "torChoiceTestFailed": "Falha no teste",
+ "torChoiceTitle": "Como a Skylight Wallet deve acessar a rede?",
+ "torChoiceSubtitle": "Nada se conecta até você escolher. O Tor oculta seu endereço IP dos servidores com os quais a Skylight Wallet se comunica.",
"torSettingsSocksPortLabel": "Porta SOCKS",
"torSettingsSocksPortHint": "ex: 9050",
"torSettingsUseOrbotLabel": "Usar Orbot/InviZible",
diff --git a/lib/main.dart b/lib/main.dart
index 2991871..7d5aa70 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -10,7 +10,6 @@ import 'package:timeago/timeago.dart' as timeago;
import 'package:skylight_wallet/models/fiat_rate_model.dart';
import 'package:skylight_wallet/models/contact_model.dart';
import 'package:skylight_wallet/services/tor_settings_service.dart';
-import 'package:skylight_wallet/screens/confirm_send.dart';
import 'package:skylight_wallet/screens/lws_details.dart';
import 'package:skylight_wallet/screens/lws_keys.dart';
import 'package:skylight_wallet/screens/scan_qr.dart';
@@ -29,10 +28,8 @@ import 'package:skylight_wallet/screens/send.dart';
import 'package:skylight_wallet/screens/create_wallet.dart';
import 'package:skylight_wallet/screens/create_wallet_password.dart';
import 'package:skylight_wallet/screens/restore_wallet.dart';
-import 'package:skylight_wallet/screens/restore_warning.dart';
import 'package:skylight_wallet/screens/wallet_home.dart';
import 'package:skylight_wallet/screens/welcome.dart';
-import 'package:skylight_wallet/screens/tor_info.dart';
import 'package:skylight_wallet/screens/tor_settings.dart';
import 'package:skylight_wallet/screens/address_book.dart';
import 'package:skylight_wallet/screens/privacy_policy.dart';
@@ -40,6 +37,8 @@ import 'package:skylight_wallet/screens/terms_of_service.dart';
import 'package:skylight_wallet/screens/unlock.dart';
import 'package:skylight_wallet/services/notifications_service.dart';
import 'package:skylight_wallet/services/shared_preferences_service.dart';
+import 'package:skylight_wallet/theme/palette.dart';
+import 'package:wallet_ui/wallet_ui.dart';
import 'package:skylight_wallet/periodic_tasks.dart';
import 'package:skylight_wallet/services/foreground_sync_service.dart';
import 'package:skylight_wallet/util/dirs.dart';
@@ -57,6 +56,7 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized();
installWalletCore();
+ BrandColors.install(skylightPalette);
// Catch Flutter framework errors
FlutterError.onError = (FlutterErrorDetails details) {
@@ -143,6 +143,53 @@ class _AppRootState extends State<_AppRoot> with WidgetsBindingObserver {
AppWallet? _announceWallet;
int _lastAnnouncedTxCount = 0;
+ // Brightness-flip repaint: screens read BrandColors globally (not via
+ // Theme.of), so a theme change doesn't dirty cached routes on its own. On an
+ // actual flip we force an in-place rebuild of the navigator subtree.
+ final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
+ Brightness? _lastBrightness;
+
+ static void _markSubtreeDirty(Element element) {
+ element.markNeedsBuild();
+ element.visitChildren(_markSubtreeDirty);
+ }
+
+ // The bottom-nav destinations. Tapping a nav tab must not animate, so these
+ // get a zero-duration route in _onGenerateRoute.
+ static const _noTransitionRoutes = {'/wallet_home', '/address_book', '/settings'};
+
+ Map<String, WidgetBuilder> get _routes => {
+ '/welcome': (context) => WelcomeScreen(),
+ '/tor_settings': (context) => TorSettingsScreen(),
+ '/connection_setup': (context) => ConnectionSetupScreen(),
+ '/fiat_api_setup': (context) => FiatApiSetupScreen(),
+ '/create_wallet_password': (context) => CreateWalletPasswordScreen(),
+ '/create_wallet': (context) => CreateWalletScreen(),
+ '/generate_seed': (context) => GenerateSeedScreen(),
+ '/lws_details': (context) => LwsDetailsScreen(),
+ '/restore_wallet': (context) => RestoreWalletScreen(),
+ '/unlock': (context) => UnlockScreen(),
+ '/wallet_home': (context) => WalletHomeScreen(),
+ '/settings': (context) => SettingsScreen(),
+ '/lws_keys': (context) => LwsKeysScreen(),
+ '/secret_keys': (context) => SecretKeysScreen(),
+ '/send': (context) => SendScreen(),
+ '/scan_qr': (context) => ScanQrScreen(),
+ '/receive': (context) => ReceiveScreen(),
+ '/address_book': (context) => AddressBookScreen(),
+ '/terms_of_service': (context) => TermsOfService(),
+ '/privacy_policy': (context) => PrivacyPolicy(),
+ };
+
+ Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
+ final builder = _routes[settings.name];
+ if (builder == null) return null;
+ if (_noTransitionRoutes.contains(settings.name)) {
+ return _NoTransitionPageRoute(builder: builder, settings: settings);
+ }
+ return MaterialPageRoute(builder: builder, settings: settings);
+ }
+
@override
void initState() {
super.initState();
@@ -236,48 +283,34 @@ class _AppRootState extends State<_AppRoot> with WidgetsBindingObserver {
}
return MaterialApp(
+ navigatorKey: _navigatorKey,
title: 'Skylight Monero Wallet',
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
- theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue)),
- darkTheme: ThemeData(
- colorScheme: ColorScheme.fromSeed(
- seedColor: Colors.blue,
- brightness: Brightness.dark,
- ),
- ),
+ theme: brandLightTheme(),
+ darkTheme: brandDarkTheme(),
themeMode: theme == 'dark'
? ThemeMode.dark
: theme == 'light'
? ThemeMode.light
: ThemeMode.system,
+ // Pin brand tokens to the resolved brightness before any screen
+ // builds; rebuild the navigator subtree in place on a real flip.
+ builder: (context, child) {
+ final brightness = Theme.of(context).brightness;
+ BrandColors.setBrightness(brightness);
+ if (_lastBrightness != null && _lastBrightness != brightness) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ final navContext = _navigatorKey.currentContext;
+ if (navContext is Element) navContext.visitChildElements(_markSubtreeDirty);
+ });
+ }
+ _lastBrightness = brightness;
+ return child ?? const SizedBox.shrink();
+ },
initialRoute: initialRoute,
locale: Locale.fromSubtags(languageCode: languageProvider.language),
- routes: {
- '/welcome': (context) => WelcomeScreen(),
- '/tor_info': (context) => TorInfoScreen(),
- '/tor_settings': (context) => TorSettingsScreen(),
- '/connection_setup': (context) => ConnectionSetupScreen(),
- '/fiat_api_setup': (context) => FiatApiSetupScreen(),
- '/create_wallet_password': (context) => CreateWalletPasswordScreen(),
- '/create_wallet': (context) => CreateWalletScreen(),
- '/generate_seed': (context) => GenerateSeedScreen(),
- '/lws_details': (context) => LwsDetailsScreen(),
- '/restore_warning': (context) => RestoreWarningScreen(),
- '/restore_wallet': (context) => RestoreWalletScreen(),
- '/unlock': (context) => UnlockScreen(),
- '/wallet_home': (context) => WalletHomeScreen(),
- '/settings': (context) => SettingsScreen(),
- '/lws_keys': (context) => LwsKeysScreen(),
- '/secret_keys': (context) => SecretKeysScreen(),
- '/send': (context) => SendScreen(),
- '/confirm_send': (context) => ConfirmSendScreen(),
- '/scan_qr': (context) => ScanQrScreen(),
- '/receive': (context) => ReceiveScreen(),
- '/address_book': (context) => AddressBookScreen(),
- '/terms_of_service': (context) => TermsOfService(),
- '/privacy_policy': (context) => PrivacyPolicy(),
- },
+ onGenerateRoute: _onGenerateRoute,
);
}
@@ -287,15 +320,13 @@ class _AppRootState extends State<_AppRoot> with WidgetsBindingObserver {
return MaterialApp(
title: 'Skylight Monero Wallet',
- theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue)),
- darkTheme: ThemeData(
- colorScheme: ColorScheme.fromSeed(
- seedColor: Colors.blue,
- brightness: Brightness.dark,
- ),
- ),
+ theme: brandLightTheme(),
+ darkTheme: brandDarkTheme(),
themeMode: ThemeMode.system,
- builder: (context, child) => Scaffold(),
+ builder: (context, child) {
+ BrandColors.setBrightness(Theme.of(context).brightness);
+ return Scaffold(backgroundColor: BrandColors.paper);
+ },
);
},
);
@@ -303,3 +334,17 @@ class _AppRootState extends State<_AppRoot> with WidgetsBindingObserver {
);
}
}
+
+/// A [MaterialPageRoute] whose own push/pop is instant — used for the bottom-nav
+/// destinations so tapping a tab doesn't animate. Subclassing (rather than a bare
+/// PageRouteBuilder) keeps Material's transition machinery, so the *secondary*
+/// transition still plays when another screen is pushed over a nav screen.
+class _NoTransitionPageRoute<T> extends MaterialPageRoute<T> {
+ _NoTransitionPageRoute({required super.builder, super.settings});
+
+ @override
+ Duration get transitionDuration => Duration.zero;
+
+ @override
+ Duration get reverseTransitionDuration => Duration.zero;
+}
diff --git a/lib/models/app_wallet.dart b/lib/models/app_wallet.dart
index 6d3a91a..96f962e 100644
--- a/lib/models/app_wallet.dart
+++ b/lib/models/app_wallet.dart
@@ -60,7 +60,6 @@ abstract interface class AppWallet implements Listenable {
required String address,
required String proxyPort,
required bool useTor,
- required bool useSsl,
String connectionType,
});
Future<void> persistCurrentConnection();
@@ -68,7 +67,6 @@ abstract interface class AppWallet implements Listenable {
Future<void> testConnection({
required String address,
String? proxyPort,
- required bool useSsl,
required bool useTor,
String connectionType,
});
@@ -77,6 +75,7 @@ abstract interface class AppWallet implements Listenable {
// Notifications
Future<void> markExistingTxsAsNotified();
+
/// [announce] false records current history as seen without firing an OS
/// notification — the foreground uses it so a tx the user watched arrive is
/// not re-notified by a background isolate.
@@ -89,16 +88,19 @@ abstract interface class AppWallet implements Listenable {
Future<String> readPublicSpendKey();
Future<String> readLegacySeed();
Future<String> readPolyseed();
+
/// The original backed-up mnemonic (bip39 shows its own words, not the
/// derived legacy seed), or null when no seed store is kept.
Future<StoredSeed?> readStoredSeed();
// Receive (serverSupportsSubaddresses is declared with the sync getters above)
String? getUnusedSubaddress();
+ int? get unusedSubaddressIndex;
bool? get unusedSubaddressIndexIsSupported;
// Send
bool isAddressValid(String address);
+
/// Estimated network fee in base units (piconero), or null when the backend
/// can't estimate it (typically insufficient balance for that priority).
Future<int?> estimateFee(
diff --git a/lib/models/language_model.dart b/lib/models/language_model.dart
index 2aedb10..93525c0 100644
--- a/lib/models/language_model.dart
+++ b/lib/models/language_model.dart
@@ -1,39 +1,3 @@
-import 'dart:ui';
-
-import 'package:flutter/material.dart';
-import 'package:skylight_wallet/services/shared_preferences_service.dart';
-
-class LanguageModel with ChangeNotifier {
- String _language = PlatformDispatcher.instance.locale.languageCode;
-
- String get language => _language;
-
- LanguageModel() {
- _loadLanguage();
- }
-
- Future<void> _loadLanguage() async {
- final String? preferencesLanguage =
- await SharedPreferencesService.get<String>(
- SharedPreferencesKeys.language,
- );
-
- if (preferencesLanguage != null) {
- _language = preferencesLanguage;
- notifyListeners();
- }
- }
-
- void setLanguage(String? newLanguage) async {
- if (newLanguage == null) return;
-
- _language = newLanguage;
-
- await SharedPreferencesService.set<String>(
- SharedPreferencesKeys.language,
- newLanguage,
- );
-
- notifyListeners();
- }
-}
+// LanguageModel lives in wallet-core (wallet_infra); kept under this path so
+// call sites are unchanged.
+export 'package:wallet_infra/wallet_infra.dart' show LanguageModel;
diff --git a/lib/models/monero_wallet_adapter.dart b/lib/models/monero_wallet_adapter.dart
index c8533ff..23fa764 100644
--- a/lib/models/monero_wallet_adapter.dart
+++ b/lib/models/monero_wallet_adapter.dart
@@ -5,10 +5,19 @@ import 'package:skylight_wallet/models/wallet_types.dart'
show TxDetails, TxRecipient, LWSConnectionDetails, ResolvedOpenAlias;
import 'package:wallet_domain/wallet_domain.dart' as domain;
+import 'package:wallet_infra/wallet_infra.dart' show requiresSecureTransport;
import 'package:wallet_monero/wallet_monero.dart' show MoneroWallet;
const _moneroDecimals = 12;
+/// Whether a bare `host:port` [address] gets a secure transport (D30).
+///
+/// wallet-core no longer stores a `useSsl` flag; the scheme is derived from the
+/// host — https for a routable one, plaintext for an onion or local one. This
+/// mirrors that so skylight's display (the connection banner) stays accurate.
+bool _deriveSsl(String address) =>
+ address.isNotEmpty && requiresSecureTransport(Uri.parse('http://$address').host);
+
/// Presents a wallet-core [MoneroWallet] through skylight's [AppWallet] surface,
/// converting wallet-core types (BigInt amounts, TxDetails, connection details)
/// to skylight's. Re-fires the wrapped wallet's notifications.
@@ -38,7 +47,7 @@ class MoneroWalletAdapter extends ChangeNotifier implements AppWallet {
@override
bool get connectionUseTor => _wallet.connectionUseTor;
@override
- bool get connectionUseSsl => _wallet.connectionUseSsl;
+ bool get connectionUseSsl => _deriveSsl(_wallet.connectionAddress);
@override
bool get usingTor => _wallet.usingTor;
@override
@@ -70,6 +79,8 @@ class MoneroWalletAdapter extends ChangeNotifier implements AppWallet {
@override
String? getUnusedSubaddress() => _wallet.getUnusedSubaddress();
@override
+ int? get unusedSubaddressIndex => _wallet.unusedSubaddressIndex;
+ @override
bool? get unusedSubaddressIndexIsSupported => _wallet.unusedSubaddressIndexIsSupported;
@override
List<TxDetails> get txHistory => [for (final t in _wallet.txHistory) _toSkylightTx(t)];
@@ -115,7 +126,7 @@ class MoneroWalletAdapter extends ChangeNotifier implements AppWallet {
address: c.address,
proxyPort: c.proxyPort,
useTor: c.useTor,
- useSsl: c.useSsl,
+ useSsl: _deriveSsl(c.address),
connectionType: c.connectionType,
);
}
@@ -125,13 +136,11 @@ class MoneroWalletAdapter extends ChangeNotifier implements AppWallet {
required String address,
required String proxyPort,
required bool useTor,
- required bool useSsl,
String connectionType = '',
}) => _wallet.setConnection(
address: address,
proxyPort: proxyPort,
useTor: useTor,
- useSsl: useSsl,
connectionType: connectionType,
);
@@ -144,13 +153,11 @@ class MoneroWalletAdapter extends ChangeNotifier implements AppWallet {
Future<void> testConnection({
required String address,
String? proxyPort,
- required bool useSsl,
required bool useTor,
String connectionType = '',
}) => _wallet.testConnection(
address: address,
proxyPort: proxyPort,
- useSsl: useSsl,
useTor: useTor,
connectionType: connectionType,
);
diff --git a/lib/models/theme_model.dart b/lib/models/theme_model.dart
index 2efc4d2..c923823 100644
--- a/lib/models/theme_model.dart
+++ b/lib/models/theme_model.dart
@@ -1,48 +1,3 @@
-import 'package:flutter/material.dart';
-import 'package:skylight_wallet/services/shared_preferences_service.dart';
-
-class ThemeModel with ChangeNotifier {
- String _theme = 'system';
-
- String get theme => _theme;
-
- ThemeModel() {
- _loadTheme();
- }
-
- Future<void> _loadTheme() async {
- final String? preferencesTheme = await SharedPreferencesService.get<String>(
- SharedPreferencesKeys.theme,
- );
-
- if (preferencesTheme != null) {
- _theme = preferencesTheme;
- }
- notifyListeners();
- }
-
- void setTheme(String? newTheme) async {
- if (newTheme == null) return;
-
- _theme = newTheme;
-
- await SharedPreferencesService.set<String>(
- SharedPreferencesKeys.theme,
- newTheme,
- );
-
- notifyListeners();
- }
-
- ThemeMode get themeMode {
- switch (_theme) {
- case 'light':
- return ThemeMode.light;
- case 'dark':
- return ThemeMode.dark;
- case 'system':
- default:
- return ThemeMode.system;
- }
- }
-}
+// ThemeModel lives in wallet-core (wallet_infra); kept under this path so call
+// sites are unchanged. The app maps ThemeModel.theme -> ThemeMode itself.
+export 'package:wallet_infra/wallet_infra.dart' show ThemeModel;
diff --git a/lib/screens/address_book.dart b/lib/screens/address_book.dart
index 25bafc6..e7704ca 100644
--- a/lib/screens/address_book.dart
+++ b/lib/screens/address_book.dart
@@ -1,9 +1,14 @@
+import 'dart:io';
+
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
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/util/secure_clipboard.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';
class AddressBookScreen extends StatefulWidget {
@@ -23,49 +28,62 @@ class _AddressBookScreenState extends State<AddressBookScreen> {
super.dispose();
}
- void _onSearchChanged(String query) {
- setState(() {
- _searchQuery = query;
- });
- }
+ void _showAddContactDialog() => _showContactSheet(null);
- void _showAddContactDialog() {
- showDialog(context: context, builder: (context) => _ContactDialog());
- }
+ void _showEditContactDialog(Contact contact) => _showContactSheet(contact);
- void _showEditContactDialog(Contact contact) {
- showDialog(
+ void _showContactSheet(Contact? contact) {
+ showBrandSheet<void>(
context: context,
- builder: (context) => _ContactDialog(contact: contact),
+ isScrollControlled: true,
+ builder: (sheetContext) => _ContactSheet(contact: contact),
);
}
void _showDeleteContactDialog(Contact contact) {
final i18n = AppLocalizations.of(context)!;
-
- showDialog(
+ showBrandSheet<void>(
context: context,
- builder: (context) {
- final screenWidth = MediaQuery.of(context).size.width;
- final dialogWidth = screenWidth.clamp(0.0, 400.0);
-
- return AlertDialog(
- constraints: BoxConstraints.tightFor(width: dialogWidth),
- insetPadding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
- title: Text(i18n.addressBookDeleteContact),
- content: Text(i18n.addressBookDeleteContactConfirmation(contact.name)),
- actions: [
- TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(i18n.cancel)),
- FilledButton(
- onPressed: () {
- Provider.of<ContactModel>(context, listen: false).deleteContact(contact.id);
- Navigator.of(context).pop();
- },
- child: Text(i18n.addressBookDelete),
- ),
- ],
- );
- },
+ 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(),
+ Row(
+ children: [
+ SheetIcon(
+ icon: Icons.delete_outline,
+ bg: BrandColors.errorBg,
+ color: BrandColors.error,
+ ),
+ const SizedBox(width: 11),
+ Text(i18n.addressBookDeleteContact, style: BrandText.sheetTitle),
+ ],
+ ),
+ const SizedBox(height: 7),
+ Text(
+ i18n.addressBookDeleteContactConfirmation(contact.name),
+ style: BrandText.bodyMuted.copyWith(fontSize: 13, height: 1.5),
+ ),
+ const SizedBox(height: 18),
+ BrandButton(label: i18n.cancel, onPressed: () => Navigator.pop(sheetContext)),
+ const SizedBox(height: 4),
+ BrandButton.ghost(
+ label: i18n.addressBookDelete,
+ color: BrandColors.error,
+ onPressed: () {
+ Provider.of<ContactModel>(context, listen: false).deleteContact(contact.id);
+ Navigator.pop(sheetContext);
+ },
+ ),
+ ],
+ ),
+ ),
+ ),
);
}
@@ -74,82 +92,116 @@ class _AddressBookScreenState extends State<AddressBookScreen> {
final i18n = AppLocalizations.of(context)!;
return Scaffold(
- appBar: AppBar(title: Text(i18n.addressBookTitle)),
- bottomNavigationBar: WalletNavigationBar(selectedIndex: 1),
- body: Column(
- children: [
- Padding(
- padding: EdgeInsets.all(16.0),
- child: Row(
+ backgroundColor: BrandColors.paper,
+ bottomNavigationBar: const WalletNavigationBar(selectedIndex: 1),
+ body: SafeArea(
+ bottom: false,
+ child: Center(
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 520),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
- Expanded(
- child: TextField(
- controller: _searchController,
- onChanged: _onSearchChanged,
- textInputAction: TextInputAction.done,
- decoration: InputDecoration(
- hintText: i18n.addressBookSearchHint,
- prefixIcon: Icon(Icons.search),
- border: OutlineInputBorder(),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+ child: BrandScreenHeader(
+ center: Text(
+ i18n.addressBookTitle,
+ style: BrandText.appBar.copyWith(fontSize: 16),
),
),
),
- SizedBox(width: 8),
- IconButton(onPressed: _showAddContactDialog, icon: Icon(Icons.add)),
+ Padding(
+ padding: const EdgeInsets.fromLTRB(16, 10, 16, 6),
+ child: Row(
+ children: [
+ Expanded(
+ child: _SearchField(
+ controller: _searchController,
+ onChanged: (q) => setState(() => _searchQuery = q),
+ ),
+ ),
+ const SizedBox(width: 10),
+ GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: _showAddContactDialog,
+ child: Container(
+ width: 52,
+ height: 52,
+ alignment: Alignment.center,
+ decoration: BoxDecoration(
+ color: BrandColors.primary,
+ borderRadius: BorderRadius.circular(BrandRadii.field),
+ ),
+ child: const Icon(Icons.add, size: 24, color: BrandColors.onPrimary),
+ ),
+ ),
+ ],
+ ),
+ ),
+ 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];
+ return _ContactTile(
+ contact: contact,
+ onEdit: () => _showEditContactDialog(contact),
+ onDelete: () => _showDeleteContactDialog(contact),
+ isLast: index == contacts.length - 1,
+ );
+ },
+ );
+ },
+ ),
+ ),
],
),
),
+ ),
+ ),
+ );
+ }
+}
+
+class _SearchField extends StatelessWidget {
+ final TextEditingController controller;
+ final ValueChanged<String> onChanged;
+
+ const _SearchField({required this.controller, required this.onChanged});
+
+ @override
+ Widget build(BuildContext context) {
+ final i18n = AppLocalizations.of(context)!;
+ return Container(
+ height: 52,
+ padding: const EdgeInsets.symmetric(horizontal: 14),
+ decoration: BoxDecoration(
+ color: BrandColors.card,
+ border: Border.all(color: BrandColors.border),
+ borderRadius: BorderRadius.circular(BrandRadii.field),
+ ),
+ child: Row(
+ children: [
+ Icon(Icons.search, size: 20, color: BrandColors.inkFaint),
+ const SizedBox(width: 10),
Expanded(
- child: Consumer<ContactModel>(
- builder: (context, contactModel, child) {
- final filteredContacts = contactModel.searchContacts(_searchQuery);
-
- if (filteredContacts.isEmpty) {
- return Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(
- Icons.contacts_outlined,
- size: 64,
- color: Theme.of(context).colorScheme.outline,
- ),
- SizedBox(height: 16),
- Text(
- _searchQuery.isEmpty
- ? i18n.addressBookNoContacts
- : i18n.addressBookNoSearchResults,
- style: Theme.of(context).textTheme.bodyLarge?.copyWith(
- color: Theme.of(context).colorScheme.outline,
- ),
- ),
- if (_searchQuery.isEmpty) ...[
- SizedBox(height: 8),
- Text(
- i18n.addressBookNoContactsDescription,
- style: Theme.of(context).textTheme.bodyMedium?.copyWith(
- color: Theme.of(context).colorScheme.outline,
- ),
- textAlign: TextAlign.center,
- ),
- ],
- ],
- ),
- );
- }
-
- return ListView.builder(
- itemCount: filteredContacts.length,
- itemBuilder: (context, index) {
- final contact = filteredContacts[index];
- return _ContactListItem(
- contact: contact,
- onEdit: () => _showEditContactDialog(contact),
- onDelete: () => _showDeleteContactDialog(contact),
- );
- },
- );
- },
+ child: TextField(
+ controller: controller,
+ onChanged: onChanged,
+ textInputAction: TextInputAction.search,
+ style: TextStyle(fontSize: 14, color: BrandColors.ink),
+ decoration: InputDecoration(
+ isCollapsed: true,
+ border: InputBorder.none,
+ hintText: i18n.addressBookSearchHint,
+ hintStyle: TextStyle(fontSize: 14, color: BrandColors.inkFaint),
+ ),
),
),
],
@@ -158,177 +210,289 @@ class _AddressBookScreenState extends State<AddressBookScreen> {
}
}
-class _ContactListItem extends StatelessWidget {
+class _EmptyState extends StatelessWidget {
+ final bool searching;
+ const _EmptyState({required this.searching});
+
+ @override
+ Widget build(BuildContext context) {
+ final i18n = AppLocalizations.of(context)!;
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(32),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(Icons.people_outline, size: 56, color: BrandColors.inkDisabled),
+ const SizedBox(height: 16),
+ Text(
+ searching ? i18n.addressBookNoSearchResults : i18n.addressBookNoContacts,
+ textAlign: TextAlign.center,
+ style: BrandText.body,
+ ),
+ if (!searching) ...[
+ const SizedBox(height: 6),
+ Text(
+ i18n.addressBookNoContactsDescription,
+ textAlign: TextAlign.center,
+ style: BrandText.bodyMuted,
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+/// A round monogram avatar (contact's first initial).
+class _Avatar extends StatelessWidget {
+ final String name;
+ const _Avatar({required this.name});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ width: 44,
+ height: 44,
+ alignment: Alignment.center,
+ decoration: BoxDecoration(color: BrandColors.primaryDeep, shape: BoxShape.circle),
+ child: Text(
+ name.isNotEmpty ? name[0].toUpperCase() : '?',
+ style: const TextStyle(
+ fontSize: 17,
+ fontWeight: FontWeight.w700,
+ color: BrandColors.onPrimary,
+ ),
+ ),
+ );
+ }
+}
+
+/// Single-address contact row: monogram + name + shortened Monero address, with
+/// a brand popup menu for copy / edit / delete.
+class _ContactTile extends StatelessWidget {
final Contact contact;
final VoidCallback onEdit;
final VoidCallback onDelete;
+ final bool isLast;
- const _ContactListItem({required this.contact, required this.onEdit, required this.onDelete});
+ const _ContactTile({
+ required this.contact,
+ required this.onEdit,
+ required this.onDelete,
+ required this.isLast,
+ });
- void _copyAddressToClipboard(BuildContext context) {
+ void _copy(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
-
- Clipboard.setData(ClipboardData(text: contact.address));
-
+ // Treat as sensitive (auto-cleared) like other address/key copies.
+ SecureClipboard.copy(contact.address);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.addressCopied)));
}
@override
Widget build(BuildContext context) {
- final i18n = AppLocalizations.of(context)!;
-
- return Card(
- margin: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
- child: ListTile(
- leading: CircleAvatar(
- backgroundColor: Theme.of(context).colorScheme.primary,
- child: Text(
- contact.name.isNotEmpty ? contact.name[0].toUpperCase() : '?',
- style: TextStyle(
- color: Theme.of(context).colorScheme.onPrimary,
- fontWeight: FontWeight.bold,
+ return Column(
+ children: [
+ InkWell(
+ onTap: () => _copy(context),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 14),
+ child: Row(
+ children: [
+ _Avatar(name: contact.name),
+ const SizedBox(width: 14),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ contact.name,
+ style: TextStyle(
+ fontSize: 15.5,
+ fontWeight: FontWeight.w700,
+ color: BrandColors.ink,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ shortenMiddle(contact.address, head: 8, tail: 8),
+ style: TextStyle(
+ fontFamily: 'Ubuntu Mono',
+ fontSize: 11.5,
+ color: BrandColors.inkMuted,
+ ),
+ ),
+ ],
+ ),
+ ),
+ _ContactMenu(onCopy: () => _copy(context), onEdit: onEdit, onDelete: onDelete),
+ ],
),
),
),
- title: Text(contact.name, style: TextStyle(fontWeight: FontWeight.w500)),
- subtitle: Text(
- contact.address,
- style: TextStyle(fontFamily: 'monospace', fontSize: 12),
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
+ if (!isLast) Divider(height: 1, thickness: 1, color: BrandColors.surfaceTinted),
+ ],
+ );
+ }
+}
+
+class _ContactMenu extends StatelessWidget {
+ final VoidCallback onCopy;
+ final VoidCallback onEdit;
+ final VoidCallback onDelete;
+
+ const _ContactMenu({required this.onCopy, required this.onEdit, required this.onDelete});
+
+ @override
+ Widget build(BuildContext context) {
+ final i18n = AppLocalizations.of(context)!;
+ return PopupMenuButton<String>(
+ icon: Icon(Icons.more_vert, size: 22, color: BrandColors.inkMuted),
+ color: BrandColors.card,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
+ onSelected: (value) {
+ switch (value) {
+ case 'copy':
+ onCopy();
+ case 'edit':
+ onEdit();
+ case 'delete':
+ onDelete();
+ }
+ },
+ itemBuilder: (context) => [
+ PopupMenuItem(
+ value: 'copy',
+ child: Row(
+ children: [
+ Icon(Icons.copy_outlined, size: 18, color: BrandColors.primaryDeep),
+ const SizedBox(width: 10),
+ Text(i18n.addressBookCopyAddress, style: BrandText.body),
+ ],
+ ),
),
- trailing: PopupMenuButton<String>(
- onSelected: (value) {
- switch (value) {
- case 'copy':
- _copyAddressToClipboard(context);
- break;
- case 'edit':
- onEdit();
- break;
- case 'delete':
- onDelete();
- break;
- }
- },
- itemBuilder: (context) => [
- PopupMenuItem(
- value: 'copy',
- child: Row(
- children: [Icon(Icons.copy), SizedBox(width: 8), Text(i18n.addressBookCopyAddress)],
- ),
- ),
- PopupMenuItem(
- value: 'edit',
- child: Row(
- children: [Icon(Icons.edit), SizedBox(width: 8), Text(i18n.addressBookEdit)],
- ),
- ),
- PopupMenuItem(
- value: 'delete',
- child: Row(
- children: [
- Icon(Icons.delete, color: Colors.red),
- SizedBox(width: 8),
- Text(i18n.addressBookDelete, style: TextStyle(color: Colors.red)),
- ],
+ PopupMenuItem(
+ value: 'edit',
+ child: Row(
+ children: [
+ Icon(Icons.edit_outlined, size: 18, color: BrandColors.primaryDeep),
+ const SizedBox(width: 10),
+ Text(i18n.addressBookEdit, style: BrandText.body),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ value: 'delete',
+ child: Row(
+ children: [
+ Icon(Icons.delete_outline, size: 18, color: BrandColors.error),
+ const SizedBox(width: 10),
+ Text(
+ i18n.addressBookDelete,
+ style: BrandText.body.copyWith(color: BrandColors.error),
),
- ),
- ],
+ ],
+ ),
),
- onTap: () => _copyAddressToClipboard(context),
- ),
+ ],
);
}
}
-class _ContactDialog extends StatefulWidget {
+/// Add / edit a single-address contact — a name and one Monero address.
+class _ContactSheet extends StatefulWidget {
final Contact? contact;
- const _ContactDialog({this.contact});
+ const _ContactSheet({required this.contact});
@override
- State<_ContactDialog> createState() => _ContactDialogState();
+ State<_ContactSheet> createState() => _ContactSheetState();
}
-class _ContactDialogState extends State<_ContactDialog> {
- final _formKey = GlobalKey<FormState>();
+class _ContactSheetState extends State<_ContactSheet> {
final _nameController = TextEditingController();
- final _addressController = TextEditingController();
- bool _isLoading = false;
+ // The address is set only via paste/scan (never typed), matching Spice.
+ String? _address;
+ bool _saving = false;
+ String? _error;
+ // Set when the last paste/scan wasn't a valid address; cleared on the next set.
+ bool _invalidAddress = false;
+
+ bool get _isEditing => widget.contact != null;
@override
void initState() {
super.initState();
if (widget.contact != null) {
_nameController.text = widget.contact!.name;
- _addressController.text = widget.contact!.address;
+ _address = widget.contact!.address;
}
}
@override
void dispose() {
_nameController.dispose();
- _addressController.dispose();
super.dispose();
}
- String? _validateName(String? value) {
- final i18n = AppLocalizations.of(context)!;
- if (value == null || value.trim().isEmpty) {
- return i18n.fieldEmptyError;
+ /// Accepts a bare address or a `monero:<address>?...` URI; null if invalid.
+ String? _extractAddress(String raw) {
+ final value = raw.trim();
+ final uri = Uri.tryParse(value);
+ if (uri != null && uri.scheme.toLowerCase() == 'monero') {
+ return appWalletOf(context).isAddressValid(uri.path) ? uri.path : null;
}
- return null;
+ return appWalletOf(context).isAddressValid(value) ? value : null;
}
- String? _validateAddress(String? value) {
- final i18n = AppLocalizations.of(context)!;
-
- if (value == null || value.trim().isEmpty) {
- return i18n.fieldEmptyError;
+ void _setAddress(String? raw) {
+ final address = raw == null ? null : _extractAddress(raw);
+ if (address == null) {
+ // Inline under the card, not a snackbar — the sheet covers snackbars.
+ setState(() => _invalidAddress = true);
+ return;
}
+ setState(() {
+ _address = address;
+ _error = null;
+ _invalidAddress = false;
+ });
+ }
- if (!appWalletOf(context).isAddressValid(value.trim())) {
- return i18n.sendInvalidAddressError;
- }
- return null;
+ Future<void> _paste() async {
+ final data = await Clipboard.getData(Clipboard.kTextPlain);
+ if (mounted) _setAddress(data?.text);
}
- Future<void> _saveContact() async {
- final i18n = AppLocalizations.of(context)!;
- if (!_formKey.currentState!.validate()) return;
+ Future<void> _scan() async {
+ final result = await Navigator.pushNamed(context, '/scan_qr');
+ if (mounted && result is String) _setAddress(result);
+ }
- setState(() {
- _isLoading = true;
- });
+ Future<void> _save() async {
+ final i18n = AppLocalizations.of(context)!;
+ final name = _nameController.text.trim();
+ final address = _address;
+ if (name.isEmpty || address == null) {
+ setState(() => _error = i18n.fieldEmptyError);
+ return;
+ }
+ setState(() => _saving = true);
try {
- final contactModel = Provider.of<ContactModel>(context, listen: false);
-
+ final model = Provider.of<ContactModel>(context, listen: false);
if (widget.contact == null) {
- await contactModel.addContact(_nameController.text.trim(), _addressController.text.trim());
+ await model.addContact(name, address);
} else {
- await contactModel.updateContact(
- widget.contact!.id,
- _nameController.text.trim(),
- _addressController.text.trim(),
- );
- }
-
- if (mounted) {
- Navigator.of(context).pop();
- }
- } catch (e) {
- if (mounted) {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(i18n.unknownError), backgroundColor: Colors.red));
+ await model.updateContact(widget.contact!.id, name, address);
}
- } finally {
+ if (mounted) Navigator.pop(context);
+ } catch (_) {
if (mounted) {
- setState(() {
- _isLoading = false;
- });
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.unknownError)));
+ setState(() => _saving = false);
}
}
}
@@ -336,64 +500,299 @@ class _ContactDialogState extends State<_ContactDialog> {
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
- final isEditing = widget.contact != null;
- final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
-
- final screenWidth = MediaQuery.of(context).size.width;
- final dialogWidth = screenWidth.clamp(0.0, 400.0);
-
- return AlertDialog(
- constraints: BoxConstraints.tightFor(width: dialogWidth),
- insetPadding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
- title: Text(isEditing ? i18n.addressBookEditContact : i18n.addressBookAddContact),
- content: Form(
- key: _formKey,
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- TextFormField(
+ final name = _nameController.text.trim();
+
+ return Padding(
+ padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
+ child: SafeArea(
+ top: false,
+ child: ConstrainedBox(
+ constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.88),
+ child: Column(
+ 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),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ SheetIcon(
+ icon: _isEditing ? Icons.edit_outlined : Icons.person_outline,
+ bg: BrandColors.surfaceTinted,
+ color: BrandColors.primaryDeep,
+ ),
+ const SizedBox(width: 11),
+ Text(
+ _isEditing ? i18n.addressBookEditContact : i18n.addressBookAddContact,
+ style: BrandText.sheetTitle,
+ ),
+ ],
+ ),
+ const SizedBox(height: 7),
+ Text(
+ _isEditing ? i18n.addressBookEditDescription : i18n.addressBookAddDescription,
+ style: BrandText.bodyMuted.copyWith(fontSize: 13, height: 1.5),
+ ),
+ ],
+ ),
+ ),
+ Flexible(
+ child: SingleChildScrollView(
+ padding: const EdgeInsets.fromLTRB(22, 0, 22, 0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SectionHeader(
+ label: i18n.addressBookContactName,
+ padding: const EdgeInsets.only(left: 4, bottom: 8),
+ ),
+ _nameField(name),
+ const SizedBox(height: 16),
+ SectionHeader(
+ label: _addressHeaderLabel(i18n),
+ padding: const EdgeInsets.only(left: 4, bottom: 8),
+ ),
+ _addressEntry(),
+ if (_error != null) ...[
+ const SizedBox(height: 10),
+ Text(_error!, style: BrandText.caption.copyWith(color: BrandColors.error)),
+ ],
+ ],
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.fromLTRB(22, 18, 22, 8),
+ child: Column(
+ children: [
+ BrandButton(
+ label: _isEditing ? i18n.addressBookUpdate : i18n.addressBookSave,
+ loading: _saving,
+ onPressed: (_saving || name.isEmpty || _address == null) ? null : _save,
+ ),
+ const SizedBox(height: 4),
+ BrandButton.ghost(
+ label: i18n.cancel,
+ color: BrandColors.inkMuted,
+ onPressed: () => Navigator.pop(context),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ String _addressHeaderLabel(AppLocalizations i18n) {
+ final coinName = xmrWallet(context)?.coinName ?? 'Monero';
+ final label = '$coinName ${i18n.address}';
+ return _address == null ? '$label · ${i18n.restoreWalletNotSet}' : label;
+ }
+
+ Widget _nameField(String name) {
+ final i18n = AppLocalizations.of(context)!;
+ final filled = name.isNotEmpty;
+ return BrandCard(
+ borderColor: filled ? BrandColors.border : BrandColors.inputBorder,
+ padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 11),
+ child: Row(
+ children: [
+ Container(
+ width: 34,
+ height: 34,
+ alignment: Alignment.center,
+ decoration: BoxDecoration(
+ color: filled ? BrandColors.primaryDeep : BrandColors.surfaceTinted,
+ shape: BoxShape.circle,
+ ),
+ child: filled
+ ? Text(
+ name[0].toUpperCase(),
+ style: const TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.w700,
+ color: BrandColors.onPrimary,
+ ),
+ )
+ : Icon(Icons.person_outline, size: 17, color: BrandColors.inkDisabled),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: TextField(
controller: _nameController,
+ onChanged: (_) => setState(() {}),
+ textCapitalization: TextCapitalization.words,
+ style: TextStyle(fontSize: 14.5, height: 1.3, color: BrandColors.ink),
decoration: InputDecoration(
- labelText: i18n.addressBookContactName,
- border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
+ isCollapsed: true,
+ border: InputBorder.none,
+ hintText: i18n.addressBookNameHint,
+ hintStyle: TextStyle(fontSize: 14.5, color: BrandColors.inkFaint),
),
- validator: _validateName,
- textCapitalization: TextCapitalization.words,
- textInputAction: TextInputAction.next,
),
- SizedBox(height: 16),
- TextFormField(
- controller: _addressController,
- decoration: InputDecoration(
- labelText: i18n.address,
- border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _addressEntry() {
+ final i18n = AppLocalizations.of(context)!;
+ final wallet = xmrWallet(context);
+ final coinName = wallet?.coinName ?? '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) {
+ return BrandCard(
+ radius: 14,
+ 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,
+ ),
+ ),
+ ],
+ ),
+ ),
+ GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: () => setState(() {
+ _address = null;
+ _invalidAddress = false;
+ }),
+ child: Container(
+ width: 26,
+ height: 26,
+ alignment: Alignment.center,
+ decoration: BoxDecoration(color: BrandColors.surfaceTinted, shape: BoxShape.circle),
+ child: Icon(Icons.close, size: 13, color: BrandColors.inkMuted),
),
- validator: _validateAddress,
- maxLines: 3,
- textInputAction: TextInputAction.done,
),
],
),
+ );
+ }
+
+ return CustomPaint(
+ foregroundPainter: _DashedBorderPainter(
+ color: _invalidAddress ? BrandColors.error : BrandColors.borderStrong,
+ radius: 14,
),
- actions: [
- TextButton(
- onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
- child: Text(i18n.cancel),
+ child: Container(
+ decoration: BoxDecoration(
+ color: BrandColors.surfaceSunken,
+ borderRadius: BorderRadius.circular(14),
),
- FilledButton(
- onPressed: _isLoading ? null : _saveContact,
- child: _isLoading
- ? SizedBox(
- width: 16,
- height: 16,
- child: CircularProgressIndicator(
- strokeWidth: 2,
- color: isDarkTheme ? Theme.of(context).colorScheme.onPrimary : Colors.white,
+ padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 11),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ tile,
+ const SizedBox(width: 11),
+ Expanded(
+ child: Text(
+ coinName,
+ style: TextStyle(
+ fontSize: 12.5,
+ fontWeight: FontWeight.w500,
+ color: BrandColors.inkFaint,
+ ),
),
- )
- : Text(isEditing ? i18n.addressBookUpdate : i18n.addressBookSave),
+ ),
+ MiniActionButton(
+ bordered: true,
+ color: BrandColors.card,
+ icon: Icons.content_paste_outlined,
+ label: i18n.sendPasteButton,
+ onTap: _paste,
+ ),
+ if (Platform.isAndroid || Platform.isIOS) ...[
+ const SizedBox(width: 7),
+ MiniActionButton(
+ bordered: true,
+ color: BrandColors.card,
+ icon: Icons.qr_code_scanner,
+ label: i18n.sendScanButton,
+ onTap: _scan,
+ ),
+ ],
+ ],
+ ),
+ if (_invalidAddress) ...[
+ const SizedBox(height: 8),
+ Text(
+ i18n.sendInvalidAddressError,
+ style: BrandText.caption.copyWith(color: BrandColors.error),
+ ),
+ ],
+ ],
),
- ],
+ ),
);
}
}
+
+/// Draws a dashed rounded-rectangle outline (used for the not-yet-set address
+/// card). Flutter's [Border] has no dashed style, so we stroke it by hand.
+class _DashedBorderPainter extends CustomPainter {
+ final Color color;
+ final double radius;
+ static const _dash = 5.0;
+ static const _gap = 4.0;
+ static const _strokeWidth = 1.3;
+
+ _DashedBorderPainter({required this.color, this.radius = 14});
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ final path = Path()
+ ..addRRect(RRect.fromRectAndRadius(Offset.zero & size, Radius.circular(radius)));
+ final paint = Paint()
+ ..color = color
+ ..style = PaintingStyle.stroke
+ ..strokeWidth = _strokeWidth;
+ for (final metric in path.computeMetrics()) {
+ var distance = 0.0;
+ while (distance < metric.length) {
+ final next = distance + _dash;
+ canvas.drawPath(metric.extractPath(distance, next.clamp(0.0, metric.length)), paint);
+ distance = next + _gap;
+ }
+ }
+ }
+
+ @override
+ bool shouldRepaint(_DashedBorderPainter old) => old.color != color || old.radius != radius;
+}
diff --git a/lib/screens/confirm_send.dart b/lib/screens/confirm_send.dart
deleted file mode 100644
index 829cc25..0000000
--- a/lib/screens/confirm_send.dart
+++ /dev/null
@@ -1,281 +0,0 @@
-import 'package:flutter/material.dart';
-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/widgets/loading_button.dart';
-import 'package:skylight_wallet/util/logging.dart';
-import 'package:skylight_wallet/wallet_core_glue.dart';
-import 'package:provider/provider.dart';
-
-class ConfirmSendScreenArgs {
- AppPendingTx tx;
- String destinationAddress;
- String? destinationOpenAlias;
-
- /// The recipient name published alongside the OpenAlias record, if any.
- String? destinationOpenAliasName;
- String? destinationContactName;
-
- ConfirmSendScreenArgs({
- required this.tx,
- required this.destinationAddress,
- this.destinationOpenAlias,
- this.destinationOpenAliasName,
- this.destinationContactName,
- });
-}
-
-class ConfirmSendScreen extends StatefulWidget {
- const ConfirmSendScreen({super.key});
-
- @override
- State<ConfirmSendScreen> createState() => _ConfirmSendScreenState();
-}
-
-class _ConfirmSendScreenState extends State<ConfirmSendScreen> {
- bool _isLoading = false;
- AppPendingTx? _tx;
- double _amount = 0.0;
- double _fee = 0.0;
- String? _destinationOpenAlias;
- String? _destinationOpenAliasName;
- String _destinationAddress = '';
- List<String> _destinationAddressSliced = [];
- String? _destinationContactName;
-
- @override
- void initState() {
- super.initState();
- }
-
- @override
- void didChangeDependencies() {
- super.didChangeDependencies();
-
- _loadTxDetails();
- }
-
- void _loadTxDetails() {
- final args = ModalRoute.of(context)!.settings.arguments as ConfirmSendScreenArgs?;
-
- if (args == null) {
- throw Exception('Args missing');
- }
-
- setState(() {
- _tx = args.tx;
- _amount = args.tx.amount;
- _fee = args.tx.fee;
- _destinationOpenAlias = args.destinationOpenAlias;
- _destinationOpenAliasName = args.destinationOpenAliasName;
- _destinationAddress = args.destinationAddress;
- _destinationAddressSliced = _sliceAddress(args.destinationAddress);
- _destinationContactName = args.destinationContactName;
- });
- }
-
- List<String> _sliceAddress(String address) {
- List<String> result = [];
-
- for (int i = 0; i < address.length; i += 5) {
- // Get the substring of the next 5 characters
- String substring = address.substring(i, i + 5 > address.length ? address.length : i + 5);
- result.add(substring);
- }
-
- return result;
- }
-
- Future<void> _confirmSend() async {
- final i18n = AppLocalizations.of(context)!;
- final wallet = appWalletOf(context);
-
- if (_tx == null) return;
-
- setState(() {
- _isLoading = true;
- });
-
- try {
- await wallet.commitTx(_tx!, _destinationAddress);
-
- setState(() {
- _isLoading = false;
- });
-
- if (mounted) {
- Navigator.pushNamed(context, '/wallet_home', arguments: {'showTxSuccessToast': true});
- }
- } on FormatException catch (error) {
- var errorMsg = error.toString().replaceFirst('FormatException: ', '');
-
- if (error.toString().contains('HTTP error code 500')) {
- errorMsg = 'Failed to send transaction. You might have insufficient unlocked balance.';
- }
-
- if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(errorMsg)));
- }
- } catch (error) {
- log(LogLevel.error, error.toString());
- if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.unknownError)));
- }
- }
-
- setState(() {
- _isLoading = false;
- });
- }
-
- @override
- Widget build(BuildContext context) {
- final i18n = AppLocalizations.of(context)!;
- final fiatRate = context.watch<FiatRateModel>();
- final fiatSymbol = fiatRate.fiatCode == 'EUR' ? '€' : '\$';
- final amountFiat = fiatRate.rateFor('XMR') is double ? _amount * fiatRate.rateFor('XMR')! : null;
- final networkFeeFiat = fiatRate.rateFor('XMR') is double ? _fee * fiatRate.rateFor('XMR')! : null;
-
- return Scaffold(
- appBar: AppBar(),
- body: Padding(
- padding: EdgeInsets.symmetric(horizontal: 20),
- child: Center(
- child: Container(
- constraints: BoxConstraints(maxWidth: 440),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- spacing: 20,
- children: [
- Padding(
- padding: EdgeInsets.symmetric(horizontal: 20),
- child: Text(
- i18n.confirmSendTitle,
- style: Theme.of(context).textTheme.headlineMedium,
- ),
- ),
- Text(
- i18n.confirmSendDescription,
- textAlign: TextAlign.center,
- style: Theme.of(context).textTheme.bodyLarge,
- ),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(i18n.amount, style: TextStyle(fontWeight: FontWeight.bold)),
- Column(
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- Text(
- '${_amount.toStringAsFixed(12)} XMR',
- style: TextStyle(fontWeight: FontWeight.w500),
- ),
- if (amountFiat is double)
- Text('$fiatSymbol${amountFiat.toStringAsFixed(2)}'),
- ],
- ),
- ],
- ),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(i18n.networkFee, style: TextStyle(fontWeight: FontWeight.bold)),
- Column(
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- Text(
- '${_fee.toStringAsFixed(12)} XMR',
- style: TextStyle(fontWeight: FontWeight.w500),
- ),
- if (networkFeeFiat is double)
- Text('$fiatSymbol${networkFeeFiat.toStringAsFixed(2)}'),
- ],
- ),
- ],
- ),
- if (_destinationOpenAlias is String)
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text('OpenAlias', style: TextStyle(fontWeight: FontWeight.bold)),
- Flexible(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- Text(_destinationOpenAlias!, textAlign: TextAlign.end),
- // The name the recipient publishes with the record,
- // so the user can sanity-check who they resolved to.
- // Recipient-supplied text: sanitized and capped by
- // the model, and bounded here so it cannot push the
- // address off the screen.
- if (_destinationOpenAliasName is String)
- Text(
- _destinationOpenAliasName!,
- textAlign: TextAlign.end,
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- style: Theme.of(context).textTheme.bodySmall,
- ),
- ],
- ),
- ),
- ],
- ),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(i18n.address, style: TextStyle(fontWeight: FontWeight.bold)),
-
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- Container(
- margin: EdgeInsetsGeometry.only(left: 40),
- child: Wrap(
- spacing: 4,
- alignment: WrapAlignment.end,
- children: _destinationAddressSliced
- .asMap()
- .entries
- .map(
- (item) => Text(
- item.value,
- style: TextStyle(
- fontFamily: 'monospace',
- // highlight start and end slices of address
- fontWeight:
- item.key == 0 ||
- item.key == _destinationAddressSliced.length - 1
- ? FontWeight.w700
- : FontWeight.w300,
- ),
- ),
- )
- .toList(),
- ),
- ),
- if (_destinationContactName is String) Text('($_destinationContactName)'),
- ],
- ),
- ),
- ],
- ),
- LoadingButton(
- isLoading: _isLoading,
- onPressed: _confirmSend,
- icon: Icons.arrow_outward_rounded,
- label: i18n.sendSendButton,
- ),
- ],
- ),
- ),
- ),
- ),
- );
- }
-}
diff --git a/lib/screens/connection_setup.dart b/lib/screens/connection_setup.dart
index 53b9953..31c356a 100644
--- a/lib/screens/connection_setup.dart
+++ b/lib/screens/connection_setup.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
import 'package:skylight_wallet/widgets/connection_settings_form.dart';
+import 'package:skylight_wallet/widgets/ui/ui.dart';
class ConnectionSetupScreen extends StatelessWidget {
const ConnectionSetupScreen({super.key});
@@ -15,42 +16,36 @@ class ConnectionSetupScreen extends StatelessWidget {
}
return Scaffold(
- appBar: AppBar(title: Text('Skylight Monero Wallet')),
- body: LayoutBuilder(
- builder: (context, constraints) => SingleChildScrollView(
+ backgroundColor: BrandColors.paper,
+ body: SafeArea(
+ child: Center(
child: ConstrainedBox(
- constraints: BoxConstraints(minHeight: constraints.maxHeight),
- child: Center(
- child: Container(
- constraints: BoxConstraints(maxWidth: 500),
- child: Padding(
- padding: EdgeInsets.all(20),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- mainAxisSize: MainAxisSize.min,
- spacing: 20,
- children: [
- Column(
- spacing: 10,
- children: [
- Text(
- i18n.lwsSetupTitle,
- style: Theme.of(context).textTheme.headlineMedium,
- ),
- Text(
- i18n.lwsSetupDescription,
- textAlign: TextAlign.center,
- style: Theme.of(context).textTheme.bodyLarge,
- ),
- ],
- ),
- ConnectionSettingsForm(
+ constraints: const BoxConstraints(maxWidth: 500),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: BrandSpacing.xl),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ const SizedBox(height: BrandSpacing.sm),
+ BrandScreenHeader(
+ onBack: () => Navigator.of(context).pop(),
+ center: const StepDots(count: 5, index: 1),
+ ),
+ const SizedBox(height: BrandSpacing.lg),
+ Text(i18n.lwsSetupTitle, style: BrandText.title),
+ const SizedBox(height: BrandSpacing.sm),
+ Text(i18n.lwsSetupDescription, style: BrandText.bodyMuted),
+ const SizedBox(height: BrandSpacing.xl),
+ Expanded(
+ child: SingleChildScrollView(
+ child: ConnectionSettingsForm(
saveButtonLabel: i18n.lwsSetupContinueButton,
onSaved: onSaved,
),
- ],
+ ),
),
- ),
+ const SizedBox(height: BrandSpacing.sm),
+ ],
),
),
),
diff --git a/lib/screens/create_wallet.dart b/lib/screens/create_wallet.dart
index ea405df..c73e9fb 100644
--- a/lib/screens/create_wallet.dart
+++ b/lib/screens/create_wallet.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
-import 'package:skylight_wallet/wallet_core_glue.dart';
+import 'package:skylight_wallet/widgets/ui/ui.dart';
class CreateWalletScreenArgs {
String toastMessage;
@@ -25,73 +25,35 @@ class _CreateWalletScreenState extends State<CreateWalletScreen> {
void _showErrorIfNeeded() {
WidgetsBinding.instance.addPostFrameCallback((_) {
- final args =
- ModalRoute.of(context)?.settings.arguments as CreateWalletScreenArgs?;
+ final args = ModalRoute.of(context)?.settings.arguments as CreateWalletScreenArgs?;
if (args != null && args.toastMessage != '') {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(args.toastMessage)));
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(args.toastMessage)));
}
});
}
- /// The warning is about the LWS server learning your whole transaction
- /// history from the view key. A full node scans locally and is told nothing,
- /// so restoring against one goes straight to the seed form.
void _restoreExisting() {
- final wallet = appWalletOf(context);
-
- Navigator.pushNamed(
- context,
- wallet.isNodeMode ? '/restore_wallet' : '/restore_warning',
- );
+ Navigator.pushNamed(context, '/restore_wallet');
}
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
- return Scaffold(
- appBar: AppBar(title: Text('Skylight Monero Wallet')),
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- spacing: 20,
- children: [
- Text(
- i18n.createWalletTitle,
- style: Theme.of(context).textTheme.headlineMedium,
- ),
- Padding(
- padding: EdgeInsets.symmetric(horizontal: 40),
- child: Text(
- i18n.createWalletDescription,
- textAlign: TextAlign.center,
- style: Theme.of(context).textTheme.bodyLarge,
- ),
- ),
- Row(
- spacing: 20,
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- TextButton(
- onPressed: _restoreExisting,
- child: Text(i18n.createWalletRestoreExistingButton),
- ),
- FilledButton(
- onPressed: () => Navigator.pushNamedAndRemoveUntil(
- context,
- '/generate_seed',
- (Route<dynamic> route) => false,
- ),
- child: Text(i18n.createWalletCreateNewButton),
- ),
- ],
- ),
- ],
- ),
+ return CreateWalletView(
+ stepCount: 5,
+ 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'),
+ onRestore: _restoreExisting,
);
}
}
diff --git a/lib/screens/create_wallet_password.dart b/lib/screens/create_wallet_password.dart
index e25311e..ca237c1 100644
--- a/lib/screens/create_wallet_password.dart
+++ b/lib/screens/create_wallet_password.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
-import 'package:skylight_wallet/widgets/loading_button.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
+import 'package:skylight_wallet/widgets/ui/ui.dart';
class CreateWalletPasswordScreen extends StatefulWidget {
const CreateWalletPasswordScreen({super.key});
@@ -11,32 +11,12 @@ class CreateWalletPasswordScreen extends StatefulWidget {
}
class _CreateWalletPasswordScreenState extends State<CreateWalletPasswordScreen> {
- final TextEditingController _passwordController = TextEditingController();
- final TextEditingController _confirmPasswordController = TextEditingController();
- final _formKey = GlobalKey<FormState>();
- bool _obscurePassword = true;
- bool _obscureConfirmPassword = true;
bool _isLoading = false;
- @override
- void dispose() {
- _passwordController.dispose();
- _confirmPasswordController.dispose();
- super.dispose();
- }
-
- Future<void> _savePassword() async {
- if (!_formKey.currentState!.validate()) {
- return;
- }
-
- setState(() {
- _isLoading = true;
- });
+ Future<void> _savePassword(String password) async {
+ setState(() => _isLoading = true);
try {
- final password = _passwordController.text;
-
if (mounted) {
setWalletPassword(context, password);
Navigator.pushNamed(context, '/create_wallet');
@@ -49,122 +29,27 @@ class _CreateWalletPasswordScreenState extends State<CreateWalletPasswordScreen>
}
} finally {
if (mounted) {
- setState(() {
- _isLoading = false;
- });
+ setState(() => _isLoading = false);
}
}
}
- String? _validatePassword(String? value) {
- if (value == null || value.isEmpty) {
- return AppLocalizations.of(context)!.fieldEmptyError;
- }
- if (value.length < 8) {
- return 'Password must be at least 8 characters long.';
- }
- return null;
- }
-
- String? _validateConfirmPassword(String? value) {
- if (value == null || value.isEmpty) {
- return AppLocalizations.of(context)!.fieldEmptyError;
- }
- if (value != _passwordController.text) {
- return 'Passwords do not match.';
- }
- return null;
- }
-
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
- return Scaffold(
- appBar: AppBar(title: Text('Skylight Monero Wallet')),
- body: Center(
- child: Container(
- constraints: BoxConstraints(maxWidth: 500),
- child: Padding(
- padding: EdgeInsets.all(20),
- child: Form(
- key: _formKey,
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- spacing: 20,
- children: [
- Column(
- spacing: 10,
- children: [
- Text(
- 'Create Wallet Password',
- style: Theme.of(context).textTheme.headlineMedium,
- ),
- Text(
- 'Create a password to protect your wallet. This password will be required to unlock your wallet.',
- textAlign: TextAlign.center,
- style: Theme.of(context).textTheme.bodyLarge,
- ),
- ],
- ),
- Column(
- spacing: 15,
- children: [
- TextFormField(
- controller: _passwordController,
- obscureText: _obscurePassword,
- textInputAction: TextInputAction.next,
- validator: _validatePassword,
- decoration: InputDecoration(
- labelText: 'Password',
- hintText: 'Enter your password',
- border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
- suffixIcon: IconButton(
- icon: Icon(_obscurePassword ? Icons.visibility : Icons.visibility_off),
- onPressed: () {
- setState(() {
- _obscurePassword = !_obscurePassword;
- });
- },
- ),
- ),
- enabled: !_isLoading,
- ),
- TextFormField(
- controller: _confirmPasswordController,
- obscureText: _obscureConfirmPassword,
- textInputAction: TextInputAction.done,
- validator: _validateConfirmPassword,
- decoration: InputDecoration(
- labelText: 'Confirm Password',
- hintText: 'Confirm your password',
- border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
- suffixIcon: IconButton(
- icon: Icon(
- _obscureConfirmPassword ? Icons.visibility : Icons.visibility_off,
- ),
- onPressed: () {
- setState(() {
- _obscureConfirmPassword = !_obscureConfirmPassword;
- });
- },
- ),
- ),
- enabled: !_isLoading,
- onFieldSubmitted: (_) => _savePassword(),
- ),
- LoadingButton(
- isLoading: _isLoading,
- onPressed: _savePassword,
- label: i18n.continueText,
- ),
- ],
- ),
- ],
- ),
- ),
- ),
- ),
+ 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,
),
);
}
diff --git a/lib/screens/fiat_api_setup_screen.dart b/lib/screens/fiat_api_setup_screen.dart
index 9e41c31..03da536 100644
--- a/lib/screens/fiat_api_setup_screen.dart
+++ b/lib/screens/fiat_api_setup_screen.dart
@@ -7,6 +7,7 @@ import 'package:skylight_wallet/l10n/app_localizations.dart';
import 'package:skylight_wallet/models/fiat_rate_model.dart';
import 'package:skylight_wallet/services/shared_preferences_service.dart';
import 'package:skylight_wallet/services/tor_settings_service.dart';
+import 'package:skylight_wallet/widgets/ui/ui.dart';
class FiatApiSetupScreen extends StatefulWidget {
const FiatApiSetupScreen({super.key});
@@ -19,8 +20,7 @@ class _FiatApiSetupScreenState extends State<FiatApiSetupScreen> {
FiatApiMode _fiatMode = FiatApiMode.torOnly;
String _fiatCurrency = 'USD';
- bool get _globalTorDisabled =>
- TorSettingsService.sharedInstance.torMode == TorMode.disabled;
+ bool get _globalTorDisabled => TorSettingsService.sharedInstance.torMode == TorMode.disabled;
@override
void initState() {
@@ -47,95 +47,32 @@ class _FiatApiSetupScreenState extends State<FiatApiSetupScreen> {
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
- final theme = Theme.of(context);
- return Scaffold(
- appBar: AppBar(title: Text('Skylight Monero Wallet')),
- body: Center(
- child: Container(
- constraints: BoxConstraints(maxWidth: 500),
- child: Padding(
- padding: EdgeInsets.all(20),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- spacing: 20,
- children: [
- Column(
- spacing: 10,
- children: [
- Text(
- i18n.fiatApiSetupTitle,
- style: theme.textTheme.headlineMedium,
- ),
- Padding(
- padding: EdgeInsets.symmetric(horizontal: 16),
- child: Text(
- i18n.fiatApiSetupDescription,
- textAlign: TextAlign.center,
- style: theme.textTheme.bodyLarge,
- ),
- ),
- ],
- ),
- Column(
- spacing: 12,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- DropdownButtonFormField<FiatApiMode>(
- decoration: InputDecoration(
- labelText: i18n.fiatApiSettingsModeLabel,
- border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
- ),
- value: _fiatMode,
- items: [
- if (!_globalTorDisabled)
- DropdownMenuItem(
- value: FiatApiMode.torOnly,
- child: Text(i18n.fiatApiSettingsModeTorOnly),
- ),
- DropdownMenuItem(
- value: FiatApiMode.clearnet,
- child: Text(i18n.fiatApiSettingsModeClearnet),
- ),
- DropdownMenuItem(
- value: FiatApiMode.disabled,
- child: Text(i18n.fiatApiSettingsModeDisabled),
- ),
- ],
- onChanged: (v) {
- if (v != null) setState(() => _fiatMode = v);
- },
- ),
- if (_fiatMode != FiatApiMode.disabled)
- DropdownButtonFormField<String>(
- decoration: InputDecoration(
- labelText: i18n.fiatApiSettingsDisplayCurrencyLabel,
- border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
- ),
- value: _fiatCurrency,
- items: supportedFiatCurrencies
- .map((c) => DropdownMenuItem<String>(value: c, child: Text(c)))
- .toList(),
- onChanged: (v) {
- if (v != null) setState(() => _fiatCurrency = v);
- },
- ),
- ],
- ),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- FilledButton(
- onPressed: _onContinue,
- child: Text(i18n.lwsSetupContinueButton),
- ),
- ],
- ),
- ],
- ),
- ),
- ),
+ 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] ?? ''),
+ ],
+ 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,
);
}
}
diff --git a/lib/screens/generate_seed.dart b/lib/screens/generate_seed.dart
index b69e82e..59ae757 100644
--- a/lib/screens/generate_seed.dart
+++ b/lib/screens/generate_seed.dart
@@ -1,13 +1,13 @@
-import 'dart:async';
-
import 'package:flutter/material.dart';
+import 'package:provider/provider.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/screens/create_wallet.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/util/logging.dart';
import 'package:skylight_wallet/util/secure_screen.dart';
-import 'package:provider/provider.dart';
+import 'package:skylight_wallet/widgets/ui/ui.dart';
class GenerateSeedScreen extends StatefulWidget {
const GenerateSeedScreen({super.key});
@@ -17,101 +17,77 @@ class GenerateSeedScreen extends StatefulWidget {
}
class _GenerateSeedScreenState extends State<GenerateSeedScreen> with SecureScreenMixin {
- List<String> _seed = [];
- int _restoreHeight = 0;
+ List<String>? _seed;
+ ({SeedSource seed, DateTime restoreDate})? _generated;
+ bool _committing = false;
@override
void initState() {
super.initState();
- _createWallet();
+ // Generate the seed in memory only — nothing is written to disk until the
+ // user confirms their backup and taps Continue (_commit).
+ final generated = generateWalletSeed(context);
+ _generated = generated;
+ _seed = generated.seed.mnemonic.split(' ');
}
- Future<void> _createWallet() async {
- try {
- final (seed, restoreHeight) = await createWallet(context);
+ Future<void> _continue() async {
+ if (_committing || _generated == null) return;
+ setState(() => _committing = true);
- setState(() {
- _seed = seed.split(' ');
- _restoreHeight = restoreHeight;
- });
+ 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());
}
-
- if (mounted) {
- Navigator.pushNamed(
- context,
- '/create_wallet',
- arguments: CreateWalletScreenArgs(toastMessage: errorMsg),
- );
- }
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(errorMsg)));
}
}
- void _continue() {
- 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);
- return;
- }
- Navigator.pushNamedAndRemoveUntil(
- context,
- '/lws_details',
- (Route<dynamic> route) => false,
- arguments: _restoreHeight,
- );
- }
-
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
- return Scaffold(
- body: SafeArea(
- child: Center(
- child: Container(
- constraints: BoxConstraints(maxWidth: 540),
- child: Padding(
- padding: EdgeInsetsGeometry.all(20),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- spacing: 20,
- children: [
- Text(i18n.generateSeedTitle, style: Theme.of(context).textTheme.headlineMedium),
- Padding(
- padding: EdgeInsets.symmetric(horizontal: 20),
- child: Text(
- i18n.generateSeedDescription,
- textAlign: TextAlign.center,
- style: Theme.of(context).textTheme.bodyLarge,
- ),
- ),
- if (_seed.isEmpty || _restoreHeight == 0) CircularProgressIndicator(),
- if (_seed.isNotEmpty && _restoreHeight > 0)
- Wrap(
- alignment: WrapAlignment.center,
- spacing: 10,
- runSpacing: 10,
- children: _seed.map((word) {
- return Chip(label: Text(word));
- }).toList(),
- ),
- if (_seed.isNotEmpty && _restoreHeight > 0)
- FilledButton(
- onPressed: _continue,
- child: Text(i18n.generateSeedContinueButton),
- ),
- ],
- ),
- ),
- ),
- ),
+ return GenerateSeedView(
+ stepCount: 5,
+ stepIndex: 4,
+ seedWords: _seed,
+ birthdayCard: null,
+ continueLoading: _committing,
+ onContinue: _continue,
+ labels: GenerateSeedLabels(
+ titleCovered: i18n.generateSeedTitleCovered,
+ titleRevealed: i18n.generateSeedTitle,
+ subtitleCovered: i18n.generateSeedSubtitleCovered,
+ subtitleRevealed: i18n.generateSeedSubtitleRevealed,
+ reveal: i18n.generateSeedReveal,
+ screenshotNote: i18n.generateSeedScreenshotNote,
+ confirm: i18n.generateSeedConfirm,
+ continueText: i18n.continueText,
),
);
}
diff --git a/lib/screens/lws_details.dart b/lib/screens/lws_details.dart
index 666aa12..ceb907f 100644
--- a/lib/screens/lws_details.dart
+++ b/lib/screens/lws_details.dart
@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
+
import 'package:skylight_wallet/l10n/app_localizations.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
+import 'package:skylight_wallet/widgets/ui/ui.dart';
class LwsDetailsScreen extends StatefulWidget {
const LwsDetailsScreen({super.key});
@@ -25,95 +27,40 @@ class _LwsDetailsScreenState extends State<LwsDetailsScreen> {
setState(() => _secretViewKey = key);
}
+ void _copy(String value, {required bool sensitive}) {
+ if (value.isEmpty) return;
+ Clipboard.setData(ClipboardData(text: value));
+ final i18n = AppLocalizations.of(context)!;
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.copiedToClipboard)));
+ }
+
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
final primaryAddress = appWalletOf(context).getPrimaryAddress();
- final secretViewKey = _secretViewKey;
final restoreHeight = ModalRoute.of(context)!.settings.arguments as int;
- return Scaffold(
- body: Center(
- child: Container(
- constraints: BoxConstraints(maxWidth: 480),
- child: Padding(
- padding: EdgeInsets.all(20),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- spacing: 20,
- children: [
- Text(i18n.lwsDetailsTitle, style: Theme.of(context).textTheme.headlineMedium),
- Text(
- i18n.lwsDetailsDescription,
- textAlign: TextAlign.center,
- style: Theme.of(context).textTheme.bodyLarge,
- ),
- Row(
- children: [
- Expanded(
- child: TextFormField(
- readOnly: true,
- decoration: InputDecoration(
- labelText: i18n.lwsDetailsPrimaryAddressLabel,
- border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
- ),
- controller: TextEditingController(text: primaryAddress),
- ),
- ),
- IconButton(
- onPressed: () => Clipboard.setData(ClipboardData(text: primaryAddress)),
- icon: Icon(Icons.copy),
- ),
- ],
- ),
- Row(
- children: [
- Expanded(
- child: TextFormField(
- readOnly: true,
- decoration: InputDecoration(
- labelText: i18n.lwsDetailsSecretViewKeyLabel,
- border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
- ),
- controller: TextEditingController(text: secretViewKey),
- ),
- ),
- IconButton(
- onPressed: () => Clipboard.setData(ClipboardData(text: secretViewKey)),
- icon: Icon(Icons.copy),
- ),
- ],
- ),
- Row(
- children: [
- Expanded(
- child: TextFormField(
- readOnly: true,
- decoration: InputDecoration(
- labelText: i18n.lwsDetailsRestoreHeightLabel,
- border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
- ),
- controller: TextEditingController(text: restoreHeight.toString()),
- ),
- ),
- IconButton(
- onPressed: () =>
- Clipboard.setData(ClipboardData(text: restoreHeight.toString())),
- icon: Icon(Icons.copy),
- ),
- ],
- ),
- FilledButton(
- onPressed: () => Navigator.pushNamedAndRemoveUntil(
- context,
- '/wallet_home',
- (Route<dynamic> route) => false,
- ),
- child: Text(i18n.continueText),
- ),
- ],
- ),
- ),
+ return LwsKeysView(
+ largeTitle: true,
+ labels: LwsKeysLabels(
+ title: i18n.lwsDetailsTitle,
+ description: i18n.lwsDetailsDescription,
+ primaryAddressLabel: i18n.lwsDetailsPrimaryAddressLabel,
+ viewKeyLabel: i18n.lwsDetailsSecretViewKeyLabel,
+ restoreHeightLabel: i18n.lwsDetailsRestoreHeightLabel,
+ reveal: i18n.generateSeedReveal,
+ warning: i18n.lwsKeysWarning,
+ ),
+ primaryAddress: primaryAddress,
+ secretViewKey: _secretViewKey,
+ restoreHeight: restoreHeight.toString(),
+ onCopy: _copy,
+ footer: BrandButton(
+ label: i18n.continueText,
+ onPressed: () => Navigator.pushNamedAndRemoveUntil(
+ context,
+ '/wallet_home',
+ (Route<dynamic> route) => false,
),
),
);
diff --git a/lib/screens/lws_keys.dart b/lib/screens/lws_keys.dart
index c0326a9..eaeea75 100644
--- a/lib/screens/lws_keys.dart
+++ b/lib/screens/lws_keys.dart
@@ -4,6 +4,7 @@ import 'package:skylight_wallet/l10n/app_localizations.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';
+import 'package:skylight_wallet/widgets/ui/ui.dart';
class LwsKeysScreen extends StatefulWidget {
const LwsKeysScreen({super.key});
@@ -35,107 +36,32 @@ class _LwsKeysScreenState extends State<LwsKeysScreen> with SecureScreenMixin {
});
}
+ void _copy(String value, {required bool sensitive}) {
+ if (value.isEmpty) return;
+ SecureClipboard.copy(value);
+ final i18n = AppLocalizations.of(context)!;
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.copiedToClipboard)));
+ }
+
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
- final primaryAddress = _primaryAddress;
- final secretViewKey = _secretViewKey;
- return Scaffold(
- appBar: AppBar(title: Text(i18n.lwsKeysTitle)),
- body: SafeArea(
- child: Padding(
- padding: EdgeInsets.symmetric(horizontal: 20),
- child: Column(
- spacing: 20,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Container(),
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- Expanded(
- child: TextFormField(
- readOnly: true,
- decoration: InputDecoration(
- labelText: i18n.lwsKeysPrimaryAddress,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8.0),
- ),
- suffixIcon: IconButton(
- onPressed: () => SecureClipboard.copy(primaryAddress),
- icon: Icon(Icons.copy),
- ),
- ),
- controller: TextEditingController(
- text: primaryAddress,
- ),
- ),
- ),
- ],
- ),
- ],
- ),
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- Expanded(
- child: TextFormField(
- readOnly: true,
- decoration: InputDecoration(
- labelText: i18n.lwsKeysSecretViewKey,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8.0),
- ),
- suffixIcon: IconButton(
- onPressed: () => SecureClipboard.copy(secretViewKey),
- icon: Icon(Icons.copy),
- ),
- ),
- controller: TextEditingController(
- text: secretViewKey,
- ),
- ),
- ),
- ],
- ),
- ],
- ),
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- Expanded(
- child: TextFormField(
- readOnly: true,
- decoration: InputDecoration(
- labelText: i18n.lwsKeysRestoreHeight,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8.0),
- ),
- suffixIcon: IconButton(
- onPressed: () => SecureClipboard.copy(_restoreHeight.toString()),
- icon: Icon(Icons.copy),
- ),
- ),
- controller: TextEditingController(
- text: _restoreHeight.toString(),
- ),
- ),
- ),
- ],
- ),
- ],
- ),
- ],
- ),
- ),
+ return LwsKeysView(
+ labels: LwsKeysLabels(
+ title: i18n.lwsKeysTitle,
+ description: i18n.lwsDetailsDescription,
+ primaryAddressLabel: i18n.lwsKeysPrimaryAddress,
+ viewKeyLabel: i18n.lwsKeysSecretViewKey,
+ restoreHeightLabel: i18n.lwsKeysRestoreHeight,
+ reveal: i18n.generateSeedReveal,
+ warning: i18n.lwsKeysWarning,
),
+ primaryAddress: _primaryAddress,
+ secretViewKey: _secretViewKey,
+ restoreHeight: _restoreHeight.toString(),
+ onCopy: _copy,
+ onBack: () => Navigator.pop(context),
);
}
}
diff --git a/lib/screens/privacy_policy.dart b/lib/screens/privacy_policy.dart
index ab15d3b..c70c167 100644
--- a/lib/screens/privacy_policy.dart
+++ b/lib/screens/privacy_policy.dart
@@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
+import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/widgets/ui/ui.dart';
+
class BulletList extends StatelessWidget {
final List<String> items;
const BulletList({super.key, required this.items});
@@ -35,16 +38,13 @@ class BulletList extends StatelessWidget {
class PrivacyPolicy extends StatelessWidget {
const PrivacyPolicy({super.key});
- TextStyle get headingStyle =>
- TextStyle(fontSize: 20, fontWeight: FontWeight.bold);
+ TextStyle get headingStyle => TextStyle(fontSize: 20, fontWeight: FontWeight.bold);
- TextStyle get subheadingStyle =>
- TextStyle(fontSize: 16, fontWeight: FontWeight.w600);
+ TextStyle get subheadingStyle => TextStyle(fontSize: 16, fontWeight: FontWeight.w600);
TextStyle get bodyStyle => TextStyle(fontSize: 14, height: 1.4);
- TextStyle get monospaceStyle =>
- TextStyle(fontSize: 13, fontFamily: 'monospace', height: 1.4);
+ TextStyle get monospaceStyle => TextStyle(fontSize: 13, fontFamily: 'Ubuntu Mono', height: 1.4);
Widget sectionHeading(String text) => Padding(
padding: EdgeInsets.only(top: 18.0, bottom: 6.0),
@@ -58,72 +58,95 @@ class PrivacyPolicy extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ final i18n = AppLocalizations.of(context)!;
return Scaffold(
- appBar: AppBar(title: Text('Privacy Policy')),
+ backgroundColor: BrandColors.paper,
body: SafeArea(
- child: SingleChildScrollView(
- padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text('Updated 2025-09-19', 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 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 Skylight Wallet, you need to specify a server to connect to. This will share your cryptocurrency address and private view key with this server. You will also share transactions you create with this server. This is necessary for the core functionality of the wallet.',
- ),
- paragraph(
- 'MAGIC Grants does not provide a default server. You must research the server that you want to use and any terms of service or privacy policies associated with that server before you connect to that server.',
- ),
- paragraph(
- 'You may choose to avoid using a third party by connecting to a server that you operate.',
- ),
-
- sectionHeading('Other Connections to Third Parties'),
- paragraph(
- 'The app connects to services provided by third parties for your convenience. These include:',
- ),
- BulletList(
- items: [
- 'Block explorers, for viewing information about your transactions.',
- 'Price quotes, for displaying richer information about your transactions.',
- 'Cryptocurrency nodes, for the app to learn information about the cryptocurrency network’s status.',
- ],
- ),
- paragraph(
- 'Within the app, connections to these services are protected automatically with Tor. However, MAGIC Grants does not warrant that these connections are bug-free, or that these protections are fit for a particular purpose.',
- ),
- paragraph(
- '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.',
- ),
-
- sectionHeading('When You Contact Us'),
- paragraph(
- 'There is no mechanism for contacting us within the application. 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 logs, make sure to review their contents for any potentially sensitive information. Remove this information before sending any information to us.',
- ),
- ],
+ child: Center(
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 520),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ 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 2025-09-19', 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 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 Skylight Wallet, you need to specify a server to connect to. This will share your cryptocurrency address and private view key with this server. You will also share transactions you create with this server. This is necessary for the core functionality of the wallet.',
+ ),
+ paragraph(
+ 'MAGIC Grants does not provide a default server. You must research the server that you want to use and any terms of service or privacy policies associated with that server before you connect to that server.',
+ ),
+ paragraph(
+ 'You may choose to avoid using a third party by connecting to a server that you operate.',
+ ),
+
+ sectionHeading('Other Connections to Third Parties'),
+ paragraph(
+ 'The app connects to services provided by third parties for your convenience. These include:',
+ ),
+ BulletList(
+ items: [
+ 'Block explorers, for viewing information about your transactions.',
+ 'Price quotes, for displaying richer information about your transactions.',
+ 'Cryptocurrency nodes, for the app to learn information about the cryptocurrency network’s status.',
+ ],
+ ),
+ paragraph(
+ 'Within the app, connections to these services are protected automatically with Tor. However, MAGIC Grants does not warrant that these connections are bug-free, or that these protections are fit for a particular purpose.',
+ ),
+ paragraph(
+ '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.',
+ ),
+
+ sectionHeading('When You Contact Us'),
+ paragraph(
+ 'There is no mechanism for contacting us within the application. 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 logs, make sure to review their contents for any potentially sensitive information. Remove this information before sending any information to us.',
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
),
),
),
diff --git a/lib/screens/receive.dart b/lib/screens/receive.dart
index 7e4ae1a..c1a01e8 100644
--- a/lib/screens/receive.dart
+++ b/lib/screens/receive.dart
@@ -2,12 +2,14 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
-import 'package:skylight_wallet/l10n/app_localizations.dart';
-import 'package:skylight_wallet/wallet_core_glue.dart';
-import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart';
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/wallet_core_glue.dart';
+import 'package:skylight_wallet/widgets/ui/ui.dart';
+
class ReceiveScreen extends StatefulWidget {
const ReceiveScreen({super.key});
@@ -19,29 +21,20 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
var _showSubaddress = true;
var _previousBrightness = 0.0;
+ static bool get _isMobile => Platform.isAndroid || Platform.isIOS;
+
@override
void initState() {
super.initState();
-
- if (Platform.isAndroid || Platform.isIOS) {
- _setBrightnessToMax();
- }
+ if (_isMobile) _setBrightnessToMax();
}
@override
void dispose() {
- if (Platform.isAndroid || Platform.isIOS) {
- _setBrightnessToNormal();
- }
+ if (_isMobile) _setBrightnessToNormal();
super.dispose();
}
- void _setShowSubaddress(bool value) {
- setState(() {
- _showSubaddress = value;
- });
- }
-
Future<void> _setBrightnessToMax() async {
_previousBrightness = await ScreenBrightness().system;
await ScreenBrightness().setApplicationScreenBrightness(1.0);
@@ -53,115 +46,63 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
void _copyAddressToClipboard(String address) {
final i18n = AppLocalizations.of(context)!;
-
Clipboard.setData(ClipboardData(text: address));
-
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.addressCopied)));
}
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
- final brightness = Theme.of(context).brightness;
- final isDarkTheme = brightness == Brightness.dark;
final wallet = appWalletOf(context, listen: true);
final primaryAddress = wallet.getPrimaryAddress();
final subaddress = wallet.getUnusedSubaddress();
final isDemoMode = wallet.connectionAddress == 'demo';
- String? address;
+ final subSupported = wallet.serverSupportsSubaddresses;
+ final canToggle = subSupported == true && !isDemoMode;
- if (wallet.serverSupportsSubaddresses == false || isDemoMode) {
+ String? address;
+ if (subSupported == false || isDemoMode) {
address = primaryAddress;
}
-
- if (wallet.serverSupportsSubaddresses == true) {
+ if (subSupported == true) {
address = _showSubaddress ? subaddress : primaryAddress;
}
- return Scaffold(
- appBar: AppBar(title: Text(i18n.receiveTitle)),
- body: Center(
- child: Container(
- constraints: BoxConstraints(maxWidth: 480),
- child: Padding(
- padding: EdgeInsets.symmetric(horizontal: 20),
- child: (wallet.serverSupportsSubaddresses != null || isDemoMode) && address != null
- ? Column(
- mainAxisAlignment: MainAxisAlignment.center,
- spacing: 20,
- children: [
- QrImageView(
- data: address,
- eyeStyle: QrEyeStyle(
- eyeShape: QrEyeShape.square,
- color: isDarkTheme ? Colors.grey[300] : Colors.black,
- ),
- dataModuleStyle: QrDataModuleStyle(
- dataModuleShape: QrDataModuleShape.square,
- color: isDarkTheme ? Colors.grey[300] : Colors.black,
- ),
- ),
- if (wallet.serverSupportsSubaddresses == false)
- Text(
- i18n.receiveServerNoSubaddressesWarn,
- textAlign: TextAlign.center,
- style: TextStyle(color: Colors.red),
- ),
- if (!_showSubaddress)
- Text(
- i18n.receivePrimaryAddressWarn,
- textAlign: TextAlign.center,
- style: TextStyle(color: Colors.red),
- ),
- if (_showSubaddress && wallet.unusedSubaddressIndexIsSupported == false)
- Text(
- i18n.receiveMaxSubaddressesReachedWarn,
- textAlign: TextAlign.center,
- style: TextStyle(color: Colors.red),
- ),
- GestureDetector(
- child: Text(
- address,
- textAlign: TextAlign.center,
- style: TextStyle(fontFamily: 'monospace'),
- ),
- onTap: () => _copyAddressToClipboard(address!),
- ),
- Row(
- spacing: 20,
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- if (Platform.isAndroid || Platform.isIOS)
- FilledButton.icon(
- onPressed: () => SharePlus.instance.share(ShareParams(text: address)),
- icon: Icon(Icons.share),
- label: Text(i18n.receiveShareButton),
- ),
- if (Platform.isLinux || Platform.isWindows || Platform.isMacOS)
- FilledButton.icon(
- onPressed: () => _copyAddressToClipboard(address!),
- icon: Icon(Icons.copy),
- label: Text(i18n.copy),
- ),
- // Only show toggle button if server supports subaddresses
- if (wallet.serverSupportsSubaddresses == true && !_showSubaddress)
- TextButton(
- onPressed: () => _setShowSubaddress(true),
- child: Text(i18n.receiveShowSubaddressButton),
- ),
- if (wallet.serverSupportsSubaddresses == true && _showSubaddress)
- TextButton(
- onPressed: () => _setShowSubaddress(false),
- child: Text(i18n.receiveShowPrimaryAddressButton),
- ),
- ],
- ),
- ],
- )
- : CircularProgressIndicator(),
- ),
- ),
- ),
+ final ready = (subSupported != null || isDemoMode) && address != null;
+ final warning = _warning(i18n, wallet, subSupported);
+
+ return ReceiveView(
+ labels: ReceiveLabels(title: i18n.receiveTitle, copyAddress: i18n.receiveCopyAddress),
+ onBack: () => Navigator.of(context).pop(),
+ onShare: _isMobile ? () => SharePlus.instance.share(ShareParams(text: address!)) : null,
+ ready: ready,
+ // Monero-only app: no coin card (would just say "Monero" redundantly).
+ coinSymbol: 'XMR',
+ iconAsset: 'assets/icons/monero.svg',
+ coinName: null,
+ blockchainSubtitle: null,
+ tabLabels: canToggle ? [i18n.receiveSubaddressTab, i18n.receivePrimaryTab] : null,
+ selectedTab: _showSubaddress ? 0 : 1,
+ onSelectTab: (index) => setState(() => _showSubaddress = index == 0),
+ address: address ?? '',
+ qrHeading: canToggle && _showSubaddress
+ ? (wallet.unusedSubaddressIndex != null
+ ? '${i18n.receiveSubaddressTab} #${wallet.unusedSubaddressIndex}'
+ : i18n.receiveSubaddressTab)
+ : i18n.receiveAddressHeading('Monero'),
+ warning: warning,
+ onCopy: () => _copyAddressToClipboard(address!),
);
}
+
+ String? _warning(AppLocalizations i18n, AppWallet wallet, bool? subSupported) {
+ if (subSupported == false) return i18n.receiveServerNoSubaddressesWarn;
+ if (subSupported == true && !_showSubaddress) return i18n.receivePrimaryAddressWarn;
+ if (subSupported == true &&
+ _showSubaddress &&
+ wallet.unusedSubaddressIndexIsSupported == false) {
+ return i18n.receiveMaxSubaddressesReachedWarn;
+ }
+ return null;
+ }
}
diff --git a/lib/screens/restore_wallet.dart b/lib/screens/restore_wallet.dart
index 2267508..6d6022d 100644
--- a/lib/screens/restore_wallet.dart
+++ b/lib/screens/restore_wallet.dart
@@ -1,7 +1,10 @@
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/services.dart';
+import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:polyseed/polyseed.dart';
@@ -10,9 +13,9 @@ import 'package:skylight_wallet/models/fiat_rate_model.dart';
import 'package:skylight_wallet/util/get_height_by_date.dart';
import 'package:skylight_wallet/util/restore_qr.dart';
import 'package:skylight_wallet/util/secure_screen.dart';
-import 'package:skylight_wallet/widgets/loading_button.dart';
import 'package:skylight_wallet/util/logging.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
+import 'package:skylight_wallet/widgets/ui/ui.dart';
class RestoreWalletScreen extends StatefulWidget {
const RestoreWalletScreen({super.key});
@@ -22,22 +25,19 @@ class RestoreWalletScreen extends StatefulWidget {
}
class _RestoreWalletScreenState extends State<RestoreWalletScreen> with SecureScreenMixin {
- final _mnemonicController = TextEditingController();
- final _restoreHeightController = TextEditingController();
- final _restoreDateController = TextEditingController();
- DateTime _restoreDate = DateTime.now();
- bool _isPolyseed = false;
+ static final Set<String> _wordSet = WORDLIST.toSet();
+
+ static final _genesis = DateTime(2014, 4, 18);
+
+ final _restoreWalletController = RestoreWalletController();
+ DateTime? _restoreDate; // null once chosen = "I'm not sure" (scan from genesis)
+ int _restoreHeight = 0;
+ bool _scanChosen = false;
+ bool _heightManuallySet = false; // date picked / QR height
bool _isLoading = false;
- String? _mnemonicError;
- String? _restoreHeightError;
+ String _seedTypeId = 'polyseed'; // the view's default (first seed type)
- @override
- void dispose() {
- _mnemonicController.dispose();
- _restoreHeightController.dispose();
- _restoreDateController.dispose();
- super.dispose();
- }
+ bool _validWord(String w) => _wordSet.contains(w);
Future<void> _scanQrCode() async {
final result = await Navigator.pushNamed(context, '/scan_qr');
@@ -46,87 +46,100 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> with SecureSc
final parsed = parseRestoreQr(result);
if (parsed == null) return;
- setState(() {
- _mnemonicController.text = parsed.seed;
- _mnemonicError = null;
+ _restoreWalletController.setWords(parsed.seed.trim().split(RegExp(r'\s+')));
- if (parsed.restoreHeight != null) {
- _restoreHeightController.text = parsed.restoreHeight.toString();
- }
- });
-
- // No explicit height in the QR — derive it from a polyseed if possible.
- if (parsed.restoreHeight == null) {
- _calculatePolyseedHeight();
+ if (parsed.restoreHeight != null) {
+ setState(() {
+ _restoreHeight = parsed.restoreHeight!;
+ _heightManuallySet = true;
+ });
+ } else {
+ // No explicit height in the QR — derive it from a polyseed if possible.
+ _applyPolyseedHeight(parsed.seed.trim());
}
}
- Future<void> _restore() async {
- if (_isLoading) return;
+ /// Fills the date/height fields from a polyseed's birthday, when [mnemonic] is
+ /// a valid polyseed. No-op otherwise.
+ void _applyPolyseedHeight(String mnemonic) {
+ if (!Polyseed.isValidSeed(mnemonic)) return;
- final i18n = AppLocalizations.of(context)!;
+ final polyseed = Polyseed.decode(
+ mnemonic,
+ PolyseedLang.getByPhrase(mnemonic),
+ PolyseedCoin.POLYSEED_MONERO,
+ );
+ final birthdayDate = DateTime.fromMillisecondsSinceEpoch(polyseed.birthday * 1000);
setState(() {
- _mnemonicError = null;
- _restoreHeightError = null;
+ _restoreDate = birthdayDate;
+ _scanChosen = true;
+ _restoreHeight = getHeightByDate(date: birthdayDate);
});
+ }
- if (_mnemonicController.text.isEmpty) {
- setState(() {
- _mnemonicError = i18n.fieldEmptyError;
- });
- return;
- }
-
- final mnemonic = _mnemonicController.text.trim();
- final restoreHeight = int.tryParse(_restoreHeightController.text) ?? 0;
+ Future<void> _openScanFrom() async {
+ final i18n = AppLocalizations.of(context)!;
+ final result = await showScanFromSheet(
+ context: context,
+ initial: _restoreDate,
+ labels: ScanFromSheetLabels(
+ title: i18n.restoreScanTitle,
+ description: i18n.restoreScanDescription,
+ pickMonth: i18n.restoreScanPickMonth,
+ notSure: i18n.restoreScanNotSure,
+ notSureDesc: i18n.restoreScanNotSureDesc,
+ done: i18n.restoreScanDone,
+ locale: Localizations.localeOf(context).toString(),
+ ),
+ );
+ if (result == null) return;
setState(() {
- _isLoading = true;
+ _restoreDate = result.date;
+ _scanChosen = true;
+ // "I'm not sure" (null) scans from genesis; a month picks that month.
+ _restoreHeight = getHeightByDate(date: result.date ?? _genesis);
+ _heightManuallySet = true;
});
+ }
+
+ Future<void> _restore(String mnemonic, String seedTypeId) async {
+ if (_isLoading) return;
+
+ final i18n = AppLocalizations.of(context)!;
+
+ // 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 (errorMsg == 'Invalid mnemonic.') {
- setState(() {
- _mnemonicError = i18n.restoreWalletInvalidMnemonic;
- });
-
- return;
- } else if (errorMsg != '') {
- setState(() {
- _mnemonicError = i18n.unknownError;
- });
- return;
- }
+ setState(() => _isLoading = false);
if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(errorMsg)));
+ final message = errorMsg == 'Invalid mnemonic.'
+ ? i18n.restoreWalletInvalidMnemonic
+ : i18n.unknownError;
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
}
return;
} catch (error) {
log(LogLevel.error, error.toString());
- setState(() {
- _isLoading = false;
- });
+ setState(() => _isLoading = false);
if (mounted) {
- final i18n = AppLocalizations.of(context)!;
-
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.unknownError)));
}
return;
}
- setState(() {
- _isLoading = false;
- });
+ setState(() => _isLoading = false);
if (mounted) {
Provider.of<FiatRateModel>(context, listen: false).startService();
@@ -144,163 +157,68 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> with SecureSc
}
}
- Future<void> _calculatePolyseedHeight() async {
- final mnemonic = _mnemonicController.text.trim();
-
- if (!Polyseed.isValidSeed(mnemonic)) {
- if (_isPolyseed) {
- setState(() {
- _isPolyseed = false;
- _restoreHeightController.text = '';
- _restoreDateController.text = '';
- });
- }
- return;
- }
-
- final polyseed = Polyseed.decode(
- mnemonic,
- PolyseedLang.getByPhrase(mnemonic),
- PolyseedCoin.POLYSEED_MONERO,
- );
-
- final birthday = polyseed.birthday;
- final birthdayDate = DateTime.fromMillisecondsSinceEpoch(birthday * 1000);
- final restoreHeight = getHeightByDate(date: birthdayDate);
-
- setState(() {
- _isPolyseed = true;
- _restoreDate = birthdayDate;
- _restoreDateController.text = _formatDate(birthdayDate);
- _restoreHeightController.text = restoreHeight.toString();
- });
- }
-
- String _formatDate(DateTime date) {
- return '${date.year.toString().padLeft(4, '0')}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
- }
-
- Future<void> _pickDate() async {
- final picked = await showDatePicker(
- context: context,
- initialDate: _restoreDate,
- firstDate: DateTime(2014, 4),
- lastDate: DateTime.now(),
- );
- if (picked == null) return;
-
- setState(() {
- _restoreDate = picked;
- _restoreDateController.text = _formatDate(picked);
- _restoreHeightController.text = getHeightByDate(date: picked).toString();
- _restoreHeightError = null;
- });
- }
-
- void _onMnemonicChanged(String value) {
- _calculatePolyseedHeight();
-
- if (_mnemonicError != null) {
- setState(() {
- _mnemonicError = null;
- });
- }
- }
-
- void _onRestoreHeightChanged(String value) {
- if (_restoreHeightError != null) {
- setState(() {
- _restoreHeightError = null;
- });
- }
- }
-
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
+ final isMobile = Platform.isAndroid || Platform.isIOS;
+
+ final String scanValue;
+ final TextStyle scanStyle;
+ if (!_scanChosen) {
+ scanValue = i18n.restoreWalletNotSet;
+ scanStyle = BrandText.body.copyWith(color: BrandColors.inkFaint);
+ } else if (_restoreDate != null) {
+ scanValue = DateFormat.yMMM(Localizations.localeOf(context).toString()).format(_restoreDate!);
+ scanStyle = BrandText.amount;
+ } else {
+ scanValue = i18n.restoreScanFromStart;
+ scanStyle = BrandText.body;
+ }
- return Scaffold(
- appBar: AppBar(title: Text('Skylight Monero Wallet')),
- body: Center(
- child: Container(
- constraints: BoxConstraints(maxWidth: 500),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- spacing: 20,
- children: [
- Text(i18n.restoreWalletTitle, style: Theme.of(context).textTheme.headlineMedium),
- Padding(
- padding: EdgeInsets.symmetric(horizontal: 40),
- child: Text(
- i18n.restoreWalletDescription,
- textAlign: TextAlign.center,
- style: Theme.of(context).textTheme.bodyLarge,
- ),
- ),
- Padding(
- padding: EdgeInsets.symmetric(horizontal: 20),
- child: TextFormField(
- controller: _mnemonicController,
- onChanged: _onMnemonicChanged,
- keyboardType: TextInputType.multiline,
- maxLines: null,
- minLines: 3,
- decoration: InputDecoration(
- labelText: i18n.restoreWalletSeedLabel,
- errorText: _mnemonicError,
- border: OutlineInputBorder(),
- suffixIcon: (Platform.isAndroid || Platform.isIOS)
- ? IconButton(
- icon: Icon(Icons.qr_code),
- onPressed: _scanQrCode,
- )
- : null,
- ),
- ),
- ),
- Padding(
- padding: EdgeInsets.symmetric(horizontal: 20),
- child: TextFormField(
- controller: _restoreDateController,
- readOnly: true,
- onTap: _pickDate,
- decoration: InputDecoration(
- labelText: i18n.restoreWalletRestoreDateLabel,
- border: OutlineInputBorder(),
- suffixIcon: Icon(Icons.calendar_today),
- ),
- ),
- ),
- Padding(
- padding: EdgeInsets.symmetric(horizontal: 20),
- child: TextFormField(
- controller: _restoreHeightController,
- onChanged: _onRestoreHeightChanged,
- keyboardType: TextInputType.number,
- textInputAction: TextInputAction.done,
- inputFormatters: <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly],
- decoration: InputDecoration(
- labelText: i18n.restoreWalletRestoreHeightLabel,
- errorText: _restoreHeightError,
- border: OutlineInputBorder(),
- ),
- ),
- ),
- Row(
- spacing: 20,
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- TextButton(onPressed: () => Navigator.pop(context), child: Text(i18n.cancel)),
- LoadingButton(
- isLoading: _isLoading,
- onPressed: _restore,
- label: i18n.restoreWalletRestoreButton,
- ),
- ],
- ),
- ],
- ),
+ return RestoreWalletView(
+ controller: _restoreWalletController,
+ restoring: _isLoading,
+ onScan: isMobile ? _scanQrCode : null,
+ onSeedTypeChanged: (id) => setState(() {
+ _seedTypeId = id;
+ _heightManuallySet = false;
+ }),
+ onRestore: _restore,
+ // A scan-from point must be chosen first — except for polyseed, which
+ // carries its own birthday to derive the restore height from.
+ canRestore: () => _scanChosen || _seedTypeId == 'polyseed',
+ labels: RestoreWalletLabels(
+ title: i18n.restoreWalletTitle,
+ subtitle: i18n.restoreWalletDescription,
+ seedLength: i18n.restoreWalletSeedLength,
+ paste: i18n.restoreWalletPaste,
+ restoreButton: i18n.restoreWalletRestoreButton,
+ badWord: (position) => i18n.restoreWalletBadWord(position),
+ didYouMean: (word) => i18n.restoreWalletDidYouMean(word),
+ ),
+ seedTypes: [
+ SeedTypeOption(
+ id: 'polyseed',
+ label: i18n.restoreWalletSeedTypePolyseed,
+ fixedWordCount: 16,
+ ),
+ SeedTypeOption(
+ id: 'bip39',
+ label: i18n.restoreWalletSeedTypeBip39,
+ lengthOptions: const [12, 15, 18, 21, 24],
+ defaultLength: 12,
+ isValidWord: _validWord,
+ mnemonicError: (mnemonic) =>
+ bip39.validateMnemonic(mnemonic) ? null : i18n.restoreWalletChecksumError,
),
+ SeedTypeOption(id: 'legacy', label: i18n.restoreWalletSeedTypeLegacy, fixedWordCount: 25),
+ ],
+ restorePointFields: ScanFromCard(
+ label: i18n.restoreWalletScanFrom,
+ reason: i18n.restoreWalletScanFromReason,
+ value: scanValue,
+ valueStyle: scanStyle,
+ onTap: _openScanFrom,
),
);
}
diff --git a/lib/screens/restore_warning.dart b/lib/screens/restore_warning.dart
deleted file mode 100644
index 96f76ba..0000000
--- a/lib/screens/restore_warning.dart
+++ /dev/null
@@ -1,50 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:skylight_wallet/l10n/app_localizations.dart';
-
-class RestoreWarningScreen extends StatelessWidget {
- const RestoreWarningScreen({super.key});
-
- @override
- Widget build(BuildContext context) {
- final i18n = AppLocalizations.of(context)!;
-
- return Scaffold(
- appBar: AppBar(title: Text('Skylight Monero Wallet')),
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- spacing: 20,
- children: [
- Text(
- i18n.restoreWarningTitle,
- style: Theme.of(context).textTheme.headlineMedium,
- ),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 40),
- child: Text(
- i18n.restoreWarningDescription,
- textAlign: TextAlign.center,
- style: Theme.of(context).textTheme.bodyLarge,
- ),
- ),
- Row(
- spacing: 20,
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- TextButton(
- onPressed: () =>
- Navigator.pushNamed(context, '/restore_wallet'),
- child: Text(i18n.restoreWarningContinueButton),
- ),
- FilledButton(
- onPressed: () => Navigator.pop(context),
- child: Text(i18n.cancel),
- ),
- ],
- ),
- ],
- ),
- ),
- );
- }
-}
diff --git a/lib/screens/scan_qr.dart b/lib/screens/scan_qr.dart
index 5c984c7..ee85199 100644
--- a/lib/screens/scan_qr.dart
+++ b/lib/screens/scan_qr.dart
@@ -1,42 +1,19 @@
import 'package:flutter/material.dart';
-import 'package:flutter_zxing/flutter_zxing.dart';
+
import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/widgets/ui/ui.dart';
-class ScanQrScreen extends StatefulWidget {
+class ScanQrScreen extends StatelessWidget {
const ScanQrScreen({super.key});
- @override
- State<ScanQrScreen> createState() => _ScanQrScreenState();
-}
-
-class _ScanQrScreenState extends State<ScanQrScreen> {
- bool _hasScanned = false;
-
- void _onScan(Code result) {
- if (_hasScanned) return;
-
- final text = result.text;
- if (text == null || text.isEmpty) return;
-
- _hasScanned = true;
- Navigator.pop(context, text);
- }
-
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
- return Scaffold(
- appBar: AppBar(title: Text(i18n.scanQrTitle)),
- body: SafeArea(
- child: ReaderWidget(
- onScan: _onScan,
- showGallery: false,
- cropPercent: 1.0,
- tryHarder: true,
- scanDelay: Duration(milliseconds: 200),
- ),
- ),
+ return ScanQrView(
+ title: i18n.scanQrTitle,
+ onResult: (text) => Navigator.pop(context, text),
+ onBack: () => Navigator.pop(context),
);
}
}
diff --git a/lib/screens/secret_keys.dart b/lib/screens/secret_keys.dart
index 0447705..60219b7 100644
--- a/lib/screens/secret_keys.dart
+++ b/lib/screens/secret_keys.dart
@@ -4,6 +4,7 @@ import 'package:skylight_wallet/l10n/app_localizations.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';
+import 'package:skylight_wallet/widgets/ui/ui.dart';
class SecretKeysScreen extends StatefulWidget {
const SecretKeysScreen({super.key});
@@ -53,48 +54,68 @@ class _SecretKeysScreenState extends State<SecretKeysScreen> with SecureScreenMi
});
}
- Widget _field(String label, String value) => TextFormField(
- readOnly: true,
- decoration: InputDecoration(
- labelText: label,
- border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
- suffixIcon: IconButton(
- onPressed: () => SecureClipboard.copy(value),
- icon: const Icon(Icons.copy),
- ),
- ),
- controller: TextEditingController(text: value),
- );
+ void _copy(String value, {required bool sensitive}) {
+ if (value.isEmpty) return;
+ SecureClipboard.copy(value);
+ final i18n = AppLocalizations.of(context)!;
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.copiedToClipboard)));
+ }
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
final data = _data;
- return Scaffold(
- appBar: AppBar(title: Text(i18n.secretKeysTitle)),
- body: SafeArea(
- child: data == null
- ? const Center(child: CircularProgressIndicator())
- : SingleChildScrollView(
- padding: const EdgeInsets.symmetric(horizontal: 20),
- child: Column(
- spacing: 20,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Container(),
- if (data.bip39 != null)
- _field('${i18n.secretKeysMnemonic} (bip39)', data.bip39!),
- _field('${i18n.secretKeysMnemonic} (legacy)', data.legacy),
- if (data.polyseed.isNotEmpty)
- _field('${i18n.secretKeysMnemonic} (polyseed)', data.polyseed),
- _field(i18n.secretKeysPublicSpendKey, data.publicSpendKey),
- _field(i18n.secretKeysSecretSpendKey, data.secretSpendKey),
- _field(i18n.secretKeysPublicViewKey, data.publicViewKey),
- ],
- ),
- ),
- ),
+ if (data == null) {
+ return Scaffold(
+ backgroundColor: BrandColors.paper,
+ body: const SafeArea(child: Center(child: CircularProgressIndicator())),
+ );
+ }
+
+ // Seeds and the secret spend key are blurred until revealed; the public keys
+ // aren't sensitive.
+ return KeyRevealView(
+ title: i18n.secretKeysTitle,
+ description: i18n.secretKeysDescription,
+ warning: i18n.secretKeysWarning,
+ revealLabel: i18n.generateSeedReveal,
+ onCopy: _copy,
+ onBack: () => Navigator.pop(context),
+ fields: [
+ if (data.bip39 != null)
+ KeyRevealField(
+ label: '${i18n.secretKeysMnemonic} (bip39)',
+ value: data.bip39!,
+ revealable: true,
+ ),
+ KeyRevealField(
+ label: '${i18n.secretKeysMnemonic} (legacy)',
+ value: data.legacy,
+ revealable: true,
+ ),
+ if (data.polyseed.isNotEmpty)
+ KeyRevealField(
+ label: '${i18n.secretKeysMnemonic} (polyseed)',
+ value: data.polyseed,
+ revealable: true,
+ ),
+ KeyRevealField(
+ label: i18n.secretKeysSecretSpendKey,
+ value: data.secretSpendKey,
+ Why this scored 18/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.