diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 407252b4..12200268 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,118 @@ Want to contribute? Great! First, read this page (including the small print at the end). +## Setup + +### Prerequisites + +- [Node.js](https://nodejs.org/) (version 18 or higher recommended) +- [pnpm](https://pnpm.io/) - This workspace uses pnpm for package management + +### Installation + +1. Clone the repository: + ```bash + git clone https://github.com/firebase/firebaseui-web.git + cd firebaseui-web + ``` + +2. Install dependencies: + ```bash + pnpm install + ``` + +### Development Workflow + +This is a monorepo managed with pnpm, containing both `packages` and `examples` sub-directories. + +#### Building + +Build all packages: +```bash +pnpm build +``` + +Build only the packages (excluding examples): +```bash +pnpm build:packages +``` + +#### Testing + +Run all tests: +```bash +pnpm test +``` + +Run tests in watch mode: +```bash +pnpm test:watch +``` + +Run tests for a specific package: +```bash +pnpm --filter= run test +``` + +#### Linting and Formatting + +Check for linting errors: +```bash +pnpm lint:check +``` + +Fix linting errors automatically: +```bash +pnpm lint:fix +``` + +Check for formatting issues: +```bash +pnpm format:check +``` + +Format code automatically: +```bash +pnpm format:write +``` + +### Project Structure + +The project is organized as follows: + +- **`packages/`**: Framework-agnostic and framework-specific packages + - `core`: Framework-agnostic core package providing `initializeUI` and authentication functions + - `translations`: Translation utilities and locale mappings + - `styles`: CSS utility classes and compiled styles + - `react`: React components and hooks + - `angular`: Angular components and DI functionality + - `shadcn`: Shadcn UI components + +- **`examples/`**: Example applications demonstrating usage + - `react`: React example + - `nextjs`: Next.js example + - `nextjs-ssr`: Next.js SSR example + - `angular`: Angular example + - `shadcn`: Shadcn example + +The dependency graph: +``` +core → translations +react → core +angular → core +react → styles +angular → styles +shadcn → react +``` + +### Additional Notes + +- All packages extend the same base `tsconfig.json` file +- Vitest is the preferred testing framework +- The workspace uses pnpm catalogs to ensure dependency version alignment +- Linting is controlled by ESLint via a root flatconfig `eslint.config.ts` file +- Formatting is controlled by Prettier integrated with ESLint via the `.prettierrc` file + ### Before you contribute Before we can use your code, you must sign the [Google Individual Contributor diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 00000000..4ffceeb8 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,377 @@ +# Migration Guide + +## Overview + +FirebaseUI for Web has been completely rewritten from the ground up. The previous version (v6) was a single JavaScript package that provided a monolithic authentication UI solution. The new version (v7) represents a modern, modular architecture that separates concerns and provides better flexibility for developers. + +### Architecture Changes + +**Previous Version (v6):** +- Single JavaScript package (`firebaseui`) that handled both authentication logic and UI rendering +- Tightly coupled to DOM manipulation and jQuery-like patterns +- Limited customization options +- Framework-agnostic but with a rigid structure + +**New Version (v7):** +- **Framework-agnostic core package** (`@invertase/firebaseui-core`): Contains all authentication logic, state management, behaviors, and utilities without any UI dependencies +- **Framework-specific packages**: Separate packages for React (`@invertase/firebaseui-react`), Angular (`@invertase/firebaseui-angular`), and Shadcn components +- **Supporting packages**: Separate packages for styles (`@invertase/firebaseui-styles`) and translations (`@invertase/firebaseui-translations`) +- **Composable architecture**: Components are designed to be composed together, allowing for greater flexibility +- **Modern patterns**: Uses reactive stores (nanostores), TypeScript throughout, and modern framework patterns + +### Migration Path + +**Important:** There is no direct migration path from v6 to v7. This is a complete rewrite with a fundamentally different architecture and API. You cannot simply update the package version and expect your existing code to work. + +Instead, you will need to: +1. Remove the old `firebaseui` package +2. Install the appropriate new package(s) for your framework +3. Rewrite your authentication UI components using the new API +4. Update your configuration and styling approach + +### What This Guide Covers + +This migration guide maps features and concepts from the old [v6 version](https://github.com/firebase/firebaseui-web/tree/v6) to the new v7 rewrite, helping you understand: +- How authentication methods translate between versions +- How configuration options map to the new behaviors system +- How UI customization works in the new architecture +- How to achieve similar functionality with the new component-based approach + +## Migrating + +### 1. Installing Packages + +First, remove the old `firebaseui` package and install the appropriate new package(s) for your framework: + +
+ React + + Remove the old package: + ```bash + npm uninstall firebaseui + ``` + + Install the new React package: + ```bash + npm install @invertase/firebaseui-react + ``` + + The package automatically includes the core package as a dependency, so you don't need to install `@invertase/firebaseui-core` separately. +
+ +
+ Angular + + Remove the old package: + ```bash + npm uninstall firebaseui + ``` + + Install the new Angular package: + ```bash + npm install @invertase/firebaseui-angular + ``` + + **Note:** The Angular package requires [AngularFire](https://github.com/angular/angularfire) to be installed and configured first. +
+ +
+ Shadcn + + Remove the old package: + ```bash + npm uninstall firebaseui + ``` + + Ensure you have [installed and setup](https://ui.shadcn.com/docs/installation) Shadcn in your project first. + + Add the Firebase UI registry to your `components.json`: + ```json + { + ... + "registries": { + "@firebase": "https://fir-ui-shadcn-registry.web.app/r/{name}.json" + } + } + ``` + + Then add components as needed: + ```bash + npx shadcn@latest add @firebase/sign-in-auth-screen + ``` + + This will automatically install all required dependencies. +
+ +### 2. Initialization + +The initialization process is fundamentally different between v6 and v7. + +**Old Way (v6):** +```javascript +// Initialize the FirebaseUI Widget using Firebase. +var ui = new firebaseui.auth.AuthUI(firebase.auth()); + +// The start method will wait until the DOM is loaded. +ui.start('#firebaseui-auth-container', uiConfig); +``` + +**New Way (v7):** + +
+ React (+Shadcn) + + ```tsx + import { initializeApp } from 'firebase/app'; + import { initializeUI } from '@invertase/firebaseui-core'; + import { FirebaseUIProvider } from '@invertase/firebaseui-react'; + + const app = initializeApp({ ... }); + + const ui = initializeUI({ + app, + // behaviors and other configuration go here + }); + + function App() { + return ( + + {/* Your app components */} + + ); + } + ``` +
+ +
+ Angular + + ```ts + import { provideFirebaseApp, initializeApp } from '@angular/fire/app'; + import { initializeUI } from '@invertase/firebaseui-core'; + + export const appConfig: ApplicationConfig = { + providers: [ + provideFirebaseApp(() => initializeApp({ ... })), + provideFirebaseUI((apps) => initializeUI({ + app: apps[0], + // behaviors and other configuration go here + })), + ] + }; + ``` +
+ +### 3. Configuration Options Migration + +The following table maps v6 configuration options to their v7 equivalents: + +| v6 Option | Migration Guide | +|----------|------------------| +| `autoUpgradeAnonymousUsers` | **Use the `autoUpgradeAnonymousUsers` behavior.**

Import `autoUpgradeAnonymousUsers` from `@invertase/firebaseui-core` and add it to your behaviors array:
`behaviors: [autoUpgradeAnonymousUsers({ async onUpgrade(ui, oldUserId, credential) { /* handle merge */ } })]`

The `onUpgrade` callback replaces the `signInFailure` callback for handling merge conflicts. | +| `callbacks` | **Use component props/events instead.**

v6 callbacks like `signInSuccessWithAuthResult`, `signInFailure`, etc. are replaced by component event handlers:

**React:** `onSignIn={(user) => { ... }}`, `onSignUp={(user) => { ... }}`, `onForgotPasswordClick={() => { ... }}`

**Angular:** `(signIn)="onSignIn($event)"`, `(signUp)="onSignUp($event)"`, `(forgotPassword)="onForgotPassword()"`

These are passed directly to the components you use, giving you more control over the flow. | +| `credentialHelper` | **Use the `oneTapSignIn` behavior.**

The credential helper (Account Chooser) from v6 is replaced by Google One Tap in v7. Import `oneTapSignIn` from `@invertase/firebaseui-core` and add it to your behaviors array:
`behaviors: [oneTapSignIn({ clientId: '...', autoSelect: false, cancelOnTapOutside: false })]`

**Note:** This requires Google Sign In to be enabled in Firebase Console. Get the `clientId` from "Web SDK configuration" settings. See [Google One Tap documentation](https://developers.google.com/identity/gsi/web/reference/js-reference) for all configuration options. | +| `queryParameterForSignInSuccessUrl` | **Handle in your routing logic.**

v7 doesn't have built-in URL parameter handling. Instead, handle redirects in your `onSignIn` callback by reading URL params:

**React/Angular:** `const urlParams = new URLSearchParams(window.location.search);`
`const redirectUrl = urlParams.get('signInSuccessUrl') || '/dashboard';`
`window.location.href = redirectUrl;`

**Angular (with Router):** Use `ActivatedRoute` to read query params and `Router` to navigate. | +| `queryParameterForWidgetMode` | **Not applicable.**

v7 doesn't use widget modes. Instead, you explicitly render the components you need:

**React:** ``, ``

**Angular:** ``, `` | +| `signInFlow` | **Use provider strategy behaviors.**

Replace `signInFlow: 'redirect'` with:
`import { providerRedirectStrategy } from '@invertase/firebaseui-core'`
`behaviors: [providerRedirectStrategy()]`

Replace `signInFlow: 'popup'` with:
`import { providerPopupStrategy } from '@invertase/firebaseui-core'`
`behaviors: [providerPopupStrategy()]`

**Note:** `popup` is the default strategy in v7. | +| `immediateFederatedRedirect` | **Control via component rendering.**

v7 doesn't have this option. Instead, you control whether to show OAuth buttons or redirect immediately by conditionally rendering components:

**React:** `{singleProvider ? : }`

**Angular:** Use `*ngIf` or `@if` to conditionally render `` or use `Router` to navigate directly. | +| `signInOptions` | **Use OAuth button components directly.**

v6's `signInOptions` array is replaced by explicitly rendering the OAuth provider buttons you want:

**React:** Import `GoogleSignInButton`, `FacebookSignInButton`, `AppleSignInButton` from `@invertase/firebaseui-react` and render them inside ``.

**Angular:** Import `GoogleSignInButtonComponent`, `FacebookSignInButtonComponent`, `AppleSignInButtonComponent` from `@invertase/firebaseui-angular` and use selectors ``, ``, `` inside ``.

The order you place the buttons determines their display order. | +| `signInSuccessUrl` | **Handle in `onSignIn` callback.**

Instead of a configuration option, handle redirects in your component's `onSignIn` callback:

**React:** ` { window.location.href = '/dashboard'; }} />`

**Angular:** `` with `onSignIn(user: User) { this.router.navigate(['/dashboard']); }`

*Required in v6 when `signInSuccessWithAuthResult` callback is not used or returns `true`. | +| `tosUrl` | **Pass via `policies` prop.**

**React:** Pass `policies={{ termsOfServiceUrl: 'https://example.com/tos', privacyPolicyUrl: 'https://example.com/privacy' }}` to ``.

**Angular:** Use `provideFirebaseUIPolicies(() => ({ termsOfServiceUrl: '...', privacyPolicyUrl: '...' }))`.

The policies are automatically rendered in auth forms and screens. | +| `privacyPolicyUrl` | **Pass via `policies` prop.**

See `tosUrl` above - both URLs are passed together in the `policies` object. | +| `adminRestrictedOperation` | **Handle in your UI logic.**

v7 doesn't have built-in support for this GCIP-specific feature. You'll need to:
(1) Check if sign-up is disabled in your Firebase project settings
(2) Handle the `auth/admin-restricted-operation` error in your error handling
(3) Display appropriate messaging to users when sign-up attempts are blocked

You can check for this error in your `onSignUp` or form error handlers and display custom UI accordingly. | + +### Additional Configuration + +#### Configure Phone Provider + +In v6, phone authentication country code configuration was handled via the `signInOptions` configuration. In v7, this is controlled by the `countryCodes` behavior. + +**v6:** +```javascript +signInOptions: [ + { + provider: firebase.auth.PhoneAuthProvider.PROVIDER_ID, + defaultCountry: 'GB', + whitelistedCountries: ['GB', 'US', 'FR'] + } +] +``` + +**v7:** +Use the `countryCodes` behavior to configure allowed countries and default country: + +```ts +import { countryCodes } from '@invertase/firebaseui-core'; + +const ui = initializeUI({ + app, + behaviors: [ + countryCodes({ + allowedCountries: ['GB', 'US', 'FR'], // only allow Great Britain, USA and France + defaultCountry: 'GB', // GB is default + }), + ], +}); +``` + +The `countryCodes` behavior affects all phone authentication flows, including regular phone sign-in and multi-factor authentication (MFA) enrollment. The `CountrySelector` component automatically uses these settings to filter and display available countries. + +#### Sign In Flows + +In v6, you configured the sign-in flow (popup vs redirect) via the `signInFlow` configuration option. In v7, this is controlled by provider strategy behaviors. + +**v6:** +```javascript +var uiConfig = { + signInFlow: 'popup', // or 'redirect' + // ... +}; +``` + +**v7:** +Use the `providerPopupStrategy` (default) or `providerRedirectStrategy` behaviors: + +```ts +import { providerPopupStrategy, providerRedirectStrategy } from '@invertase/firebaseui-core'; + +// For popup flow (default) +const ui = initializeUI({ + app, + behaviors: [providerPopupStrategy()], +}); + +// For redirect flow +const ui = initializeUI({ + app, + behaviors: [providerRedirectStrategy()], +}); +``` + +**Note:** The popup strategy is the default in v7. If you don't specify a strategy, popup will be used. The strategy applies to all OAuth providers (Google, Facebook, Apple, etc.). + +#### Multi-tenancy Support + +v7 supports multi-tenancy by allowing you to pass a custom `Auth` instance with a `tenantId` configured to `initializeUI`. + +**v6:** +```javascript +var tenantAuth = firebase.auth(app).tenantId = 'tenant-id'; +var ui = new firebaseui.auth.AuthUI(tenantAuth); +``` + +**v7:** + +**React:** +```tsx +import { getAuth } from 'firebase/auth'; +import { initializeUI } from '@invertase/firebaseui-core'; + +const auth = getAuth(app); +auth.tenantId = 'tenant-id'; + +const ui = initializeUI({ + app, + auth, // Pass the auth instance with tenantId +}); +``` + +**Angular:** +```ts +import { getAuth } from 'firebase/auth'; +import { initializeUI } from '@invertase/firebaseui-core'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideFirebaseApp(() => initializeApp({ ... })), + provideAuth(() => { + const auth = getAuth(); + auth.tenantId = 'tenant-id'; + return auth; + }), + provideFirebaseUI((apps) => { + const auth = getAuth(apps[0]); + auth.tenantId = 'tenant-id'; + return initializeUI({ + app: apps[0], + auth, + }); + }), + ], +}; +``` + +#### Enabling Anonymous User Upgrade + +In v6, anonymous user upgrade was configured via the `autoUpgradeAnonymousUsers` option. In v7, this is handled by the `autoUpgradeAnonymousUsers` behavior. + +**v6:** +```javascript +var uiConfig = { + autoUpgradeAnonymousUsers: true, + callbacks: { + signInFailure: function(error) { + // Handle merge conflicts + } + } +}; +``` + +**v7:** +Use the `autoUpgradeAnonymousUsers` behavior: + +```ts +import { autoUpgradeAnonymousUsers } from '@invertase/firebaseui-core'; + +const ui = initializeUI({ + app, + behaviors: [ + autoUpgradeAnonymousUsers({ + async onUpgrade(ui, oldUserId, credential) { + // Handle account merge logic + // e.g., migrate data from oldUserId to new user + console.log(`Upgrading anonymous user ${oldUserId} to ${credential.user.uid}`); + }, + }), + ], +}); +``` + +The behavior automatically upgrades anonymous users when they sign in with any credential (email/password, OAuth, phone, etc.). The `onUpgrade` callback is optional and allows you to perform custom logic during the upgrade, such as migrating user data. + +#### Handling Anonymous User Upgrade Merge Conflicts + +In v6, merge conflicts (when an account already exists with the same credential) were handled via the `signInFailure` callback. In v7, the upgrade process handles this differently. + +**How it works in v7:** + +When an anonymous user attempts to sign in with a credential that's already associated with an existing account, Firebase Auth will automatically link the anonymous account to the existing account. The upgrade process: + +1. **Stores the anonymous user ID** in `localStorage` before redirect flows (for OAuth redirects) +2. **Automatically links** the anonymous account to the existing account +3. **Calls the `onUpgrade` callback** (if provided) with both the old anonymous user ID and the new credential +4. **Cleans up** the stored anonymous user ID from `localStorage` + +**Example:** +```ts +const ui = initializeUI({ + app, + behaviors: [ + autoUpgradeAnonymousUsers({ + async onUpgrade(ui, oldUserId, credential) { + // oldUserId is the anonymous user's ID + // credential.user.uid is the existing account's ID after linking + + // Migrate any data from the anonymous account + await migrateUserData(oldUserId, credential.user.uid); + + // The anonymous account is now linked to the existing account + // The user is signed in with their existing account + }, + }), + ], +}); +``` + +**Note:** If a merge conflict occurs and the linking fails (e.g., due to account linking restrictions), Firebase Auth will throw an error that you can handle in your error handling logic. The `onUpgrade` callback will only be called if the upgrade is successful. +