diff --git a/src/app.cpp b/src/app.cpp index ff1e58d..d10db7e 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -88,6 +88,7 @@ #include #include #include +#include #include #include #include @@ -1922,54 +1923,193 @@ void App::reloadThemeImages(const std::string& bgPath, const std::string& logoPa void App::renderLiteFirstRunPrompt() { - // Lite-only: a brief welcome shown when no wallet file exists yet (full-node uses the wizard, - // which is skipped in lite). Routes to the existing create/restore + Backup flows in Settings. + // Lite-only guided onboarding (full-node uses its own wizard). Welcome -> (on create) reveal + // the new seed + birthday with backup warnings -> verify the user wrote it down -> done. if (!lite_wallet_ || lite_firstrun_dismissed_) return; - if (lite_wallet_->walletOpen() || lite_wallet_->walletExists()) return; + + // Wizard state (persists across frames). The seed is SECRET — wiped in finish(). + static int step = 0; // 0 welcome, 1 reveal seed, 2 verify + static std::string seed; // full seed phrase + static unsigned long long birthday = 0; + static std::vector words; // correct order + static std::vector> chips; // shuffled (word, consumed) + static int progress = 0; // # words confirmed in order + static double wrongFlashUntil = 0.0; // brief "not the next word" hint + + // The welcome page is only relevant before any wallet exists; once create has started the + // reveal/verify (step>0), keep showing it through to completion even though the wallet is open. + if (step == 0 && (lite_wallet_->walletOpen() || lite_wallet_->walletExists())) return; if (!ImGui::IsPopupOpen("##LiteFirstRun")) ImGui::OpenPopup("##LiteFirstRun"); - ImGuiViewport* vp = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(ImVec2(vp->Pos.x + vp->Size.x * 0.5f, vp->Pos.y + vp->Size.y * 0.5f), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + + auto finish = [&]() { + wallet::secureWipeLiteSecret(seed); + words.clear(); + chips.clear(); + progress = 0; + step = 0; + lite_firstrun_dismissed_ = true; + ImGui::CloseCurrentPopup(); + }; + auto splitWords = [](const std::string& s) { + std::vector out; + std::string w; + for (char c : s) { + if (std::isspace(static_cast(c))) { if (!w.empty()) { out.push_back(w); w.clear(); } } + else w.push_back(c); + } + if (!w.empty()) out.push_back(w); + return out; + }; + if (ImGui::BeginPopupModal("##LiteFirstRun", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) { - ImGui::PushFont(ui::material::Type().subtitle1()); - ImGui::TextUnformatted(TR("lite_welcome_title")); - ImGui::PopFont(); - ImGui::Spacing(); - ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 360.0f); - ImGui::TextUnformatted(TR("lite_welcome_msg")); - ImGui::PopTextWrapPos(); - ImGui::Spacing(); - ImGui::Spacing(); - const float btnW = 170.0f; - if (ImGui::Button(TR("lite_welcome_create"), ImVec2(btnW, 0))) { - // One-click create (unencrypted by default); the user backs up the seed next via the - // Settings → Backup & keys flow, where the seed can be revealed and copied. - const auto result = lite_wallet_->createWallet(wallet::LiteWalletCreateRequest{}); - if (result.walletReady) { - ui::Notifications::instance().success(TR("lite_welcome_created"), 8.0f); - setCurrentPage(ui::NavPage::Settings); - } else { - ui::Notifications::instance().warning(TR("lite_welcome_create_failed")); + + if (step == 0) { + // ── Welcome ────────────────────────────────────────────────────────── + ImGui::PushFont(ui::material::Type().subtitle1()); + ImGui::TextUnformatted(TR("lite_welcome_title")); + ImGui::PopFont(); + ImGui::Spacing(); + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 360.0f); + ImGui::TextUnformatted(TR("lite_welcome_msg")); + ImGui::PopTextWrapPos(); + ImGui::Spacing(); ImGui::Spacing(); + + if (ImGui::Button(TR("lite_welcome_create"), ImVec2(btnW, 0))) { + const auto result = lite_wallet_->createWallet(wallet::LiteWalletCreateRequest{}); + if (result.walletReady) { + // Read the freshly generated seed back so we can show + verify it. + auto s = lite_wallet_->exportSeed(); + if (s.ok && !s.seedPhrase.empty()) { + seed = s.seedPhrase; + birthday = s.birthday; + wallet::secureWipeLiteSecret(s.seedPhrase); + words = splitWords(seed); + step = 1; + } else { + // Created, but couldn't reveal the seed now — fall back to Settings backup. + ui::Notifications::instance().success(TR("lite_welcome_created"), 8.0f); + setCurrentPage(ui::NavPage::Settings); + finish(); + } + } else { + ui::Notifications::instance().warning(TR("lite_welcome_create_failed")); + } + } + ImGui::SameLine(); + if (ImGui::Button(TR("lite_welcome_restore"), ImVec2(btnW, 0))) { + ui::Notifications::instance().info(TR("lite_welcome_restore_hint"), 8.0f); + setCurrentPage(ui::NavPage::Settings); + finish(); + } + ImGui::Spacing(); + if (ImGui::Button(TR("lite_welcome_later"), + ImVec2(btnW * 2 + ImGui::GetStyle().ItemSpacing.x, 0))) { + finish(); + } + } else if (step == 1) { + // ── Reveal the seed + birthday with backup warnings ───────────────────── + ImGui::PushFont(ui::material::Type().subtitle1()); + ImGui::TextUnformatted("Back up your seed phrase"); + ImGui::PopFont(); + ImGui::Spacing(); + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f); + ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Error()); + ImGui::TextUnformatted("These 24 words are the ONLY way to restore your wallet. " + "Write them down in order, store them offline, and never share " + "them. If you lose them, your funds are gone forever."); + ImGui::PopStyleColor(); + ImGui::PopTextWrapPos(); + ImGui::Spacing(); + + // Numbered word grid (4 columns). + for (size_t i = 0; i < words.size(); ++i) { + char cell[96]; + snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str()); + ImGui::TextUnformatted(cell); + if ((i % 4) != 3 && i + 1 < words.size()) ImGui::SameLine(((i % 4) + 1) * 130.0f); + } + ImGui::Spacing(); + char bday[80]; + snprintf(bday, sizeof(bday), "Birthday (block height): %llu — back this up too.", birthday); + ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium()); + ImGui::TextUnformatted(bday); + ImGui::PopStyleColor(); + ImGui::Spacing(); ImGui::Spacing(); + + if (ImGui::Button("I've written it down", ImVec2(btnW, 0))) { + chips.clear(); + for (const auto& w : words) chips.emplace_back(w, false); + std::mt19937 rng{std::random_device{}()}; + std::shuffle(chips.begin(), chips.end(), rng); + progress = 0; + step = 2; + } + ImGui::SameLine(); + if (ImGui::Button("Copy", ImVec2(80, 0))) ImGui::SetClipboardText(seed.c_str()); + ImGui::SameLine(); + if (ImGui::Button("Skip", ImVec2(80, 0))) { + ui::Notifications::instance().success(TR("lite_welcome_created"), 6.0f); + finish(); + } + } else { // step == 2 + // ── Verify: tap the words in order ────────────────────────────────────── + ImGui::PushFont(ui::material::Type().subtitle1()); + ImGui::TextUnformatted("Confirm your backup"); + ImGui::PopFont(); + ImGui::Spacing(); + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f); + ImGui::TextUnformatted("Tap the words in the correct order to confirm you saved them."); + ImGui::PopTextWrapPos(); + ImGui::Spacing(); + + ImGui::TextDisabled("Progress: %d / %d", progress, (int)words.size()); + if (ImGui::GetTime() < wrongFlashUntil) { + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Error()); + ImGui::TextUnformatted(" — that's not the next word"); + ImGui::PopStyleColor(); + } + ImGui::Spacing(); + + for (size_t i = 0; i < chips.size(); ++i) { + ImGui::PushID((int)i); + if (chips[i].second) { + ImGui::BeginDisabled(); + ImGui::Button(chips[i].first.c_str(), ImVec2(125, 0)); + ImGui::EndDisabled(); + } else if (ImGui::Button(chips[i].first.c_str(), ImVec2(125, 0))) { + if (progress < (int)words.size() && chips[i].first == words[progress]) { + chips[i].second = true; // correct next word + ++progress; + } else { + wrongFlashUntil = ImGui::GetTime() + 1.5; // out of order + } + } + ImGui::PopID(); + if ((i % 4) != 3 && i + 1 < chips.size()) ImGui::SameLine(); + } + ImGui::Spacing(); ImGui::Spacing(); + + const bool verified = progress == (int)words.size(); + if (!verified) ImGui::BeginDisabled(); + if (ImGui::Button("Done", ImVec2(btnW, 0))) { + ui::Notifications::instance().success("Wallet created and backed up.", 6.0f); + finish(); + } + if (!verified) ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::Button("Back", ImVec2(80, 0))) step = 1; + ImGui::SameLine(); + if (ImGui::Button("Skip", ImVec2(80, 0))) { + ui::Notifications::instance().success(TR("lite_welcome_created"), 6.0f); + finish(); } - lite_firstrun_dismissed_ = true; - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button(TR("lite_welcome_restore"), ImVec2(btnW, 0))) { - ui::Notifications::instance().info(TR("lite_welcome_restore_hint"), 8.0f); - setCurrentPage(ui::NavPage::Settings); - lite_firstrun_dismissed_ = true; - ImGui::CloseCurrentPopup(); - } - ImGui::Spacing(); - if (ImGui::Button(TR("lite_welcome_later"), - ImVec2(btnW * 2 + ImGui::GetStyle().ItemSpacing.x, 0))) { - lite_firstrun_dismissed_ = true; - ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); }