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
};