Skip to content

Commit 3c5931c

Browse files
AchoArnoldCopilot
andcommitted
feat(web): migrate to modular Firebase SDK and use Vuetify 4 default breakpoints
- Replace FirebaseUI + compat SDK with custom auth UI using signInWithPopup - Support Google, GitHub, and Email/Password auth via modular Firebase SDK - Remove firebaseui dependency and firebase/compat imports - Remove async plugin (no longer needed without compat initialization) - Remove custom display.thresholds to use Vuetify 4 defaults - Remove firebase/compat/app and firebase/compat/auth from Vite optimizeDeps Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6d98ef2 commit 3c5931c

5 files changed

Lines changed: 175 additions & 122 deletions

File tree

Lines changed: 174 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
<script setup lang="ts">
2-
import firebase from "firebase/compat/app";
3-
import "firebase/compat/auth";
42
import {
3+
getAuth,
4+
signInWithPopup,
55
GoogleAuthProvider,
66
GithubAuthProvider,
7-
EmailAuthProvider,
7+
signInWithEmailAndPassword,
8+
createUserWithEmailAndPassword,
89
} from "firebase/auth";
10+
import { mdiGoogle, mdiGithub, mdiEmail } from "@mdi/js";
911
1012
const props = withDefaults(
1113
defineProps<{
@@ -18,72 +20,182 @@ const router = useRouter();
1820
const authStore = useAuthStore();
1921
const notificationsStore = useNotificationsStore();
2022
const appStore = useAppStore();
21-
const authContainer = ref<HTMLElement | null>(null);
22-
const firebaseUIInitialized = ref(false);
23-
let ui: any = null;
2423
25-
onMounted(async () => {
26-
if (!import.meta.client) return;
24+
const loading = ref(false);
25+
const showEmailForm = ref(false);
26+
const isSignUp = ref(false);
27+
const email = ref("");
28+
const password = ref("");
29+
const emailError = ref("");
2730
28-
const firebaseui = await import("firebaseui");
29-
await import("firebaseui/dist/firebaseui.css");
31+
async function signInWithGoogle() {
32+
loading.value = true;
33+
try {
34+
const auth = getAuth();
35+
const result = await signInWithPopup(auth, new GoogleAuthProvider());
36+
onSuccess(result.user);
37+
} catch (error: unknown) {
38+
handleError(error);
39+
} finally {
40+
loading.value = false;
41+
}
42+
}
3043
31-
// FirebaseUI requires the compat auth instance
32-
const auth = firebase.auth();
33-
ui = firebaseui.auth.AuthUI.getInstance() || new firebaseui.auth.AuthUI(auth);
34-
ui.start("#firebaseui-auth-container", {
35-
callbacks: {
36-
signInSuccessWithAuthResult: (authResult: any) => {
37-
notificationsStore.addNotification({
38-
message: "Login successful!",
39-
type: "success",
40-
});
41-
authStore.onAuthStateChanged(authResult.user);
42-
router.push({ path: props.to });
43-
return false;
44-
},
45-
uiShown: () => {
46-
firebaseUIInitialized.value = true;
47-
if (authContainer.value) {
48-
Array.from(
49-
authContainer.value.getElementsByClassName(
50-
"firebaseui-idp-text-long",
51-
),
52-
).forEach((item: Element) => {
53-
item.textContent =
54-
item.textContent?.replace("Sign in with", "Continue with") ||
55-
null;
56-
});
57-
}
58-
},
59-
},
60-
signInFlow: "popup",
61-
signInSuccessUrl: window.location.href,
62-
signInOptions: [
63-
GoogleAuthProvider.PROVIDER_ID,
64-
GithubAuthProvider.PROVIDER_ID,
65-
EmailAuthProvider.PROVIDER_ID,
66-
],
67-
tosUrl: appStore.appData.url + "/terms-and-conditions",
68-
privacyPolicyUrl: appStore.appData.url + "/privacy-policy",
44+
async function signInWithGithub() {
45+
loading.value = true;
46+
try {
47+
const auth = getAuth();
48+
const result = await signInWithPopup(auth, new GithubAuthProvider());
49+
onSuccess(result.user);
50+
} catch (error: unknown) {
51+
handleError(error);
52+
} finally {
53+
loading.value = false;
54+
}
55+
}
56+
57+
async function submitEmail() {
58+
emailError.value = "";
59+
loading.value = true;
60+
try {
61+
const auth = getAuth();
62+
let result;
63+
if (isSignUp.value) {
64+
result = await createUserWithEmailAndPassword(
65+
auth,
66+
email.value,
67+
password.value,
68+
);
69+
} else {
70+
result = await signInWithEmailAndPassword(
71+
auth,
72+
email.value,
73+
password.value,
74+
);
75+
}
76+
onSuccess(result.user);
77+
} catch (error: unknown) {
78+
handleError(error);
79+
} finally {
80+
loading.value = false;
81+
}
82+
}
83+
84+
function onSuccess(user: unknown) {
85+
notificationsStore.addNotification({
86+
message: "Login successful!",
87+
type: "success",
6988
});
70-
});
89+
authStore.onAuthStateChanged(user);
90+
router.push({ path: props.to });
91+
}
7192
72-
onBeforeUnmount(() => {
73-
if (ui) ui.delete();
74-
});
93+
function handleError(error: unknown) {
94+
const firebaseError = error as { code?: string; message?: string };
95+
const code = firebaseError.code || "";
96+
if (code === "auth/user-not-found" || code === "auth/invalid-credential") {
97+
emailError.value = "Invalid email or password";
98+
} else if (code === "auth/email-already-in-use") {
99+
emailError.value = "An account with this email already exists";
100+
} else if (code === "auth/weak-password") {
101+
emailError.value = "Password must be at least 6 characters";
102+
} else if (code === "auth/popup-closed-by-user") {
103+
// User closed popup, no error to show
104+
} else {
105+
emailError.value = firebaseError.message || "An error occurred";
106+
}
107+
}
75108
</script>
76109

77110
<template>
78-
<div>
79-
<div id="firebaseui-auth-container" ref="authContainer" />
80-
<v-progress-circular
81-
v-if="!firebaseUIInitialized"
82-
class="mx-auto d-block my-16"
83-
:size="80"
84-
:width="5"
85-
color="primary"
86-
indeterminate
87-
/>
111+
<div class="text-center">
112+
<v-btn
113+
block
114+
size="large"
115+
variant="outlined"
116+
class="mb-3"
117+
:loading="loading"
118+
@click="signInWithGoogle"
119+
>
120+
<v-icon :icon="mdiGoogle" class="mr-2" />
121+
Continue with Google
122+
</v-btn>
123+
124+
<v-btn
125+
block
126+
size="large"
127+
variant="outlined"
128+
class="mb-3"
129+
:loading="loading"
130+
@click="signInWithGithub"
131+
>
132+
<v-icon :icon="mdiGithub" class="mr-2" />
133+
Continue with GitHub
134+
</v-btn>
135+
136+
<v-btn
137+
v-if="!showEmailForm"
138+
block
139+
size="large"
140+
variant="outlined"
141+
class="mb-3"
142+
@click="showEmailForm = true"
143+
>
144+
<v-icon :icon="mdiEmail" class="mr-2" />
145+
Continue with Email
146+
</v-btn>
147+
148+
<v-form v-if="showEmailForm" class="mt-4" @submit.prevent="submitEmail">
149+
<v-text-field
150+
v-model="email"
151+
label="Email"
152+
type="email"
153+
variant="outlined"
154+
density="comfortable"
155+
class="mb-2"
156+
required
157+
/>
158+
<v-text-field
159+
v-model="password"
160+
label="Password"
161+
type="password"
162+
variant="outlined"
163+
density="comfortable"
164+
class="mb-2"
165+
required
166+
/>
167+
<v-alert v-if="emailError" type="error" density="compact" class="mb-3">
168+
{{ emailError }}
169+
</v-alert>
170+
<v-btn
171+
block
172+
size="large"
173+
color="primary"
174+
type="submit"
175+
:loading="loading"
176+
>
177+
{{ isSignUp ? "Sign Up" : "Sign In" }}
178+
</v-btn>
179+
<v-btn
180+
block
181+
variant="text"
182+
size="small"
183+
class="mt-2"
184+
@click="isSignUp = !isSignUp"
185+
>
186+
{{
187+
isSignUp ? "Already have an account? Sign In" : "No account? Sign Up"
188+
}}
189+
</v-btn>
190+
</v-form>
191+
192+
<p class="text-body-small text-medium-emphasis mt-4">
193+
By continuing, you agree to the
194+
<a :href="appStore.appData.url + '/terms-and-conditions'">
195+
Terms & Conditions
196+
</a>
197+
and
198+
<a :href="appStore.appData.url + '/privacy-policy'">Privacy Policy</a>
199+
</p>
88200
</div>
89201
</template>

web/app/plugins/firebase.client.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { FirebaseApp } from "firebase/app";
33
import { getAuth, onAuthStateChanged } from "firebase/auth";
44
import { useAuthStore } from "../stores/auth";
55

6-
export default defineNuxtPlugin(async () => {
6+
export default defineNuxtPlugin(() => {
77
const config = useRuntimeConfig();
88
const publicConfig = config.public as Record<string, string>;
99

@@ -30,19 +30,6 @@ export default defineNuxtPlugin(async () => {
3030
getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
3131
const auth = getAuth(app);
3232

33-
// Also initialize the compat SDK for FirebaseUI
34-
if (import.meta.client) {
35-
const firebase = (await import("firebase/compat/app")) as {
36-
default: {
37-
apps: unknown[];
38-
initializeApp: (config: typeof firebaseConfig) => void;
39-
};
40-
};
41-
if (!firebase.default.apps.length) {
42-
firebase.default.initializeApp(firebaseConfig);
43-
}
44-
}
45-
4633
// Listen for auth state changes and update the auth store
4734
const authStore = useAuthStore();
4835
onAuthStateChanged(auth, (user) => {

web/nuxt.config.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@ export default defineNuxtConfig({
3030
"date-fns",
3131
"firebase/app",
3232
"firebase/auth",
33-
"firebase/compat/app",
34-
"firebase/compat/auth",
35-
"firebaseui",
3633
"libphonenumber-js",
3734
"pusher-js",
3835
"qrcode",
@@ -51,14 +48,6 @@ export default defineNuxtConfig({
5148
icons: {
5249
defaultSet: "mdi-svg",
5350
},
54-
display: {
55-
thresholds: {
56-
md: 960,
57-
lg: 1280,
58-
xl: 1920,
59-
xxl: 2560,
60-
},
61-
},
6251
},
6352
},
6453

web/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
"chartjs-adapter-moment": "^1.0.1",
1919
"date-fns": "^4.3.0",
2020
"firebase": "^12.13.0",
21-
"firebaseui": "^6.1.0",
2221
"flag-icons": "^7.5.0",
2322
"libphonenumber-js": "^1.13.3",
2423
"moment": "^2.30.1",

0 commit comments

Comments
 (0)