feat/refactor: Breaking Changes - Replace WPF with Rust/Vue 3#218
Merged
feat/refactor: Breaking Changes - Replace WPF with Rust/Vue 3#218
Conversation
Design artifacts for the beanfun-next rewrite: - stitch-prompt.md: Glassmorphism + Fluent + Soft Depth design brief handed to Stitch with 8 preset palettes + custom hex support - beanfun-next/mockups/: 25 HTML mockups covering every Page, Dialog, and Game Tool Window, plus a shared _design-system.html showcasing theme tokens and utility classes
Bootstrap the beanfun-next workspace for the full rewrite: - Tauri v2 shell (window + bundle icons sourced from legacy Beanfun/Resources/icon.ico via `tauri icon`) - Vue 3 + Vite + TypeScript frontend baseline - Runtime deps: element-plus, pinia, pinia-plugin-persistedstate, vue-i18n@11, vue-router@4, vuedraggable@4, @tauri-apps/api - Dev deps: vitest@4, @vue/test-utils, jsdom, @types/node - Rust deps: reqwest (rustls), reqwest_cookie_store, tokio, serde, serde_json, thiserror@2, anyhow, tracing, des, cipher, sha2, quick-xml, regex, url, base64, chrono, plus Windows-only windows@0.58 / winreg / wmi, and dev deps (wiremock, axum@0.8, assert_matches, tempfile, pretty_assertions, tokio-test) - Todo.md: full rewrite plan, P-1 mockup tracking, and P0 progress with chunked delivery structure Verified: `cargo check` passes in 1m 00s; `npm run tauri dev` launches a blank Tauri window (Rust build 47s, Vite 1.6s).
Set up the code-quality and formatting baseline for beanfun-next: - ESLint 9 flat config via `@vue/eslint-config-typescript` (defineConfigWithVueTs + vueTsConfigs.recommended) with `@vue/eslint-config-prettier/skip-formatting` - Prettier 3 (.prettierrc.json + .prettierignore): single quotes, no semi, trailing comma, printWidth 100, LF - rustfmt.toml: max_width 100, LF, default heuristics - clippy.toml: msrv 1.80, complexity/argument thresholds - .editorconfig scoped to beanfun-next/ (UTF-8, LF, 2-space with 4-space override for .rs/.toml) — legacy Beanfun/ WPF project intentionally left untouched - package.json scripts: typecheck, lint, lint:fix, format, format:check, test, test:watch - DevDeps: eslint@9, eslint-plugin-vue, @vue/eslint-config- typescript, @vue/eslint-config-prettier, prettier Also applies the one-time Prettier / cargo fmt run over the Tauri scaffold (LF line endings, consistent quotes/spacing). No runtime behaviour change.
Smoke tests (P0 step 0.5): - beanfun-next/tests/unit/smoke.spec.ts + vitest.config.ts (jsdom env, globals, coverage v8) — 3 tests: vitest harness, jsdom availability, and a basic @vue/test-utils mount - beanfun-next/src-tauri/tests/smoke.rs — 4 tests covering arithmetic harness, serde_json roundtrip, reqwest client build, and sha2::Sha256 digest shape GitHub Actions CI (P0 step 0.6): - .github/workflows/beanfun-next-ci.yml - Triggers: push / pull_request on branch `code` scoped to `beanfun-next/**` + workflow_dispatch; concurrency cancels outdated runs per ref - Matrix: windows-latest + macos-latest (Windows is the real target; macOS runs provide a cross-platform sanity net) - `frontend` job: npm ci → lint → format:check → typecheck → vitest - `rust` job: dtolnay/rust-toolchain@stable + Swatinem/rust- cache@v2 → cargo fmt --check → cargo clippy --all-targets -- -D warnings → cargo test Verified locally: ESLint, Prettier, vue-tsc, Vitest, cargo fmt --check, cargo clippy -- -D warnings, and cargo test all green; YAML syntax validated.
Enforce Conventional Commits on pull requests targeting `code`.
- commitlint.config.js (repo root): extends
`@commitlint/config-conventional`. Relaxations tailored for
this repo: `header-max-length` 120 (default 72 is too tight
for scoped subjects), `body-max-line-length` / `footer-max-
line-length` disabled (Chinese bodies use multi-byte chars),
`scope-enum` disabled (we mix `next` / `updater` / `ui` /
`deps` / `ci` scopes). Ignores dependabot ("Bump X from A
to B") and "Merge pull request|branch" commits.
- .github/workflows/commitlint.yml: runs
`wagoid/commitlint-github-action@v6` on `pull_request` to
`code`, ubuntu-latest, fetch-depth 0. Read-only permissions.
Verified by running commitlint locally (via npx) against the
last 4 commits on this branch - all pass with 0 problems and
0 warnings.
…tep 0.8) Replace the Tauri + Vue scaffold README with a project-specific Traditional Chinese README covering: - Project positioning: rewrite of pungin/Beanfun (C# / WPF) in Tauri v2 + Rust + Vue 3, with the legacy C# project staying in ../Beanfun until feature parity is reached - Tech stack table (shell / backend / Windows FFI / frontend / UI / state-i18n-router / testing) - Development environment table (Node.js >= 22, Rust stable MSVC, WebView2, VS Build Tools with C++ desktop workload) - Quick Start (`npm install` + `npm run tauri dev`) - Common commands (frontend npm scripts + backend cargo commands) - Project structure tree - Testing locations (Vitest unit + cargo integration tests) - Contribution guidelines (branch / Conventional Commits / CI matrix / formatting) with link to commitlint.config.js - Roadmap section linking to the root Todo.md, with P-1 and P0 marked as complete Also marks P0 and all three chunks as complete in Todo.md (section heading gets a completion check, individual steps get `[x]` marks with a summary of what was actually shipped). No code changes.
Port the legacy C# `WCDESComp` class (Beanfun/API/WCDESComp.cs)
to Rust. Byte-compatible with the original so the existing OTP
flow (see Beanfun/Tools/BeanfunClient.OTP.cs) can be rebuilt on
top of it in P3.
Scope:
- src-tauri/src/core/mod.rs — framework-agnostic domain module
- src-tauri/src/core/wcdes/mod.rs — DES/ECB/NoPadding impl +
tests
- src-tauri/src/lib.rs — `pub mod core;`
Public API (matches Todo.md P1 spec):
- `encrypt_hex(plaintext: &str, key: &str) -> Result<String>`
Uppercase hex, ASCII plaintext, no padding.
- `decrypt_hex(hex_str: &str, key: &str) -> Result<String>`
Case-insensitive hex, ASCII output (may include \0; caller
decides whether to trim, same as the C# caller).
- `WcdesError` — typed error enum (thiserror) covering
InvalidKeyLength / InvalidPlaintextLength /
InvalidCiphertextHexLength / InvalidHexChar. The C# version
swallows all exceptions into `null`; we surface a typed
error without changing byte-level behaviour.
Parity with C# (verified by test):
- `Encoding.ASCII` lossy fallback: code points > 0x7F map to
`?` (0x3F) on both encode and decode.
- `BitConverter.ToString(..).Replace("-","")` → uppercase hex.
- `Convert.ToByte(s, 16)` → case-insensitive hex input.
- `PaddingMode.None` → plaintext length must be a multiple of
8 bytes (no zero-padding in core; caller responsible).
Tests (19 passing):
- 6 WPF-compat fixtures from Node `crypto.createCipheriv(
'des-ecb', autoPadding=false)` (byte-equal to C# DES.Create)
× 2 directions (encrypt + decrypt) = 12 effective checks
- 4 roundtrip cases (8 / 16 / 24 bytes + trailing \0)
- Hex case-insensitivity + uppercase-hex output assertions
- 9 error-path tests (key length 0/5/7/9, plaintext not
multiple of 8, hex odd length, hex non-block length, hex
with invalid char, empty plaintext allowed)
- 2 non-ASCII fallback tests (key and plaintext with `é`,
`中` mapping to `?`)
Deps: `des 0.8` + `cipher 0.4` + `thiserror 2` (already in
Cargo.toml from P0.3).
Verified locally: `cargo test`, `cargo fmt --check`,
`cargo clippy --all-targets -- -D warnings` all green.
Note: a real runtime fixture from WPF (recorded from an actual
`get_webstart_otp.ashx` response) will be added as an
integration test in P3 once the login flow is wired up.
Self-review of 87578aa flagged the block-by-block loop in `encrypt_hex` and `decrypt_hex` as duplicated: 10 lines of identical iterator / `GenericArray` / `copy_from_slice` logic differing only by `cipher.encrypt_block` vs `cipher.decrypt_block`. Centralise it in a new private `process_blocks` helper that takes the cipher, the data slice, and the per-block operation as a closure. Both public functions now collapse to a single call, and the "orchestration" responsibility (validate -> bytes -> process -> format) is no longer entangled with the low-level block iteration. - Add `DesBlock` type alias to name `GenericArray<u8, U8>` once, reused by the helper signature (the closure's `&mut DesBlock` parameter). - No behaviour change: all 19 `core::wcdes` tests still pass byte-identically (fixtures, roundtrip, error paths, ASCII fallback); no new tests required. Verified: cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test all green.
1:1 port of the WPF `ApplicationUpdater.IsNewerVersion`
(Beanfun/Update/ApplicationUpdater.cs L220-292).
- `VersionInfo { major, minor, patch, timestamp }` with raw `String` fields,
matching the C# regex-capture workflow and side-stepping premature integer
overflow on arbitrary-length timestamps.
- `is_newer(local: &str, remote: &VersionInfo) -> bool` with the exact same
semantics as C#:
* Regex branch `(\d+)\.(\d+)\.?(\d+)?\.?\((\d+)\)` for "maj.min[.patch](ts)"
local strings. Same-timestamp short-circuits to "not newer" (defensive
against release-tag typos).
* Missing local patch defaults to 0 (older `5.8(ts)` builds).
* Packs both sides as `{maj:03}{min:03}{patch:03}{ts}` and compares as `i64`.
* Regex-miss fallback: strip non-digits, left-pad to 19, parse as `i64`.
* Any parse / overflow error is swallowed and reported as "not newer",
mirroring C# `try { ... } catch { return false; }`.
15 unit tests cover: patch boundary 5.8.9 < 5.8.10, major/minor/patch/timestamp
newer cases, identical version, old "5.8(ts)" format without patch,
trailing-dot form, same-timestamp short-circuit, unparseable-local fallback,
intentionally-lossy fallback (locked in), non-numeric remote fields, and i64
overflow on both regex and fallback branches.
P2 Todo.md items covered:
- core/version/mod.rs is_newer signature
- WPF IsNewerVersion 所有 case (5.8.9 < 5.8.10, timestamp 相同, 舊格式無 patch)
Four pure HTML/URL parsers under `core::parser`, each 1:1 aligned with a
regex from the legacy WPF code so services/beanfun (P3+) can consume ASP.NET
WebForms responses without behavioural drift.
- `ParserError` (thiserror, shared) with variants
`MissingViewState`, `MissingAkey`, `MissingRequestVerificationToken`. Plus
a `Result<T>` alias, mirroring the `core::wcdes` convention.
- `viewstate.rs::extract_viewstate(html) -> Result<ViewStateForm>`:
* `ViewStateForm { viewstate: String, viewstate_generator, event_validation: Option<String> }`.
* Uses the WPF **loose** regex `id="<field>"[^>]+value="([^"]+)"` (also used
by `MainWindow.xaml.cs` for the verify flow), which subsumes the strict
variant and survives ASP.NET attribute-order reshuffles.
* Required: `__VIEWSTATE` (else `MissingViewState`). Optional:
`__VIEWSTATEGENERATOR`, `__EVENTVALIDATION` — matches the varying
expectations of the 10+ WPF call sites (initial GET vs POST round-trip,
verify-code page).
- `account.rs`: `ServiceAccountRow { is_enable, sid, ssn, sname }` with:
* `extract_service_accounts(html) -> Vec<ServiceAccountRow>` — regex
`onclick="([^"]*)"><div id="(\w+)" sn="(\d+)" name="([^"]+)"`, verbatim
from `BeanfunClient.Account.cs` L87-110, including the empty-group
`continue;` guard. `sname` is HTML-entity decoded via `html-escape`.
* `extract_account_limit_notice(html) -> Option<String>` — regex
`<div id="divServiceAccountAmountLimitNotice" class="InnerContent">(.*)</div>`.
- `akey.rs::extract_akey(input) -> Result<String>`: greedy `akey=(.*)`,
**intentionally** preserved from the WPF pattern (L261/L365/L688 in
`BeanfunClient.Login.cs`). Regression-guarded so a future "helpful" fix to
stop at `&` cannot silently change behaviour.
- `token.rs::extract_verification_token(html) -> Result<String>`: regex
`__RequestVerificationToken[^>]+value="([^"]+)"`. Tolerates both `name="..."`
(Razor) and `id="..."` (hand-rolled form) shapes.
Dependency: `html-escape = "0.2"` (v0.2.13, pure Rust, tiny). Used only in
`account.rs` to decode display names that come back as e.g. `François`.
Tests: 28 parser unit tests, ≥5 per parser (viewstate 7, account 8, akey 7,
token 6). All hand-crafted HTML snippets aligned with the WPF regexes.
Real login-flow response fixtures will be recorded in P3 and added as an
integration-test suite.
SRP/DRY self-review before commit:
- Extracted `capture_first` + `compile_field` helpers in viewstate.rs to
eliminate a dispatch-with-panic branch; each hidden field now has its
own function-local `OnceLock` adjacent to its sole use-site.
- Shared `ParserError` enum + `Result<T>` alias across all parsers.
- Each parser is a single submodule with its own regex(es) and tests —
no cross-module coupling except the shared error type.
P2 Todo.md items: all 6 parser/version sub-tasks ticked, acceptance
criterion (parser tests green, coverage ≥ 95%) met.
- Extract `capture_first(re, input) -> Option<String>` into `core/parser/mod.rs` as a crate-private helper; every parser under this module used the identical `captures -> group(1) -> owned string` chain. - Inline the helper at viewstate / account / akey / token call sites; each one collapses to a single line, with callers appending `.ok_or(err)` as needed. - Upgrade `core/version` regex init from `.unwrap()` to `.expect(...)` for consistency with the rest of the parser stack. No behavioural change. All 62 lib tests, `cargo fmt --check`, and `cargo clippy -D warnings` remain green.
Foundation for the Rust port of BeanfunClient's login surface. - `services/beanfun/client.rs`: `BeanfunClient` wrapping reqwest with a per-session cookie store shared across two internal clients (default follow-redirects, and a no-redirect variant for the `return.aspx` Set-Cookie capture step later in the flow). `ClientConfig` carries region, endpoints, timeout (30s default), body cap (16 MiB default), and UA matching the WPF client. `bounded_text` streams the response body chunk-by-chunk and aborts past the cap, preventing OOM. - `services/beanfun/error.rs`: `LoginError` enum with 18 variants mirroring every WPF `errmsg` string plus transport / parser cascades. - `services/beanfun/session.rs`: `Credentials` (zeroize on drop, redact Debug) and `Session` (redact Debug for skey / web_token). - `services/beanfun/login/session_key.rs`: region-aware `get_session_key` — TW scrapes `pSKey=…` from the final redirected URL, HK scrapes the `ctl00_ContentPlaceHolder1_lblOtp1` span from the body. Both paths reuse the same portal entry URL helper. - `tests/session_key.rs`: 7 wiremock integration tests covering happy path, missing key, empty body, body cap, and UA compliance for both regions. - `Cargo.toml`: adds `zeroize` (everything else was already pulled in during P0 scaffolding). Quality gates: 77 lib tests + 7 session_key integration + 4 smoke = 88 tests all green. `cargo fmt --check` clean. `cargo clippy --all-targets -- -D warnings` clean. Ref: WPF `Beanfun/Tools/BeanfunClient.Login.cs::GetSessionkey`.
Port the full WPF `TwRegularLogin` flow to async Rust: session key
→ Login/Index → CheckAccountType → AccountLogin → SendLogin
→ return.aspx → bfWebToken → Session.
- services/beanfun/login:
- index.rs — GET Login/Index, remap ParserError →
LoginError::MissingVerificationToken
- check_account_type.rs — POST + JSON body-sniff captcha extraction
- account_login.rs — POST + pure classify_outcome() for the 4 WPF
result-code branches (success / advance-check none / advance-check
http-url / server-message)
- send_login.rs — GET + hidden-input scrape via core/parser/form
- return_aspx.rs — POST via http_no_redirect, raw Set-Cookie scan
for bfWebToken
- tw_regular.rs — orchestrator composing the 6 steps into a Session
- mod.rs — shared ensure_success + apply_json_headers helpers
(DRY across current and future HK/TOTP/QR flows)
- core/parser/form.rs — extract_hidden_inputs() non-submit input
scraper, reused by send_login (and later the QR flow)
- client.rs — login_url / login_url_with_skey / portal_url URL helpers
Tests (115 total green, +27 from previous chunk):
- 20 new unit tests (form scraper, classify_outcome branches,
bfWebToken scan, url helpers)
- 7 new wiremock integration tests in tests/tw_login.rs covering
happy path + 6 error branches (wrong password, both advance-check
shapes, empty SendLogin, missing bfWebToken, missing Index token)
…e (P3 chunk 2 polish)
Post-review alignment for Chunk 3.2:
- account_login::classify_outcome reordered to match WPF L101-107:
`("1", "1") => AdvanceCheck`, `("1", _) => Ok` (any other
`Result` under `ResultCode=1` is success in WPF, including `""`,
`null`, `"0"`, `"2"`). Our earlier tight match on `("1", "0")`
narrowed the contract and would have spuriously rejected legal
server responses. Truth table in the module docs updated.
- account_login tests: rename `success_returns_ok` and add
`result_code_1_empty_result_returns_ok` +
`result_code_1_unknown_result_returns_ok` locking in the WPF-
permissive shape.
- return_aspx::scan_bfwebtoken: documented the `(?i)` flag as an
intentional divergence (WPF is case-sensitive) with rationale.
Integration test coverage bumped from 7 → 10 in tests/tw_login.rs:
- `check_account_type_non_json_response_falls_through_empty_captcha`
locks in the HTML-body tolerance WPF exercises at L70-78.
- `account_login_payload_propagates_captcha_from_check_account_type`
uses `body_partial_json` to prove the captcha from step 2 reaches
step 3's request body.
- `session_cookies_persist_across_login_steps` verifies the shared
cookie jar on `BeanfunClient` forwards `Set-Cookie` from
Login/Index onto CheckAccountType.
Totals: 99 lib + 7 session_key + 4 smoke + 10 tw_login = 120 green;
fmt --check and clippy -D warnings green.
Port WPF LoginCompleted (L838-882) as the fixed five-field POST to return.aspx that every non-QR login funnels through. Reuses the Chunk 3.2 post_return_aspx for transport and wraps the bfWebToken result into a Session keyed to the client's configured region. - build_completed_form(session_key, akey) emits the exact WPF field order: SessionKey, AuthKey, ServiceCode="", ServiceRegion="", ServiceAccountSN="0". ServiceCode/ServiceRegion are intentionally empty on the wire (WPF L856-857) even when the caller has real values in scope; the real metadata travels on the returned Session. - login_completed(...) debug_asserts non-empty session_key+akey then delegates to post_return_aspx, propagating MissingWebToken unchanged. - 6 unit tests lock down field order, values, empty-string wire shape and length. - 5 integration tests cover TW/HK region stamping, URL-encoded POST body substrings (SessionKey=..., AuthKey=..., ServiceAccountSN=0, ServiceCode=&, ServiceRegion=&), and MissingWebToken propagation. Intentional divergences from WPF (documented in the module docs): - Skip the redundant `Location` GET (WPF L865): with auto-redirect enabled, WPF's `ResponseHeaders["Location"]` reflects the final 200 and is usually empty — we read bfWebToken from the 302 directly via the Chunk 3.2 no-redirect pattern, which is strictly equivalent. - No `GetAccounts` / `getRemainPoint` tail (WPF L874-879): those are account-service concerns and belong in P4 `services/beanfun/account`. Keeping login_completed narrowly scoped keeps SRP clean and avoids coupling the login path to independent downstream APIs. Validation: 105 lib + 5 login_completed + 7 session_key + 4 smoke + 10 tw_login = 131 tests pass, clippy -D warnings clean, rustfmt clean, and the two rustdoc warnings introduced by the first draft are gone (the remaining 6 warnings are pre-existing and out of scope here).
Implements the HK Regular credential login end-to-end, with full
TOTP-challenge continuation surfaced through the Err channel of
`login_hk_regular`, aligning 1:1 with WPF `HkRegularLogin`
(Beanfun/Tools/BeanfunClient.Login.cs L202-293).
New modules
- `services/beanfun/login/totp_challenge.rs` -- typed continuation
handle storing the parsed ViewStateForm + session key + account id
+ HK URL + service metadata, with a Debug impl that redacts
session_key and viewstate contents. Preferred over WPF's
stash-raw-HTML-on-client approach for smaller footprint, clearer
invariants, and safer tracing.
- `services/beanfun/login/hk_error.rs` -- pure HTML parser that
classifies HK error bodies into MsgBox / pollRequest / Unrecognized,
mirroring WPF's two-regex precedence (L266 .. L274). The
pollRequest variant preserves `token` for chunk 3.3.4
(CheckIsRegisteDevice) even though 3.3.2 only surfaces the
concatenated message. Also hosts `is_advance_check` and
`classify_missing_akey_body` which both HK Regular and the
upcoming TOTP flow consume (DRY, single source of truth).
- `services/beanfun/login/hk_regular.rs` -- orchestrator that chains
get_session_key -> GET -> POST -> 4-way branch in WPF's exact
precedence order. Uses the loose `extract_viewstate` regex but
enforces HK's stricter "all three required" contract by mapping
the Option fields to typed LoginError variants.
Behavioural contract
- LoginError::TotpRequired refactored from unit variant to
`TotpRequired(Box<TotpChallenge>)` so the caller can dispatch into
`login_totp` in chunk 3.3.3 without a second HTTP round-trip.
- MissingViewState mapped from ParserError to the flattened
LoginError variant so callers see a single shape across all
three viewstate-required errors.
- `service_code` / `service_region` threaded through the signature
(WPF defaults `610074` / `T9`) -- mirrors WPF's
`HkRegularLogin(AccountID, Password, service_code, service_region)`
L191-195 and `MainWindow.xaml.cs::this.service_code` which flows
from saved config at startup (L72-73, L357-358). Values populate
the Session contract and ride on the TotpChallenge for the TOTP
continuation.
Testing
- 19 new unit tests across totp_challenge (4), hk_error (9), and
hk_regular (6) covering redaction, regex precedence, form order,
URL build, and the four-way branch classifier.
- 11 new integration tests in `tests/hk_login.rs` covering: HK
happy path / custom service metadata flows to Session / TOTP
triggered / advance check / MsgBox / pollRequest concat /
Missing{ViewState,Generator,EventValidation} / both-optional-fields
precedence / Unrecognized no-akey / POST body field presence.
- All quality gates green (fmt / clippy -D warnings / cargo doc 0
warnings).
Housekeeping
- Also cleaned 5 pre-existing rustdoc warnings from P3.2 files
(redundant explicit link targets in account_login/send_login;
ambiguous fn/mod links in check_account_type/index).
Consume `LoginError::TotpRequired(TotpChallenge)` handed back by the HK Regular flow (chunk 3.3.2) and post the 6-digit OTP to complete login. Mirrors WPF `BeanfunClient.Login.cs::TotpLogin` (L303-399) including: - 6 positional `&str` OTPs (otp1..otp6), matching WPF's signature so the otpCode1..6 wire mapping is obvious at call sites. - Region-conditional `__VIEWSTATEENCRYPTED` -- HK emits the empty field, TW drops it entirely (WPF L347-348). - WPF payload order (EVENTTARGET / EVENTARGUMENT / VIEWSTATE / VIEWSTATEGENERATOR / [VIEWSTATEENCRYPTED] / EVENTVALIDATION / otpCode1..6 / totpLoginBtn). - Branch precedence mirroring WPF L359-388: RELOAD_CAPTCHA_CODE + alert (AdvanceCheck), akey (success, login_completed), else MsgBox / pollRequest / Unrecognized via the shared classifier. - Defensive viewstate guards in WPF's check order (VIEWSTATE first, then EVENTVALIDATION, then VIEWSTATEGENERATOR) even though the challenge producer already validated them. Service metadata (`service_code` / `service_region`) rides on the TotpChallenge rather than being re-accepted on the `login_totp` signature. WPF reads `this.service_code` at both HkRegularLogin and TotpLogin call sites from the same MainWindow instance field; since the OTP UI is modal, the value cannot change between those two reads. Capture-at-HK-Regular is therefore observably equivalent to capture-at-TOTP and spares the UI layer from re-threading app-config state across the async OTP prompt await. `TotpChallenge` drops its `#[allow(dead_code)]` now that `viewstate` / `session_key` / `service_code` / `service_region` are all consumed by `login_totp`. Coverage: 4 unit tests (HK vs TW field shape, positional OTP mapping, fill values) plus 8 integration tests (happy path, custom service metadata flows to Session, advance check, MsgBox, pollRequest, unrecognized body, HK wire shape, TW wire shape verifying __VIEWSTATEENCRYPTED absence). Quality gates: fmt clean, clippy -D warnings clean, 178 tests pass, cargo doc 0 warnings.
…chunk 3.3.4)
Implements the single-shot `login_registered_device` orchestrator so HK
Regular / TOTP flows that surface a `pollRequest(...)` continuation can
drive `bfAPPAutoLogin.ashx` polling to completion, matching WPF's
`CheckIsRegisteDevice` (BeanfunClient.Login.cs L667-700) +
`bfAPPAutoLogin_Tick` (MainWindow.xaml.cs L2400-2441) behaviour.
- `LoginError`: add `DeviceRegistrationRequired { login_token, poll_url,
param }`, `DeviceLoginTimeout`, `DeviceLoginRejected` to model the
poll-response switch branches.
- `hk_error::classify_missing_akey_body`: `pollRequest` signal now
produces `DeviceRegistrationRequired` (was display-only
`ServerMessage`); tests in `hk_error`, `tests/hk_login.rs`,
`tests/totp_login.rs` updated to assert the new variant + preserve
login_token / url / param.
- `Endpoints::hk().newlogin_base`: point at
`https://tw.newlogin.beanfun.com/`, matching WPF's hard-coded host for
`CheckIsRegisteDevice` on HK region (L675-676); test + doc updated.
- `login/registered_device.rs`: new single-shot module. `Ok(Some)` on
`IntResult==2` (after `login_completed` tail), `Ok(None)` on `0/1` or
when `StrReslut` has no parseable `akey` (WPF's silent-retry path);
`-1 -> ServerMessage`, `-2 -> DeviceLoginTimeout`, `-3 ->
DeviceLoginRejected`, other/missing -> `Unknown`. `LT=<login_token>`
POSTed through `newlogin_base/login/bfAPPAutoLogin.ashx` with no
Referer (matching WPF).
- `tests/registered_device.rs`: 11 integ tests covering every IntResult
branch, akey-less StrReslut silent retry, LT payload shape, and
newlogin_base host routing.
Quality gates: `cargo fmt --check`, `cargo clippy --all-targets --
-D warnings`, `cargo test` (191 passed), `cargo doc -D warnings` all
green.
Implements `init_qr_login` — the first leg of the QR-code flow that fetches the antiforgery token, the base64 PNG and the Beanfun mobile deeplink. Mirrors WPF `BeanfunClient.GetQRCodeValue` (L409-453) + `getQRCodeStrEncryptData` (L455-476) and reuses `get_login_index` so the antiforgery extraction path lives in one place. Behaviour highlights: - Region guard: HK short-circuits with `LoginError::QrUnsupportedRegion` before any HTTP traffic, matching WPF UI's `loginMethodInit` L1099-1114 disable-button behaviour. - `bitmap_base64` keeps WPF's storage shape (`data:image/png;base64,...`) so the future Tauri UI can drop it straight into `<img src=...>`. - `deeplink: Option<String>` mirrors WPF's null-tolerant storage and passes the value through `normalize_beanfun_app_deeplink` to unwrap `play.games.gamania.com/.../deeplink/?url=...` redirects (WPF L478-504, case-insensitive host/path match for `OrdinalIgnoreCase` parity). - Layered `Result == 0` / `ResultData` / `QRImage` checks all funnel to `LoginError::QrInitResultError`, matching WPF's `errmsg = "LoginIntResultError"` branch (L429-441 + L469-473). - JSON parse failure surfaces as `LoginError::Json(...)` instead of WPF's unhandled `JObject.Parse` exception (same safety rationale as P3 chunk 3.3.4). Coverage: 10 unit tests next to the source (deeplink helper edge cases + serde shape) and 15 integration tests in `tests/qr_init.rs` spanning happy / region guard / 4-header wire shape / each layered error branch. Total suite now 216 (up from 191).
Implements `poll_qr_login_status` — the single-shot polling round-
trip that the UI fires once per second to check whether the user has
scanned / confirmed / cancelled the QR code. Mirrors WPF
`BeanfunClient.QRCodeCheckLoginStatus` (L609-665) and bundles the
classification logic into a typed `QrPollOutcome` enum so callers
can pattern-match without string compares.
Behaviour highlights:
- Single-shot API: function performs exactly one round-trip and
returns one outcome. Caller (UI / orchestrator) owns the polling
loop, cadence, and cancellation. Same contract as P3 chunk 3.3.4
device-registration polling.
- `QrPollOutcome` keeps all four WPF `ResultMessage` strings as
distinct variants (`Failed` / `WaitLogin` / `TokenExpired` /
`Approved`) instead of conflating the two "keep polling" values
into one — per the chunk 3.4 design decision (option B / no
conflation). UI can show different copy if it wants to.
- `Approved` is a unit variant: WPF's `Success` branch never reads
`ResultData` (L647-648); the downstream `qr_finalize` step pulls
`skey` / `verification_token` from the cached `QrLoginInit`.
- Region guard short-circuits with `QrUnsupportedRegion` before any
HTTP traffic, mirroring `qr_init`.
Wire shape (matches WPF L611-627 byte-for-byte):
- POST `/QRLogin/CheckLoginStatus` (note: NOT under `/Login/`).
- Headers: Accept + Referer + Origin + RequestVerificationToken +
Content-Type. Deliberately omits `X-Requested-With` because WPF
`SetBaseHeaders` clears all headers and the QR-poll path doesn't
re-add it (unlike `qr_init`, which does add it).
- Empty form body. reqwest doesn't auto-set
`Content-Type: application/x-www-form-urlencoded` for `.body("")`,
so we set it explicitly to match `WebClient.UploadString`'s
default for an empty NameValueCollection.
Error mapping:
- Unknown / missing `ResultMessage` → `LoginError::ServerMessage(raw)`
(WPF L649-652 `errmsg = response`).
- JSON parse failure → `LoginError::QrJsonParseFailed`
(WPF L634-638 `errmsg = "LoginJsonParseFailed"`).
- HTTP transport / non-2xx surface as the existing `Http` / `Unknown`
variants via `?` and `ensure_success`.
Side change: `QrLoginInit` gains a `skey: String` field so the
poll/finalize steps can rebuild the `Referer` URL without the caller
threading the value separately. Mirrors WPF `QRCodeClass.skey`.
Tests updated accordingly.
Coverage: 3 unit tests on the `PollResponse` serde shape (extras
ignored, key spelling locked, missing field tolerated) and 9
integration tests in `tests/qr_poll.rs` (4 happy `ResultMessage` →
each outcome, unknown / missing → `ServerMessage(raw)`, non-JSON →
`QrJsonParseFailed`, HK region short-circuit, full wire-shape
verification including the negative `X-Requested-With` assertion).
Total suite now 228 (up from 216).
Implements `finalize_qr_login(client, &init) -> Session`, the final HTTP sequence after `poll_qr_login_status` returns `Approved`. Mirrors WPF `BeanfunClient.Login.cs::QRCodeLogin` (L530-607) split into three discrete round-trips for testability: - step 1: GET `QRLogin/QRLogin` (handshake; body discarded, mirroring WPF L535-541 which only `Debug.WriteLine`'s the response). - step 2: GET `Login/SendLogin` via the shared `send_login` helper, passing the QR-specific Accept string (WPF L545 — adds image/avif, image/webp, image/apng over the TW Regular L124 string). - step 3: POST `return.aspx` via the shared `post_return_aspx` helper, scraping bfWebToken from the raw Set-Cookie header (WPF L588-598). Skips WPF `LoginCompleted`'s second `return.aspx` POST with the garbage `AuthKey="OK"` payload (L838-882) — its only useful side effect, the bfWebToken capture, is already done in step 3. `GetAccounts` is left to P3.5. DRY: parameterises `send_login`'s Accept header (callers now pass the exact byte string; TW Regular and QR send different values per WPF). SRP: Accept is a "self-description" detail and now lives at the callsite, where the WPF reference is grep-able. `Session.account_id` is set to "" because QR login has no user-typed account id; P3.5's GetAccounts will populate it. Documented divergence: reqwest 0.12 (via hyper) auto-injects `Accept: */*` on every request with no public API to suppress it, so step 3 sends `Accept: */*` while WPF sends none. RFC 9110 §12.5.1 specifies these as semantically equivalent. Locked by the wire-shape test so any other unintended Accept value still trips an assertion. Quality gates: cargo fmt --check, cargo clippy --all-targets -D warnings, cargo test --workspace (239 pass — +11: 9 integration + 2 unit), RUSTDOCFLAGS=-D warnings cargo doc --no-deps all green.
…view)
Chunk 3.4 self-review uncovered an unverified assumption in the QR
finalize flow: the initial implementation skipped WPF's second
return.aspx POST inside LoginCompleted (BeanfunClient.Login.cs L838-882)
on the rationale that "the only useful side effect (capturing
bfWebToken) was already done in step 3". A line-by-line re-read showed
WPF deliberately re-reads bfWebToken from the cookie jar AFTER that
second POST (L868), meaning the WPF developers expected the POST to
either rotate the token or carry session-affirmation state we don't
observe from the outside.
Per the 1:1 functional parity rule, and to eliminate the stale-token
risk in the absence of a real-server test bed, finalize_qr_login now
runs all four WPF steps:
1. GET QRLogin/QRLogin (handshake, body discarded)
2. GET Login/SendLogin
3. POST return.aspx with the SendLogin form (token captured but
deliberately discarded -- transient)
4. login_completed("OK", ...) -- shared 5-field LoginCompleted POST,
canonical bfWebToken comes from here
The QR flow now reuses login_completed verbatim (DRY: same helper that
HK Regular and TOTP funnel through), with the QR-specific bits limited
to the akey="OK" sentinel and an empty account_id.
Drive-by:
- Fix completed.rs doc bug: it referenced "QRCodeCompleted" (a method
that doesn't exist in WPF) when describing the shared tail. The
actual WPF method is LoginCompleted; QR / HK Regular / TOTP all
funnel through it. TW Regular is the lone exception (its return.aspx
form is scraped from SendLogin, not the fixed 5-field shape).
- Document why the QR flow passes "OK" as akey on the parameter docs.
Tests:
- happy_path now locks Session.web_token == step 4 token AND
!= step 3 token, so a future regression that flips back to
"skip step 4" trips the assertion.
- Split return_aspx_missing_set_cookie into per-step variants:
step3_missing_set_cookie_yields_missing_web_token (with
short-circuit assertion) and
step4_login_completed_missing_token_yields_missing_web_token
(the canonical user-facing failure surface).
- New step4_login_completed_posts_five_field_form_with_authkey_ok
asserts the 5-field LoginCompleted body shape and proves step 3's
fields don't leak into step 4.
- New steps_3_and_4_post_return_aspx_in_that_order asserts the two
return.aspx POSTs happen in the right sequence.
- Wiremock body_string_contains discriminators (AuthKey=AUTH_INNER
for step 3, AuthKey=OK for step 4) keep the two POSTs cleanly
separated despite sharing the same path.
Quality gates: cargo fmt / clippy -D warnings / cargo test
(qr_finalize 12 integ + 2 unit, all 11 binaries green) /
cargo doc --no-deps all pass.
Port WPF `BeanfunClient.Logout()` (Login.cs L884-909) as a 3-step
region-aware sequence:
1. GET portal_base/generic_handlers/remove_bflogin_session.ashx
2. GET {logout_host}/logout.aspx?service=999999_T0
(TW → newlogin_base, HK → login_base, mirroring WPF's overloaded
`loginHost` local at L887-897)
3. POST newlogin_base/generic_handlers/erase_token.ashx with
`web_token=1` (TW only; WPF L900 region guard)
Failure policy: best-effort — every step runs regardless of earlier
failures, and we return the FIRST error encountered. WPF's callers
swallow errors entirely (try/catch in App.xaml.cs L72-76 +
MainWindow.xaml.cs L237-241), but surfacing the first error gives
us diagnostic value without breaking that fire-and-forget contract
(callers can still `let _ = logout(&client).await;`).
Cookie jar deliberately not cleared, mirroring WPF (which never
clears its WebClient cookies in Logout). Long-lived isolation is
done by dropping and rebuilding the BeanfunClient.
Also adds `BeanfunClient::newlogin_url()` helper to mirror the
existing `portal_url` / `login_url` API; first user is logout's
step-2-TW and step-3 URL builds.
Test coverage:
- tests/logout.rs (10 tests): TW happy / HK happy + step-3 skip /
service=999999_T0 query / web_token=1 form body + Content-Type /
per-step 5xx failure × 3 + still-attempts-remaining-steps proof /
multi-step-fail returns FIRST error / TW step-2 routes through
newlogin_base / HK step-2 routes through login_base.
- tests/login_then_logout.rs (2 tests): TW Regular login → logout
hits all 3 logout endpoints; HK Regular login → logout hits
only 2; both assert cookie jar is non-empty after logout
(WPF-aligned never-clear policy lock).
Quality gates: fmt / clippy -D warnings / cargo test
(255 tests) / cargo doc all green.
Refs: WPF BeanfunClient.Login.cs::Logout L884-909
Add `login::orchestrator` — a typed `LoginMethod` enum + `login_with`
single-call dispatcher that routes single-shot password flows to
the right underlying `login_*` function. This is the entry point
the Tauri command layer (P6) will call from the IPC boundary.
Variants:
- `LoginMethod::TwRegular` → `login_tw_regular`
- `LoginMethod::HkRegular { service_code, service_region }`
→ `login_hk_regular`
TOTP and QR are deliberately NOT in the enum — module docs explain
why in detail. Short version:
- HK TOTP needs a mid-flow interactive 6-digit code from the user;
fits a two-call (`TotpChallenge` → submit) shape, not a single
`dispatcher → Session` shape.
- QR Code is a 3-step UI-driven flow (`init` → poll-loop →
`finalize`); the UI must observe `QrPollOutcome` transitions to
render the QR image and react to user actions.
- HK device-registered re-login is a recovery path off TOTP, not a
user-selectable top-level method.
All four flows remain reachable as direct public exports from the
parent module — the dispatcher is purely a typing / routing
convenience for the single-shot subset.
Test coverage:
- One unit test in `orchestrator.rs` pinning `LoginMethod: Send`
(the dispatcher runs from tokio worker threads via Tauri).
- `tests/orchestrator.rs` (3 tests): TW dispatch produces a
TW session; HK dispatch with region defaults produces an HK
session; HK dispatch with custom service_code/service_region
forwards them all the way to the returned `Session` (creds +
service-args plumb-through lock).
Per-flow branch coverage already lives in `tw_login.rs` /
`hk_login.rs`; this file's job is solely the dispatch contract.
Also updates `Todo.md` to tick chunk 3.5 and document the
dispatcher / cookie-jar / first-error design decisions.
Quality gates: fmt / clippy -D warnings / cargo test
(259 tests) / cargo doc all green.
… chunk 3.5 review) Doc-only sweep after reviewing chunk 3.5 against the WPF reference and the current Rust surface. Four staleness/accuracy fixes, no behaviour changes. - orchestrator.rs::LoginMethod::TwRegular: previous wording claimed TW scrapes service_code/service_region from the SendLogin form's hidden inputs. Reality: login_tw_regular's signature does not take them and the returned Session is always populated with LoginRegion defaults. The real reason the dispatcher omits them is that GetAccounts (the only consumer of those args in WPF's TwRegularLogin) is deferred to P4. Doc rewritten to describe the signature reason and the P4 forward-compatibility path. - logout.rs failure policy: previous wording mentioned only one divergence from WPF (we return the first error vs WPF callers silently swallowing). It missed that WebClient throws on the first non-2xx response, so WPF's Logout() actually short-circuits internally too -- our best-effort all-steps behaviour is the second, intentional divergence. Both are now spelled out, with a separate subsection explaining the choice of returning the first error rather than a Vec<LoginError>. - client.rs::cookie_store(): previous wording said "(logout)", but the chunk 3.5 never_clear policy means logout no longer touches this API -- the only caller in the entire codebase is tests/login_then_logout.rs asserting the never_clear invariant. Doc rewritten to be honest about current usage and to point at logout.rs for the rationale. Visibility kept pub: integration tests only see pub items, and future P4/P6 callers (multi-session diagnostics) may legitimately need direct jar access. - logout.rs cookie-jar section: replaced the brittle "see client.rs module docs L20-22" hard-coded line range with an intra-doc link to the "Cookie jar" section name. Quality gates: cargo fmt, cargo clippy --all-targets -D warnings, cargo test --workspace (259 tests), cargo doc --no-deps --workspace (0 warnings).
Implement `services/beanfun/account.rs` covering the four account
JSON-shaped endpoints from `BeanfunClient.Account.cs`:
- `get_accounts(...)` — orchestrates auth.aspx → list page →
per-row `get_create_time` (silenced to None per row, matching
WPF `try { ... } catch { return null; }`) → ssn-asc sort.
- `get_service_contract(...)` — POST `gamezone.ashx`
`GetServiceContract`, returns the JSON `strResult` (empty when
`intResult != 1`, matching WPF).
- `add_service_account(...)` — POST `gamezone.ashx`
`AddServiceAccount`. Empty `name` short-circuits to `Ok(false)`
before firing, matching WPF's guard.
- `change_service_account_display_name(...)` — POST `gamezone.ashx`
`ChangeServiceAccountDisplayName`. Empty / unchanged name
short-circuits to `Ok(false)`, matching WPF's two early returns.
Public types: `ServiceAccount`, `AccountListResult`,
`AmountLimitNotice` (None / AuthReLoginRequired / Other(String)).
Service layer keeps the notice as raw Traditional Chinese; i18n
and simplified-Chinese conversion are deferred to the UI layer
(WPF's `I18n.ToSimplified()` + `TryFindResource("AuthReLogin")`
were view-layer concerns).
User-defined account ordering (`AccountList.ApplyAccountOrder`) is
intentionally deferred to P5 (storage) / P6 (commands); P4.1 only
performs the deterministic ssn-asc first-pass sort that WPF does
inline.
Supporting changes:
- `core/time.rs`: new `dt_compact` (`Y(M-1)DDhhmmssfff`) and
`dt_iso` (`yyyyMMddHHmmss.fff`) cache-buster formatters
reproducing `BeanfunClient.cs::GetCurrentTime(2)` / `(1)` from
spec (no WPF source referenced). Both functions take a
`chrono::DateTime<Local>` so unit tests can pin the timestamp.
- `core/parser/account.rs`: new
`extract_service_account_create_time` + supporting regex for
the `<input id="dteCreate" value="..." />` field that
`get_create_time` scrapes.
- Re-exports added in `core/parser/mod.rs` and
`services/beanfun/mod.rs`.
Tests:
- 13 integration cases in `tests/account.rs` covering each
endpoint's happy path plus the WPF-aligned edge cases (sorting,
quota notices, partial create-time failures, empty list, empty
name, same-name guard, `intResult != 1`).
- Unit tests for `parse_int_result_eq_one` (empty body, null,
missing field, non-1 ints, invalid JSON) and
`classify_amount_limit_notice` (absent, advance-auth substring,
arbitrary text preserved verbatim).
- Unit tests in `core/time` use pinned `DateTime` values to
assert exact formatter output, including January (month0=0),
October (month0=9), December (month0=11), and the
zero-padding edge cases.
Quality gates: cargo fmt --check, cargo clippy --all-targets
-D warnings, cargo test --all-targets, cargo doc
--no-deps --document-private-items all green.
Port `BeanfunClient.OTP.cs::GetOTP` (151 lines C#) to async Rust as
`services/beanfun/otp.rs`. Issues a 5-step HTTP sequence against the
Beanfun portal, then DES-ECB-decrypts the resulting envelope into the
8-character OTP the launcher hands to the game client.
What lands
----------
* `services/beanfun/otp.rs` — `get_otp(client, session, account, sc, sr)`
drives the full flow:
1. GET game_zone/game_start_step2.aspx -> longPollingKey [+ TW unkData] [+ screatetime fallback]
2. GET generic_handlers/get_cookies.ashx -> m_strSecretCode
3. POST generic_handlers/record_service_start.ashx (response discarded; primes server-side state)
4. GET get_result.ashx?meth=GetResultByLongPolling (response discarded; long-poll trigger)
5. GET generic_handlers/get_webstart_otp.ashx -> "1;{key8}{ciphertext_hex}"
6. WCDES decrypt -> trim trailing NULs -> OTP
Each step is a private SRP-clean async helper; pure parsing /
envelope-decoding logic is split into `parse_*` + `step_6_decrypt`
functions for unit testing.
* `services/beanfun/error.rs` — 7 new typed `LoginError` variants
(1:1 with WPF `errmsg` strings):
OtpMissingLongPollingKey { snippet }
OtpMissingUnkData (TW only)
OtpMissingCreateTime
OtpMissingSecretCode
OtpEmptyResponse
OtpServerRejected { message }
OtpDecryptionFailed { cause }
* `services/beanfun/mod.rs` — register `pub mod otp` + re-export
`get_otp`; Layers table picks up the new row.
* `tests/otp.rs` — 12 wiremock-backed integration tests covering
TW happy / HK happy / 4 step1 errors / 1 step2 error / 3 step5
errors / 1 step6 decrypt failure / 2 wire-shape locks.
Design decisions worth flagging
-------------------------------
* OTP step 2 host is region-asymmetric (TW = tw.newlogin.beanfun.com,
HK = login.hk.beanfun.com). Existing `Endpoints` schema doesn't
cover this exact split; rather than adding a fourth base URL for
one call, `step_2_get_secret_code` branches on
`client.config().region` between `newlogin_url` (TW) and
`login_url` (HK). Documented in module doc.
* `account.screatetime == None` fallback: WPF mutates the input
account; we keep `&ServiceAccount` immutable and store the
fallback in a local `Step1Data.screatetime`. Re-uses
`core::parser::extract_service_account_create_time` from P4.1
(DRY win).
* WPF dev artifacts NOT ported (per cross-chunk policy):
`ServicePointManager.Expect100Continue = false` (reqwest's
default behaviour is byte-equivalent), and the commented-out
`Thread.Sleep` / `Console.WriteLine` (dead code).
* `step_5_get_otp` builds its URL via `format!` rather than
reqwest's `.query()` builder to preserve byte-for-byte WPF wire
format: `CreateTime` uses `%20` (not form-encoded `+`), and the
64-char `ppppp=` hex literal must appear verbatim.
* `OtpServerRejected.message` carries server text raw — UI prepends
the localised "Get OTP failed" prefix, matching the
`AmountLimitNotice` separation-of-concerns established in P4.1.
* `tick_count_ms()` mirrors .NET's `Environment.TickCount` (i32 ms,
wraps every ~24.8 days); pure cache-buster, server doesn't
validate.
* New crate dep: `percent-encoding = "2"` (already a transitive dep
via `url`); `Uri.UnescapeDataString`-equivalent for parsing the
TW unk_data URL-encoded fragment.
Quality gates
-------------
* cargo fmt --all -- --check green
* cargo clippy --all-targets -D warnings green
* cargo test --all-targets 193 lib + 12 otp + all prior
integration tests pass
* cargo doc --no-deps --document-private-items zero warnings
Port BeanfunClient.Verify.cs (getVerifyPageInfo / getVerifyCaptcha /
verify) plus MainWindow.xaml.cs::reLoadVerifyPage parsing and
verifyWorker_DoWork response classification into
services/beanfun/verify.rs. Surfaces the 3-call captcha re-auth
path triggered by LoginError::AdvanceCheckRequired.
Public API (TW only by design):
- get_verify_page_info(client, advance_check_url: Option<&str>)
-> Result<VerifyPageInfo, LoginError>
- get_verify_captcha(client, samplecaptcha) -> Result<Vec<u8>, LoginError>
- submit_verify(client, page_info, verify_code, captcha_code)
-> Result<VerifyOutcome, LoginError>
Six new typed LoginError variants (1:1 mapped to WPF errmsg
strings): VerifyUnsupportedRegion, VerifyMissingViewState,
VerifyMissingEventValidation, VerifyMissingSampleCaptcha,
VerifyMissingLblAuthType, VerifyCaptchaImageTooSmall { actual }.
VerifyOutcome enum captures the four ways verifyWorker_DoWork reads
the POST response: Success, ServerMessage(String), WrongCaptcha,
WrongAuthInfo. All four variants are valid HTTP-200 business
results returned through Ok(); only transport / parse failures take
the Err channel.
HK clients are rejected up front with VerifyUnsupportedRegion. WPF
hardcodes tw.newlogin.beanfun.com on all three endpoints
(BeanfunClient.Verify.cs L23-25 / L43-45 / L90-92 +
MainWindow.xaml.cs L797-803) and only the TW account_login branch
sets advanceCheckUrl (BeanfunClient.Login.cs L186), so HK regular /
TOTP triggers of LoginAdvanceCheck would hit a TW host with HK
cookies in WPF -- a silent dead path. We surface the typed error
instead, leaving the UI to fall back to "please re-login" rather
than render a verify form for HK sessions.
Pure helpers (ensure_tw, build_default_advance_check_url,
build_captcha_url, build_verify_form, parse_verify_page,
classify_verify_response) keep all parsing / form construction /
outcome classification side-effect-free for unit testing. The
private bounded_bytes helper mirrors BeanfunClient::bounded_text
without UTF-8 validation; lives in verify.rs because captcha is
the only byte-returning call across the entire service surface.
Tests: 18 unit (region guard + URL builders + form shape + parse
field-missing branches + outcome classification) + 15 integration
(wiremock-backed full 3-call flow + HK rejection x 3 + URL routing
+ alert short-circuit + captcha size threshold + POST body wire
order + 4 outcome variants). Full suite: 214 lib + 123 integration,
fmt / clippy --all-targets -- -D warnings / cargo doc all clean.
Drop the strict TW-only region guard from services/beanfun/verify.rs and
let HK clients run the same 3-call flow as TW clients, matching WPF
byte-for-byte.
WPF rationale (re-audited):
- BeanfunClient.Verify.cs L23-25 / L43-45 / L90-92 hardcode
`tw.newlogin.beanfun.com` for AdvanceCheck.aspx GET, BotDetectCaptcha
GET, and AdvanceCheck.aspx POST regardless of LoginRegion.
- MainWindow.xaml.cs::reLoadVerifyPage L797-803 strips and re-prepends
the same TW host onto the form action.
- HK regular (BeanfunClient.Login.cs L249) and HK TOTP (L361) both
raise `LoginAdvanceCheck` when the server returns
`RELOAD_CAPTCHA_CODE` + `alert`. That signal is server-driven, so HK
reaching this flow is a supported recovery path, not a dead branch.
Earlier port rejected HK with a typed `VerifyUnsupportedRegion`,
assuming "HK cookies on TW host" was a guaranteed-fail dead path.
Re-audit shows that interpretation removes a flow WPF has shipped to HK
users for years; align with WPF instead and let the server decide cookie
acceptance. Failure modes degrade gracefully into existing typed
`VerifyMissing*` errors (same observable surface as WPF's
`VerifyNoViewstate` / `VerifyNoEventvalidation`).
Changes:
- error.rs: remove `LoginError::VerifyUnsupportedRegion` (was unused
after the guard came out).
- verify.rs: delete `ensure_tw` helper + its 3 call sites + 2 unit
tests; rewrite module-level "Region routing" docs and per-fn
`# Errors` sections to document the region-agnostic contract; add
`url_helpers_target_tw_newlogin_host_even_for_hk_client` unit test
to lock the `Endpoints::hk().newlogin_base = TW` invariant we now
rely on.
- tests/verify.rs: drop 3 hk-rejection integration tests; add
`hk_happy_path_runs_full_flow_via_tw_newlogin_routing` exercising
the full 3-step flow against an HK-configured client.
- Todo.md: rewrite chunk 4.3 design decisions to record the new HK
alignment policy and updated test counts (17 unit + 13 integration).
Quality gates:
- cargo fmt --all -- --check ........ pass
- cargo clippy --all-targets -- -D warnings .. pass
- cargo test --all-targets .......... 213 lib + 121 integration pass
- cargo doc --no-deps --document-private-items .. 0 warnings
Port `BeanfunClient.UnconnectedGame_*` (init / check / check_nickname /
add / change_password) from `BeanfunClient.Account.cs` to
`services/beanfun/account.rs`, preserving 1:1 functional parity with
the WPF implementation while exposing a typed, region-aware Rust API.
- Public types: `AddAccountSession` (viewstate triplet + region for
round-tripping WebForms hidden state, with HK-only `__VIEWSTATEENCRYPTED`
materialised internally), `AddAccountInit`, `CheckOutcome`,
`AddAccountOutcome` (`Success` | `ErrorMessage`), `ChangePasswordOutcome`
(`VerifyCodeSent` | `ErrorMessage`).
- Public functions: `unconnected_game_init_add_account_payload`,
`unconnected_game_add_account_check`,
`unconnected_game_add_account_check_nickname` (shares
`add_account_check_inner`), `unconnected_game_add_account`,
`unconnected_game_change_password` (5-step orchestration).
- 5 typed errors mirroring `VerifyMissing*` pattern:
`AccountMgmtMissingViewState{,Generator}`,
`AccountMgmtMissingEventValidation`, `AccountMgmtMissingGameName`,
`AccountMgmtMissingAccountLen`.
- Region handling: `mgmt_url` injects `TW/` vs `HK/` portal prefix;
`change_password_url` deliberately downgrades scheme to `http://` for
the three HK-only `change_password` steps to match the apparent WPF
typo (documented as a `# WPF deviation candidate` for P10 security
review).
- 20 unit tests + 15 integration tests in
`tests/account_management.rs` covering TW/HK happy paths, missing
hidden-field typed errors, DN field-name divergence (`t1` vs
`txtServiceAccountDN`), `__VIEWSTATEENCRYPTED` HK toggle,
change_password 5-step success / `lblErrorMessage` rejection /
Unknown outcome.
- Quality gates: `cargo fmt --check` clean, `cargo clippy --all-targets
-- -D warnings` 0 warning, `cargo test --all-targets` 237 lib units +
13 integration binaries 0 failed, `cargo doc --no-deps
--document-private-items` 0 warning.
Beanfun prod returns ResultCode / Result / ResultMessage / Captcha as integers for some branches; WPF reads them via JToken.ToString() and tolerates any JSON scalar, while Rust serde Option<String> rejects `invalid type: integer 1, expected a string` and fails the whole login. Add a shared `deserialize_jtoken_to_string` visitor (str / i64 / u64 / f64 / bool / null, bool maps to .NET-capitalised "True" / "False", object + array still rejected as a regression probe) and a `parse_step_json` wrapper that logs a bounded body_preview via tracing::warn! when parsing fails, applied to the four affected fields in account_login.rs + check_account_type.rs. 20 new tests pin the parity matrix, legacy all-string shape, mixed int/str shape, and the UTF-8-safe log truncation.
…sites Happy-path login had no operator-visible log, and the three diagnostic failure sites (TW / HK session-key regex miss, SendLogin empty scrape) surfaced only as typed `LoginError` variants with no context on the wire shape that actually came back from Beanfun. That left us without enough data to tell a transient server-side hiccup apart from a real protocol change when a user reports a failure. Add a symmetrical pair of `tracing::info!` lines at the two terminal "Session assembled" sites (`tw_regular` for TW, `completed` for HK Regular / HK TOTP / QR Code — the three flows that funnel through `login_completed`), logging `step` / `region` / `account_id`. Session bearers (skey, web_token, akey) are deliberately not logged; the `Session::Debug` impl already redacts them, and `account_id` is the same field `SessionInfo` exposes to the frontend. On the failure side, wrap the three `.ok_or(...)` / `if empty` sites with `tracing::warn!` that includes either the final redirected URL (TW session-key) or a bounded `body_preview` (HK session-key + SendLogin) capped at the shared `BODY_LOG_PREVIEW_CHARS` via the existing `truncate_chars` helper (UTF-8 safe, CJK won't split mid-codepoint). Helper + const stay private to `login/mod.rs` — submodules access them as descendants, no visibility escalation needed. No behavioural change: the helper / const already existed for the P3 JToken-parity fix and every `LoginError` variant and error-code mapping is unchanged. This is purely observability.
- Initialise a tracing_subscriber in lib.rs so backend tracing::info!/warn! events reach the Tauri dev console. This is the prerequisite for the D5 GamePass cookie-jar dumps. - Hoist read_bfwebtoken_from_jar into login/mod.rs, scoped to the portal origin to match WPF BeanfunClient.GetCookie. Used by login_completed now; reused by GamePass later. - login_completed: follow the Location header on 302 like WPF L803-836, read the token from the jar instead of the Set-Cookie header, and use account_id="<deferred>" as the sentinel the orchestrator resolves via GetAccountInfo afterwards. - qr_poll: accept the "411 Length Required" body shape the live server occasionally returns (WPF L1114-1141 parity). - qr_finalize: swallow MissingWebToken on step 3 so the next poll cycle picks the token up (empty-webtoken retry, WPF parity). - return_aspx: docstring-only clarification, no behaviour change. - Tests: rename the QR-finalize fixture hook to mount_after_landing across hk/tw/totp/qr/orchestrator/logout/registered-device harnesses, and add a login_completed case covering the <deferred> sentinel. - P11 hotfix: restore the LightBlue accent override path in useThemeColor + matching spec coverage.
Rebuild the WPF login flow as Vue pages under `/login`, with the
parent LoginPage shell, region picker, id-pass, QR, and GamePass
forms all wired into the hash-mode router. The 4 forms mirror the
WPF XAML-driven flow 1:1 while adopting Element Plus for layout and
keeping i18n / Pinia / theme plumbing centralised.
Frontend (D1-D5):
- `pages/LoginPage.vue`: parent shell rendering `<RouterView />`
and page-scoped chrome. Swaps the P11 placeholder page out.
- `pages/LoginRegionSelection.vue` (D2): Taiwan / Hong Kong picker
persisted to config store; sets the default region honoured by
subsequent form-to-form navigations.
- `pages/IdPassForm.vue` (D3): account + password form covering
regular + TOTP + registered-device + verify branches via the
`loginRegular` / `loginTotp` / `loginRegistered` store actions;
switch links to QR and GamePass.
- `pages/QrForm.vue` (D4): QR poll with expired / approved /
pending / retry handling; HK redirect toast; inline
connection-lost fallback with a Reload affordance.
- `pages/GamepassForm.vue` (D5): CP2 frontend — 4-step progress
tracker (prepare / openWindow / authenticate / complete),
HK-unsupported redirect toast, connection-lost / window-error
banners, Reload affordance.
Router / shared infra:
- `router/index.ts`: replace the P11 placeholder with the nested
`/login` shell + named children (`login-region` / `login-id-pass`
/ `login-qr` / `login-gamepass`).
- `i18n/messages.ts`: retire `placeholder.*` namespace; add
`loginShell.*` / `loginRegion.*` / `loginQr.*` / `loginGamepass.*`
and `errors.auth.gamepass_window_already_open` across zh-TW /
zh-CN / en-US.
- `stores/auth.ts`: switch `loginQrCheck` to `SafeResult<QrStatus>`
so poll loops can branch on success / error without duplicate
toasts; add `loginGamepassStart` + `applyGamepassSession`
actions and the `LoginGamepassStart` guard slot.
- `types/bindings.ts`: add `loginGamepassStart` /
`openGamepassWindow` bindings generated by tauri-specta.
- Specs: `tests/unit/pages/*` for each new page, updated router /
i18n / auth-store specs.
Backend (D5):
- `services/beanfun/login/gamepass.rs`: new module owning the WPF
GamePass flow parity — `seed_webview_cookies_from_client`,
`try_complete_gamepass_login`, `inject_webview_cookies`, and the
host-only cookie domain-rehydration fix discovered during live
retest (host-only cookies are re-emitted with an explicit
`Domain` attribute so the Tauri WebView accepts them).
- `commands/auth.rs`: `login_gamepass_start` +
`open_gamepass_window` commands plus the Tauri event emitters
(`gamepass-login-success` / `gamepass-login-failed`) and the
`trace_cookie_jar` diagnostic helper used for debugging the
cookie-seed race.
- `commands/state.rs`: `pending_gamepass` slot holding the
`PendingGamepass { client, skey }` struct across the user's
WebView round-trip.
- `commands/mod.rs`: register the new commands via
`collect_commands!` turbofish.
- `commands/error.rs`: map `LoginError::GamepassUnsupportedRegion`
→ `auth.gamepass_unsupported_region`, plus inline race-guard /
navigate codes (`auth.gamepass_window_already_open`,
`ui.gamepass_navigate_failed`).
- `services/beanfun/error.rs`: add
`LoginError::GamepassUnsupportedRegion` variant.
- `services/beanfun/login/mod.rs`: re-export the new gamepass
module.
Mark the P12.1 D1-D5 login-flow milestones + the two D5 live-test hotfixes (cookie-seed race guard, host-only cookie domain rehydration) as complete in Todo.md. Keeps the Todo.md history as the single source of truth for D-step sequencing per user rule #5.
… audit + auth guards
D6 - TOTP entry: extract reusable useOtpInputs composable (single-source
six-digit OTP state + paste/keyboard handling, SRP-conformant), wire
LoginTotp.vue page + spec, route /login/totp guarded by pendingTotp.
D7 - LoginWait splash: add LoginWait.vue + spec for the post-login
service-token redirect window, route /login/wait keyed off the auth
store session-pending state.
D8 - Verify (advanced auth): add VerifyPage.vue + spec covering the
captcha refresh / submit / wrong-captcha / wrong-auth-info / alert
branches; rename auth.verify_required to auth.advance_check_required
across services/invoke doc + IdPassForm spec to match the WPF
AdvanceCheck.aspx flow exactly. pendingVerify slot added to auth store
to gate /login/verify entry.
D9 - i18n key-usage audit: add tests/unit/i18n/key-usage.spec.ts that
statically scans src/{pages,composables,components,stores}/**/*.{vue,ts}
for literal t('...') call sites and asserts (a) every literal key
exists in zh-TW messages (missing-key guard, fail-loud) and (b) every
frontend-only zh-TW leaf path is reachable from a literal call site or
an explicitly-declared dynamic consumer (errors.{code} /
themePreset.{name} / loginRegion.defaultBadge / loginRegion.totpHint).
messages.ts docblock updated with the DYNAMIC_KEY_CONSUMERS extension
protocol.
D10 - router catch-all + auth guard infra: add LOGGED_IN_LANDING_PATH
constant + RouteMeta.requiresAuth + installRouterGuards(router, deps)
that wires beforeEach (redirect unauth -> /login?redirect=<path>, with
defensive isLoginFunnel check to avoid encoding redirects to the login
funnel itself) plus a session-expired bridge that registers a handler
through services/invoke::registerSessionExpiredHandler to clear the
local session and route to /login?sessionExpired=1. New
auth.clearSession() action does the local-only state wipe and is
reused by logout() (DRY); main.ts wires the guards after app.use(router)
with the auth store's isLoggedIn + clearSession injected. P12.1 itself
declares zero requiresAuth routes -- the infra is staged for P12.2
when /accounts (the LOGGED_IN_LANDING_PATH target) lands.
Quality gates green: vitest 251/251, vue-tsc 0, eslint 0, prettier
clean, cargo fmt + clippy -D warnings + cargo test all pass.
Mark D6-D10 complete in Todo.md (TOTP entry, LoginWait splash, Verify advanced-auth page, i18n key-usage audit, router auth guard infra) and record D9.5 RememberVerify-persistence as cancelled -- WPF accountManager.addAccount writes passwdList + verifyList atomically per account, so splitting persistence ahead of P12.2 D1 (AccountList read side) would create dead-write intermediate state and violate SRP. Persistence (RememberPassword + RememberVerify + AutoLogin + LoginMethod) is deferred wholesale to P12.2 D1 where the read-side UI will land alongside.
Port the WPF account-management surface to the Tauri/Vue stack with
strict 1:1 functional parity to `Beanfun/Pages/AccountList.xaml(.cs)`,
`Pages/ManageAccount.xaml(.cs)`, and seven WPF dialog windows.
Pages
- pages/AccountList.vue: 4 list states + per-row context menu
(Change Alias / Account Info / Check Email), Get OTP + clipboard +
auto-paste preference, vuedraggable reorder persisted via
Config.xml AccountOrder_<gameCode>, Gash balance auto-fetch on
mount + force refresh button (matches WPF updateRemainPoint
formatting incl. HK in-game suffix carve-out for remainPoint == 0).
- pages/ManageAccount.vue: Users.dat CRUD page with single table +
region chip + search + import/export (plaintext JSON via backend
commands.import_records/export_records) + AES backup/restore entry
via Data Backup toolbar button.
Dialogs (windows/*.vue)
- AddServiceAccount.vue: WPF parity for service-account add flow
using Contract.vue for terms preview.
- ChangeServiceAccountDisplayName.vue: per-row alias rename.
- ServiceAccountInfo.vue: read-only account-detail dialog.
- AddAccount.vue / ChangeAccount.vue: Users.dat record CRUD
(P12.2 D8) with method/region/auto-login fields.
- AccRecovery.vue: AES-128-CBC backup/restore (WPF wire format:
MD5(password) -> AES key, MD5("pungin") -> IV, PKCS7 padding,
base64 ciphertext) with Export / Recovery flows; decrypt errors
map to MsgDecryptFailed, other backend errors to RecoveryFailed.
- Contract.vue: pure terms-of-service viewer (mockup acceptance gate
intentionally not ported - WPF has no agreement state).
- CopyBox.vue: generic (title, value) + Copy clipboard dialog,
reused by AccountList row context menu Get Email action.
Backend
- services/storage/aes_backup.rs: new module with WPF-compatible
AES-128-CBC encrypt/decrypt + 12 unit tests covering round-trip,
empty plaintext, wrong password, malformed base64, PKCS7 padding
boundary, and 4 fixed WPF reference vectors for byte-for-byte
cross-impl compatibility.
- commands/storage.rs: new backup_export / backup_restore IPC.
- commands/error.rs: BackupError -> CommandError mapping
(storage.aes_backup_invalid_ciphertext / decrypt_failed /
invalid_utf8).
- Cargo.toml: add md-5 / aes / cbc crates + cipher block-padding
+ alloc features.
Plumbing
- router/index.ts: register /accounts (AccountList) and
/manage-account (ManageAccount); session-expired guard now also
clears the account store via clearAccountSession callback so the
next login does not flash stale service-account rows.
- stores/account.ts: add Users.dat CRUD wrappers + serviceAccounts
cache + selectedSid + email/remainPoint/contract lazy lookups +
drag-order persistence + clearSessionData.
- stores/auth.ts: integrate clearAccountSession side-channel.
- types/bindings.ts: regenerated with new commands + Account /
AccountListResult / SessionInfo / BackupError DTOs.
i18n
- i18n/messages.ts: new frontend-only namespaces for accountList /
manageAccount / addAccountDialog / changeAccountDialog /
accRecovery scoped strings (zh-TW / zh-CN / en-US).
- locales/zh-CN.json: backfill missing WPF-derived keys.
Tests (vitest 408 -> 413 passing; cargo lib 647 incl. 12 aes_backup)
- tests/unit/pages/AccountList.spec.ts: 30 cases covering all
states + every D-step branch (D5 OTP decision tree, D7 drag
persistence, D10.5 GetEmail/Tools split, D11 Gash balance).
- tests/unit/pages/ManageAccount.spec.ts: 15 cases for CRUD +
import/export + AccRecovery integration.
- tests/unit/windows/*.spec.ts: 8 new dialog specs.
- existing login + router + auth + account specs updated for new
routes and clearAccountSession integration.
Mockups
- mockups/AccountList.html / IdPassForm.html / QrForm.html /
Settings.html: design references used during the WPF-vs-mockup
parity reviews documented in Todo.md.
Mark P12.2 D1-D11 sub-step trees complete in Todo.md and document the WPF-vs-mockup parity decisions taken during each D-step (notably the strict WPF parity overrides for D9 manage-account scope and D10 dialog designs, plus the D10.5 Tools button / GetEmail context-menu split that realigned with WPF call-site semantics).
Implements the third milestone of the Tauri/Vue rewrite (P12.3 - game
launch + 3 windows), at strict WPF parity:
Backend (Rust)
- services/beanfun/games.rs: INI parser + ServiceList parser +
list_games orchestrator with unit tests
- commands/game.rs: list_games Tauri command
- commands/account.rs: get_service_contract + 5 unconnected_game_*
commands (init_add_account_payload / add_account_check /
add_account_check_nickname / add_account / change_password) with
serde-tagged AddAccountOutcome and ChangePasswordOutcome enums
- services/beanfun/account.rs: set_active_service command mutates
AppState.auth.session.{service_code, service_region} so subsequent
session-gated calls follow the user's selected game (mirrors WPF
MainWindow.service_code/region field mutation, which has no IPC)
Frontend (Vue)
- stores/game.ts: Pinia game catalogue store (services + ini map +
selectedGameCode), gameCodeOf + imageUrl helpers,
UNCONNECTED_GAME_CODES set, isUnconnectedGame computed
- windows/GameList.vue: game picker dialog with region-aware banners,
emits select(serviceCode, serviceRegion)
- windows/UnconnectedGame_AddAccount.vue: WPF parity flow (id +
password + confirm password + nickname + verify code), inline
lblErrorMessage, emits created
- windows/UnconnectedGame_ChangePassword.vue: WPF parity email-only
reset flow with MsgChangePassword token unescape via
unescapeWpfCRLF helper, emits verify-code-sent
- pages/AccountList.vue D8 integration:
- mount-time setupGameOnMount: loadGames -> restore loginGame from
Config.xml -> selectActiveGame OR auto-open GameList picker
- selectActiveGame pipeline: persist loginGame -> setActiveService
IPC (gated, idempotent) -> clear stale account selection -> reload
- game info bar with real name + region-aware image + Change Game
- Tools button conditional visibility (TOOLS_GAME_CODES set)
- Start Game pipeline: resolveGamePath -> wide-char warn ->
process check -> mode resolve -> launchGame, with OTP+launch
chain branch when login_action_type=1 and tradLogin=false
- Add Service Account branches on isUnconnectedGame
- per-row Change Password dropdown item (unconnected games only)
- autoPaste className from selectedIni.win_class_name
i18n + bindings
- src/i18n/messages.ts: gamePlaceholder + gamePathPickerPending +
toolsButton + changeGame + dragHandle + moreActions for all three
locales; audit confirms all WPF keys present in zh-TW / zh-CN /
en-US locale jsons
- src/types/bindings.ts: regenerated via cargo run --example
export_bindings (adds setActiveService + listGames + 5
unconnectedGame_* + getServiceContract bindings)
Tests
- AccountList.spec.ts: 15 new cases covering D8c-D8h (mount-time game
setup, info bar render, Tools visibility, Start Game pipeline
branches, Add Account branching, per-row Change Password); 2
existing tests updated to seed an active game
- stores/game.spec.ts + windows/GameList.spec.ts +
windows/UnconnectedGame_AddAccount.spec.ts +
windows/UnconnectedGame_ChangePassword.spec.ts
- backend unit tests for set_active_service +
AddAccountOutcome / ChangePasswordOutcome serde shape +
parse_service_ini / parse_service_list
Quality gates: cargo fmt clean, cargo clippy --all-targets
--all-features -- -D warnings 0 warning, cargo test all green,
npm run typecheck / lint / format:check / test (476 passed) all
green.
Mark P12.3 D0-D12 (incl. D8a-D8i sub-step tree) complete in Todo.md and record the feat commit hash (a04774c) on D12. Documents the strict-WPF-parity decisions taken during each D-step (notably D8a adding the set_active_service backend command to mirror WPF's mutable MainWindow.service_code/region field, D8c's mount-time single-fetch pipeline replacing the redundant loadList call, D8f's OTP+launch chain branching for tradLogin=false / login_action_type=1, and D7's email-only password reset that overrides the mockup's 3-field design to preserve WPF behavior). Also records the D9 i18n audit result (zero missing keys across zh-TW / zh-CN / en-US) and the D10 quality-gate fixes (3 needless_borrows_for_generic_args clippy warnings in commands/account.rs test asserts + 2 no-unused-vars eslint errors in AccountList.spec.ts imports). Follows the P10.3 D9 / P11 D14 / P12.2 D12 convention of committing the feature first (without the hash it would produce) and doing the Todo backfill as a separate chore commit to avoid the amend-and-rehash cycle from P10.2 D15.
- pages/Settings.vue: WPF Settings.xaml 1:1 — App section (ManageAccount / UpdateChannel / Language / ThemeColor free-form hex + el-color-picker / LoginMode TW-only / 4 boolean checkboxes with DisableHardwareAcceleration restart alert) + Game section (GamePath via @tauri-apps/plugin-dialog open() / TraditionalLogin / KillPatcher / SkipPlayWindow / Tools stub) + Back via router.back() with history-length fallback - pages/About.vue: WPF About.xaml 1:1 — app icon (assets/icon.png copied from src-tauri/icons), version via commands.version, CheckUpdate via commands.checkUpdate + ElMessageBox.confirm, Email/Github via commands.openUrl, AboutText mini-markup flattened to plain text (P13 may add full parser) - windows/WebBrowser.vue: minimal skeleton — iframe for non-cookie hosts, immediate commands.openUrl pop-out + toast for tw.beanfun.com / hk.beanfun.com (full WebviewWindow + cookie injection deferred to P13 once P12.5 KartTools/MapleTools have a real consumer) - AccountList.vue: top-bar Settings + About icon buttons mirroring WPF MainWindow titlebar - router: add /settings + /about routes (public, no requiresAuth — WPF allowed entry from login page too) - stores/ui.ts: add 5 boolean Config keys (autoStartGame / askUpdate / tradLogin / autoKillPatcher / skipPlayWnd) + loginMethod (0/1) + correct UpdateChannel literal from 'Development' to 'Beta' to match backend Channel enum and WPF Config.xml schema - i18n/messages.ts: add settings.* (8 leaf) + webBrowser.* (4 leaf) frontend-only keys across zh-TW / zh-CN / en-US
- Mark P12.4 D0–D11 complete in Todo.md with WPF parity / design decision / quality-gate fix notes (D12 is this commit pair) - Apply pre-existing cargo fmt drift in commands/account.rs left over from P12.3 so `cargo fmt --all -- --check` is clean again
Adds the four MapleStory/KartRider tool dialogs invoked from the Tools toolbar button (mirrors WPF `AccountList.xaml.cs` `btn_Tools_Click`): - `windows/MapleTools.vue` — Recycling / PlayerReport / VideoReport / EquipCalculator / CoreCalculator - `windows/KartTools.vue` — 6 guild hyperlinks - `windows/CoreCalculator.vue` — V-Matrix perfect-core search - `windows/EquipCalculator.vue` — equipment star-force / scroll stat calculator - `windows/ToolsDialogStack.vue` — wrapper hosting the five sibling dialogs (incl. WebBrowser) and exposing `openForGame(gameCode)` for AccountList / Settings Backend `clean_maple_game_cache` Tauri command + pure FS service powers the Recycling button (`services/maple_cache/` + `commands/maple_cache.rs`); 7 typed `MapleCache*` `CommandError` variants surface partial-failure detail to the frontend toast (observability-over-WPF-silent-swallow). Pure algorithms split out for unit testability: - `services/coreCalculator.ts` — combinatorial search ported verbatim from WPF `CoreCalculator.xaml.cs::btn_Calculator_Click` - `services/equipCalculator.ts` — `SCROLLS` table + `getStarForceStats` + `calcStat`, ported verbatim from WPF `EquipCalculator.xaml.cs` (data quirks preserved with spec pins to prevent good-meaning cleanup) Shared `constants/tools.ts` (`MAPLE_TOOLS_CODES` / `KART_TOOLS_CODE` / `TOOLS_GAME_CODES`) drives both routing and the `Settings.vue` `v-if` Tools-button visibility gate (WPF `MainWindow.xaml.cs` L621 / L630-633 parity that the SPA Settings page was previously missing). i18n: per-locale fallback chain `zh-CN -> zh-TW -> en-US` (`i18n/index.ts::createAppI18n.fallbackLocale`) mirrors WPF `Helper/I18n.cs::LoadLanguage`'s `MergedDictionaries` two-layer culture-parent stack (`zh-Hans -> zh`). `Beanfun/Lang/zh-Hans.xaml` is missing ~30 keys (`Cancel`, `Yes`, `No`, `Tools`, `Weapon`, `ScrollBlack`, `PerfectCoreNeedSkills`, ...) that the WPF runtime resolves from `zh.xaml` at runtime — without this fallback chain, Simplified Chinese SPA users would have seen English fallback for every WPF-missing key, including the `Tools` button label and the new EquipCalculator / CoreCalculator UI strings. New `index.spec.ts` case pins the parity contract by exercising `Cancel` (zh-Hans-missing, zh-TW = "取消", en-US = "Cancel"). Locale source sync: `Beanfun/Lang/zh-Hans.xaml` `ToolBox` backfilled (was missing vs zh.xaml / en.xaml); `src/locales/zh-CN.json` regenerated from the updated XAML via `scripts/convert-lang.mjs` (the regen drops `Cancel` / `IAgree` / `Normal` to match the WPF source — the new fallback chain ensures runtime parity by resolving those from zh-TW). 3 new frontend-only i18n keys: `mapleTools.subtitle` / `mapleTools.recyclingErrors` / `kartTools.subtitle` (synced across all three locales). Quality gates: vue-tsc 0, eslint 0, prettier 0, vitest 553 passed (38 files), cargo fmt 0, cargo clippy 0, cargo test 698 unit + integration suites all green.
…auncher composable P12 acceptance Phase A surfaced 4 missing WPF buttons on the login forms: RegisterAccount / ForgotPassword / GameStart on IdPassForm, and GameStart on QrForm. WPF gates the LoginPage GameStart on MainWindow instance state (service_code / game_exe / dir_value_name / dir_reg) which survives logout because it lives on the singleton window. Pinia clearGameData wipes the SPA equivalent on logout, so we mirror the WPF lifetime by parking a JSON snapshot of the active GameIniEntry under a new lastSelectedIni config key (paired with the existing loginGame key for the gameCode). - constants/login.ts: LOGIN_EXTERNAL_URLS table (region x kind) ports the 4 signup / forgot_pwd URLs from id-pass_form.xaml.cs L42-50 + L57-64. - composables/useGameLauncher.ts: extracts the 5-step WPF runGame chain (resolveStartMode / pathHasWideChar / resolveGamePath / checkAndKillRunningGameProcesses / runGame) out of AccountList so both LoginForm components can re-use it (SRP/DRY). Calls restoreLastSelected internally so consumers do not have to know about the persistence shape. resolveGamePath hard selectedGame guard is downgraded to a graceful pending-Settings fallback so LoginPage launches survive an empty services list. - stores/game.ts: restoreLastSelected(configStore) action patches selectedGameCode + ini map from the persisted snapshot without touching services (LoginPage has no consumer for the catalogue and the fetch requires an authenticated session). clearGameData docblock updated to document that loginGame / lastSelectedIni intentionally outlive logout. - pages/AccountList.vue: refactored to delegate to useGameLauncher (5 helper functions deleted, behaviour 0 change) and now persists the active GameIniEntry to lastSelectedIni alongside the existing loginGame write inside selectActiveGame. - pages/IdPassForm.vue: 3 new buttons (RegisterAccount / ForgotPassword inline next to the checkboxes; GameStart paired with Login in the primary action grid) + a self-mounted WebBrowser dialog for the two external URLs (the dialog auto-falls-back to the system browser for beanfun.com URLs so existing Beanfun cookies apply). - pages/QrForm.vue: GameStart button below the Back / Refresh row, delegating to the same launcher composable. Tests: useGameLauncher.spec.ts new file with 12 cases covering the full launch chain + restoration + every mode / process / wide-char branch; IdPassForm.spec.ts +5 cases for the 3 new buttons (TW/HK URL routing + launcher delegation); QrForm.spec.ts +1 case for GameStart delegation; AccountList.spec.ts +1 D5 case asserting selectActiveGame writes both loginGame and lastSelectedIni JSON in the same selection. Quality gates: vue-tsc / eslint / prettier / vitest (572 pass) all green; cargo fmt / clippy green (no Rust changes in this followup).
…2.4-followup-B) Restore WPF parity for `new WebBrowser(uri).Show()`: a fresh `tauri::WebviewWindow` per click with the logged-in BeanfunClient session cookies pre-seeded, so RegisterAccount / ForgotPassword on LoginPage and the Maple/Kart `open-web-browser` events render the authenticated `tw.beanfun.com` / `bfweb.hk.beanfun.com` pages embedded instead of degrading to the system browser. - backend: new `commands/web_browser::open_in_app_browser` with a host allowlist (tw/hk/bfweb), `about:blank -> seed -> navigate` pattern reusing `seed_webview_cookies_from_client`, async build for the Windows WebView2 message-pump deadlock guard - frontend: new `useInAppBrowser` composable as the single dispatch point; `system.invalid_url` from the backend triggers a one-toast fallback to `commands.openUrl` (system browser); IdPassForm and ToolsDialogStack delegate to the composable - cleanup: delete `windows/WebBrowser.vue` (placeholder dialog) + drop `webBrowser.*` i18n keys; replace with `inAppBrowser.*` - tests: 4 cases for the composable, 13 Rust unit cases for the URL allowlist + label generator, refit IdPassForm / ToolsDialogStack specs to assert the spy instead of the dialog
… (P12.4-followup-B-fix) F1 backend allowlist *.beanfun.com suffix match F2 remove VideoReport button (target URL 404) F3 mirror WPF game image URL passthrough + unified fallback host F6 ManageAccount back button F7 WebviewWindow black-flash fix (hidden build + on_page_load show) F8 customer service button wire-up F9 member center button wire-up (web_token stays in Rust) A About page: dual author credit, GitHub link, dual email contacts B System tray: minimize-to-tray with TrayIconBuilder + config toggle C Login page: outline icon, heading text, region TW/HK labels, v6.0.0 D1 Windows manifest highestAvailable for release builds (PostMessageW UIPI) D3 Updater: null_as_empty deserializer for GitHub release JSON D4 Updater: semver Path C comparison for pure X.Y.Z local versions D5 Default window size 720x720
- Remove legacy WPF project (Beanfun/, Beanfun.sln, .config/) - Flatten beanfun-next/ to repo root - Rename crate/package from beanfun-next to beanfun (lib: beanfun_lib) - Update productName/title/identifier in tauri.conf.json - Fix include_bytes! paths for LocaleRemulator after relocation - Replace all beanfun-next references in code, comments, and tests - Add dependabot.yml (npm, cargo, github-actions) - Add Tauri build-and-release.yml (standalone exe, no installer) - Rename CI workflow to "Lint, Format & Test" with descriptive job names - Remove format-check.yml (dotnet) and old build-and-release.yml (dotnet) - Delete mockups/ (HTML design mockups no longer needed) - Update README to match original Beanfun format - Update .gitignore for new structure - Set bundle targets to [] (bare exe only via --no-bundle)
…rite Resolve modify/delete conflicts: WPF files deleted in HEAD (Tauri rewrite replaces them entirely), accept deletions for all 10 conflicted files. Auto-merged Lang/zh-Hans.xaml (new QR Code i18n strings from code).
- Rename commitlint.config.js to .mjs (wagoid/commitlint-github-action@v6 requires ESM) - Fix unused_mut clippy warning in build.rs by using variable shadowing - Auto-format workflow YAML files with Prettier - Remove macOS matrix from CI (Windows-only application) - Clean up residual beanfun-next/ directory files
- Add .gitattributes with `* text=auto eol=lf` to prevent CRLF issues on Windows CI runners (rustfmt, prettier) - Set commitDepth: 1 in commitlint workflow to only check the latest commit (avoids flagging historical commits in the PR) - Renormalize existing files with CRLF line endings
- Delete commitlint.yml workflow and commitlint.config.mjs - Add project architecture tree to README - Remove commitlint references from contributing section
- Use regex replace + WriteAllText instead of ConvertTo-Json + Set-Content to preserve file formatting and UTF-8 encoding - Fix git log range arg passing in PowerShell (split into array)
- Redact URL query params (pSKey) from all GamePass tracing logs (C1/S1) - Remove login_token from DeviceRegistrationRequired IPC details (S2) - Add 5 MiB response body limit to fetch_releases_at (S6) - Stop logging full backend error details to browser console (S12) - Narrow tokio features from "full" to specific set (S24) - Untrack Todo.md and add to .gitignore
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Full rewrite of the Beanfun launcher from C# / WPF to Tauri v2 + Rust + Vue 3, achieving 1:1 functional and UI parity with the legacy version.
Closes #217
What's included
Backend (Rust)
Users.dat(byte-for-byte interop with WPF)highestAvailableUAC manifest (release builds)Frontend (Vue 3 + TypeScript)
Repo restructure
beanfun-next/to repo root, removed WPF codebeanfun-nexttobeanfunproductName=Beanfun,identifier=tw.beanfun.appCI / Infrastructure
ci.yml: ESLint, Prettier, TypeCheck, Vitest, cargo fmt, Clippy, cargo test (Windows + macOS)build-and-release.yml: Tauri build → standaloneBeanfun.exe→ GitHub Releasecommitlint.yml: Conventional Commits enforcementdependabot.yml: npm, cargo, github-actions weekly updatesTest plan
cargo checkpassescargo test— 722 tests passedcargo fmt --checkpassesnpm run typecheckpassesnpm run lintpassesnpm run test— 586 tests passed (40 test files)npm run format:checkpassesnpm run tauri build -- --no-bundle— produces 18.97 MB standalone exe