fix account provider

Seto committed Nov 28, 2025 at 23:32 UTC ba9f9dbe1ed04144bf5ae1bc49f4178169cbcb9c
3 files changed +188 -37
src/App.tsx
+29 -26
@@ -6,6 +6,7 @@ import AnimatedPage from "./components/AnimatedPage";
6 import Home from "./pages/Home";
7 import Cut from "./pages/Cut";
8 import Profile from "./pages/Profile";
9 +import { AccountProvider } from "./context/AccountContext";
10
11 export default function App() {
12 const location = useLocation();
@@ -14,32 +15,34 @@ export default function App() {
15 <div className="bg-gradient-to-tr from-fuchsia-100 to-sky-100 min-h-screen w-full font-sans relative safe-area">
16 <Navbar />
17 <main className="py-4 bottom-nav-safe max-w-2xl mx-auto px-4">
17 - <Routes location={location} key={location.pathname}>
18 - <Route
19 - path="/"
20 - element={
21 - <AnimatedPage>
22 - <Home />
23 - </AnimatedPage>
24 - }
25 - />
26 - <Route
27 - path="/cut"
28 - element={
29 - <AnimatedPage>
30 - <Cut />
31 - </AnimatedPage>
32 - }
33 - />
34 - <Route
35 - path="/profile"
36 - element={
37 - <AnimatedPage>
38 - <Profile />
39 - </AnimatedPage>
40 - }
41 - />
42 - </Routes>
18 + <AccountProvider>
19 + <Routes location={location} key={location.pathname}>
20 + <Route
21 + path="/"
22 + element={
23 + <AnimatedPage>
24 + <Home />
25 + </AnimatedPage>
26 + }
27 + />
28 + <Route
29 + path="/cut"
30 + element={
31 + <AnimatedPage>
32 + <Cut />
33 + </AnimatedPage>
34 + }
35 + />
36 + <Route
37 + path="/profile"
38 + element={
39 + <AnimatedPage>
40 + <Profile />
41 + </AnimatedPage>
42 + }
43 + />
44 + </Routes>
45 + </AccountProvider>
46 </main>
47 </div>
48 </Tooltip.Provider>
src/pages/Profile.tsx
+147 -11
@@ -1,30 +1,166 @@
1 +import { useEffect, useState } from "react";
2 import { useLang } from "../context/LanguageContext";
3 import { SupportedLanguage } from "../utils/i18n";
4 import haptics from "./../utils/haptics";
5 +import { debug, error } from "@tauri-apps/plugin-log";
6 +import { invoke } from "@tauri-apps/api/core";
7 +import { OauthRedirect, TokenResponse } from "../lib/crate/generated";
8 +import { listen } from "@tauri-apps/api/event";
9 +import { openUrl } from "@tauri-apps/plugin-opener";
10 +import { useAccount } from "../context/AccountContext";
11 +import { cancel } from "@fabianlars/tauri-plugin-oauth";
12
13 export default function Profile() {
14 const handleNavClick = async () => {
15 await haptics.navigation();
16 };
17 const { t, lang, setLang } = useLang();
18 + const { user, onLoggedIn, onLoggedOut } = useAccount();
19 const isRTL = lang === "fa";
20 const dir = isRTL ? "rtl" : "ltr";
21 const align = isRTL ? "text-right" : "text-left";
22 + const [port, setPort] = useState<number>();
23 +
24 + const handleOAuthLogin = async (provider: "google" | "apple") => {
25 + try {
26 + // Handle all operations in the Rust backend.
27 + const port = await invoke<number>("start_server");
28 + setPort(port);
29 + debug(`OAuth server started on port ${port}`);
30 + // Tauri OAuth plugin server.
31 + const redirectUri = `http://localhost:${port}`;
32 + if (provider === "google") {
33 + const baseUrl = await invoke<string>("get_consent_url", {
34 + redirectUri,
35 + });
36 + debug(`Google OAuth URL: ${baseUrl}`);
37 + await openUrl(baseUrl);
38 + }
39 + } catch (e) {
40 + error(`Error: ${e}`);
41 + }
42 + };
43 +
44 + const handleLogout = async () => {
45 + onLoggedOut();
46 + };
47 +
48 + // Don't forget to stop the server when you're done
49 + async function stopOAuthServer() {
50 + if (!port) return;
51 +
52 + try {
53 + await cancel(port);
54 + console.log("OAuth server stopped");
55 + } catch (error) {
56 + console.error("Error stopping OAuth server:", error);
57 + }
58 + }
59 +
60 + const init = async () => {
61 + try {
62 + // Listen for OAuth emitted events from the Rust backend.
63 + await listen<OauthRedirect>("oauth_redirect_uri", async (event) => {
64 + debug(
65 + `Received OAuth redirect event: ${JSON.stringify(event.payload)}`,
66 + );
67 + const oauthRedirect = event.payload;
68 + // Handle the redirect event here
69 + try {
70 + const res = await invoke<TokenResponse>("get_token", {
71 + oauthRedirect,
72 + });
73 + debug(`OAuth response: ${JSON.stringify(res)}`);
74 + onLoggedIn("google", res.access_token);
75 + await stopOAuthServer();
76 + } catch (e) {
77 + error(`Error processing OAuth response: ${JSON.stringify(e)}`);
78 + }
79 + });
80 + } catch (e) {
81 + error(`Error listening OAuth event: ${e}`);
82 + }
83 + };
84 +
85 + useEffect(() => {
86 + init();
87 + return () => {
88 + stopOAuthServer();
89 + };
90 + });
91
92 return (
93 <section dir={dir} className={`flex flex-col items-center py-8 ${align}`}>
16 - <img
17 - src="https://api.dicebear.com/7.x/bottts/svg?seed=persian"
18 - alt="Avatar"
19 - className="w-24 h-24 mb-4 rounded-full border-4 border-blue-300"
20 - />
21 - <h2 className="text-xl font-bold mb-2 text-blue-700">{t.profile}</h2>
22 - <div className="bg-white/80 shadow rounded-xl px-8 py-4 mt-2 w-full max-w-xs">
23 - <div className="mb-2">
24 - <span className="font-semibold">{t.user}:</span> {t.guest}
94 + {user && (
95 + <>
96 + {/* Profile Header */}
97 + <div className="bg-white rounded-lg shadow-md p-6">
98 + <div className="flex items-center space-x-4 mb-4">
99 + <img
100 + src={user.picture}
101 + alt="Avatar"
102 + className="w-20 h-20 mb-4 rounded-full border-4 border-food-orange bg-gradient-to-br from-food-orange to-food-red "
103 + />
104 + <div>
105 + <h2 className="text-2xl font-bold text-gray-800">
106 + {user.name}
107 + </h2>
108 + </div>
109 + </div>
110 + <div className="px-8 py-4 mt-2 w-full max-w-xs">
111 + <button
112 + onClick={async () => {
113 + await haptics.buttonPress();
114 + await handleLogout();
115 + }}
116 + className="w-full bg-white hover:bg-gray-50 active:bg-gray-100 text-gray-700 font-medium rounded-lg px-4 py-3 flex items-center justify-center gap-3 transition-colors border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 shadow-sm"
117 + aria-label={t.googleOAuthLogin}
118 + >
119 + {t.logout}
120 + </button>
121 + </div>
122 + </div>
123 + </>
124 + )}
125 + {/* LOGGED OUT */}
126 + {!user && (
127 + <div className=" rounded-lg shadow-md p-6">
128 + <button
129 + onClick={async () => {
130 + await haptics.buttonPress();
131 + await handleOAuthLogin("google");
132 + }}
133 + className="w-full bg-white hover:bg-gray-50 active:bg-gray-100 text-gray-700 font-medium rounded-lg px-4 py-3 flex items-center justify-center gap-3 transition-colors border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 shadow-sm"
134 + aria-label={t.googleOAuthLogin}
135 + >
136 + <svg
137 + className="w-5 h-5"
138 + viewBox="0 0 24 24"
139 + xmlns="http://www.w3.org/2000/svg"
140 + >
141 + <path
142 + d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
143 + fill="#4285F4"
144 + />
145 + <path
146 + d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
147 + fill="#34A853"
148 + />
149 + <path
150 + d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
151 + fill="#FBBC05"
152 + />
153 + <path
154 + d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
155 + fill="#EA4335"
156 + />
157 + </svg>
158 + <span className="text-sm font-medium leading-none">
159 + {t.googleOAuthLogin}
160 + </span>
161 + </button>
162 </div>
26 - </div>
27 -
163 + )}
164 <div className="bg-white/80 shadow rounded-xl px-8 py-4 mt-2 w-full max-w-xs">
165 <h2 className="text-lg font-bold mb-2 text-blue-700">App Settings</h2>
166 <div>
src/utils/i18n.ts
+12
@@ -37,6 +37,9 @@ export interface Translation {
37 bookingSuccess: string;
38 bookingError: string;
39 textToBook: string;
40 + googleOAuthLogin: string;
41 + appleOAuthLogin: string;
42 + logout: string;
43 }
44
45 export type SupportedLanguage = "fa" | "en" | "sv";
@@ -81,6 +84,9 @@ export const translations: Record<SupportedLanguage, Translation> = {
84 bookingSuccess: "رزرو با موفقیت انجام شد!",
85 bookingError: "خطا در رزرو. لطفاً دوباره تلاش کنید.",
86 textToBook: "برای رزرو پیامک بفرستید",
87 + googleOAuthLogin: "Login with Google",
88 + appleOAuthLogin: "Sign in with Apple",
89 + logout: "خروج از سیستم",
90 },
91 en: {
92 appName: "Hägersten StreetCut",
@@ -121,6 +127,9 @@ export const translations: Record<SupportedLanguage, Translation> = {
127 bookingSuccess: "Booking successful!",
128 bookingError: "Booking error. Please try again.",
129 textToBook: "Text to book",
130 + googleOAuthLogin: "Login with Google",
131 + appleOAuthLogin: "Sign in with Apple",
132 + logout: "Log out",
133 },
134 sv: {
135 appName: "Hägersten StreetCut",
@@ -161,5 +170,8 @@ export const translations: Record<SupportedLanguage, Translation> = {
170 bookingSuccess: "Bokning lyckades!",
171 bookingError: "Bokningsfel. Försök igen.",
172 textToBook: "SMS för att boka",
173 + googleOAuthLogin: "Loga in med Google",
174 + appleOAuthLogin: "Loga in med Apple",
175 + logout: "Loga ut",
176 },
177 };