Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions examples/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@
"@firebase-ui/styles": "workspace:*",
"@firebase-ui/translations": "workspace:*",
"firebase": "^11.6.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react": "catalog:",
"react-dom": "catalog:",
"react-router": "^7.5.1"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.4",
"@eslint/js": "^9.22.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.22.0",
"eslint-plugin-react-hooks": "^5.2.0",
Expand Down
5 changes: 5 additions & 0 deletions examples/react/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ function App() {
Password Reset Screen
</NavLink>
</li>
<li>
<NavLink to="/screens/mfa-enrollment-screen" className="text-blue-500 hover:underline">
MFA Enrollment Screen
</NavLink>
</li>
</ul>
</div>
</div>
Expand Down
9 changes: 8 additions & 1 deletion examples/react/src/firebase/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,11 @@
* limitations under the License.
*/

export const firebaseConfig = {};
export const firebaseConfig = {
apiKey: "AIzaSyCvMftIUCD9lUQ3BzIrimfSfBbCUQYZf-I",
authDomain: "fir-ui-rework.firebaseapp.com",
projectId: "fir-ui-rework",
storageBucket: "fir-ui-rework.firebasestorage.app",
messagingSenderId: "200312857118",
appId: "1:200312857118:web:94e3f69b0e0a4a863f040f"
};
4 changes: 4 additions & 0 deletions examples/react/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ import OAuthScreenPage from "./screens/oauth-screen";
/** Password Reset */
import ForgotPasswordPage from "./screens/forgot-password-screen";

/** MFA Enrollment */
import MultiFactorAuthEnrollmentScreenPage from "./screens/mfa-enrollment-screen";

const root = document.getElementById("root")!;

ReactDOM.createRoot(root).render(
Expand All @@ -72,6 +75,7 @@ ReactDOM.createRoot(root).render(
<Route path="/screens/sign-up-auth-screen-w-oauth" element={<SignUpAuthScreenWithOAuthPage />} />
<Route path="/screens/oauth-screen" element={<OAuthScreenPage />} />
<Route path="/screens/forgot-password-screen" element={<ForgotPasswordPage />} />
<Route path="/screens/mfa-enrollment-screen" element={<MultiFactorAuthEnrollmentScreenPage />} />
</Routes>
</FirebaseUIProvider>
</BrowserRouter>
Expand Down
29 changes: 29 additions & 0 deletions examples/react/src/screens/mfa-enrollment-screen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

"use client";

import { MultiFactorAuthEnrollmentScreen } from "@firebase-ui/react";
import { FactorId } from "firebase/auth";

export default function MultiFactorAuthEnrollmentScreenPage() {
return <MultiFactorAuthEnrollmentScreen
hints={[FactorId.TOTP, FactorId.PHONE]}
onEnrollment={() => {
console.log("Enrollment successful");
}}
/>;
}
54 changes: 50 additions & 4 deletions packages/core/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ import {
EmailAuthProvider,
linkWithCredential,
PhoneAuthProvider,
TotpMultiFactorGenerator,
multiFactor,
type ActionCodeSettings,
type ApplicationVerifier,
type AuthProvider,
type UserCredential,
type AuthCredential,
type TotpSecret,
type PhoneInfoOptions,
type MultiFactorAssertion,
type MultiFactorUser,
} from "firebase/auth";
import QRCode from "qrcode-generator";
import { type FirebaseUI } from "./config";
Expand Down Expand Up @@ -122,13 +125,23 @@ export async function createUserWithEmailAndPassword(

export async function verifyPhoneNumber(
ui: FirebaseUI,
phoneNumber: PhoneInfoOptions | string,
appVerifier: ApplicationVerifier
phoneNumber: string,
appVerifier: ApplicationVerifier,
mfaUser?: MultiFactorUser
): Promise<string> {
try {
ui.setState("pending");
const provider = new PhoneAuthProvider(ui.auth);
return await provider.verifyPhoneNumber(phoneNumber, appVerifier);
const session = await mfaUser?.getSession();
return await provider.verifyPhoneNumber(
session
? {
phoneNumber,
session,
}
: phoneNumber,
appVerifier
);
} catch (error) {
handleFirebaseError(ui, error);
} finally {
Expand Down Expand Up @@ -292,3 +305,36 @@ export function generateTotpQrCode(ui: FirebaseUI, secret: TotpSecret, accountNa
qr.make();
return qr.createDataURL();
}

export async function signInWithMultiFactorAssertion(ui: FirebaseUI, assertion: MultiFactorAssertion) {
await ui.multiFactorResolver?.resolveSignIn(assertion);
throw new Error("Not implemented");
}

export async function enrollWithMultiFactorAssertion(
ui: FirebaseUI,
assertion: MultiFactorAssertion,
displayName?: string
): Promise<void> {
try {
ui.setState("pending");
await multiFactor(ui.auth.currentUser!).enroll(assertion, displayName);
} catch (error) {
handleFirebaseError(ui, error);
} finally {
ui.setState("idle");
}
}

export async function generateTotpSecret(ui: FirebaseUI): Promise<TotpSecret> {
try {
ui.setState("pending");
const mfaUser = multiFactor(ui.auth.currentUser!);
const session = await mfaUser.getSession();
return await TotpMultiFactorGenerator.generateSecret(session);
} catch (error) {
handleFirebaseError(ui, error);
} finally {
ui.setState("idle");
}
}
30 changes: 30 additions & 0 deletions packages/core/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,39 @@ export function createPhoneAuthVerifyFormSchema(ui: FirebaseUI) {
});
}

export function createMultiFactorPhoneAuthNumberFormSchema(ui: FirebaseUI) {
const base = createPhoneAuthNumberFormSchema(ui);
return base.extend({
displayName: z.string().min(1, getTranslation(ui, "errors", "displayNameRequired")),
});
}

export function createMultiFactorPhoneAuthVerifyFormSchema(ui: FirebaseUI) {
return createPhoneAuthVerifyFormSchema(ui);
}

export function createMultiFactorTotpAuthNumberFormSchema(ui: FirebaseUI) {
return z.object({
displayName: z.string().min(1, getTranslation(ui, "errors", "displayNameRequired")),
});
}

export function createMultiFactorTotpAuthVerifyFormSchema(ui: FirebaseUI) {
return z.object({
verificationCode: z.string().refine((val) => val.length === 6, {
error: getTranslation(ui, "errors", "invalidVerificationCode"),
}),
});
}

export type SignInAuthFormSchema = z.infer<ReturnType<typeof createSignInAuthFormSchema>>;
export type SignUpAuthFormSchema = z.infer<ReturnType<typeof createSignUpAuthFormSchema>>;
export type ForgotPasswordAuthFormSchema = z.infer<ReturnType<typeof createForgotPasswordAuthFormSchema>>;
export type EmailLinkAuthFormSchema = z.infer<ReturnType<typeof createEmailLinkAuthFormSchema>>;
export type PhoneAuthNumberFormSchema = z.infer<ReturnType<typeof createPhoneAuthNumberFormSchema>>;
export type PhoneAuthVerifyFormSchema = z.infer<ReturnType<typeof createPhoneAuthVerifyFormSchema>>;
export type MultiFactorPhoneAuthNumberFormSchema = z.infer<
ReturnType<typeof createMultiFactorPhoneAuthNumberFormSchema>
>;
export type MultiFactorTotpAuthNumberFormSchema = z.infer<ReturnType<typeof createMultiFactorTotpAuthNumberFormSchema>>;
export type MultiFactorTotpAuthVerifyFormSchema = z.infer<ReturnType<typeof createMultiFactorTotpAuthVerifyFormSchema>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function SmsMultiFactorAssertionForm() {
return <div>TODO: SmsMultiFactorAssertionForm</div>;
}
Loading