From f49685f30fe93f105ad4bde61d639f6e0a5c2c0f Mon Sep 17 00:00:00 2001
From: HsiangNianian <i@jyunko.cn>
Date: Tue, 13 Jan 2026 18:22:49 +0800
Subject: feat: implement Microsoft account login flow and refactor account
 handling

---
 ui/src/App.svelte | 170 +++++++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 150 insertions(+), 20 deletions(-)

(limited to 'ui/src/App.svelte')

diff --git a/ui/src/App.svelte b/ui/src/App.svelte
index d9dd7d0..8ec0da1 100644
--- a/ui/src/App.svelte
+++ b/ui/src/App.svelte
@@ -25,11 +25,19 @@
     releaseTime: string;
   }
 
-  interface OfflineAccount {
+  interface Account {
+    type: "Offline" | "Microsoft";
     username: string;
     uuid: string;
   }
 
+  interface DeviceCodeResponse {
+    user_code: string;
+    device_code: string;
+    verification_uri: string;
+    message?: string;
+  }
+
   interface LauncherConfig {
     min_memory: number;
     max_memory: number;
@@ -40,7 +48,7 @@
 
   let versions: Version[] = [];
   let selectedVersion = "";
-  let currentAccount: OfflineAccount | null = null;
+  let currentAccount: Account | null = null;
   let settings: LauncherConfig = {
     min_memory: 1024,
     max_memory: 2048,
@@ -49,6 +57,13 @@
     height: 480,
   };
 
+  // Login UI State
+  let isLoginModalOpen = false;
+  let loginMode: 'select' | 'offline' | 'microsoft' = 'select';
+  let offlineUsername = "";
+  let deviceCodeData: DeviceCodeResponse | null = null;
+  let msLoginLoading = false;
+
   onMount(async () => {
     checkAccount();
     loadSettings();
@@ -68,7 +83,7 @@
   async function checkAccount() {
     try {
       const acc = await invoke("get_active_account");
-      currentAccount = acc as OfflineAccount | null;
+      currentAccount = acc as Account | null;
     } catch (e) {
       console.error("Failed to check account:", e);
     }
@@ -92,32 +107,75 @@
     }
   }
 
-  async function login() {
+  // --- Auth Functions ---
+
+  function openLoginModal() {
     if (currentAccount) {
       if (confirm("Logout " + currentAccount.username + "?")) {
-        try {
-          await invoke("logout");
-          currentAccount = null;
-        } catch (e) {
-          console.error("Logout failed:", e);
-        }
+        invoke("logout").then(() => currentAccount = null);
       }
       return;
     }
-    const username = prompt("Enter username for offline login:");
-    if (username) {
-      try {
-        currentAccount = await invoke("login_offline", { username });
-      } catch (e) {
-        alert("Login failed: " + e);
-      }
+    // Reset state
+    isLoginModalOpen = true;
+    loginMode = 'select';
+    offlineUsername = "";
+    deviceCodeData = null;
+    msLoginLoading = false;
+  }
+
+  function closeLoginModal() {
+    isLoginModalOpen = false;
+  }
+
+  async function  performOfflineLogin() {
+    if (!offlineUsername) return;
+    try {
+      currentAccount = await invoke("login_offline", { username: offlineUsername }) as Account;
+      isLoginModalOpen = false;
+    } catch (e) {
+      alert("Login failed: " + e);
+    }
+  }
+
+  async function startMicrosoftLogin() {
+    loginMode = 'microsoft';
+    msLoginLoading = true;
+    try {
+      deviceCodeData = await invoke("start_microsoft_login") as DeviceCodeResponse;
+    } catch(e) {
+        alert("Failed to start Microsoft login: " + e);
+        loginMode = 'select'; // Go back
+    } finally {
+        msLoginLoading = false;
     }
   }
+  
+  async function completeMicrosoftLogin() {
+    if(!deviceCodeData) return;
+    msLoginLoading = true;
+    try {
+        currentAccount = await invoke("complete_microsoft_login", { deviceCode: deviceCodeData.device_code }) as Account;
+        isLoginModalOpen = false;
+    } catch(e) {
+        alert("Login failed: " + e + "\n\nMake sure you authorized the app in your browser.");
+    } finally {
+        msLoginLoading = false;
+    }
+  }
+
+  function openLink(url: string) {
+      // Use tauri open if possible, or window.open
+      invoke('start_microsoft_login').catch(() => window.open(url, '_blank'));
+      // Wait, we invoke 'start_microsoft_login' above already. 
+      // Just use window.open for the verification URI
+      window.open(url, '_blank');
+  }
 
   async function startGame() {
     if (!currentAccount) {
       alert("Please login first!");
-      login();
+      openLoginModal();
       return;
     }
 
@@ -359,10 +417,10 @@
       <div class="flex items-center gap-4">
         <div
           class="flex items-center gap-4 cursor-pointer hover:opacity-80 transition-opacity"
-          onclick={login}
+          onclick={openLoginModal}
           role="button"
           tabindex="0"
-          onkeydown={(e) => e.key === "Enter" && login()}
+          onkeydown={(e) => e.key === "Enter" && openLoginModal()}
         >
           <div
             class="w-12 h-12 rounded bg-gradient-to-tr from-indigo-500 to-purple-500 shadow-lg flex items-center justify-center text-white font-bold text-xl overflow-hidden"
@@ -435,6 +493,78 @@
     </div>
   </main>
 
+  <!-- Login Modal -->
+  {#if isLoginModalOpen}
+    <div class="fixed inset-0 z-[60] flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
+      <div class="bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl p-6 w-full max-w-md animate-in fade-in zoom-in-0 duration-200">
+        
+        <div class="flex justify-between items-center mb-6">
+          <h2 class="text-2xl font-bold text-white">Login</h2>
+          <button onclick={closeLoginModal} class="text-zinc-500 hover:text-white transition group">
+             ✕
+          </button>
+        </div>
+
+        {#if loginMode === 'select'}
+            <div class="space-y-4">
+                <button onclick={startMicrosoftLogin} class="w-full flex items-center justify-center gap-3 bg-[#2F2F2F] hover:bg-[#3F3F3F] text-white p-4 rounded-lg font-bold border border-transparent hover:border-zinc-500 transition-all group">
+                    <!-- Microsoft Logo SVG -->
+                    <svg class="w-5 h-5" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="#f35325" d="M1 1h10v10H1z"/><path fill="#81bc06" d="M12 1h10v10H12z"/><path fill="#05a6f0" d="M1 12h10v10H1z"/><path fill="#ffba08" d="M12 12h10v10H12z"/></svg>
+                    Microsoft Account
+                </button>
+                
+                <div class="relative py-2">
+                    <div class="absolute inset-0 flex items-center"><div class="w-full border-t border-zinc-700"></div></div>
+                    <div class="relative flex justify-center text-xs uppercase"><span class="bg-zinc-900 px-2 text-zinc-500">OR</span></div>
+                </div>
+
+                <div class="space-y-2">
+                    <input type="text" bind:value={offlineUsername} placeholder="Offline Username" class="w-full bg-zinc-950 border border-zinc-700 rounded p-3 text-white focus:border-indigo-500 outline-none" 
+                        onkeydown={(e) => e.key === 'Enter' && performOfflineLogin()} />
+                    <button onclick={performOfflineLogin} class="w-full bg-zinc-800 hover:bg-zinc-700 text-zinc-300 p-3 rounded font-medium transition-colors">
+                        Offline Login
+                    </button>
+                </div>
+            </div>
+
+        {:else if loginMode === 'microsoft'}
+            <div class="text-center">
+                {#if msLoginLoading && !deviceCodeData}
+                    <div class="py-8 text-zinc-400 animate-pulse">Starting login flow...</div>
+                {:else if deviceCodeData}
+                    <div class="space-y-4">
+                        <p class="text-sm text-zinc-400">1. Go to this URL:</p>
+                        <button onclick={() => deviceCodeData && openLink(deviceCodeData.verification_uri)} class="text-indigo-400 hover:text-indigo-300 underline break-all font-mono text-sm">
+                            {deviceCodeData.verification_uri}
+                        </button>
+                        
+                        <p class="text-sm text-zinc-400 mt-2">2. Enter this code:</p>
+                        <div class="bg-zinc-950 p-4 rounded border border-zinc-700 font-mono text-2xl tracking-widest text-center select-all cursor-pointer hover:border-indigo-500 transition-colors" onclick={() => navigator.clipboard.writeText(deviceCodeData?.user_code || '')}>
+                            {deviceCodeData.user_code}
+                        </div>
+                        <p class="text-xs text-zinc-500">Click code to copy</p>
+
+                        <div class="pt-4">
+                             {#if msLoginLoading}
+                                <button disabled class="w-full bg-indigo-600/50 text-white/50 p-3 rounded font-bold cursor-not-allowed">
+                                    Checking...
+                                </button>
+                             {:else}
+                                <button onclick={completeMicrosoftLogin} class="w-full bg-indigo-600 hover:bg-indigo-500 text-white p-3 rounded font-bold transition-transform active:scale-95 shadow-lg shadow-indigo-500/20">
+                                    I Have Authenticated
+                                </button>
+                             {/if}
+                        </div>
+                        
+                        <button onclick={() => loginMode = 'select'} class="text-xs text-zinc-500 hover:text-zinc-300 mt-4 underline">Cancel</button>
+                    </div>
+                {/if}
+            </div>
+        {/if}
+      </div>
+    </div>
+  {/if}
+
   <!-- Overlay Status (Toast) -->
   {#if status !== "Ready"}
     <div
-- 
cgit v1.2.3-70-g09d2

