diff --git a/apps/amm/qml/Main.qml b/apps/amm/qml/Main.qml index 792f011..1250bbc 100644 --- a/apps/amm/qml/Main.qml +++ b/apps/amm/qml/Main.qml @@ -45,7 +45,7 @@ Item { height: show ? 32 : 0 visible: height > 0 clip: true - color: Theme.palette.warning + color: Theme.palette.error Behavior on height { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } } @@ -56,7 +56,7 @@ Item { elide: Text.ElideMiddle font.pixelSize: 12 font.weight: Font.Medium - color: Theme.palette.background + color: Theme.palette.text text: qsTr("Unable to connect to network") } } diff --git a/apps/amm/qml/components/wallet/AccountControl.qml b/apps/amm/qml/components/wallet/AccountControl.qml index 9956eeb..378291a 100644 --- a/apps/amm/qml/components/wallet/AccountControl.qml +++ b/apps/amm/qml/components/wallet/AccountControl.qml @@ -428,14 +428,29 @@ Item { Layout.fillWidth: true height: 40 text: qsTr("Save") - onClicked: { + // The new endpoint only goes live after an app restart (the + // module can't re-open an already-open wallet), so confirm + // the user understands that before persisting. + onClicked: restartDialog.open() + } + + // Persist the change; the running wallet keeps the old endpoint + // until the user restarts (we can't reliably quit this host). + RestartRequiredDialog { + id: restartDialog + title: qsTr("Restart to apply") + confirmLabel: qsTr("Save") + message: qsTr("Changing the network endpoint only takes effect after restarting the app. " + + "Save now, then quit and reopen the app to apply it.") + onConfirmed: { if (!root.backend) return seqStatus.text = "" logos.watch(root.backend.changeSequencerAddr(seqField.text), function(ok) { seqStatus.ok = ok - seqStatus.text = ok ? qsTr("Network updated.") - : qsTr("Failed to update network.") + seqStatus.text = ok + ? qsTr("Saved — quit and reopen the app to apply.") + : qsTr("Invalid network URL.") }, function(error) { seqStatus.ok = false diff --git a/apps/amm/qml/components/wallet/RestartRequiredDialog.qml b/apps/amm/qml/components/wallet/RestartRequiredDialog.qml new file mode 100644 index 0000000..05c4289 --- /dev/null +++ b/apps/amm/qml/components/wallet/RestartRequiredDialog.qml @@ -0,0 +1,73 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Logos.Theme +import Logos.Controls + +// Modal shown before applying a setting that only takes effect after a restart. +// The caller handles `confirmed` (persist the change, then close the app). +Popup { + id: root + + property string title: qsTr("Restart required") + property string message: "" + property string confirmLabel: qsTr("Save & close") + + signal confirmed() + + modal: true + dim: true + padding: Theme.spacing.large + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + // Center on the full-window overlay rather than the small control this is + // declared inside. + parent: Overlay.overlay + anchors.centerIn: parent + width: 360 + + background: Rectangle { + color: Theme.palette.backgroundSecondary + radius: Theme.spacing.radiusXlarge + border.color: Theme.palette.backgroundElevated + } + + contentItem: ColumnLayout { + width: root.availableWidth + spacing: Theme.spacing.large + + LogosText { + text: root.title + font.pixelSize: Theme.typography.titleText + font.weight: Theme.typography.weightBold + color: Theme.palette.text + } + LogosText { + Layout.fillWidth: true + text: root.message + wrapMode: Text.WordWrap + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + RowLayout { + Layout.topMargin: Theme.spacing.medium + Layout.fillWidth: true + spacing: Theme.spacing.medium + + LogosButton { + text: qsTr("Cancel") + Layout.fillWidth: true + onClicked: root.close() + } + LogosButton { + text: root.confirmLabel + Layout.fillWidth: true + onClicked: { + root.confirmed() + root.close() + } + } + } + } +} diff --git a/apps/amm/src/AmmUiBackend.cpp b/apps/amm/src/AmmUiBackend.cpp index f937d03..c4176e3 100644 --- a/apps/amm/src/AmmUiBackend.cpp +++ b/apps/amm/src/AmmUiBackend.cpp @@ -354,12 +354,28 @@ void AmmUiBackend::persistStoragePath(const QString& path) bool AmmUiBackend::changeSequencerAddr(QString url) { - const QString trimmed = url.trimmed(); - if (trimmed.isEmpty()) { + QString normalized = url.trimmed(); + if (normalized.isEmpty()) { qWarning() << "AmmUiBackend: refusing to set empty sequencer_addr"; return false; } + // The wallet config parses sequencer_addr as a strict URL — a missing + // scheme makes the whole config fail to deserialize (and would leave the + // wallet unopenable). Default to http:// so users can type just host:port, + // then validate before writing anything. + if (!normalized.contains(QStringLiteral("://"))) + normalized.prepend(QStringLiteral("http://")); + + const QUrl parsed(normalized, QUrl::StrictMode); + if (!parsed.isValid() || parsed.host().isEmpty() + || (parsed.scheme() != QStringLiteral("http") + && parsed.scheme() != QStringLiteral("https"))) { + qWarning() << "AmmUiBackend: invalid sequencer URL" << url; + return false; + } + normalized = parsed.toString(); + const QString cfg = configPath().isEmpty() ? defaultConfigPath() : configPath(); // Preserve the other config fields (poll timeouts, retries) — only swap the @@ -370,7 +386,7 @@ bool AmmUiBackend::changeSequencerAddr(QString url) obj = QJsonDocument::fromJson(in.readAll()).object(); in.close(); } - obj.insert(QStringLiteral("sequencer_addr"), trimmed); + obj.insert(QStringLiteral("sequencer_addr"), normalized); QFile out(cfg); if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) { @@ -380,17 +396,14 @@ bool AmmUiBackend::changeSequencerAddr(QString url) out.write(QJsonDocument(obj).toJson(QJsonDocument::Indented)); out.close(); - // Re-open so the live wallet uses the new endpoint right away. - if (isWalletOpen()) { - const QString stg = storagePath().isEmpty() ? defaultStoragePath() : storagePath(); - const int err = m_logos->logos_execution_zone.open(cfg, stg); - if (err != WALLET_FFI_SUCCESS) { - qWarning() << "AmmUiBackend: reopen after sequencer change failed, code" << err; - return false; - } - refreshSequencerAddr(); - refreshAccounts(); - } + // Config is now the source of truth — reflect the change in the UI. + if (sequencerAddr() != normalized) + setSequencerAddr(normalized); + checkReachability(); + + // The module can't re-open an already-open wallet, so the new endpoint only + // takes effect on the next launch. The UI confirms a restart before calling + // this and closes the app afterwards. return true; }