diff --git a/.agents/skills/firebaseui-android-getting-started/SKILL.md b/.agents/skills/firebaseui-android-getting-started/SKILL.md new file mode 100644 index 0000000000..de9bb5d685 --- /dev/null +++ b/.agents/skills/firebaseui-android-getting-started/SKILL.md @@ -0,0 +1,256 @@ +--- +name: firebaseui-android-getting-started +description: Set up FirebaseUI Android Auth with predefined Compose screens in a consumer Android app. Use when adding Firebase Authentication UI, FirebaseUI Auth, FirebaseAuthScreen, authUIConfiguration, or sign-in providers to an Android/Kotlin project. +--- + +# FirebaseUI Android Auth Setup + +Use this skill when the user wants to add FirebaseUI Auth to an existing Android app. Assume you are working in the user's app repo, not the FirebaseUI source repo. + +Default to the high-level predefined screen API: `FirebaseAuthScreen` with `authUIConfiguration {}`. Only reach for the low-level `AuthFlowController` (`authUI.createAuthFlow(configuration)`, returning `createIntent`/`authStateFlow`/`cancel`/`dispose` for manual `ActivityResultLauncher`-based flows) or custom slot UIs when the user explicitly asks for custom auth screens. + +## Source References + +Use these when details are needed beyond this skill: + +- FirebaseUI Auth docs: `https://github.com/firebase/FirebaseUI-Android/blob/master/auth/README.md` +- Firebase Android setup: `https://firebase.google.com/docs/android/setup` +- Firebase Auth provider setup: `https://firebase.google.com/docs/auth` +- `AuthUIStringProvider` customization sample: `https://github.com/firebase/FirebaseUI-Android/blob/master/auth/src/main/java/com/firebase/ui/auth/configuration/string_provider/AuthUIStringProviderSample.kt` + +## Setup Workflow + +Track this checklist while working: + +- [ ] Inspect the Android project structure and identify the app module. +- [ ] Verify Firebase project configuration: `google-services.json`, Google Services Gradle plugin, package name, and enabled Auth providers. +- [ ] Verify Compose, Kotlin, min SDK, and FirebaseUI Auth dependencies. +- [ ] Add or adapt a predefined auth screen using `FirebaseAuthScreen`. +- [ ] Wire success, failure, cancel, and signed-in state handling into the app's navigation. +- [ ] Run the smallest relevant Gradle build or test task. + +## Firebase Project Configuration + +Do not invent Firebase config values. The consumer must get them from their Firebase project. + +1. In the Firebase Console, add an Android app using the app's real `applicationId`. +2. Download `google-services.json` and place it in the app module, usually `app/google-services.json`. +3. Enable each provider the app will expose in Firebase Console > Authentication > Sign-in method. +4. For Google Sign-In, ensure the Google Services Gradle plugin is applied and the app's package name/SHA certificates are configured in Firebase. +5. For phone auth, verify the user's Firebase project supports the target regions and test numbers if needed. +6. For OAuth providers such as Facebook, Twitter/X, GitHub, Microsoft, Yahoo, Apple, or custom OIDC, configure provider credentials in Firebase Console before wiring the Android UI. + +## Gradle Defaults + +Prefer Kotlin DSL snippets when the project uses `build.gradle.kts`; translate to Groovy only if the project already uses Groovy. + +App module essentials: + +```kotlin +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") + id("com.google.gms.google-services") +} + +android { + buildFeatures { + compose = true + } +} + +dependencies { + implementation("com.firebaseui:firebase-ui-auth:10.0.0-beta03") + + implementation(platform("com.google.firebase:firebase-bom:34.7.0")) + implementation("com.google.firebase:firebase-auth") + + implementation(platform("androidx.compose:compose-bom:2025.10.00")) + implementation("androidx.activity:activity-compose") + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.material3:material3") +} +``` + +Root/plugin configuration varies by project. If `com.google.gms.google-services` is not already available, add the Google Services plugin using the project's existing convention: `plugins { ... apply false }`, `buildscript` classpath, or version catalog. + +Add provider-specific dependencies only when used. Facebook login needs: + +```kotlin +implementation("com.facebook.android:facebook-login:18.0.3") +``` + +Minimum expectations from the FirebaseUI Auth docs: Android SDK 21+, Kotlin 1.9+, Compose compiler 1.5+, and Firebase Auth 22.0.0+. Respect stricter versions already present in the user repo. + +## Predefined Auth Screen Template + +Adapt names, theme, navigation, and provider list to the app. Keep provider setup aligned with what is enabled in Firebase Console. + +```kotlin +import android.os.Bundle +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.remember +import com.firebase.ui.auth.AuthException +import com.firebase.ui.auth.FirebaseAuthUI +import com.firebase.ui.auth.configuration.authUIConfiguration +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.configuration.theme.AuthUITheme +import com.firebase.ui.auth.ui.screens.FirebaseAuthScreen + +class AuthActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val authUI = FirebaseAuthUI.getInstance() + + if (authUI.isSignedIn()) { + navigateToHome() + finish() + return + } + + setContent { + AppTheme { + val authTheme = AuthUITheme.fromMaterialTheme() + val configuration = remember(authTheme) { + authUIConfiguration { + context = applicationContext + theme = authTheme + providers { + provider( + AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList(), + ) + ) + provider( + AuthProvider.Google( + scopes = emptyList(), + serverClientId = null, + ) + ) + } + } + } + + FirebaseAuthScreen( + configuration = configuration, + authUI = authUI, + onSignInSuccess = { result -> + navigateToHome() + }, + onSignInFailure = { exception: AuthException -> + Toast.makeText( + this, + exception.message ?: "Authentication failed", + Toast.LENGTH_SHORT + ).show() + }, + onSignInCancelled = { + finish() + } + ) + } + } + } +} +``` + +If the project uses Navigation Compose, prefer making `FirebaseAuthScreen` a destination and call the existing `NavController` from callbacks instead of creating a new Activity. + +## Provider Notes + +- Email/password works with `AuthProvider.Email()` and includes sign-in, sign-up, password reset, and optional display name collection. +- Google works with `AuthProvider.Google()` once `google-services.json`, Firebase provider enablement, and SHA certificates are correct. +- Anonymous sign-in uses `AuthProvider.Anonymous`; enable anonymous auth in Firebase Console first. +- Phone uses `AuthProvider.Phone(...)`; configure country defaults only when the product has a clear country policy. +- Facebook uses `AuthProvider.Facebook()` plus the Facebook SDK dependency and these string resources: + +```xml +YOUR_FACEBOOK_APP_ID +fbYOUR_FACEBOOK_APP_ID +CHANGE-ME +``` + +- Generic OIDC and SAML providers use `AuthProvider.GenericOAuth(...)` with the provider ID exactly as configured in Firebase Console, for example `oidc.line` or `saml.mycompany`. + +## String Customization + +Only add this when the user explicitly asks to change wording or branding, not by default. + +Set `stringProvider` on `authUIConfiguration { }` to override built-in UI copy. Delegate to `DefaultAuthUIStringProvider(context)` via Kotlin interface delegation and override only the strings that need to change: + +```kotlin +class CustomAuthUIStringProvider( + private val defaultProvider: AuthUIStringProvider +) : AuthUIStringProvider by defaultProvider { + override val signInWithGoogle: String = "Continue with Google" + override val continueText: String = "Continue to MyApp" +} + +val configuration = authUIConfiguration { + context = applicationContext + providers { /* ... */ } + stringProvider = CustomAuthUIStringProvider(DefaultAuthUIStringProvider(applicationContext)) +} +``` + +## Theming + +Only customize shapes/colors when the user asks for brand-specific styling; the defaults are fine otherwise. Set `theme` on `authUIConfiguration { }` using `AuthUITheme`: + +```kotlin +// Adjust provider button corner radius globally +val theme = AuthUITheme.Default.copy(providerButtonShape = RoundedCornerShape(12.dp)) + +// Inherit from the app's Material theme instead of FirebaseUI defaults +val theme = AuthUITheme.fromMaterialTheme(providerButtonShape = RoundedCornerShape(12.dp)) + +// Override shape/color for individual providers +val theme = AuthUITheme.Default.copy( + providerButtonShape = RoundedCornerShape(12.dp), + providerStyles = mapOf( + "google.com" to ProviderStyleDefaults.Google.copy(shape = RoundedCornerShape(24.dp)), + "facebook.com" to ProviderStyleDefaults.Facebook.copy(shape = RoundedCornerShape(8.dp)), + ) +) +``` + +## Content Slots + +Only wire these when the user explicitly asks for custom auth screens; default to the predefined `FirebaseAuthScreen` UI otherwise. `FirebaseAuthScreen` accepts optional composable slot parameters that replace individual screens while keeping navigation, error handling, and MFA flow intact: + +- `emailContent: (@Composable (EmailAuthContentState) -> Unit)?` — replaces the email sign-in/sign-up/reset UI. +- `phoneContent: (@Composable (PhoneAuthContentState) -> Unit)?` — replaces the phone auth UI. +- `customMethodPickerLayout: (@Composable (List, (AuthProvider) -> Unit) -> Unit)?` — replaces the provider list on the method picker screen. +- `customMethodPickerTermsConfiguration: MethodPickerTermsConfiguration?` — replaces the default "By continuing..." terms footer with custom content, for example a consent checkbox. Takes `content`, `accepted`, and optionally `disableProvidersUntilAccepted = true` to gate sign-in until consent is given. +- `mfaEnrollmentContent`, `mfaChallengeContent`, `reauthContent`, `authenticatedContent` — replace MFA enrollment, MFA challenge, reauthentication, and the post-sign-in state respectively. + +Each slot receives a state object with the same fields/callbacks the default UI uses (e.g. `EmailAuthContentState` exposes `email`, `password`, `isLoading`, `onEmailChange`, `onSignInClick`, etc.). Read the corresponding state class before writing custom UI so field names are correct. + +`reauthContent` only appears when triggered. Wrap sensitive operations (updating a password, deleting the account, etc.) in `authUI.withReauth(context, reason = "...") { /* operation */ }`; if the wrapped call throws `AuthException.InvalidCredentialsException`, `FirebaseAuthScreen` automatically navigates to `reauthContent` (or its default bottom sheet if no custom slot is provided) to re-verify the user before retrying. + +## Gotchas + +- Never commit a real `google-services.json` unless the user's repo already treats Firebase config as committable and they explicitly want it included. +- Do not hard-code demo package names, server client IDs, OAuth IDs, policy URLs, or Firebase project values from FirebaseUI samples. +- FirebaseUI Auth releases may be built with newer Kotlin metadata than the app. If compilation reports incompatible Kotlin metadata, update the app's Kotlin plugin or choose a FirebaseUI version compatible with the app's Kotlin version. +- If provider constructors fail because optional parameters have no defaults, pass explicit values such as `AuthProvider.Email(emailLinkActionCodeSettings = null, passwordValidationRules = emptyList())` and `AuthProvider.Google(scopes = emptyList(), serverClientId = null)`. +- Remember the `authUIConfiguration` object in Compose so recomposition does not recreate or restart the auth flow. +- Google Sign-In failures are often Firebase Console or SHA certificate issues, not Kotlin code issues. +- Keep the provider list small at first. Add only providers the user has configured and can test. +- Set `theme` in `authUIConfiguration` for clarity. Use an `AuthUITheme` wrapper only if surrounding UI must share that theme. +- For email-link sign-in, configure `actionCodeSettings`, handle the incoming deep link, and add the matching manifest intent filter. Do not add email-link support unless requested. + +## Validation + +After edits, run the smallest command that proves the integration compiles, such as: + +```bash +./gradlew :app:assembleDebug +``` + +If the build fails, first check dependency versions, Compose enablement, the Google Services plugin, and whether `google-services.json` is in the app module. Report any Firebase Console steps the agent cannot complete locally. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..575952e01a --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @samtstern @SUPERCILEX diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..31614499d9 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at samstern@google.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000000..8178aef420 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,42 @@ +Want to contribute? Great! First, read this page (including the small print at +the end). + +### Before you contribute + +Before we can use your code, you must sign the [Google Individual Contributor +License Agreement](https://cla.developers.google.com/about/google-individual) +(CLA), which you can do online. The CLA is necessary mainly because you own the +copyright to your changes, even after your contribution becomes part of our +codebase, so we need your permission to use and distribute your code. We also +need to be sure of various other things—for instance that you'll tell us if you +know that your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review and a member has +approved it, but you must do it before we can put your code into our codebase. + +### Adding new features + +Before you start working on a larger contribution, you should get in touch with +us first through the issue tracker with your idea so that we can help out and +possibly guide you. Coordinating up front makes it much easier to avoid +frustration later on. + +If this has been discussed in an issue, make sure to mention the issue number. +If not, go file an issue about this to make sure this is a desirable change. + +If this is a new feature please co-ordinate with someone on [FirebaseUI-iOS](https://github.com/firebase/FirebaseUI-iOS) +to make sure that we can implement this on both platforms and maintain feature parity. +Feature parity (where it makes sense) is a strict requirement for feature development in FirebaseUI. + +### Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. We adhere to the +[Google Java style guide](https://google.github.io/styleguide/javaguide.html). +In addition, style and lint checks are run on each Travis build to ensure quality. To run the full +suite of tests, checks, lint, etc, use `./gradlew check` (this will ensure the Travis build passes). + +### The small print + +Contributions made by corporations are covered by a different agreement than the +one above, the [Software Grant and Corporate Contributor License +Agreement](https://cla.developers.google.com/about/google-corporate). diff --git a/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md similarity index 94% rename from ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE.md index 344958f812..c35ae9eb92 100644 --- a/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,3 +1,8 @@ + + Welcome to FirebaseUI and thanks for submitting an issue! Please take a look at [open issues](https://github.com/firebase/FirebaseUI-Android/issues?q=is%3Aopen+is%3Aissue), as well as [resolved issues](https://github.com/firebase/FirebaseUI-Android/issues?q=is%3Aissue+is%3Aclosed), to see if your issue is either already being addressed, or has been solved by someone else. diff --git a/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 73% rename from PULL_REQUEST_TEMPLATE.md rename to .github/PULL_REQUEST_TEMPLATE.md index ef2ac817ad..d0dd1e04b5 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,6 @@ Hey there! So you want to contribute to FirebaseUI? Before you file this pull request, follow these steps: * Read [the contribution guidelines](CONTRIBUTING.md). - * If this has been discussed in an issue, make sure to mention the issue number here. If not, go file an issue about this to make sure this is a desirable change. + * Run `./gradlew check` to ensure the Travis build passes. + * If this has been discussed in an issue, make sure to mention the issue number here. If not, go file an issue about this to make sure this is a desirable change. * If this is a new feature please co-ordinate with someone on [FirebaseUI-iOS](https://github.com/firebase/firebaseui-ios) to make sure that we can implement this on both platforms and maintain feature parity. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..8382f9ef1f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +version: 2 +updates: + - package-ecosystem: 'gradle' + directory: '/' + schedule: + interval: 'daily' + cooldown: + default-days: 7 + semver-major-days: 7 + semver-minor-days: 7 + semver-patch-days: 7 + labels: + - 'dependencies' + + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'daily' + cooldown: + default-days: 7 + labels: + - 'dependencies' diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000000..e3621b0903 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,38 @@ +name: Android CI + +on: + - pull_request + - push + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Cache Gradle packages + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + + - name: Set up JDK 21 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + java-version: '21' + distribution: 'temurin' + + - name: Build and Test + run: ./scripts/build.sh + + - name: Print Logs + if: failure() + run: ./scripts/print_build_logs.sh diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml new file mode 100644 index 0000000000..2b90e7d27a --- /dev/null +++ b/.github/workflows/e2e_test.yml @@ -0,0 +1,56 @@ +name: E2E Tests (Firebase Emulator) + +on: + - pull_request + +permissions: + contents: read + +jobs: + e2e-tests: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Cache Gradle packages + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + + - name: Firebase Emulator Cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/firebase/emulators + key: firebase-emulators-v3-${{ runner.os }} + + - name: Install Node.js 20 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '20' + + - name: Set up JDK 21 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + java-version: '21' + distribution: 'temurin' + + - name: Install Firebase Tools + run: | + npm i -g firebase-tools + + - name: Start Firebase Auth Emulator + run: ./scripts/start-firebase-emulator.sh + + - name: Run E2E Tests + run: | + ./gradlew e2eTest + + - name: Print Logs + if: failure() + run: ./scripts/print_build_logs.sh diff --git a/.github/workflows/issue-labels.yaml b/.github/workflows/issue-labels.yaml new file mode 100644 index 0000000000..0e9741ef0f --- /dev/null +++ b/.github/workflows/issue-labels.yaml @@ -0,0 +1,84 @@ +name: Update labels on issues and pull requests + +on: + issue_comment: + types: [created] + issues: + types: [opened] + +permissions: {} + +jobs: + label-new-issue: + if: github.event_name == 'issues' + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Add needs-attention label on creation + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ['needs-attention'], + }); + + label-op-response: + if: github.event_name == 'issue_comment' + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Check if the comment is from the issue author + id: check-op + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const isOpComment = + context.payload.comment.user.login === + context.payload.issue.user.login; + core.setOutput('op_comment', isOpComment ? 'true' : 'false'); + + - name: Update labels when the issue author responded on an open item + if: steps.check-op.outputs.op_comment == 'true' && github.event.issue.state == 'open' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const issueNumber = context.payload.issue.number; + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + labels: ['needs-attention'], + }); + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + name: 'blocked: await customer response', + }); + } catch (error) { + if (error.status !== 404) { + throw error; + } + } + + - name: Comment when the issue author responded on a closed item + if: steps.check-op.outputs.op_comment == 'true' && github.event.issue.state == 'closed' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: [ + 'This item is closed, so it may not receive regular attention.', + '', + 'If it is still relevant, please open a new issue with an up-to-date reproduction, or open a new pull request.', + ].join('\n'), + }); diff --git a/.github/workflows/stale-issue.yaml b/.github/workflows/stale-issue.yaml new file mode 100644 index 0000000000..3ae4b171c1 --- /dev/null +++ b/.github/workflows/stale-issue.yaml @@ -0,0 +1,42 @@ +--- +name: Mark stale issues and pull requests + +on: + workflow_dispatch: + schedule: + - cron: '30 1 * * *' + +permissions: + contents: read + +jobs: + stale: + permissions: + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 + with: + operations-per-run: 1000 + stale-issue-message: | + Hello, to help manage issues we automatically close stale issues. + + This issue has been automatically marked as stale because it has not had activity for quite some time. If it is still relevant, leave a comment to keep it open. + + > This issue will be closed in 15 days if no further activity occurs. + + Thank you for your contributions. + stale-pr-message: | + Hello, this PR has been open for more than 28 days with no activity. + + If you think this is a mistake, please comment to keep this open. Thanks for contributing! + + > This PR will be closed in 15 days if no further activity occurs. + exempt-issue-labels: 'keep open' + exempt-pr-labels: 'keep open' + close-issue-reason: 'not_planned' + days-before-stale: 28 + days-before-close: 15 + stale-issue-label: 'not-planned' + stale-pr-label: 'stale' diff --git a/.gitignore b/.gitignore index ddffdfe7ff..1582a5ae65 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,14 @@ .gradle +*.iml /local.properties -.idea +.idea/** +!.idea/codeStyleSettings.xml .DS_Store -/build -/captures -/library/target -/**/*.iml +build google-services.json -build/ +!/library/google-services.json + +crashlytics-build.properties +auth/src/main/res/values/com_crashlytics_export_strings.xml +*.log +.kotlin/ \ No newline at end of file diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 0000000000..2d2745308b --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,698 @@ + + + + + + \ No newline at end of file diff --git a/.opensource/project.json b/.opensource/project.json new file mode 100644 index 0000000000..1109bb4592 --- /dev/null +++ b/.opensource/project.json @@ -0,0 +1,32 @@ +{ + "name": "FirebaseUI for Android", + "parent": "firebaseui", + "type": "library", + + "platforms": [ + "Android" + ], + + "content": "README.md", + + "pages" : { + "auth/README.md": "Authentication", + "database/README.md": "Realtime Database", + "storage/README.md": "Cloud Storage", + "firestore/README.md": "Cloud Firestore", + "docs/upgrade-to-2.0.md": "Upgrade to v2.0", + "docs/upgrade-to-3.0.md": "Upgrade to v3.0", + "docs/upgrade-to-4.0.md": "Upgrade to v4.0", + "docs/upgrade-to-5.0.md": "Upgrade to v5.0", + "docs/upgrade-to-6.0.md": "Upgrade to v6.0", + "docs/upgrade-to-7.0.md": "Upgrade to v7.0", + "docs/upgrade-to-8.0.md": "Upgrade to v8.0" + }, + + "related": [ + "firebase/firebaseui-ios", + "firebase/firebaseui-web" + ] + +} + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index df4e731ff8..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: android -jdk: oraclejdk8 -# See https://github.com/travis-ci/travis-ci/issues/5582 -sudo: required -before_cache: - - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ -cache: - directories: - - $HOME/.gradle/caches/ - - $HOME/.gradle/wrapper/ -android: - components: - - platform-tools - - tools - - build-tools-25.0.1 - - android-25 - - # Extras - - extra-google-m2repository - - extra-android-m2repository -script: ./gradlew clean :library:testAll :library:prepareArtifacts diff --git a/AUTHORS b/AUTHORS deleted file mode 100644 index 7d5805a03d..0000000000 --- a/AUTHORS +++ /dev/null @@ -1,10 +0,0 @@ -# This is the official list of FirebaseUI-Android authors for copyright purposes. -# This file is distinct from the CONTRIBUTORS files. -# See the latter for an explanation. - -# Names should be added to this file as: -# Name or Organization -# The email address is not required for organizations. - -Google Inc. -Marios Harrane diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..7fa45ce6e2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +Release notes moved to [Releases](https://github.com/firebase/FirebaseUI-Android/releases) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6f5f2c969b..de3ba2f5ba 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,40 +1,370 @@ -Want to contribute? Great! First, read this page (including the small print at -the end). +# Contributing to FirebaseUI-Android -### Before you contribute + + Android CI GitHub Workflow Status + -Before we can use your code, you must sign the [Google Individual Contributor -License Agreement](https://cla.developers.google.com/about/google-individual) -(CLA), which you can do online. The CLA is necessary mainly because you own the -copyright to your changes, even after your contribution becomes part of our -codebase, so we need your permission to use and distribute your code. We also -need to be sure of various other things—for instance that you'll tell us if you -know that your code infringes on other people's patents. You don't have to sign -the CLA until after you've submitted your code for review and a member has -approved it, but you must do it before we can put your code into our codebase. +_See also: [Firebase's code of conduct](https://firebase.google.com/support/guides/code-conduct)_ -### Adding new features +## 1. Things you will need -Before you start working on a larger contribution, you should get in touch with -us first through the issue tracker with your idea so that we can help out and -possibly guide you. Coordinating up front makes it much easier to avoid -frustration later on. +- Linux, Mac OS X, or Windows. +- [git](https://git-scm.com) (used for source version control). +- An ssh client (used to authenticate with GitHub). +- [Android Studio](https://developer.android.com/studio) or [IntelliJ IDEA](https://www.jetbrains.com/idea/). +- [JDK 21](https://adoptium.net/) or higher. +- [Android SDK](https://developer.android.com/studio) with minimum API level 21. -If this has been discussed in an issue, make sure to mention the issue number. -If not, go file an issue about this to make sure this is a desirable change. +## 2. Forking & cloning the repository -If this is a new feature please co-ordinate with someone on [FirebaseUI-iOS](https://github.com/firebase/FirebaseUI-iOS) -to make sure that we can implement this on both platforms and maintain feature parity. -Feature parity (where it makes sense) is a strict requirement for feature development in FirebaseUI. +- Ensure all the dependencies described in the previous section are installed. +- Fork `https://github.com/firebase/FirebaseUI-Android` into your own GitHub account. If + you already have a fork, and are now installing a development environment on + a new machine, make sure you've updated your fork so that you don't use stale + configuration options from long ago. +- If you haven't configured your machine with an SSH key that's known to github, then + follow [GitHub's directions](https://help.github.com/articles/generating-ssh-keys/) + to generate an SSH key. +- `git clone git@github.com:/FirebaseUI-Android.git` +- `git remote add upstream git@github.com:firebase/FirebaseUI-Android.git` (So that you + fetch from the main repository, not your clone, when running `git fetch` + et al.) -### Code reviews +## 3. Environment Setup -All submissions, including submissions by project members, require review. We -use Github pull requests for this purpose. We adhere to the -[Google Java style guide](https://google.github.io/styleguide/javaguide.html). +FirebaseUI-Android uses Gradle to manage the project and dependencies. -### The small print +The repository includes a `library/google-services.json` file that is copied to the app and test modules during builds. This is handled automatically by the build scripts. -Contributions made by corporations are covered by a different agreement than the -one above, the [Software Grant and Corporate Contributor License -Agreement](https://cla.developers.google.com/about/google-corporate). +To verify your environment is set up correctly, run: + +```bash +./scripts/build.sh +``` + +This will: +- Copy the necessary `google-services.json` files +- Download all dependencies +- Build all modules +- Run checkstyle +- Run unit tests + +> If you need to use your own Firebase project, replace `library/google-services.json` with your own configuration file from the [Firebase Console](https://console.firebase.google.com/). + +## 4. Running an example + +The project provides a demo app in the `app` module which showcases the main use-cases of FirebaseUI Auth. + +To run the example app: + +**Option 1: Android Studio** +- Open the project in Android Studio +- Select the `app` configuration +- Run on a device or emulator + +**Option 2: Command line** +```bash +./gradlew :app:installDebug +``` + +Then launch the app on your device. + +Any changes made to the library modules (auth, database, firestore, storage) locally will be automatically reflected in the example application. + +## 5. Running tests + +FirebaseUI-Android comprises of a number of tests, including unit tests and E2E tests. + +### Unit tests + +Unit tests are responsible for ensuring expected behavior whilst developing the library's Kotlin/Java code. Unit tests do not +interact with 3rd party Firebase services, and mock where possible. To run unit tests for all modules (excluding e2eTest), run the following command from the root directory: + +```bash +./gradlew testDebugUnitTest -x :e2eTest:testDebugUnitTest +``` + +To run unit tests for a specific module (e.g., auth): + +```bash +./gradlew :auth:testDebugUnitTest +``` + +### E2E tests + +E2E tests run against Firebase Auth Emulator and test the full integration with Firebase services. To run e2e tests, you first need to start the Firebase Emulator: + +```bash +# Install Firebase Tools (if not already installed) +npm install -g firebase-tools + +# Start the Firebase Auth Emulator +./scripts/start-firebase-emulator.sh +``` + +Then in a separate terminal, run the e2e tests: + +```bash +./gradlew e2eTest +``` + +> Note: E2E tests use Firebase Emulator Suite, so you don't need a real Firebase project to run them. + +### Lint and Code Analysis + +To run lint checks: + +```bash +./gradlew checkstyle +``` + +## 6. Contributing code + +We gladly accept contributions via GitHub pull requests. + +Please peruse the +[Kotlin coding conventions](https://kotlinlang.org/docs/coding-conventions.html) and +[Android code style guide](https://developer.android.com/kotlin/style-guide) before +working on anything non-trivial. These guidelines are intended to +keep the code consistent and avoid common pitfalls. + +To start working on a patch: + +1. `git fetch upstream` +2. `git checkout upstream/master -b ` +3. Hack away! + +Once you have made your changes, ensure that it passes the internal analyzer & formatting checks. The following +commands can be run locally to highlight any issues before committing your code: + +```bash +# Run the full CI build script +./scripts/build.sh +``` + +This script runs: +- `./gradlew clean` +- `./gradlew assembleDebug` - Build all modules +- `./gradlew checkstyle` - Run code style checks +- `./gradlew testDebugUnitTest -x :e2eTest:testDebugUnitTest` - Run unit tests + +You can also run these commands individually if needed. + +Assuming all is successful, commit and push your code: + +1. `git commit -a -m ""` +2. `git push origin ` + +To send us a pull request: + +- `git pull-request` (if you are using [Hub](http://github.com/github/hub/)) or + go to `https://github.com/firebase/FirebaseUI-Android` and click the + "Compare & pull request" button + +Please make sure all your check-ins have detailed commit messages explaining the patch. + +When naming the title of your pull request, please follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) +guide. For example, for a fix to the FirebaseUI Auth module: + +`fix(auth): fixed a bug with email sign-in!` + +For a new feature: + +`feat(auth): add support for passkey authentication` + +Tests are run automatically on contributions using GitHub Actions. Depending on +your code contributions, various tests will be run against your updated code automatically. + +Once you've gotten an LGTM from a project maintainer and once your PR has received +the green light from all our automated testing, wait for one of the package maintainers +to merge the pull request. + +### Code style + +FirebaseUI-Android follows standard Kotlin and Android conventions: + +#### Kotlin + +- Use 4 spaces for indentation +- Maximum line length: 120 characters +- Use meaningful variable and function names +- Follow [Kotlin coding conventions](https://kotlinlang.org/docs/coding-conventions.html) + +#### Jetpack Compose + +- Follow [Compose API guidelines](https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md) +- Composables should be stateless when possible +- Use `remember` for state that survives recomposition +- Hoist state when it needs to be shared + +#### Example + +```kotlin +@Composable +fun SignInScreen( + configuration: AuthUIConfiguration, + onSignInSuccess: (AuthResult) -> Unit, + modifier: Modifier = Modifier +) { + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp) + ) { + OutlinedTextField( + value = email, + onValueChange = { email = it }, + label = { Text("Email") }, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = password, + onValueChange = { password = it }, + label = { Text("Password") }, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier.fillMaxWidth() + ) + } +} +``` + +### Documentation + +All public APIs should be documented with KDoc: + +```kotlin +/** + * Authenticates a user with email and password. + * + * @param email The user's email address + * @param password The user's password + * @return [AuthResult] containing the signed-in user + * @throws AuthException.InvalidCredentialsException if credentials are invalid + * @throws AuthException.NetworkException if network is unavailable + * + * Example usage: + * ```kotlin + * val result = authUI.signInWithEmailAndPassword( + * email = "user@example.com", + * password = "securePassword123" + * ) + * ``` + */ +suspend fun signInWithEmailAndPassword( + email: String, + password: String +): AuthResult +``` + +### Contributor License Agreement + +You must complete the +[Contributor License Agreement](https://cla.developers.google.com/clas). +You can do this online, and it only takes a minute. +If you've never submitted code before, you must add your (or your +organization's) name and contact info to the [AUTHORS](AUTHORS) file. + +### License Headers + +If you create a new file, do not forget to add the license header: + +```kotlin +/* + * Copyright 2025 Google Inc. All Rights Reserved. + * + * 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. + */ +``` + +### The review process + +Newly opened PRs first go through initial triage which results in one of: + +- **Merging the PR** - if the PR can be quickly reviewed and looks good. +- **Closing the PR** - if the PR maintainer decides that the PR should not be merged. +- **Moving the PR to the backlog** - if the review requires non-trivial effort and the issue isn't a priority; in this case the maintainer will: + - Make sure that the PR has an associated issue labeled with "auth", "database", "firestore", or "storage". + - Add the "backlog" label to the issue. + - Leave a comment on the PR explaining that the review is not trivial and that the issue will be looked at according to priority order. +- **Starting a non-trivial review** - if the review requires non-trivial effort and the issue is a priority; in this case the maintainer will: + - Add the "in review" label to the issue. + - Self assign the PR. +- **API Changes** + - If a change or improvement will affect public API, the team will take longer in the review process. + +### The release process + +We push releases manually, using Gradle and Maven publishing. + +Changelogs and version updates are managed by project maintainers. The new version is automatically +generated via the commit types and changelogs via the commit messages. + +Some things to keep in mind before publishing the release: + +- Has CI run on the main commit and gone green? Even if CI shows as green on + the PR it's still possible for it to fail on merge, for multiple reasons. + There may have been some bug in the merge that introduced new failures. CI + runs on PRs as it's configured on their branch state, and not on tip of tree. +- [Publishing is + forever.](https://central.sonatype.org/publish/publish-guide/#deployment) + Hopefully any bugs or breaking changes in this PR have already been caught + in PR review, but now's a second chance to revert before anything goes live. +- "Don't deploy on a Friday." Consider carefully whether or not it's worth + immediately publishing an update before a stretch of time where you're going + to be unavailable. There may be bugs with the release or questions about it + from people that immediately adopt it, and uncovering and resolving those + support issues will take more time if you're unavailable. + +## 7. Contributing documentation + +We gladly accept contributions to the SDK documentation. As our docs are also part of this repo, +see "Contributing code" above for how to prepare and submit a PR to the repo. + +FirebaseUI-Android documentation lives in the README files for each module: +- `auth/README.md` - FirebaseUI Auth documentation +- `database/README.md` - FirebaseUI Realtime Database documentation +- `firestore/README.md` - FirebaseUI Firestore documentation +- `storage/README.md` - FirebaseUI Storage documentation + +Firebase follows the [Google developer documentation style guide](https://developers.google.com/style), +which you should read before writing substantial contributions. + +When updating documentation: + +1. Ensure code samples are tested and working +2. Follow Markdown best practices +3. Include screenshots or GIFs for UI-related changes +4. Update table of contents if adding new sections +5. Ensure links are valid and point to the correct locations + +## 8. Getting help + +If you have questions about contributing: + +- Check the module README files +- Check existing [issues](https://github.com/firebase/FirebaseUI-Android/issues) and [pull requests](https://github.com/firebase/FirebaseUI-Android/pulls) +- Ask on [Stack Overflow](https://stackoverflow.com/questions/tagged/firebaseui) with the `firebaseui` tag +- Create a new issue for discussion + +## 9. Recognition + +Contributors will be recognized in: +- Release notes +- GitHub contributors page +- Project AUTHORS file + +Thank you for making FirebaseUI-Android better! 🎉 diff --git a/CONTRIBUTORS b/CONTRIBUTORS deleted file mode 100644 index 88f390a966..0000000000 --- a/CONTRIBUTORS +++ /dev/null @@ -1,16 +0,0 @@ -# People who have agreed to one of the CLAs and can contribute patches. -# The AUTHORS file lists the copyright holders; this file -# lists people. For example, Google employees are listed here -# but not in AUTHORS, because Google holds the copyright. -# -# https://developers.google.com/open-source/cla/individual -# https://developers.google.com/open-source/cla/corporate -# -# Names should be added to this file as: -# Name - -Frank van Puffelen -Abraham Haskins -David East -Mike McDonald -Marios Harrane diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md new file mode 100644 index 0000000000..24501e031b --- /dev/null +++ b/GETTING_STARTED.md @@ -0,0 +1,243 @@ +# Easily add sign-in to your Android app with FirebaseUI + +[FirebaseUI](https://github.com/firebase/firebaseui-android) Auth is a library built on top of the Firebase Authentication SDK that provides drop-in UI flows for use in your app. + +Caution: Version 10.x is currently a **beta release**. This means that the functionality might change in backward-incompatible ways or have limited support. A beta release is not subject to any SLA or deprecation policy. + +The recommended sign-in flow uses Compose screens. For apps that still use Activities, see the [Existing Activity-based apps](#existing-activity-based-apps) section. + +FirebaseUI Auth provides the following benefits: + +- **Multiple Providers** — sign-in flows for email/password, phone, Google, Facebook, Apple, GitHub, Microsoft, Yahoo, Twitter, anonymous auth, and custom OAuth. +- **Account Management** — flows to handle account management tasks, such as account creation and password resets. +- **Account Linking** — flows to safely link user accounts across identity providers. +- **Anonymous User Upgrading** — flows to safely upgrade anonymous users. +- **Custom Themes** — Material 3 UI support that can inherit your app theme. Also, because FirebaseUI is open source, you can fork the project and customize it exactly to your needs. +- **Credential Manager** — automatic integration with [Credential Manager](https://developer.android.com/identity/sign-in/credential-manager) for fast cross-device sign-in. +- **Multi-Factor Authentication** — SMS and TOTP support for additional security. + +## Before you begin + +1. If you haven't already, [add Firebase to your Android project](https://firebase.google.com/docs/android/setup). +2. In the [Firebase console](https://console.firebase.google.com/), enable the sign-in methods you want to support. +3. Add FirebaseUI Auth to your app module: + +```kotlin +dependencies { + // Check Maven Central for the latest version: + // https://central.sonatype.com/artifact/com.firebaseui/firebase-ui-auth/versions + implementation("com.firebaseui:firebase-ui-auth:10.0.0-beta03") + + // Required only if Facebook login support is required + // Find the latest Facebook SDK releases here: https://goo.gl/Ce5L94 + implementation("com.facebook.android:facebook-android-sdk:8.x") +} +``` + +## Set up sign-in methods + +### Google Sign-In + +Google Sign-In configuration is automatically provided by the [google-services Gradle plugin](https://developers.google.com/android/guides/google-services-plugin). Ensure you have enabled Google Sign-In in the [Firebase Console](https://console.firebase.google.com/project/_/authentication/providers). + +### Facebook Login + +If using Facebook Login, add your Facebook App ID to `strings.xml`: + +```xml + + YOUR_FACEBOOK_APP_ID + fbYOUR_FACEBOOK_APP_ID + CHANGE-ME + +``` + +See the [Facebook for Developers](https://developers.facebook.com/) documentation for setup instructions. + +### Other Providers + +Twitter, GitHub, Microsoft, Yahoo, and Apple providers require configuration in the Firebase Console but no additional Android-specific setup. See the [Firebase Auth documentation](https://firebase.google.com/docs/auth) for provider-specific instructions. + +Choose the providers you want inside `authUIConfiguration`: + +```kotlin +val configuration = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Email()) + provider( + AuthProvider.Phone( + defaultCountryCode = "US", + ) + ) + provider( + AuthProvider.Google( + scopes = listOf("email"), + serverClientId = null, + ) + ) + provider(AuthProvider.Facebook()) + } +} +``` + +### Email link sign-in + +Email link sign-in lives in the email provider configuration: + +```kotlin +val configuration = authUIConfiguration { + context = applicationContext + providers { + provider( + AuthProvider.Email( + isEmailLinkSignInEnabled = true, + emailLinkActionCodeSettings = actionCodeSettings { + url = "https://example.com/auth" + handleCodeInApp = true + setAndroidPackageName( + "com.example.app", + true, + null, + ) + }, + ) + ) + } +} +``` + +For the full deep-link handling flow, see [the Email Link Sign-In section of the README in GitHub](https://github.com/firebase/FirebaseUI-Android/blob/master/auth/README.md#email-link-sign-in). + +## Sign in + +Create an `AuthUIConfiguration`, then show `FirebaseAuthScreen`. + +```kotlin +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val authUI = FirebaseAuthUI.getInstance() + + setContent { + MyAppTheme { + val configuration = authUIConfiguration { + context = applicationContext + theme = AuthUITheme.fromMaterialTheme() + providers { + provider(AuthProvider.Email()) + provider( + AuthProvider.Google( + scopes = listOf("email"), + serverClientId = null, + ) + ) + } + } + + if (authUI.isSignedIn()) { + HomeScreen() + } else { + FirebaseAuthScreen( + configuration = configuration, + authUI = authUI, + onSignInSuccess = { result -> + // User signed in successfully + }, + onSignInFailure = { exception -> + // Sign in failed + }, + onSignInCancelled = { + finish() + }, + ) + } + } + } + } +} +``` + +This gives you a complete authentication flow with: + +- Password Authentication. +- Google Sign-In. +- Password reset. +- Material 3 styling. +- Credential Manager support. +- Error handling through direct callbacks. + +## Sign out + +FirebaseUI Auth provides convenience methods for sign-out and account deletion: + +```kotlin +lifecycleScope.launch { + FirebaseAuthUI.getInstance().signOut(applicationContext) +} +``` + +```kotlin +lifecycleScope.launch { + FirebaseAuthUI.getInstance().delete(applicationContext) +} +``` + +## Customization + +FirebaseUI Auth is customizable, and the simplest way to get started is to set a theme directly in `authUIConfiguration`: + +```kotlin +val configuration = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Email()) + provider(AuthProvider.Google(scopes = listOf("email"), serverClientId = null)) + } + theme = AuthUITheme.Adaptive +} +``` + +You can also: + +- Use `AuthUITheme.Default`, `AuthUITheme.DefaultDark`, or `AuthUITheme.Adaptive`. +- Inherit your app theme with `AuthUITheme.fromMaterialTheme()`. +- Customize the default theme with `.copy()`. +- Build a fully custom `AuthUITheme`. +- Set a logo, Terms of Service URL, and Privacy Policy URL in `authUIConfiguration`. + +For full theming and customization details, including theme precedence, provider button styling, and custom themes, see [the Theming and Customization section of the readme in GitHub](https://github.com/firebase/FirebaseUI-Android/blob/master/auth/README.md#theming--customization). + +## Existing Activity-based apps + +If your app still uses Activities and the Activity Result API, you can keep an Activity-based launch flow by using `AuthFlowController`: + +```kotlin +private val authLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), +) { result -> + if (result.resultCode == RESULT_OK) { + val user = FirebaseAuth.getInstance().currentUser + // ... + } else { + // User cancelled or sign-in failed + } +} + +val configuration = authUIConfiguration { + context = applicationContext + providers { + provider(AuthProvider.Email()) + provider( + AuthProvider.Google( + scopes = listOf("email"), + serverClientId = null, + ) + ) + } +} + +val controller = FirebaseAuthUI.getInstance().createAuthFlow(configuration) +authLauncher.launch(controller.createIntent(this)) +``` diff --git a/README.md b/README.md index 7e031f804b..314458a2bc 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,39 @@ # FirebaseUI for Android — UI Bindings for Firebase -[![Build Status](https://travis-ci.org/firebase/FirebaseUI-Android.svg?branch=master)](https://travis-ci.org/firebase/FirebaseUI-Android) +[![FirebaseOpensource.com](https://img.shields.io/badge/Docs-firebaseopensource.com-orange.svg)]( +https://firebaseopensource.com/projects/firebase/firebaseui-android +) +[![Actions Status][gh-actions-badge]][gh-actions] FirebaseUI is an open-source library for Android that allows you to -quickly connect common UI elements to [Firebase](https://firebase.google.com) -APIs like the Realtime Database or Firebase Authentication. +quickly connect common UI elements to [Firebase](https://firebase.google.com) APIs. A compatible FirebaseUI client is also available for [iOS](https://github.com/firebase/firebaseui-ios). -## Table of Contents +## Table of contents + +1. [Usage](#usage) +1. [Installation](#installation) + 1. [Upgrading](#upgrading) +1. [Dependencies](#dependencies) + 1. [Compatibility](#compatibility-with-firebase--google-play-services-libraries) + 1. [Upgrading dependencies](#upgrading-dependencies) +1. [Sample App](#sample-app) +1. [Snapshot Builds](#snapshot-builds) +1. [Contributing](#contributing) + 1. [Installing](#installing-locally) + 1. [License agreements](#contributor-license-agreements) + 1. [Process](#contribution-process) - 1. [Installation](#installation) - 1. [Usage](#usage) - 1. [Sample App](#sample-app) - 1. [Contributing](#contributing) +## Usage + +FirebaseUI has separate modules for using Firebase Realtime Database, Cloud Firestore, +Firebase Auth, and Cloud Storage. To get started, see the individual instructions for each module: + +* [FirebaseUI Auth](auth/README.md) +* [FirebaseUI Firestore](firestore/README.md) +* [FirebaseUI Database](database/README.md) +* [FirebaseUI Storage](storage/README.md) ## Installation @@ -27,23 +47,43 @@ libraries. ```groovy dependencies { - // FirebaseUI Database only - compile 'com.firebaseui:firebase-ui-database:1.0.1' + // FirebaseUI for Firebase Realtime Database + implementation 'com.firebaseui:firebase-ui-database:10.0.0-beta03' - // FirebaseUI Auth only - compile 'com.firebaseui:firebase-ui-auth:1.0.1' + // FirebaseUI for Cloud Firestore + implementation 'com.firebaseui:firebase-ui-firestore:10.0.0-beta03' - // FirebaseUI Storage only - compile 'com.firebaseui:firebase-ui-storage:1.0.1' + // FirebaseUI for Firebase Auth + implementation 'com.firebaseui:firebase-ui-auth:10.0.0-beta03' - // Single target that includes all FirebaseUI libraries above - compile 'com.firebaseui:firebase-ui:1.0.1' + // FirebaseUI for Cloud Storage + implementation 'com.firebaseui:firebase-ui-storage:10.0.0-beta03' } ``` +If you're including the `firebase-ui-auth` dependency, there's a little +[more setup](auth/README.md#configuration) required. + After the project is synchronized, we're ready to start using Firebase functionality in our app. -### Compatibility with Firebase / Google Play Services Libraries +### Upgrading + +If you are using an old version of FirebaseUI and upgrading, please see the appropriate +migration guide: + +* [Upgrade from 9.1.1 to 10.x.x](./docs/upgrade-to-10.0.md) +* [Upgrade from 8.0.2 to 9.x.x](./docs/upgrade-to-9.0.md) +* [Upgrade from 7.2.0 to 8.x.x](./docs/upgrade-to-8.0.md) +* [Upgrade from 6.4.0 to 7.x.x](./docs/upgrade-to-7.0.md) +* [Upgrade from 5.1.0 to 6.x.x](./docs/upgrade-to-6.0.md) +* [Upgrade from 4.3.2 to 5.x.x](./docs/upgrade-to-5.0.md) +* [Upgrade from 3.3.1 to 4.x.x](./docs/upgrade-to-4.0.md) +* [Upgrade from 2.3.0 to 3.x.x](./docs/upgrade-to-3.0.md) +* [Upgrade from 1.2.0 to 2.x.x](./docs/upgrade-to-2.0.md) + +## Dependencies + +### Compatibility with Firebase / Google Play Services libraries FirebaseUI libraries have the following transitive dependencies on the Firebase SDK: ``` @@ -54,66 +94,126 @@ firebase-ui-auth firebase-ui-database |--- com.google.firebase:firebase-database +firebase-ui-firestore +|--- com.google.firebase:firebase-firestore + firebase-ui-storage |--- com.google.firebase:firebase-storage ``` -Each version of FirebaseUI has dependency on a fixed version of these libraries, defined as the variable `firebase_version` -in `common/constants.gradle`. If you are using any dependencies in your app of the form -`compile 'com.google.firebase:firebase-*:x.y.z'` or `compile 'com.google.android.gms:play-services-*:x.y.z'` -you need to make sure that you use the same version that your chosen version of FirebaseUI requires. - -For convenience, here are some examples: - -| FirebaseUI Version | Firebase/Play Services Version | -|--------------------|--------------------------------| -| 1.0.1 | 10.0.0 | -| 1.0.0 | 9.8.0 | -| 0.6.2 | 9.8.0 | -| 0.6.1 | 9.6.1 | -| 0.6.0 | 9.6.0 | -| 0.5.3 | 9.4.0 | -| 0.4.4 | 9.4.0 | -| 0.4.3 | 9.2.1 | -| 0.4.2 | 9.2.0 | -| 0.4.1 | 9.0.2 | -| 0.4.0 | 9.0.0 | +You can see the specific dependencies associated with each release on the +[Releases page](https://github.com/firebase/FirebaseUI-Android/releases). -## Usage +### Upgrading dependencies + +If you would like to use a newer version of one of FirebaseUI's transitive dependencies, such +as Firebase, Play services, or the Android support libraries, you need to add explicit +`implementation` declarations in your `build.gradle` for all of FirebaseUI's dependencies at the version +you want to use. Here are some examples listing all of the critical dependencies: + +#### Auth + +```groovy +implementation "com.google.firebase:firebase-auth:$X.Y.Z" +implementation "com.google.android.gms:play-services-auth:$X.Y.Z" + +implementation "androidx.lifecycle:lifecycle-extensions:$X.Y.Z" +implementation "androidx.browser:browser:$X.Y.Z" +implementation "androidx.cardview:cardview:$X.Y.Z" +implementation "androidx.constraintlayout:constraintlayout:$X.Y.Z" +implementation "androidx.legacy:legacy-support-v4:$X.Y.Z" +implementation "com.google.android.material:material:$X.Y.Z" +``` + +#### Firestore + +```groovy +implementation "com.google.firebase:firebase-firestore:$X.Y.Z" + +implementation "androidx.legacy:legacy-support-v4:$X.Y.Z" +implementation "androidx.recyclerview:recyclerview:$X.Y.Z" +``` - * [firebase-ui-database](database/README.md) - * [firebase-ui-auth](auth/README.md) - * [firebase-ui-storage](storage/README.md) +#### Realtime Database -## Sample App +```groovy +implementation "com.google.firebase:firebase-database:$X.Y.Z" + +implementation "androidx.legacy:legacy-support-v4:$X.Y.Z" +implementation "androidx.recyclerview:recyclerview:$X.Y.Z" +``` + +#### Storage + +```groovy +implementation "com.google.firebase:firebase-storage:$X.Y.Z" -There is a sample app in the `app/` directory that demonstrates most +implementation "androidx.legacy:legacy-support-v4:$X.Y.Z" +``` + + +## Sample app + +There is a sample app in the [`app/`](app) directory that demonstrates most of the features of FirebaseUI. Load the project in Android Studio and run it on your Android device to see a demonstration. -## Contributing +Before you can run the sample app, you must create a project in +the Firebase console. Add an Android app to the project, and copy +the generated google-services.json file into the `app/` directory. +Also enable [anonymous authentication](https://firebase.google.com/docs/auth/android/anonymous-auth) +for the Firebase project, since some components of the sample app +requires it. -### Installing locally +If you encounter a version incompatibility error between Android Studio +and Gradle while trying to run the sample app, try disabling the Instant +Run feature of Android Studio. Alternatively, update Android Studio and +Gradle to their latest versions. -You can download FirebaseUI and install it locally by cloning this -repository and running: +A note on importing the project using Android Studio: Using 'Project from +Version Control' will not automatically link the project with Gradle +(issue [#1349](https://github.com/firebase/FirebaseUI-Android/issues/1349)). +When doing so and opening any `build.gradle.kts` file, an error shows up: +`Project 'FirebaseUI-Android' isn't linked with Gradle`. To resolve this +issue, please `git checkout` the project manually and import with `Import +from external model`. - ./gradlew :library:prepareArtifacts :library:publishAllToMavenLocal +## Snapshot builds -### Deployment +Like to live on the cutting edge? Want to try the next release of FirebaseUI before anyone else? +FirebaseUI hosts "snapshot" builds on oss.jfrog.org. -To deploy FirebaseUI to Bintray +Just add the following to your `build.gradle`: - 1. Set `BINTRAY_USER` and `BINTRAY_KEY` in your environment. You must - be a member of the firebaseui Bintray organization. - 1. Run `./gradlew clean :library:prepareArtifacts :library:bintrayUploadAll` - 1. Go to the Bintray dashboard and click 'Publish'. - 1. In Bintray click the 'Maven Central' tab and publish the release. +```groovy +repositories { + maven { url "https://oss.jfrog.org/artifactory/oss-snapshot-local" } +} +``` -### Tag a release on GitHub +Then you can depend on snapshot versions: -* Ensure that all your changes are on master and that your local build is on master -* Ensure that the correct version number is in `common/constants.gradle` +```groovy +implementation 'com.firebaseui:firebase-ui-auth:$X.Y.Z-SNAPSHOT' +``` + +You can see which `SNAPSHOT` builds are available here: +https://oss.jfrog.org/webapp/#/artifacts/browse/tree/General/oss-snapshot-local/com/firebaseui + +Snapshot builds come with absolutely no guarantees and we will close any issues asking to troubleshoot +a snapshot report unless they identify a bug that should block the release launch. Experiment +at your own risk! + +## Contributing + +### Installing locally + +You can download FirebaseUI and install it locally by cloning this +repository and running: + +```sh +./gradlew :library:prepareArtifacts publishToMavenLocal +``` ### Contributor License Agreements @@ -123,25 +223,28 @@ have to jump a couple of legal hurdles. Please fill out either the individual or corporate Contributor License Agreement (CLA). - * If you are an individual writing original source code and you're sure you - own the intellectual property, then you'll need to sign an - [individual CLA](https://developers.google.com/open-source/cla/individual). - * If you work for a company that wants to allow you to contribute your work, - then you'll need to sign a - [corporate CLA](https://developers.google.com/open-source/cla/corporate). +* If you are an individual writing original source code and you're sure you + own the intellectual property, then you'll need to sign an + [individual CLA](https://developers.google.com/open-source/cla/individual). +* If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a + [corporate CLA](https://developers.google.com/open-source/cla/corporate). Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll be able to accept your pull requests. -### Contribution Process +### Contribution process 1. Submit an issue describing your proposed change to the repo in question. 1. The repo owner will respond to your issue promptly. 1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above). -1. Fork the desired repo, develop and test your code changes. +1. Fork the desired repo, develop, and then test your code changes **on the latest dev branch**. 1. Ensure that your code adheres to the existing style of the library to which you are contributing. 1. Ensure that your code has an appropriate set of unit tests which all pass. -1. Submit a pull request and cc @puf or @samtstern +1. Submit a pull request targeting the latest dev branch. + +[gh-actions]: https://github.com/firebase/FirebaseUI-Android/actions +[gh-actions-badge]: https://github.com/firebase/FirebaseUI-Android/workflows/Android%20CI/badge.svg diff --git a/app/.gitignore b/app/.gitignore index 796b96d1c4..42afabfd2a 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1 @@ -/build +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index f077dd862f..0000000000 --- a/app/build.gradle +++ /dev/null @@ -1,54 +0,0 @@ -apply plugin: 'com.android.application' -apply plugin: 'com.neenbedankt.android-apt' -apply from: "../common/constants.gradle" - -android { - compileSdkVersion compileSdk - buildToolsVersion buildTools - - defaultConfig { - applicationId "com.firebase.uidemo" - minSdkVersion 16 - targetSdkVersion targetSdk - versionCode 1 - versionName "1.0" - } - - buildTypes { - release { - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - - // For the purposes of the sample, allow testing of a proguarded release build - // using the debug key - signingConfig signingConfigs.debug - } - } -} - -dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - compile "com.android.support:design:$support_library_version" - - compile project(path: ':auth') - compile project(path: ':database') - compile project(path: ':storage') - - compile "com.google.android.gms:play-services-auth:$firebase_version" - compile "com.google.firebase:firebase-auth:$firebase_version" - compile "com.google.firebase:firebase-database:$firebase_version" - compile "com.google.firebase:firebase-storage:$firebase_version" - - // The following dependencies are not required to use the Firebase UI library. - // They are used to make some aspects of the demo app implementation simpler for - // demonstrative purposes, and you may find them useful in your own apps; YMMV. - compile 'com.github.bumptech.glide:glide:3.7.0' - compile 'pub.devrel:easypermissions:0.2.1' - compile 'com.jakewharton:butterknife:8.4.0' - apt 'com.jakewharton:butterknife-compiler:8.4.0' - debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5' - releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5' - testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5' -} - -apply plugin: 'com.google.gms.google-services' diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000000..bb74b0a759 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,83 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") + id("com.google.gms.google-services") + id("kotlin-kapt") +} + +android { + namespace = "com.firebaseui.android.demo" + compileSdk = Config.SdkVersions.compile + + defaultConfig { + applicationId = "com.firebaseui.android.demo" + minSdk = Config.SdkVersions.min + targetSdk = Config.SdkVersions.target + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + // Only sign with debug keystore if it exists (for local testing) + val debugKeystoreFile = file("${System.getProperty("user.home")}/.android/debug.keystore") + if (debugKeystoreFile.exists()) { + signingConfig = signingConfigs.getByName("debug") + } + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + buildFeatures { + compose = true + buildConfig = true + } +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_17 + } +} + +dependencies { + implementation(project(":auth")) + implementation(project(":database")) + implementation(project(":firestore")) + implementation(project(":storage")) + implementation(libs.androidx.paging) + kapt(libs.glide.compiler) + + implementation(libs.kotlin.stdlib) + implementation(libs.androidx.lifecycle.runtime) + implementation(libs.compose.activity) + implementation(platform(libs.compose.bom)) + implementation(libs.compose.ui) + implementation(libs.compose.ui.graphics) + implementation(libs.compose.ui.tooling.preview) + implementation(libs.compose.material3) + + // Facebook + implementation(libs.facebook.login) + + testImplementation(libs.junit) + androidTestImplementation(libs.junit.ext) + androidTestImplementation(platform(libs.compose.bom)) + androidTestImplementation(libs.compose.ui.test.junit4) + + debugImplementation(libs.compose.ui.tooling) + + implementation(platform(libs.firebase.bom)) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f59df819a4..481bb43481 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,14 +1,10 @@ # Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in /Users/puf/Library/Android/sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html -# Add any project specific keep options here: - # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: @@ -16,15 +12,10 @@ # public *; #} -# See: -# https://firebase.google.com/docs/auth/android/start/#proguard --keepattributes Signature --keepattributes *Annotation* +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable -# See: -# storage/README.md --assumenosideeffects class android.util.Log { - public static *** w(...); - public static *** d(...); - public static *** v(...); -} +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/scripts/run-demo.sh b/app/scripts/run-demo.sh new file mode 100755 index 0000000000..5f6f8325db --- /dev/null +++ b/app/scripts/run-demo.sh @@ -0,0 +1,259 @@ +#!/usr/bin/env bash + +set -euo pipefail + +APP_ID="com.firebaseui.android.demo" +MAIN_ACTIVITY="com.firebaseui.android.demo.MainActivity" +APK_RELATIVE_PATH="app/build/outputs/apk/debug/app-debug.apk" +EMULATOR_LOG="${TMPDIR:-/tmp}/firebaseui-android-emulator.log" +EMULATOR_PID="" +LAUNCHED_EMULATOR=0 +CONNECTED_DEVICES=() +AVAILABLE_AVDS=() +KNOWN_DEVICE_SERIALS=() + +usage() { + cat <<'EOF' +Usage: run-demo.sh [--help] + +Builds, installs, and launches the FirebaseUI Android demo app. + +The script lets you: +1. Use an already connected Android device or emulator. +2. Start an AVD selected from `emulator -list-avds`. +EOF +} + +require_command() { + local command_name="$1" + if ! command -v "$command_name" >/dev/null 2>&1; then + echo "Missing required command: $command_name" >&2 + exit 1 + fi +} + +collect_connected_devices() { + CONNECTED_DEVICES=() + while IFS= read -r serial; do + if [[ -n "$serial" ]]; then + CONNECTED_DEVICES+=("$serial") + fi + done < <(adb devices | awk 'NR > 1 && $2 == "device" { print $1 }') +} + +collect_available_avds() { + AVAILABLE_AVDS=() + while IFS= read -r avd_name; do + if [[ -n "$avd_name" ]]; then + AVAILABLE_AVDS+=("$avd_name") + fi + done < <(emulator -list-avds 2>/dev/null) +} + +prompt_for_choice() { + local prompt="$1" + shift + local options=("$@") + local selection + local index=1 + + echo "$prompt" + for option in "${options[@]}"; do + printf " %d) %s\n" "$index" "$option" + index=$((index + 1)) + done + + while true; do + printf "Select an option [1-%d]: " "${#options[@]}" + read -r selection + + if [[ "$selection" =~ ^[0-9]+$ ]] && (( selection >= 1 && selection <= ${#options[@]} )); then + CHOICE_INDEX=$((selection - 1)) + return 0 + fi + + echo "Please enter a number between 1 and ${#options[@]}." + done +} + +is_known_device() { + local candidate="$1" + local known + local index + + for (( index=0; index<${#KNOWN_DEVICE_SERIALS[@]}; index++ )); do + known="${KNOWN_DEVICE_SERIALS[$index]}" + if [[ "$known" == "$candidate" ]]; then + return 0 + fi + done + + return 1 +} + +cleanup_on_exit() { + local exit_code="$?" + + if (( exit_code != 0 )) && (( LAUNCHED_EMULATOR == 1 )) && [[ -n "$EMULATOR_PID" ]]; then + echo "Stopping emulator started by this script..." >&2 + kill "$EMULATOR_PID" 2>/dev/null || true + fi + + exit "$exit_code" +} + +wait_for_device_boot() { + local serial="$1" + local attempt + local boot_completed + + adb -s "$serial" wait-for-device >/dev/null + + echo "Waiting for $serial to finish booting..." + for (( attempt=1; attempt<=120; attempt++ )); do + boot_completed="$(adb -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" + if [[ "$boot_completed" == "1" ]]; then + echo "$serial is ready." + return 0 + fi + sleep 2 + done + + echo "Timed out waiting for $serial to boot." >&2 + exit 1 +} + +start_selected_avd() { + local avd_name="$1" + local attempt + local serial + local index + + collect_connected_devices + KNOWN_DEVICE_SERIALS=() + for (( index=0; index<${#CONNECTED_DEVICES[@]}; index++ )); do + KNOWN_DEVICE_SERIALS+=("${CONNECTED_DEVICES[$index]}") + done + + echo "Starting emulator '$avd_name'..." + echo "Emulator logs: $EMULATOR_LOG" + emulator -avd "$avd_name" >"$EMULATOR_LOG" 2>&1 & + EMULATOR_PID=$! + LAUNCHED_EMULATOR=1 + + for (( attempt=1; attempt<=120; attempt++ )); do + sleep 2 + collect_connected_devices + for (( index=0; index<${#CONNECTED_DEVICES[@]}; index++ )); do + serial="${CONNECTED_DEVICES[$index]}" + case "$serial" in + emulator-*) + if ! is_known_device "$serial"; then + TARGET_SERIAL="$serial" + wait_for_device_boot "$TARGET_SERIAL" + return 0 + fi + ;; + esac + done + done + + echo "Failed to detect the new emulator for AVD '$avd_name'." >&2 + exit 1 +} + +choose_target_device() { + local option_labels=() + local option_types=() + local option_values=() + local serial + local avd_name + local index + + collect_connected_devices + collect_available_avds + + for (( index=0; index<${#CONNECTED_DEVICES[@]}; index++ )); do + serial="${CONNECTED_DEVICES[$index]}" + option_labels+=("Use connected device: $serial") + option_types+=("device") + option_values+=("$serial") + done + + for (( index=0; index<${#AVAILABLE_AVDS[@]}; index++ )); do + avd_name="${AVAILABLE_AVDS[$index]}" + option_labels+=("Start emulator: $avd_name") + option_types+=("avd") + option_values+=("$avd_name") + done + + if (( ${#option_labels[@]} == 0 )); then + cat >&2 <<'EOF' +No connected Android devices were found, and no AVDs are available. +Connect a device or create an emulator first, then rerun this script. +EOF + exit 1 + fi + + prompt_for_choice "Choose a target for the demo app:" "${option_labels[@]}" + + case "${option_types[$CHOICE_INDEX]}" in + device) + TARGET_SERIAL="${option_values[$CHOICE_INDEX]}" + ;; + avd) + start_selected_avd "${option_values[$CHOICE_INDEX]}" + ;; + esac +} + +main() { + local script_dir + local repo_root + local apk_path + local launch_output + + if [[ "${1:-}" == "--help" ]]; then + usage + exit 0 + fi + + trap cleanup_on_exit EXIT + + require_command adb + require_command emulator + + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + repo_root="$(cd "$script_dir/../.." && pwd)" + apk_path="$repo_root/$APK_RELATIVE_PATH" + + if [[ ! -x "$repo_root/gradlew" ]]; then + echo "gradlew not found or not executable at $repo_root/gradlew" >&2 + exit 1 + fi + + choose_target_device + + echo "Building debug APK..." + "$repo_root/gradlew" :app:assembleDebug + + if [[ ! -f "$apk_path" ]]; then + echo "APK not found at $apk_path" >&2 + exit 1 + fi + + echo "Installing app on $TARGET_SERIAL..." + adb -s "$TARGET_SERIAL" install -r "$apk_path" + + echo "Launching demo app on $TARGET_SERIAL..." + launch_output="$(adb -s "$TARGET_SERIAL" shell am start -n "$APP_ID/$MAIN_ACTIVITY")" + echo "$launch_output" + if [[ "$launch_output" == *"Error:"* ]]; then + echo "Failed to launch the demo app." >&2 + exit 1 + fi + + trap - EXIT +} + +main "$@" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 124bf4d2bb..77abc57765 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,44 +1,113 @@ - + - - + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true" + android:theme="@style/Theme.FirebaseUIAndroid" + android:usesCleartextTraffic="true"> + - - + + + + + + + + + - - + + + + + + + + android:name="com.firebaseui.android.demo.auth.CustomSlotsThemingDemoActivity" + android:label="Custom Slots & Theming Demo" + android:exported="false" + android:theme="@style/Theme.FirebaseUIAndroid" /> - + android:name="com.firebaseui.android.demo.auth.CredentialLinkingDemoActivity" + android:label="Credential Linking Demo" + android:exported="false" + android:theme="@style/Theme.FirebaseUIAndroid" /> + + + + + + + + + + + + + + android:name="com.firebaseui.android.demo.firestore.FirestoreDemoActivity" + android:label="Firestore Demo" + android:exported="false" + android:theme="@style/Theme.FirebaseUIAndroid" /> - + + android:name="com.firebaseui.android.demo.storage.StorageDemoActivity" + android:label="Storage Demo" + android:exported="false" + android:theme="@style/Theme.FirebaseUIAndroid" /> diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000..2612b90d22 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/firebase/uidemo/ChooserActivity.java b/app/src/main/java/com/firebase/uidemo/ChooserActivity.java deleted file mode 100644 index c2bb3e1757..0000000000 --- a/app/src/main/java/com/firebase/uidemo/ChooserActivity.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * 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. - */ - -package com.firebase.uidemo; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; - -import com.firebase.uidemo.auth.AuthUiActivity; -import com.firebase.uidemo.database.ChatActivity; -import com.firebase.uidemo.storage.ImageActivity; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnItemClick; - -public class ChooserActivity extends AppCompatActivity { - - private static final Class[] CLASSES = new Class[]{ - ChatActivity.class, - AuthUiActivity.class, - ImageActivity.class, - }; - - private static final int[] DESCRIPTION_NAMES = new int[] { - R.string.name_chat, - R.string.name_auth_ui, - R.string.name_image - }; - - private static final int[] DESCRIPTION_IDS = new int[] { - R.string.desc_chat, - R.string.desc_auth_ui, - R.string.desc_image - }; - - @BindView(R.id.list_view) - ListView mListView; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_chooser); - ButterKnife.bind(this); - - mListView.setAdapter(new MyArrayAdapter( - this, - android.R.layout.simple_list_item_2, - CLASSES)); - } - - @OnItemClick(R.id.list_view) - public void onItemClick(int position) { - Class clicked = CLASSES[position]; - startActivity(new Intent(this, clicked)); - } - - public static class MyArrayAdapter extends ArrayAdapter { - - private Context mContext; - private Class[] mClasses; - - public MyArrayAdapter(Context context, int resource, Class[] objects) { - super(context, resource, objects); - - mContext = context; - mClasses = objects; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = convertView; - - if (convertView == null) { - LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(LAYOUT_INFLATER_SERVICE); - view = inflater.inflate(android.R.layout.simple_list_item_2, null); - } - - ((TextView) view.findViewById(android.R.id.text1)).setText(DESCRIPTION_NAMES[position]); - ((TextView) view.findViewById(android.R.id.text2)).setText(DESCRIPTION_IDS[position]); - - return view; - } - } -} diff --git a/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java b/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java deleted file mode 100644 index f1ddddf9de..0000000000 --- a/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java +++ /dev/null @@ -1,358 +0,0 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * 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. - */ - -package com.firebase.uidemo.auth; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.support.annotation.DrawableRes; -import android.support.annotation.MainThread; -import android.support.annotation.StringRes; -import android.support.annotation.StyleRes; -import android.support.design.widget.Snackbar; -import android.support.v7.app.AppCompatActivity; -import android.view.View; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.CompoundButton.OnCheckedChangeListener; -import android.widget.RadioButton; -import android.widget.TextView; - -import com.firebase.ui.auth.AuthUI; -import com.firebase.ui.auth.AuthUI.IdpConfig; -import com.firebase.ui.auth.IdpResponse; -import com.firebase.ui.auth.ui.ResultCodes; -import com.firebase.uidemo.R; -import com.google.android.gms.common.Scopes; -import com.google.firebase.auth.FirebaseAuth; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; - -public class AuthUiActivity extends AppCompatActivity { - private static final String UNCHANGED_CONFIG_VALUE = "CHANGE-ME"; - private static final String GOOGLE_TOS_URL = "https://www.google.com/policies/terms/"; - private static final String FIREBASE_TOS_URL = "https://www.firebase.com/terms/terms-of-service.html"; - private static final int RC_SIGN_IN = 100; - - @BindView(R.id.default_theme) - RadioButton mUseDefaultTheme; - - @BindView(R.id.green_theme) - RadioButton mUseGreenTheme; - - @BindView(R.id.purple_theme) - RadioButton mUsePurpleTheme; - - @BindView(R.id.dark_theme) - RadioButton mUseDarkTheme; - - @BindView(R.id.email_provider) - CheckBox mUseEmailProvider; - - @BindView(R.id.google_provider) - CheckBox mUseGoogleProvider; - - @BindView(R.id.facebook_provider) - CheckBox mUseFacebookProvider; - - @BindView(R.id.twitter_provider) - CheckBox mUseTwitterProvider; - - @BindView(R.id.google_tos) - RadioButton mUseGoogleTos; - - @BindView(R.id.firebase_tos) - RadioButton mUseFirebaseTos; - - @BindView(R.id.sign_in) - Button mSignIn; - - @BindView(android.R.id.content) - View mRootView; - - @BindView(R.id.firebase_logo) - RadioButton mFirebaseLogo; - - @BindView(R.id.google_logo) - RadioButton mGoogleLogo; - - @BindView(R.id.no_logo) - RadioButton mNoLogo; - - @BindView(R.id.smartlock_enabled) - CheckBox mEnableSmartLock; - - @BindView(R.id.facebook_scopes_label) - TextView mFacebookScopesLabel; - - @BindView(R.id.facebook_scope_friends) - CheckBox mFacebookScopeFriends; - - @BindView(R.id.facebook_scope_photos) - CheckBox mFacebookScopePhotos; - - @BindView(R.id.google_scopes_label) - TextView mGoogleScopesLabel; - - @BindView(R.id.google_scope_drive_file) - CheckBox mGoogleScopeDriveFile; - - @BindView(R.id.google_scope_games) - CheckBox mGoogleScopeGames; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - FirebaseAuth auth = FirebaseAuth.getInstance(); - if (auth.getCurrentUser() != null) { - startActivity(SignedInActivity.createIntent(this, null)); - finish(); - } - - setContentView(R.layout.auth_ui_layout); - ButterKnife.bind(this); - - if (!isGoogleConfigured()) { - mUseGoogleProvider.setChecked(false); - mUseGoogleProvider.setEnabled(false); - mUseGoogleProvider.setText(R.string.google_label_missing_config); - setGoogleScopesEnabled(false); - } else { - setGoogleScopesEnabled(mUseGoogleProvider.isChecked()); - mUseGoogleProvider.setOnCheckedChangeListener(new OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { - setGoogleScopesEnabled(checked); - } - }); - } - - if (!isFacebookConfigured()) { - mUseFacebookProvider.setChecked(false); - mUseFacebookProvider.setEnabled(false); - mUseFacebookProvider.setText(R.string.facebook_label_missing_config); - setFacebookScopesEnabled(false); - } else { - setFacebookScopesEnabled(mUseFacebookProvider.isChecked()); - mUseFacebookProvider.setOnCheckedChangeListener(new OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { - setFacebookScopesEnabled(checked); - } - }); - } - - if (!isTwitterConfigured()) { - mUseTwitterProvider.setChecked(false); - mUseTwitterProvider.setEnabled(false); - mUseTwitterProvider.setText(R.string.twitter_label_missing_config); - } - - if (!isGoogleConfigured() || !isFacebookConfigured() || !isTwitterConfigured()) { - showSnackbar(R.string.configuration_required); - } - } - - @OnClick(R.id.sign_in) - public void signIn(View view) { - startActivityForResult( - AuthUI.getInstance().createSignInIntentBuilder() - .setTheme(getSelectedTheme()) - .setLogo(getSelectedLogo()) - .setProviders(getSelectedProviders()) - .setTosUrl(getSelectedTosUrl()) - .setIsSmartLockEnabled(mEnableSmartLock.isChecked()) - .build(), - RC_SIGN_IN); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == RC_SIGN_IN) { - handleSignInResponse(resultCode, data); - return; - } - - showSnackbar(R.string.unknown_response); - } - - @MainThread - private void handleSignInResponse(int resultCode, Intent data) { - if (resultCode == RESULT_OK) { - startActivity(SignedInActivity.createIntent(this, IdpResponse.fromResultIntent(data))); - finish(); - return; - } - - if (resultCode == RESULT_CANCELED) { - showSnackbar(R.string.sign_in_cancelled); - return; - } - - if (resultCode == ResultCodes.RESULT_NO_NETWORK) { - showSnackbar(R.string.no_internet_connection); - return; - } - - showSnackbar(R.string.unknown_sign_in_response); - } - - @MainThread - private void setGoogleScopesEnabled(boolean enabled) { - mGoogleScopesLabel.setEnabled(enabled); - mGoogleScopeDriveFile.setEnabled(enabled); - mGoogleScopeGames.setEnabled(enabled); - } - - @MainThread - private void setFacebookScopesEnabled(boolean enabled) { - mFacebookScopesLabel.setEnabled(enabled); - mFacebookScopeFriends.setEnabled(enabled); - mFacebookScopePhotos.setEnabled(enabled); - } - - @MainThread - @StyleRes - private int getSelectedTheme() { - if (mUseDefaultTheme.isChecked()) { - return AuthUI.getDefaultTheme(); - } - - if (mUsePurpleTheme.isChecked()) { - return R.style.PurpleTheme; - } - - if (mUseDarkTheme.isChecked()) { - return R.style.DarkTheme; - } - - return R.style.GreenTheme; - } - - @MainThread - @DrawableRes - private int getSelectedLogo() { - if (mFirebaseLogo.isChecked()) { - return R.drawable.firebase_auth_120dp; - } else if (mGoogleLogo.isChecked()) { - return R.drawable.logo_googleg_color_144dp; - } - return AuthUI.NO_LOGO; - } - - @MainThread - private List getSelectedProviders() { - List selectedProviders = new ArrayList<>(); - - if (mUseEmailProvider.isChecked()) { - selectedProviders.add(new IdpConfig.Builder(AuthUI.EMAIL_PROVIDER).build()); - } - - if (mUseFacebookProvider.isChecked()) { - selectedProviders.add( - new IdpConfig.Builder(AuthUI.FACEBOOK_PROVIDER) - .setPermissions(getFacebookPermissions()) - .build()); - } - - if (mUseGoogleProvider.isChecked()) { - selectedProviders.add( - new IdpConfig.Builder(AuthUI.GOOGLE_PROVIDER) - .setPermissions(getGooglePermissions()) - .build()); - } - - if (mUseTwitterProvider.isChecked()) { - selectedProviders.add(new IdpConfig.Builder(AuthUI.TWITTER_PROVIDER).build()); - } - - return selectedProviders; - } - - @MainThread - private String getSelectedTosUrl() { - if (mUseGoogleTos.isChecked()) { - return GOOGLE_TOS_URL; - } - - return FIREBASE_TOS_URL; - } - - @MainThread - private boolean isGoogleConfigured() { - return !UNCHANGED_CONFIG_VALUE.equals( - getResources().getString(R.string.default_web_client_id)); - } - - @MainThread - private boolean isFacebookConfigured() { - return !UNCHANGED_CONFIG_VALUE.equals( - getResources().getString(R.string.facebook_application_id)); - } - - @MainThread - private boolean isTwitterConfigured() { - List twitterConfigs = Arrays.asList( - getResources().getString(R.string.twitter_consumer_key), - getResources().getString(R.string.twitter_consumer_secret) - ); - - return !twitterConfigs.contains(UNCHANGED_CONFIG_VALUE); - } - - @MainThread - private void showSnackbar(@StringRes int errorMessageRes) { - Snackbar.make(mRootView, errorMessageRes, Snackbar.LENGTH_LONG).show(); - } - - @MainThread - private List getFacebookPermissions() { - List result = new ArrayList<>(); - if (mFacebookScopeFriends.isChecked()) { - result.add("user_friends"); - } - if (mFacebookScopePhotos.isChecked()) { - result.add("user_photos"); - } - return result; - } - - @MainThread - private List getGooglePermissions() { - List result = new ArrayList<>(); - if (mGoogleScopeGames.isChecked()) { - result.add(Scopes.GAMES); - } - if (mGoogleScopeDriveFile.isChecked()) { - result.add(Scopes.DRIVE_FILE); - } - return result; - } - - public static Intent createIntent(Context context) { - Intent in = new Intent(); - in.setClass(context, AuthUiActivity.class); - return in; - } -} diff --git a/app/src/main/java/com/firebase/uidemo/auth/LeakCatcher.java b/app/src/main/java/com/firebase/uidemo/auth/LeakCatcher.java deleted file mode 100644 index e0a3f3b063..0000000000 --- a/app/src/main/java/com/firebase/uidemo/auth/LeakCatcher.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.firebase.uidemo.auth; - -import android.app.Application; -import android.content.Context; - -import com.squareup.leakcanary.LeakCanary; -import com.squareup.leakcanary.RefWatcher; - -public class LeakCatcher extends Application { - private RefWatcher mRefWatcher; - - public static RefWatcher getRefWatcher(Context context) { - return ((LeakCatcher) context.getApplicationContext()).mRefWatcher; - } - - @Override - public void onCreate() { - super.onCreate(); - if (LeakCanary.isInAnalyzerProcess(this)) { - // This process is dedicated to LeakCanary for heap analysis. - // You should not init your app in this process. - return; - } - mRefWatcher = LeakCanary.install(this); - } -} diff --git a/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java b/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java deleted file mode 100644 index d6f000ecb9..0000000000 --- a/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * 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. - */ - -package com.firebase.uidemo.auth; - -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.support.annotation.MainThread; -import android.support.annotation.NonNull; -import android.support.annotation.StringRes; -import android.support.design.widget.Snackbar; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; -import android.text.TextUtils; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; -import com.bumptech.glide.Glide; -import com.firebase.ui.auth.AuthUI; -import com.firebase.ui.auth.IdpResponse; -import com.firebase.uidemo.R; -import com.google.android.gms.tasks.OnCompleteListener; -import com.google.android.gms.tasks.Task; -import com.google.firebase.auth.EmailAuthProvider; -import com.google.firebase.auth.FacebookAuthProvider; -import com.google.firebase.auth.FirebaseAuth; -import com.google.firebase.auth.FirebaseUser; -import com.google.firebase.auth.GoogleAuthProvider; -import java.util.Iterator; - -public class SignedInActivity extends AppCompatActivity { - private static final String EXTRA_IDP_RESPONSE = "extra_idp_response"; - - @BindView(android.R.id.content) - View mRootView; - - @BindView(R.id.user_profile_picture) - ImageView mUserProfilePicture; - - @BindView(R.id.user_email) - TextView mUserEmail; - - @BindView(R.id.user_display_name) - TextView mUserDisplayName; - - @BindView(R.id.user_enabled_providers) - TextView mEnabledProviders; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser(); - if (currentUser == null) { - startActivity(AuthUiActivity.createIntent(this)); - finish(); - return; - } - - setContentView(R.layout.signed_in_layout); - ButterKnife.bind(this); - populateProfile(); - populateIdpToken(); - } - - @OnClick(R.id.sign_out) - public void signOut() { - AuthUI.getInstance() - .signOut(this) - .addOnCompleteListener(new OnCompleteListener() { - @Override - public void onComplete(@NonNull Task task) { - if (task.isSuccessful()) { - startActivity(AuthUiActivity.createIntent(SignedInActivity.this)); - finish(); - } else { - showSnackbar(R.string.sign_out_failed); - } - } - }); - } - - @OnClick(R.id.delete_account) - public void deleteAccountClicked() { - - AlertDialog dialog = new AlertDialog.Builder(this) - .setMessage("Are you sure you want to delete this account?") - .setPositiveButton("Yes, nuke it!", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - deleteAccount(); - } - }) - .setNegativeButton("No", null) - .create(); - - dialog.show(); - } - - private void deleteAccount() { - AuthUI.getInstance() - .delete(this) - .addOnCompleteListener(new OnCompleteListener() { - @Override - public void onComplete(@NonNull Task task) { - if (task.isSuccessful()) { - startActivity(AuthUiActivity.createIntent(SignedInActivity.this)); - finish(); - } else { - showSnackbar(R.string.delete_account_failed); - } - } - }); - } - - @MainThread - private void populateProfile() { - FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser(); - if (user.getPhotoUrl() != null) { - Glide.with(this) - .load(user.getPhotoUrl()) - .fitCenter() - .into(mUserProfilePicture); - } - - mUserEmail.setText( - TextUtils.isEmpty(user.getEmail()) ? "No email" : user.getEmail()); - mUserDisplayName.setText( - TextUtils.isEmpty(user.getDisplayName()) ? "No display name" : user.getDisplayName()); - - StringBuilder providerList = new StringBuilder(); - - providerList.append("Providers used: "); - - if (user.getProviders() == null || user.getProviders().isEmpty()) { - providerList.append("none"); - } else { - Iterator providerIter = user.getProviders().iterator(); - while (providerIter.hasNext()) { - String provider = providerIter.next(); - if (GoogleAuthProvider.PROVIDER_ID.equals(provider)) { - providerList.append("Google"); - } else if (FacebookAuthProvider.PROVIDER_ID.equals(provider)) { - providerList.append("Facebook"); - } else if (EmailAuthProvider.PROVIDER_ID.equals(provider)) { - providerList.append("Password"); - } else { - providerList.append(provider); - } - - if (providerIter.hasNext()) { - providerList.append(", "); - } - } - } - - mEnabledProviders.setText(providerList); - } - - private void populateIdpToken() { - IdpResponse idpResponse = getIntent().getParcelableExtra(EXTRA_IDP_RESPONSE); - if (idpResponse != null) { - String token = idpResponse.getIdpToken(); - String secret = idpResponse.getIdpSecret(); - if (token == null) { - findViewById(R.id.idp_token_layout).setVisibility(View.GONE); - } else { - ((TextView) findViewById(R.id.idp_token)).setText(token); - } - if (secret == null) { - findViewById(R.id.idp_secret_layout).setVisibility(View.GONE); - } else { - ((TextView) findViewById(R.id.idp_secret)).setText(secret); - } - } - } - - @MainThread - private void showSnackbar(@StringRes int errorMessageRes) { - Snackbar.make(mRootView, errorMessageRes, Snackbar.LENGTH_LONG) - .show(); - } - - public static Intent createIntent(Context context, IdpResponse idpResponse) { - Intent in = new Intent(); - in.setClass(context, SignedInActivity.class); - in.putExtra(EXTRA_IDP_RESPONSE, idpResponse); - return in; - } -} diff --git a/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java b/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java deleted file mode 100644 index 9745a7e8a4..0000000000 --- a/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * 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. - */ - -package com.firebase.uidemo.database; - -import android.graphics.PorterDuff; -import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.RotateDrawable; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.Gravity; -import android.view.View; -import android.widget.Button; -import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.TextView; -import android.widget.Toast; - -import com.firebase.ui.database.FirebaseRecyclerAdapter; -import com.firebase.uidemo.R; -import com.google.android.gms.tasks.OnCompleteListener; -import com.google.android.gms.tasks.Task; -import com.google.firebase.auth.AuthResult; -import com.google.firebase.auth.FirebaseAuth; -import com.google.firebase.auth.FirebaseUser; -import com.google.firebase.database.DatabaseError; -import com.google.firebase.database.DatabaseReference; -import com.google.firebase.database.FirebaseDatabase; -import com.google.firebase.database.Query; - -public class ChatActivity extends AppCompatActivity implements FirebaseAuth.AuthStateListener { - - public static final String TAG = "RecyclerViewDemo"; - - private FirebaseAuth mAuth; - private DatabaseReference mRef; - private DatabaseReference mChatRef; - private Button mSendButton; - private EditText mMessageEdit; - - private RecyclerView mMessages; - private LinearLayoutManager mManager; - private FirebaseRecyclerAdapter mRecyclerViewAdapter; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_chat); - - mAuth = FirebaseAuth.getInstance(); - mAuth.addAuthStateListener(this); - - mSendButton = (Button) findViewById(R.id.sendButton); - mMessageEdit = (EditText) findViewById(R.id.messageEdit); - - mRef = FirebaseDatabase.getInstance().getReference(); - mChatRef = mRef.child("chats"); - - mSendButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - String uid = mAuth.getCurrentUser().getUid(); - String name = "User " + uid.substring(0, 6); - - Chat chat = new Chat(name, uid, mMessageEdit.getText().toString()); - mChatRef.push().setValue(chat, new DatabaseReference.CompletionListener() { - @Override - public void onComplete(DatabaseError databaseError, DatabaseReference reference) { - if (databaseError != null) { - Log.e(TAG, "Failed to write message", databaseError.toException()); - } - } - }); - - mMessageEdit.setText(""); - } - }); - - mMessages = (RecyclerView) findViewById(R.id.messagesList); - - mManager = new LinearLayoutManager(this); - mManager.setReverseLayout(false); - - mMessages.setHasFixedSize(false); - mMessages.setLayoutManager(mManager); - } - - @Override - public void onStart() { - super.onStart(); - - // Default Database rules do not allow unauthenticated reads, so we need to - // sign in before attaching the RecyclerView adapter otherwise the Adapter will - // not be able to read any data from the Database. - if (!isSignedIn()) { - signInAnonymously(); - } else { - attachRecyclerViewAdapter(); - } - } - - @Override - public void onStop() { - super.onStop(); - if (mRecyclerViewAdapter != null) { - mRecyclerViewAdapter.cleanup(); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (mAuth != null) { - mAuth.removeAuthStateListener(this); - } - } - - @Override - public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { - updateUI(); - } - - private void attachRecyclerViewAdapter() { - Query lastFifty = mChatRef.limitToLast(50); - mRecyclerViewAdapter = new FirebaseRecyclerAdapter( - Chat.class, R.layout.message, ChatHolder.class, lastFifty) { - - @Override - public void populateViewHolder(ChatHolder chatView, Chat chat, int position) { - chatView.setName(chat.getName()); - chatView.setText(chat.getText()); - - FirebaseUser currentUser = mAuth.getCurrentUser(); - if (currentUser != null && chat.getUid().equals(currentUser.getUid())) { - chatView.setIsSender(true); - } else { - chatView.setIsSender(false); - } - } - }; - - // Scroll to bottom on new messages - mRecyclerViewAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { - @Override - public void onItemRangeInserted(int positionStart, int itemCount) { - mManager.smoothScrollToPosition(mMessages, null, mRecyclerViewAdapter.getItemCount()); - } - }); - - mMessages.setAdapter(mRecyclerViewAdapter); - } - - private void signInAnonymously() { - Toast.makeText(this, "Signing in...", Toast.LENGTH_SHORT).show(); - mAuth.signInAnonymously() - .addOnCompleteListener(this, new OnCompleteListener() { - @Override - public void onComplete(@NonNull Task task) { - Log.d(TAG, "signInAnonymously:onComplete:" + task.isSuccessful()); - if (task.isSuccessful()) { - Toast.makeText(ChatActivity.this, "Signed In", - Toast.LENGTH_SHORT).show(); - attachRecyclerViewAdapter(); - } else { - Toast.makeText(ChatActivity.this, "Sign In Failed", - Toast.LENGTH_SHORT).show(); - } - } - }); - } - - public boolean isSignedIn() { - return (mAuth.getCurrentUser() != null); - } - - public void updateUI() { - // Sending only allowed when signed in - mSendButton.setEnabled(isSignedIn()); - mMessageEdit.setEnabled(isSignedIn()); - } - - public static class Chat { - - String name; - String text; - String uid; - - public Chat() { - } - - public Chat(String name, String uid, String message) { - this.name = name; - this.text = message; - this.uid = uid; - } - - public String getName() { - return name; - } - - public String getUid() { - return uid; - } - - public String getText() { - return text; - } - } - - public static class ChatHolder extends RecyclerView.ViewHolder { - private final TextView mNameField; - private final TextView mTextField; - private final FrameLayout mLeftArrow; - private final FrameLayout mRightArrow; - private final RelativeLayout mMessageContainer; - private final LinearLayout mMessage; - private final int mGreen300; - private final int mGray300; - - public ChatHolder(View itemView) { - super(itemView); - mNameField = (TextView) itemView.findViewById(R.id.name_text); - mTextField = (TextView) itemView.findViewById(R.id.message_text); - mLeftArrow = (FrameLayout) itemView.findViewById(R.id.left_arrow); - mRightArrow = (FrameLayout) itemView.findViewById(R.id.right_arrow); - mMessageContainer = (RelativeLayout) itemView.findViewById(R.id.message_container); - mMessage = (LinearLayout) itemView.findViewById(R.id.message); - mGreen300 = ContextCompat.getColor(itemView.getContext(), R.color.material_green_300); - mGray300 = ContextCompat.getColor(itemView.getContext(), R.color.material_gray_300); - } - - public void setIsSender(boolean isSender) { - final int color; - if (isSender) { - color = mGreen300; - mLeftArrow.setVisibility(View.GONE); - mRightArrow.setVisibility(View.VISIBLE); - mMessageContainer.setGravity(Gravity.END); - } else { - color = mGray300; - mLeftArrow.setVisibility(View.VISIBLE); - mRightArrow.setVisibility(View.GONE); - mMessageContainer.setGravity(Gravity.START); - } - - ((GradientDrawable) mMessage.getBackground()).setColor(color); - ((RotateDrawable) mLeftArrow.getBackground()).getDrawable() - .setColorFilter(color, PorterDuff.Mode.SRC); - ((RotateDrawable) mRightArrow.getBackground()).getDrawable() - .setColorFilter(color, PorterDuff.Mode.SRC); - } - - public void setName(String name) { - mNameField.setText(name); - } - - public void setText(String text) { - mTextField.setText(text); - } - } -} diff --git a/app/src/main/java/com/firebase/uidemo/storage/ImageActivity.java b/app/src/main/java/com/firebase/uidemo/storage/ImageActivity.java deleted file mode 100644 index 4d1471af8a..0000000000 --- a/app/src/main/java/com/firebase/uidemo/storage/ImageActivity.java +++ /dev/null @@ -1,167 +0,0 @@ -package com.firebase.uidemo.storage; - -import android.Manifest; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.provider.MediaStore; -import android.support.annotation.NonNull; -import android.support.v7.app.AppCompatActivity; -import android.util.Log; -import android.view.View; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.Toast; - -import com.bumptech.glide.Glide; -import com.firebase.ui.storage.images.FirebaseImageLoader; -import com.firebase.uidemo.R; -import com.google.android.gms.tasks.OnCompleteListener; -import com.google.android.gms.tasks.OnFailureListener; -import com.google.android.gms.tasks.OnSuccessListener; -import com.google.android.gms.tasks.Task; -import com.google.firebase.auth.AuthResult; -import com.google.firebase.auth.FirebaseAuth; -import com.google.firebase.storage.FirebaseStorage; -import com.google.firebase.storage.StorageReference; -import com.google.firebase.storage.UploadTask; - -import java.util.UUID; - -import butterknife.BindView; -import butterknife.ButterKnife; -import butterknife.OnClick; -import pub.devrel.easypermissions.AfterPermissionGranted; -import pub.devrel.easypermissions.EasyPermissions; - -public class ImageActivity extends AppCompatActivity { - - private static final String TAG = "ImageDemo"; - private static final int RC_CHOOSE_PHOTO = 101; - private static final int RC_IMAGE_PERMS = 102; - - private StorageReference mImageRef; - - @BindView(R.id.button_choose_photo) - Button mUploadButton; - - @BindView(R.id.button_download_direct) - Button mDownloadDirectButton; - - @BindView(R.id.first_image) - ImageView mImageView; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_image); - ButterKnife.bind(this); - - // By default, Firebase Storage files require authentication to read or write. - // For this sample to function correctly, enable Anonymous Auth in the Firebase console: - // https://console.firebase.google.com/project/_/authentication/providers - FirebaseAuth.getInstance().signInAnonymously() - .addOnCompleteListener(new OnCompleteListener() { - @Override - public void onComplete(@NonNull Task task) { - Log.d(TAG, "signInAnonymously:" + task.isSuccessful()); - if (!task.isSuccessful()) { - Log.w(TAG, "signInAnonymously", task.getException()); - Log.w(TAG, getString(R.string.anonymous_auth_failed_msg)); - - Toast.makeText(ImageActivity.this, - getString(R.string.anonymous_auth_failed_toast), - Toast.LENGTH_SHORT).show(); - } - } - }); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (requestCode == RC_CHOOSE_PHOTO) { - if (resultCode == RESULT_OK) { - Uri selectedImage = data.getData(); - uploadPhoto(selectedImage); - } else { - Toast.makeText(this, "No image chosen", Toast.LENGTH_SHORT).show(); - } - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this); - } - - @OnClick(R.id.button_choose_photo) - @AfterPermissionGranted(RC_IMAGE_PERMS) - protected void choosePhoto() { - String perm = Manifest.permission.READ_EXTERNAL_STORAGE; - if (!EasyPermissions.hasPermissions(this, perm)) { - EasyPermissions.requestPermissions(this, getString(R.string.rational_image_perm), - RC_IMAGE_PERMS, perm); - return; - } - - Intent i = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); - startActivityForResult(i, RC_CHOOSE_PHOTO); - } - - protected void uploadPhoto(Uri uri) { - // Reset UI - hideDownloadUI(); - Toast.makeText(this, "Uploading...", Toast.LENGTH_SHORT).show(); - - // Upload to Firebase Storage - String uuid = UUID.randomUUID().toString(); - mImageRef = FirebaseStorage.getInstance().getReference(uuid); - mImageRef.putFile(uri) - .addOnSuccessListener(this, new OnSuccessListener() { - @Override - public void onSuccess(UploadTask.TaskSnapshot taskSnapshot) { - Log.d(TAG, "uploadPhoto:onSuccess:" + - taskSnapshot.getMetadata().getReference().getPath()); - Toast.makeText(ImageActivity.this, "Image uploaded", - Toast.LENGTH_SHORT).show(); - - showDownloadUI(); - } - }) - .addOnFailureListener(this, new OnFailureListener() { - @Override - public void onFailure(@NonNull Exception e) { - Log.w(TAG, "uploadPhoto:onError", e); - Toast.makeText(ImageActivity.this, "Upload failed", - Toast.LENGTH_SHORT).show(); - } - }); - } - - @OnClick(R.id.button_download_direct) - protected void downloadDirect() { - // Download directly from StorageReference using Glide - Glide.with(this) - .using(new FirebaseImageLoader()) - .load(mImageRef) - .centerCrop() - .crossFade() - .into(mImageView); - } - - private void hideDownloadUI() { - mDownloadDirectButton.setEnabled(false); - - mImageView.setImageResource(0); - mImageView.setVisibility(View.INVISIBLE); - } - - private void showDownloadUI() { - mDownloadDirectButton.setEnabled(true); - - mImageView.setVisibility(View.VISIBLE); - } -} diff --git a/app/src/main/java/com/firebaseui/android/demo/MainActivity.kt b/app/src/main/java/com/firebaseui/android/demo/MainActivity.kt new file mode 100644 index 0000000000..131d11cb6b --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/MainActivity.kt @@ -0,0 +1,228 @@ +package com.firebaseui.android.demo + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.FirebaseAuthUI +import com.firebase.ui.auth.util.EmailLinkConstants +import com.firebaseui.android.demo.auth.AuthChooserActivity +import com.firebaseui.android.demo.auth.HighLevelApiDemoActivity +import com.firebaseui.android.demo.database.DatabaseDemoActivity +import com.firebaseui.android.demo.firestore.FirestoreDemoActivity +import com.firebaseui.android.demo.storage.StorageDemoActivity +import com.google.firebase.FirebaseApp +import com.google.firebase.database.FirebaseDatabase +import com.google.firebase.firestore.FirebaseFirestore + +class MainActivity : ComponentActivity() { + companion object { + internal const val USE_AUTH_EMULATOR = true + private const val AUTH_EMULATOR_HOST = "10.0.2.2" + private const val AUTH_EMULATOR_PORT = 9099 + + // 10.0.2.2 is the Android emulator's alias for the host machine's localhost. + private const val USE_FIRESTORE_EMULATOR = true + private const val FIRESTORE_EMULATOR_HOST = "10.0.2.2" + private const val FIRESTORE_EMULATOR_PORT = 8080 + + private const val USE_DATABASE_EMULATOR = true + private const val DATABASE_EMULATOR_HOST = "10.0.2.2" + private const val DATABASE_EMULATOR_PORT = 8199 + + // useEmulator() throws once the Firestore/Database client has been used elsewhere in + // the process, so this must only run once per process, not on every onCreate(). + private var emulatorsConfigured = false + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + FirebaseApp.initializeApp(applicationContext) + val authUI = FirebaseAuthUI.getInstance() + + if (!emulatorsConfigured) { + if (USE_AUTH_EMULATOR) { + authUI.auth.useEmulator(AUTH_EMULATOR_HOST, AUTH_EMULATOR_PORT) + } + + if (USE_FIRESTORE_EMULATOR) { + FirebaseFirestore.getInstance() + .useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT) + } + + if (USE_DATABASE_EMULATOR) { + FirebaseDatabase.getInstance() + .useEmulator(DATABASE_EMULATOR_HOST, DATABASE_EMULATOR_PORT) + } + emulatorsConfigured = true + } + + var pendingEmailLink = intent.getStringExtra(EmailLinkConstants.EXTRA_EMAIL_LINK) + if (pendingEmailLink.isNullOrEmpty() && authUI.canHandleIntent(intent)) { + pendingEmailLink = intent.data?.toString() + } + + Log.d("MainActivity", "Pending email link: $pendingEmailLink") + + fun launchHighLevelDemo() { + val demoIntent = Intent(this, HighLevelApiDemoActivity::class.java).apply { + pendingEmailLink?.let { link -> + putExtra(EmailLinkConstants.EXTRA_EMAIL_LINK, link) + pendingEmailLink = null + } + } + startActivity(demoIntent) + } + + if (savedInstanceState == null && !pendingEmailLink.isNullOrEmpty()) { + launchHighLevelDemo() + finish() + return + } + + setContent { + MaterialTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + ChooserScreen( + onAuthClick = { + startActivity(Intent(this, AuthChooserActivity::class.java)) + }, + onDatabaseClick = { + startActivity(Intent(this, DatabaseDemoActivity::class.java)) + }, + onFirestoreClick = { + startActivity(Intent(this, FirestoreDemoActivity::class.java)) + }, + onStorageClick = { + startActivity(Intent(this, StorageDemoActivity::class.java)) + } + ) + } + } + } + } +} + +@Composable +fun ChooserScreen( + onAuthClick: () -> Unit, + onDatabaseClick: () -> Unit, + onFirestoreClick: () -> Unit, + onStorageClick: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .systemBarsPadding() + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + Spacer(modifier = Modifier.height(8.dp)) + + Text("FirebaseUI Android", style = MaterialTheme.typography.headlineLarge) + Text( + "Choose a module to explore its demos", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Card(modifier = Modifier.fillMaxWidth(), onClick = onAuthClick) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "Auth", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = "High-Level API, Low-Level API, Custom Slots & Theming, Credential Linking", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Card(modifier = Modifier.fillMaxWidth(), onClick = onDatabaseClick) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "Database", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = "Paginated list with FirebaseRecyclerPagingAdapter and orderByChild", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Card(modifier = Modifier.fillMaxWidth(), onClick = onFirestoreClick) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "Firestore", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = "Paginated list with FirestorePagingAdapter and orderBy", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Card(modifier = Modifier.fillMaxWidth(), onClick = onStorageClick) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "Storage", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = "Loading images from Firebase Storage with Glide", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/AuthChooserActivity.kt b/app/src/main/java/com/firebaseui/android/demo/auth/AuthChooserActivity.kt new file mode 100644 index 0000000000..a418427daa --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/AuthChooserActivity.kt @@ -0,0 +1,285 @@ +package com.firebaseui.android.demo.auth + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.firebaseui.android.demo.MainActivity + +class AuthChooserActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + MaterialTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + AuthChooserScreen( + onHighLevelApiClick = { + startActivity(Intent(this, HighLevelApiDemoActivity::class.java)) + }, + onLowLevelApiClick = { + startActivity(Intent(this, AuthFlowControllerDemoActivity::class.java)) + }, + onCustomSlotsClick = { + startActivity(Intent(this, CustomSlotsThemingDemoActivity::class.java)) + }, + onCredentialLinkingClick = { + startActivity(Intent(this, CredentialLinkingDemoActivity::class.java)) + }, + isEmulatorMode = MainActivity.USE_AUTH_EMULATOR + ) + } + } + } + } +} + +@Composable +fun AuthChooserScreen( + onHighLevelApiClick: () -> Unit, + onLowLevelApiClick: () -> Unit, + onCustomSlotsClick: () -> Unit, + onCredentialLinkingClick: () -> Unit = {}, + isEmulatorMode: Boolean = false +) { + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .systemBarsPadding() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + Spacer(modifier = Modifier.height(16.dp)) + // Header + Text( + text = "Firebase Auth UI Compose", + style = MaterialTheme.typography.headlineLarge, + textAlign = TextAlign.Center + ) + + Text( + text = "Choose a demo to explore different authentication APIs", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + // Emulator Mode Warning + if (isEmulatorMode) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "⚠️ Emulator Mode", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onErrorContainer + ) + Text( + text = "Running with Firebase Auth Emulator. Some features like third-party" + + " OAuth providers (Facebook, Twitter, LINE etc.) may not work correctly." + + " Disable Firebase Auth Emulator using" + + " MainActivity.USE_AUTH_EMULATOR = false", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onErrorContainer + ) + } + } + } + + // High-Level API Card + Card( + modifier = Modifier.fillMaxWidth(), + onClick = onHighLevelApiClick + ) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "🎨 High-Level API", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = "FirebaseAuthScreen Composable", + style = MaterialTheme.typography.titleMedium + ) + Text( + text = "Best for: Pure Compose applications that want a complete, ready-to-use authentication UI with minimal setup.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Features:", + style = MaterialTheme.typography.labelLarge + ) + Text( + text = "• Drop-in Composable\n• Automatic navigation\n• State management included\n• Customizable content", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + // Low-Level API Card + Card( + modifier = Modifier.fillMaxWidth(), + onClick = onLowLevelApiClick + ) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "⚙️ Low-Level API", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = "AuthFlowController", + style = MaterialTheme.typography.titleMedium + ) + Text( + text = "Best for: Applications that need fine-grained control over the authentication flow with ActivityResultLauncher integration.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Features:", + style = MaterialTheme.typography.labelLarge + ) + Text( + text = "• Lifecycle-safe controller\n• ActivityResultLauncher\n• Observable state with Flow\n• Manual flow control", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + // Custom Slots & Theming Card + Card( + modifier = Modifier.fillMaxWidth(), + onClick = onCustomSlotsClick + ) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "🎨 Custom Slots & Theming", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = "Slot APIs & Theme Customization", + style = MaterialTheme.typography.titleMedium + ) + Text( + text = "Best for: Applications that need fully custom UI while leveraging the authentication logic and state management.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Features:", + style = MaterialTheme.typography.labelLarge + ) + Text( + text = "• Custom email auth UI via slots\n• Custom phone auth UI via slots\n• AuthUITheme.fromMaterialTheme()\n• Custom ProviderStyle examples", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + // Credential Linking Card + Card( + modifier = Modifier.fillMaxWidth(), + onClick = onCredentialLinkingClick + ) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "🔗 Credential Linking", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = "isCredentialLinkingEnabled", + style = MaterialTheme.typography.titleMedium + ) + Text( + text = "Sign in with one provider, then add another to the same account without losing your UID.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Info card + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "💡 Tip", + style = MaterialTheme.typography.labelLarge + ) + Text( + text = "Both APIs provide the same authentication capabilities. Choose based on your app's architecture and control requirements.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/AuthFlowControllerDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/auth/AuthFlowControllerDemoActivity.kt new file mode 100644 index 0000000000..7d82c1a186 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/AuthFlowControllerDemoActivity.kt @@ -0,0 +1,335 @@ +package com.firebaseui.android.demo.auth + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.AuthFlowController +import com.firebase.ui.auth.AuthState +import com.firebase.ui.auth.FirebaseAuthActivity +import com.firebase.ui.auth.FirebaseAuthUI +import com.firebase.ui.auth.configuration.AuthUIConfiguration +import com.firebase.ui.auth.configuration.PasswordRule +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.actionCodeSettings +import kotlinx.coroutines.launch + +/** + * Demo activity showcasing the AuthFlowController API for managing + * Firebase authentication with lifecycle-safe control. + * + * This demonstrates: + * - Creating an AuthFlowController with configuration + * - Starting the auth flow using ActivityResultLauncher + * - Observing auth state changes + * - Handling results (success, cancelled, error) + * - Proper lifecycle management with dispose() + */ +class AuthFlowControllerDemoActivity : ComponentActivity() { + + private lateinit var authController: AuthFlowController + + // Modern ActivityResultLauncher for auth flow + private val authLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + when (result.resultCode) { + Activity.RESULT_OK -> { + // Get user data from result + val userId = result.data?.getStringExtra(FirebaseAuthActivity.EXTRA_USER_ID) + val isNewUser = result.data?.getBooleanExtra( + FirebaseAuthActivity.EXTRA_IS_NEW_USER, + false + ) ?: false + + val user = FirebaseAuth.getInstance().currentUser + val message = if (isNewUser) { + "Welcome new user! ${user?.email ?: userId}" + } else { + "Welcome back! ${user?.email ?: userId}" + } + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + } + Activity.RESULT_CANCELED -> { + Toast.makeText(this, "Auth cancelled", Toast.LENGTH_SHORT).show() + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Initialize FirebaseAuthUI + val authUI = FirebaseAuthUI.getInstance() + + // Create auth configuration + val configuration = AuthUIConfiguration( + context = applicationContext, + providers = listOf( + AuthProvider.Email( + isDisplayNameRequired = true, + isEmailLinkForceSameDeviceEnabled = true, + isEmailLinkSignInEnabled = false, + emailLinkActionCodeSettings = actionCodeSettings { + url = "https://flutterfire-e2e-tests.firebaseapp.com" + handleCodeInApp = true + setAndroidPackageName( + "com.firebaseui.android.demo", + true, + null + ) + }, + isNewAccountsAllowed = true, + minimumPasswordLength = 8, + passwordValidationRules = listOf( + PasswordRule.MinimumLength(8), + PasswordRule.RequireLowercase, + PasswordRule.RequireUppercase, + ) + ), + AuthProvider.Phone( + defaultNumber = null, + defaultCountryCode = null, + allowedCountries = emptyList(), + smsCodeLength = 6, + timeout = 120L, + isInstantVerificationEnabled = true + ), + AuthProvider.Facebook() + ), + tosUrl = "https://policies.google.com/terms?hl=en-NG&fg=1", + privacyPolicyUrl = "https://policies.google.com/privacy?hl=en-NG&fg=1" + ) + + // Create AuthFlowController + authController = authUI.createAuthFlow(configuration) + + setContent { + MaterialTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + AuthFlowDemo( + authController = authController, + onStartAuth = { startAuthFlow() }, + onCancelAuth = { cancelAuthFlow() } + ) + } + } + } + } + + override fun onDestroy() { + super.onDestroy() + // Clean up resources + authController.dispose() + } + + private fun startAuthFlow() { + val intent = authController.createIntent(this) + authLauncher.launch(intent) + } + + private fun cancelAuthFlow() { + authController.cancel() + Toast.makeText(this, "Auth flow cancelled", Toast.LENGTH_SHORT).show() + } + + companion object { + fun createIntent(context: Context): Intent { + return Intent(context, AuthFlowControllerDemoActivity::class.java) + } + } +} + +@Composable +fun AuthFlowDemo( + authController: AuthFlowController, + onStartAuth: () -> Unit, + onCancelAuth: () -> Unit +) { + val authState by authController.authStateFlow.collectAsState(AuthState.Idle) + var currentUser by remember { mutableStateOf(FirebaseAuth.getInstance().currentUser) } + + // Observe Firebase auth state changes + DisposableEffect(Unit) { + val authStateListener = FirebaseAuth.AuthStateListener { auth -> + currentUser = auth.currentUser + } + FirebaseAuth.getInstance().addAuthStateListener(authStateListener) + + onDispose { + FirebaseAuth.getInstance().removeAuthStateListener(authStateListener) + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically) + ) { + Text( + text = "⚙️ Low-Level API Demo", + style = MaterialTheme.typography.headlineMedium, + textAlign = TextAlign.Center + ) + + Text( + text = "AuthFlowController with ActivityResultLauncher", + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.primary + ) + + Text( + text = "This demonstrates manual control over the authentication flow with lifecycle-safe management.", + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Current Auth State Card + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "Current State:", + style = MaterialTheme.typography.labelLarge + ) + Text( + text = when (authState) { + is AuthState.Idle -> "Idle" + is AuthState.Loading -> "Loading: ${(authState as AuthState.Loading).message}" + is AuthState.Success -> "Success - User: ${(authState as AuthState.Success).user.email}" + is AuthState.Error -> "Error: ${(authState as AuthState.Error).exception.message}" + is AuthState.Cancelled -> "Cancelled" + is AuthState.RequiresMfa -> "MFA Required" + is AuthState.RequiresEmailVerification -> "Email Verification Required" + else -> "Unknown" + }, + style = MaterialTheme.typography.bodyMedium, + color = when (authState) { + is AuthState.Success -> MaterialTheme.colorScheme.primary + is AuthState.Error -> MaterialTheme.colorScheme.error + is AuthState.Loading -> MaterialTheme.colorScheme.tertiary + else -> MaterialTheme.colorScheme.onSurfaceVariant + } + ) + } + } + + // Current User Card + currentUser?.let { user -> + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "Signed In User:", + style = MaterialTheme.typography.labelLarge + ) + Text( + text = "Email: ${user.email ?: "N/A"}", + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = "UID: ${user.uid}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Action Buttons + if (currentUser == null) { + Button( + onClick = onStartAuth, + modifier = Modifier.fillMaxWidth() + ) { + Text("Start Auth Flow") + } + + if (authState is AuthState.Loading) { + OutlinedButton( + onClick = onCancelAuth, + modifier = Modifier.fillMaxWidth() + ) { + Text("Cancel Auth Flow") + } + } + } else { + Button( + onClick = { + FirebaseAuth.getInstance().signOut() + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Sign Out") + } + } + + // Info Card + Card( + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "Features:", + style = MaterialTheme.typography.labelLarge + ) + Text( + text = "• Lifecycle-safe auth flow management", + style = MaterialTheme.typography.bodySmall + ) + Text( + text = "• Observable auth state with Flow", + style = MaterialTheme.typography.bodySmall + ) + Text( + text = "• Modern ActivityResultLauncher API", + style = MaterialTheme.typography.bodySmall + ) + Text( + text = "• Automatic resource cleanup", + style = MaterialTheme.typography.bodySmall + ) + } + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/CredentialLinkingDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/auth/CredentialLinkingDemoActivity.kt new file mode 100644 index 0000000000..afced8665b --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/CredentialLinkingDemoActivity.kt @@ -0,0 +1,186 @@ +package com.firebaseui.android.demo.auth + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.AuthException +import com.firebase.ui.auth.AuthState +import com.firebase.ui.auth.FirebaseAuthUI +import com.firebase.ui.auth.configuration.authUIConfiguration +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.ui.screens.AuthRoute +import com.firebase.ui.auth.ui.screens.AuthSuccessUiContext +import com.firebase.ui.auth.ui.screens.FirebaseAuthScreen + +class CredentialLinkingDemoActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val authUI = FirebaseAuthUI.getInstance() + + val configuration = authUIConfiguration { + context = applicationContext + isCredentialLinkingEnabled = true + providers { + provider( + AuthProvider.Email( + isNewAccountsAllowed = true, + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList(), + ) + ) + provider( + AuthProvider.Google( + scopes = listOf("email"), + serverClientId = "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + ) + ) + provider( + AuthProvider.Phone( + defaultNumber = null, + defaultCountryCode = null, + allowedCountries = emptyList(), + timeout = 120L, + ) + ) + } + } + + setContent { + MaterialTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + FirebaseAuthScreen( + configuration = configuration, + authUI = authUI, + onSignInSuccess = {}, + onSignInFailure = { _: AuthException -> }, + onSignInCancelled = {}, + authenticatedContent = { state, uiContext -> + CredentialLinkingAuthenticatedContent(state, uiContext) + } + ) + } + } + } + } +} + +@Composable +private fun CredentialLinkingAuthenticatedContent( + state: AuthState, + uiContext: AuthSuccessUiContext, +) { + when (state) { + is AuthState.Success -> { + val user = state.user + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Signed in", + style = MaterialTheme.typography.headlineSmall, + ) + Spacer(modifier = Modifier.height(16.dp)) + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text("UID: ${user.uid}", style = MaterialTheme.typography.bodySmall) + Text("Email: ${user.email ?: "—"}") + Text("Phone: ${user.phoneNumber ?: "—"}") + Text( + "Providers: ${user.providerData.map { it.providerId }}", + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Start + ) + } + } + Spacer(modifier = Modifier.height(24.dp)) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { uiContext.onNavigate(AuthRoute.MethodPicker) } + ) { + Text("Add sign-in method") + } + Spacer(modifier = Modifier.height(8.dp)) + OutlinedButton( + modifier = Modifier.fillMaxWidth(), + onClick = uiContext.onSignOut + ) { + Text("Sign out") + } + } + } + + is AuthState.RequiresEmailVerification -> { + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Verify your email", + style = MaterialTheme.typography.headlineSmall, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "A verification link was sent to ${state.email}. Once verified, tap the button below.", + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(24.dp)) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = uiContext.onReloadUser + ) { + Text("I've verified my email") + } + Spacer(modifier = Modifier.height(8.dp)) + OutlinedButton( + modifier = Modifier.fillMaxWidth(), + onClick = uiContext.onSignOut + ) { + Text("Sign out") + } + } + } + + else -> {} + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/CustomMethodPickerDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/auth/CustomMethodPickerDemoActivity.kt new file mode 100644 index 0000000000..5228927d9f --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/CustomMethodPickerDemoActivity.kt @@ -0,0 +1,360 @@ +package com.firebaseui.android.demo.auth + +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.AuthException +import com.firebase.ui.auth.FirebaseAuthUI +import com.firebase.ui.auth.configuration.authUIConfiguration +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvider +import com.firebase.ui.auth.configuration.theme.AuthUIAsset +import com.firebase.ui.auth.configuration.theme.AuthUITheme +import com.firebase.ui.auth.configuration.theme.ProviderStyleDefaults +import com.firebase.ui.auth.ui.components.AuthProviderButton +import com.firebase.ui.auth.ui.screens.FirebaseAuthScreen +import com.firebaseui.android.demo.R + +class CustomMethodPickerDemoActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val authUI = FirebaseAuthUI.getInstance() + + val configuration = authUIConfiguration { + context = applicationContext + logo = AuthUIAsset.Resource(R.drawable.firebase_auth) + tosUrl = "https://policies.google.com/terms" + privacyPolicyUrl = "https://policies.google.com/privacy" + providers { + provider( + AuthProvider.Google( + scopes = listOf("email"), + serverClientId = "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + ) + ) + provider(AuthProvider.Apple(customParameters = emptyMap(), locale = null)) + provider(AuthProvider.Facebook()) + provider(AuthProvider.Twitter(customParameters = emptyMap())) + provider(AuthProvider.Github(customParameters = emptyMap())) + provider(AuthProvider.Microsoft(tenant = null, customParameters = emptyMap())) + provider(AuthProvider.Yahoo(customParameters = emptyMap())) + provider( + AuthProvider.GenericOAuth( + providerName = "Discord", + providerId = "oidc.discord", + scopes = emptyList(), + customParameters = emptyMap(), + buttonLabel = "Sign in with Discord", + buttonIcon = AuthUIAsset.Resource(R.drawable.ic_discord_24dp), + buttonColor = Color(0xFF5865F2), + contentColor = Color.White + ) + ) + provider( + AuthProvider.GenericOAuth( + providerName = "LINE", + providerId = "oidc.line", + scopes = emptyList(), + customParameters = emptyMap(), + buttonLabel = "Sign in with LINE", + buttonIcon = AuthUIAsset.Resource(R.drawable.ic_line_logo_24dp), + buttonColor = Color(0xFF06C755), + contentColor = Color.White + ) + ) + provider( + AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + ) + provider( + AuthProvider.Phone( + defaultNumber = null, + defaultCountryCode = null, + allowedCountries = null + ) + ) + provider(AuthProvider.Anonymous) + } + } + + setContent { + AuthUITheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + var termsAccepted by remember { mutableStateOf(false) } + + FirebaseAuthScreen( + configuration = configuration, + authUI = authUI, + onSignInSuccess = { result -> + Log.d("CustomMethodPickerDemo", "Auth success: ${result.user?.uid}") + }, + onSignInFailure = { exception: AuthException -> + Log.e("CustomMethodPickerDemo", "Auth failed", exception) + }, + onSignInCancelled = { + Log.d("CustomMethodPickerDemo", "Auth cancelled") + }, + customMethodPickerLayout = { providers, onProviderSelected -> + SpotlightMethodPicker( + providers = providers, + onProviderSelected = onProviderSelected, + enabled = termsAccepted, + termsAccepted = termsAccepted, + onTermsAcceptedChange = { termsAccepted = it } + ) + }, + ) + } + } + } + } +} + +@Composable +fun SpotlightMethodPicker( + providers: List, + onProviderSelected: (AuthProvider) -> Unit, + enabled: Boolean = true, + termsAccepted: Boolean = true, + onTermsAcceptedChange: (Boolean) -> Unit = {}, +) { + val stringProvider = LocalAuthUIStringProvider.current + + val groups = providers.groupBy { + when (it) { + is AuthProvider.Google, is AuthProvider.Apple -> "featured" + is AuthProvider.Email, is AuthProvider.Phone -> "credential" + is AuthProvider.Anonymous -> "anonymous" + else -> "social" + } + } + val featured = groups.getOrElse("featured") { emptyList() } + val social = groups.getOrElse("social") { emptyList() } + val credential = groups.getOrElse("credential") { emptyList() } + val anonymous = groups["anonymous"]?.firstOrNull() + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding(), + contentPadding = PaddingValues(vertical = 48.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + item { + Text( + text = "Sign in", + style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold), + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 32.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Choose how you'd like to continue", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 32.dp) + ) + Spacer(modifier = Modifier.height(24.dp)) + } + + items(featured) { provider -> + AuthProviderButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + provider = provider, + onClick = { onProviderSelected(provider) }, + enabled = enabled, + stringProvider = stringProvider + ) + } + + if (social.isNotEmpty()) { + item { + Spacer(modifier = Modifier.height(4.dp)) + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.padding(horizontal = 32.dp) + ) + Spacer(modifier = Modifier.height(4.dp)) + } + item { + LazyRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(horizontal = 16.dp) + ) { + items(social) { provider -> + val style = styleForProvider(provider) + ProviderIconButton( + style = style, + contentDescription = provider.providerId, + enabled = enabled, + onClick = { onProviderSelected(provider) } + ) + } + } + } + } + + if (credential.isNotEmpty()) { + item { + Spacer(modifier = Modifier.height(4.dp)) + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.padding(horizontal = 32.dp) + ) + Spacer(modifier = Modifier.height(4.dp)) + } + items(credential) { provider -> + AuthProviderButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + provider = provider, + onClick = { onProviderSelected(provider) }, + enabled = enabled, + stringProvider = stringProvider + ) + } + } + + anonymous?.let { + item { + Spacer(modifier = Modifier.height(8.dp)) + TextButton(onClick = { onProviderSelected(it) }, enabled = enabled) { + Text("Continue as guest") + } + } + } + + item { + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = termsAccepted, + onCheckedChange = onTermsAcceptedChange + ) + Text( + text = "I have read and accept the Terms of Service and Privacy Policy", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + } +} + +@Composable +private fun ProviderIconButton( + style: AuthUITheme.ProviderStyle, + contentDescription: String, + onClick: () -> Unit, + enabled: Boolean = true, +) { + Button( + onClick = onClick, + enabled = enabled, + modifier = Modifier.size(52.dp), + shape = CircleShape, + colors = ButtonDefaults.buttonColors(containerColor = style.backgroundColor), + contentPadding = PaddingValues(0.dp), + elevation = ButtonDefaults.buttonElevation(defaultElevation = style.elevation) + ) { + style.icon?.let { asset -> + val painter = asset.asPainter() + val tint = style.iconTint + if (tint != null) { + Icon( + painter = painter, + contentDescription = contentDescription, + tint = tint, + modifier = Modifier.size(22.dp) + ) + } else { + Image( + painter = painter, + contentDescription = contentDescription, + modifier = Modifier.size(22.dp) + ) + } + } + } +} + +@Composable +private fun AuthUIAsset.asPainter(): Painter = when (this) { + is AuthUIAsset.Resource -> painterResource(resId) + is AuthUIAsset.Vector -> rememberVectorPainter(image) +} + +private fun styleForProvider(provider: AuthProvider): AuthUITheme.ProviderStyle = when (provider) { + is AuthProvider.Facebook -> ProviderStyleDefaults.Facebook + is AuthProvider.Twitter -> ProviderStyleDefaults.Twitter + is AuthProvider.Github -> ProviderStyleDefaults.Github + is AuthProvider.Microsoft -> ProviderStyleDefaults.Microsoft + is AuthProvider.Yahoo -> ProviderStyleDefaults.Yahoo + is AuthProvider.GenericOAuth -> AuthUITheme.ProviderStyle( + icon = provider.buttonIcon, + backgroundColor = provider.buttonColor ?: Color(0xFF666666), + contentColor = provider.contentColor ?: Color.White + ) + + else -> AuthUITheme.ProviderStyle( + icon = null, + backgroundColor = Color(0xFF666666), + contentColor = Color.White + ) +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/CustomSlotsThemingDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/auth/CustomSlotsThemingDemoActivity.kt new file mode 100644 index 0000000000..df069091f9 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/CustomSlotsThemingDemoActivity.kt @@ -0,0 +1,134 @@ +package com.firebaseui.android.demo.auth + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +class CustomSlotsThemingDemoActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + setContent { + MaterialTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + CustomSlotsDemoChooser( + onEmailAuthSlotClick = { + startActivity(Intent(this, EmailAuthSlotDemoActivity::class.java)) + }, + onPhoneAuthSlotClick = { + startActivity(Intent(this, PhoneAuthSlotDemoActivity::class.java)) + }, + onShapeCustomizationClick = { + startActivity(Intent(this, ShapeCustomizationDemoActivity::class.java)) + }, + onCustomMethodPickerClick = { + startActivity(Intent(this, CustomMethodPickerDemoActivity::class.java)) + } + ) + } + } + } + } +} + +@Composable +fun CustomSlotsDemoChooser( + onEmailAuthSlotClick: () -> Unit, + onPhoneAuthSlotClick: () -> Unit, + onShapeCustomizationClick: () -> Unit, + onCustomMethodPickerClick: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .systemBarsPadding() + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Custom Slots & Theming", + style = MaterialTheme.typography.headlineMedium + ) + Text( + text = "Select a demo to explore slot APIs and theme customization", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(8.dp)) + + DemoCard( + title = "Email Auth — Custom Slot", + description = "Replace the default email sign-in UI with a fully custom composable using the content slot.", + onClick = onEmailAuthSlotClick + ) + + DemoCard( + title = "Phone Auth — Custom Slot", + description = "Replace the default phone auth UI with a fully custom composable using the content slot.", + onClick = onPhoneAuthSlotClick + ) + + DemoCard( + title = "Shape Customization", + description = "Preview provider button shapes using global and per-provider overrides via AuthUITheme.", + onClick = onShapeCustomizationClick + ) + + DemoCard( + title = "Custom Method Picker Layout & Terms", + description = "Replace the default provider list with a custom layout, and swap the 'By continuing...' footer with a checkbox using customMethodPickerLayout and customMethodPickerTermsConfiguration on FirebaseAuthScreen.", + onClick = onCustomMethodPickerClick + ) + } +} + +@Composable +private fun DemoCard( + title: String, + description: String, + onClick: () -> Unit, +) { + Card( + modifier = Modifier.fillMaxWidth(), + onClick = onClick + ) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(text = title, style = MaterialTheme.typography.titleMedium) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/EmailAuthSlotDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/auth/EmailAuthSlotDemoActivity.kt new file mode 100644 index 0000000000..5c5e6e13c0 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/EmailAuthSlotDemoActivity.kt @@ -0,0 +1,437 @@ +package com.firebaseui.android.demo.auth + +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.AuthException +import com.firebase.ui.auth.FirebaseAuthUI +import com.firebase.ui.auth.configuration.AuthUIConfiguration +import com.firebase.ui.auth.configuration.PasswordRule +import com.firebase.ui.auth.configuration.authUIConfiguration +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvider +import com.firebase.ui.auth.configuration.theme.AuthUITheme +import com.firebase.ui.auth.ui.screens.email.EmailAuthContentState +import com.firebase.ui.auth.ui.screens.email.EmailAuthMode +import com.firebase.ui.auth.ui.screens.email.EmailAuthScreen +import com.google.firebase.auth.AuthResult + +class EmailAuthSlotDemoActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val authUI = FirebaseAuthUI.getInstance() + val appContext = applicationContext + + val configuration = authUIConfiguration { + context = appContext + providers { + provider( + AuthProvider.Email( + isDisplayNameRequired = true, + isNewAccountsAllowed = true, + isEmailLinkSignInEnabled = false, + emailLinkActionCodeSettings = null, + isEmailLinkForceSameDeviceEnabled = false, + minimumPasswordLength = 8, + passwordValidationRules = listOf( + PasswordRule.MinimumLength(8), + PasswordRule.RequireLowercase, + PasswordRule.RequireUppercase, + PasswordRule.RequireDigit + ) + ) + ) + } + tosUrl = "https://policies.google.com/terms" + privacyPolicyUrl = "https://policies.google.com/privacy" + } + + setContent { + CustomAuthUITheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + Column( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + ) { + EmailAuthDemo( + authUI = authUI, + configuration = configuration, + context = appContext + ) + } + } + } + } + } +} + +@Composable +fun CustomAuthUITheme(content: @Composable () -> Unit) { + MaterialTheme { + val authTheme = AuthUITheme.fromMaterialTheme( + providerButtonShape = RoundedCornerShape(12.dp) + ) + AuthUITheme(theme = authTheme) { + content() + } + } +} + +@Composable +fun EmailAuthDemo( + authUI: FirebaseAuthUI, + configuration: AuthUIConfiguration, + context: android.content.Context +) { + var currentUser by remember { mutableStateOf(authUI.getCurrentUser()) } + + LaunchedEffect(Unit) { + authUI.authStateFlow().collect { _ -> + currentUser = authUI.getCurrentUser() + } + } + + if (currentUser != null) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Successfully Authenticated!", + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = currentUser?.email ?: "Signed in", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(32.dp)) + Button(onClick = { authUI.auth.signOut() }) { + Text("Sign Out") + } + } + } else { + CompositionLocalProvider(LocalAuthUIStringProvider provides configuration.stringProvider) { + EmailAuthScreen( + context = context, + configuration = configuration, + authUI = authUI, + onSuccess = { result: AuthResult -> + Log.d("EmailAuthSlotDemo", "Auth success: ${result.user?.uid}") + }, + onError = { exception: AuthException -> + Log.e("EmailAuthSlotDemo", "Auth error", exception) + }, + onCancel = { + Log.d("EmailAuthSlotDemo", "Auth cancelled") + } + ) { state: EmailAuthContentState -> + CustomEmailAuthUI(state) + } + } + } +} + +@Composable +fun CustomEmailAuthUI(state: EmailAuthContentState) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = when (state.mode) { + EmailAuthMode.SignIn, EmailAuthMode.EmailLinkSignIn -> "Welcome Back" + EmailAuthMode.SignUp -> "Create Account" + EmailAuthMode.ResetPassword -> "Reset Password" + }, + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(8.dp)) + + state.error?.let { errorMessage -> + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ), + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = errorMessage, + modifier = Modifier.padding(12.dp), + color = MaterialTheme.colorScheme.onErrorContainer, + style = MaterialTheme.typography.bodySmall + ) + } + } + + when (state.mode) { + EmailAuthMode.SignIn, EmailAuthMode.EmailLinkSignIn -> SignInUI(state) + EmailAuthMode.SignUp -> SignUpUI(state) + EmailAuthMode.ResetPassword -> ResetPasswordUI(state) + } + } +} + +@Composable +fun SignInUI(state: EmailAuthContentState) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + OutlinedTextField( + value = state.email, + onValueChange = state.onEmailChange, + label = { Text("Email") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !state.isLoading + ) + + OutlinedTextField( + value = state.password, + onValueChange = state.onPasswordChange, + label = { Text("Password") }, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !state.isLoading + ) + + if (state.emailSignInLinkSent) { + Text( + text = "Sign-in link sent! Check your email.", + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.fillMaxWidth() + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = state.onSignInClick, + modifier = Modifier.fillMaxWidth(), + enabled = !state.isLoading + ) { + if (state.isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = MaterialTheme.colorScheme.onPrimary + ) + } else { + Text("Sign In") + } + } + + TextButton( + onClick = state.onGoToResetPassword, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Text("Forgot Password?") + } + + HorizontalDivider() + + TextButton( + onClick = state.onGoToSignUp, + modifier = Modifier.fillMaxWidth() + ) { + Text("Don't have an account? Sign Up") + } + } +} + +@Composable +fun SignUpUI(state: EmailAuthContentState) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + OutlinedTextField( + value = state.displayName, + onValueChange = state.onDisplayNameChange, + label = { Text("Display Name") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !state.isLoading + ) + + OutlinedTextField( + value = state.email, + onValueChange = state.onEmailChange, + label = { Text("Email") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !state.isLoading + ) + + OutlinedTextField( + value = state.password, + onValueChange = state.onPasswordChange, + label = { Text("Password") }, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !state.isLoading + ) + + OutlinedTextField( + value = state.confirmPassword, + onValueChange = state.onConfirmPasswordChange, + label = { Text("Confirm Password") }, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !state.isLoading + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = state.onSignUpClick, + modifier = Modifier.fillMaxWidth(), + enabled = !state.isLoading + ) { + if (state.isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = MaterialTheme.colorScheme.onPrimary + ) + } else { + Text("Create Account") + } + } + + HorizontalDivider() + + TextButton( + onClick = state.onGoToSignIn, + modifier = Modifier.fillMaxWidth() + ) { + Text("Already have an account? Sign In") + } + } +} + +@Composable +fun ResetPasswordUI(state: EmailAuthContentState) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "Enter your email address and we'll send you a link to reset your password.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = state.email, + onValueChange = state.onEmailChange, + label = { Text("Email") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !state.isLoading + ) + + if (state.resetLinkSent) { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ), + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Password reset link sent! Check your email.", + modifier = Modifier.padding(12.dp), + color = MaterialTheme.colorScheme.onPrimaryContainer, + style = MaterialTheme.typography.bodyMedium + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = state.onSendResetLinkClick, + modifier = Modifier.fillMaxWidth(), + enabled = !state.isLoading && !state.resetLinkSent + ) { + if (state.isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = MaterialTheme.colorScheme.onPrimary + ) + } else { + Text("Send Reset Link") + } + } + + HorizontalDivider() + + TextButton( + onClick = state.onGoToSignIn, + modifier = Modifier.fillMaxWidth() + ) { + Text("Back to Sign In") + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/HighLevelApiDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/auth/HighLevelApiDemoActivity.kt new file mode 100644 index 0000000000..cfa10b93b2 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/HighLevelApiDemoActivity.kt @@ -0,0 +1,611 @@ +package com.firebaseui.android.demo.auth + +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PlainTooltip +import androidx.compose.material3.ShapeDefaults +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TooltipAnchorPosition +import androidx.compose.material3.TooltipBox +import androidx.compose.material3.TooltipDefaults +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTooltipState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await +import com.firebase.ui.auth.AuthException +import com.firebase.ui.auth.AuthState +import com.firebase.ui.auth.FirebaseAuthUI +import com.firebase.ui.auth.configuration.AuthUITransitions +import com.firebase.ui.auth.configuration.PasswordRule +import com.firebase.ui.auth.configuration.authUIConfiguration +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProviderSample.CustomAuthUIStringProvider +import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.configuration.theme.AuthUIAsset +import com.firebase.ui.auth.configuration.theme.AuthUITheme +import com.firebase.ui.auth.ui.screens.AuthSuccessUiContext +import com.firebase.ui.auth.ui.screens.FirebaseAuthScreen +import com.firebase.ui.auth.util.EmailLinkConstants +import com.firebase.ui.auth.util.displayIdentifier +import com.firebase.ui.auth.util.getDisplayEmail +import com.firebaseui.android.demo.R +import com.google.firebase.auth.actionCodeSettings + +class HighLevelApiDemoActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val authUI = FirebaseAuthUI.getInstance() + val emailLink = intent.getStringExtra(EmailLinkConstants.EXTRA_EMAIL_LINK) + + class CustomAuthUIStringProvider( + private val defaultProvider: AuthUIStringProvider + ) : AuthUIStringProvider by defaultProvider { + + override val loadingSigningInAnonymously: String + get() = "Overriding signing in anonymously loading message..." + } + + val customStringProvider = + CustomAuthUIStringProvider(DefaultAuthUIStringProvider(applicationContext)) + + setContent { + val customTheme = AuthUITheme.Adaptive.copy( + providerButtonShape = ShapeDefaults.ExtraLarge, + topAppBarColors = TopAppBarDefaults.topAppBarColors( + containerColor = Color(0xFFFFA000), + scrolledContainerColor = Color(0xFFFFA000), + ) + ) + + val configuration = authUIConfiguration { + context = applicationContext + theme = customTheme + logo = AuthUIAsset.Resource(R.drawable.firebase_auth) + tosUrl = "https://policies.google.com/terms" + privacyPolicyUrl = "https://policies.google.com/privacy" + isAnonymousUpgradeEnabled = false + isMfaEnabled = false + stringProvider = customStringProvider + transitions = AuthUITransitions( + enterTransition = { slideInHorizontally { it } }, + exitTransition = { slideOutHorizontally { -it } }, + popEnterTransition = { slideInHorizontally { -it } }, + popExitTransition = { slideOutHorizontally { it } } + ) + providers { + provider(AuthProvider.Anonymous) + provider( + AuthProvider.Google( + scopes = listOf("email"), + serverClientId = "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + ) + ) + provider( + AuthProvider.Email( + isDisplayNameRequired = true, + isEmailLinkForceSameDeviceEnabled = false, + isEmailLinkSignInEnabled = true, + emailLinkActionCodeSettings = actionCodeSettings { + url = "https://flutterfire-e2e-tests.firebaseapp.com" + handleCodeInApp = true + setAndroidPackageName( + "com.firebaseui.android.demo", + true, + null + ) + }, + isNewAccountsAllowed = true, + minimumPasswordLength = 8, + passwordValidationRules = listOf( + PasswordRule.MinimumLength(8), + PasswordRule.RequireLowercase, + PasswordRule.RequireUppercase, + ), + ) + ) + provider( + AuthProvider.Phone( + defaultNumber = null, + defaultCountryCode = null, + allowedCountries = emptyList(), + smsCodeLength = 6, + timeout = 120L, + isInstantVerificationEnabled = true + ) + ) + provider( + AuthProvider.Facebook() + ) + provider( + AuthProvider.Twitter( + customParameters = emptyMap() + ) + ) + provider( + AuthProvider.Apple( + customParameters = emptyMap(), + locale = null + ) + ) + provider( + AuthProvider.Microsoft( + scopes = emptyList(), + tenant = "", + customParameters = emptyMap(), + ) + ) + provider( + AuthProvider.Github( + scopes = emptyList(), + customParameters = emptyMap(), + ) + ) + provider( + AuthProvider.Yahoo( + scopes = emptyList(), + customParameters = emptyMap(), + ) + ) + provider( + AuthProvider.GenericOAuth( + providerName = "LINE", + providerId = "oidc.line", + scopes = emptyList(), + customParameters = emptyMap(), + buttonLabel = "Sign in with LINE", + buttonIcon = AuthUIAsset.Resource(R.drawable.ic_line_logo_24dp), + buttonColor = Color(0xFF06C755), + contentColor = Color.White + ) + ) + provider( + AuthProvider.GenericOAuth( + providerName = "Discord", + providerId = "oidc.discord", + scopes = emptyList(), + customParameters = emptyMap(), + buttonLabel = "Sign in with Discord", + buttonIcon = AuthUIAsset.Resource(R.drawable.ic_discord_24dp), + buttonColor = Color(0xFF5865F2), + contentColor = Color.White + ) + ) + } + } + + AuthUITheme(theme = customTheme) { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + FirebaseAuthScreen( + configuration = configuration, + authUI = authUI, + emailLink = emailLink, + onSignInSuccess = { result -> + Log.d( + "HighLevelApiDemoActivity", + "Authentication success: ${result.user?.uid}" + ) + }, + onSignInFailure = { exception: AuthException -> + Log.e("HighLevelApiDemoActivity", "Authentication failed", exception) + }, + onSignInCancelled = { + Log.d("HighLevelApiDemoActivity", "Authentication cancelled") + }, + reauthContent = { state, onDismiss -> + ReauthDialog( + authUI = authUI, + state = state, + onDismiss = onDismiss, + ) + }, + authenticatedContent = { state, uiContext -> + AppAuthenticatedContent(state, uiContext) + } + ) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun AppAuthenticatedContent( + state: AuthState, + uiContext: AuthSuccessUiContext +) { + val stringProvider = uiContext.stringProvider + val configuration = uiContext.configuration + when (state) { + is AuthState.Success -> { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + var isDeletingAccount by remember { mutableStateOf(false) } + val user = uiContext.authUI.getCurrentUser() + val identifier = user.displayIdentifier() + var showChangePasswordDialog by remember { mutableStateOf(false) } + + if (showChangePasswordDialog) { + ChangePasswordDialog( + authUI = uiContext.authUI, + configuration = uiContext.configuration, + stringProvider = uiContext.stringProvider, + context = context, + lifecycleOwner = lifecycleOwner, + onDismiss = { showChangePasswordDialog = false }, + ) + } + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + if (identifier.isNotBlank()) { + Text( + text = stringProvider.signedInAs(identifier), + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(16.dp)) + } + Text( + "isAnonymous - ${state.user.isAnonymous}", + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(16.dp)) + Text( + "Providers - ${state.user.providerData.map { it.providerId }}", + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(16.dp)) + TooltipBox( + positionProvider = TooltipDefaults.rememberTooltipPositionProvider( + TooltipAnchorPosition.Above + ), + tooltip = { + PlainTooltip { + Text(stringProvider.mfaDisabledTooltip) + } + }, + state = rememberTooltipState( + initialIsVisible = false + ) + ) { + Button( + onClick = uiContext.onManageMfa, + enabled = configuration.isMfaEnabled + ) { + Text(stringProvider.manageMfaAction) + } + } + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = uiContext.onSignOut) { + Text(stringProvider.signOutAction) + } + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = { showChangePasswordDialog = true }) { + Text("Change password (withReauth)") + } + Spacer(modifier = Modifier.height(8.dp)) + Button( + onClick = { + lifecycleOwner.lifecycleScope.launch { + isDeletingAccount = true + try { + uiContext.authUI.delete(context) + } catch (e: AuthException.InvalidCredentialsException) { + // ReauthenticationRequired state was emitted — + // FirebaseAuthScreen navigates to the reauth flow automatically. + Log.d("HighLevelApiDemoActivity", "Reauth required before delete") + } catch (e: AuthException) { + Log.e("HighLevelApiDemoActivity", "Delete failed", e) + } finally { + isDeletingAccount = false + } + } + }, + enabled = !isDeletingAccount + ) { + if (isDeletingAccount) CircularProgressIndicator() else Text("Delete account") + } + } + } + + is AuthState.RequiresEmailVerification -> { + val email = + uiContext.authUI.getCurrentUser().getDisplayEmail(stringProvider.emailProvider) + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringProvider.verifyEmailInstruction(email), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium + ) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { uiContext.authUI.getCurrentUser()?.sendEmailVerification() }) { + Text(stringProvider.resendVerificationEmailAction) + } + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = uiContext.onReloadUser) { + Text(stringProvider.verifiedEmailAction) + } + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = uiContext.onSignOut) { + Text(stringProvider.signOutAction) + } + } + } + + is AuthState.RequiresProfileCompletion -> { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringProvider.profileCompletionMessage, + textAlign = TextAlign.Center + ) + if (state.missingFields.isNotEmpty()) { + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = stringProvider.profileMissingFieldsMessage(state.missingFields.joinToString()), + textAlign = TextAlign.Center + ) + } + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = uiContext.onSignOut) { + Text(stringProvider.signOutAction) + } + } + } + + else -> { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + CircularProgressIndicator() + } + } + } +} + +@Composable +private fun ReauthDialog( + authUI: FirebaseAuthUI, + state: AuthState.ReauthenticationRequired, + onDismiss: () -> Unit, +) { + var password by remember { mutableStateOf("") } + var isVerifying by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } + val coroutineScope = rememberCoroutineScope() + val email = state.user.email.orEmpty() + + AlertDialog( + onDismissRequest = onDismiss, + containerColor = MaterialTheme.colorScheme.surfaceVariant, + title = { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("Verify your identity") + state.reason?.let { reason -> + Text( + reason, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text( + "Signing in as $email", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + ) + com.firebase.ui.auth.ui.components.AuthTextField( + value = password, + onValueChange = { + password = it + errorMessage = null + }, + label = { Text("Password") }, + isSecureTextField = true, + isError = errorMessage != null, + errorMessage = errorMessage, + ) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { Text("Cancel") } + }, + confirmButton = { + Button( + onClick = { + coroutineScope.launch { + isVerifying = true + errorMessage = null + try { + val result = authUI.auth + .signInWithEmailAndPassword(email, password) + .await() + result.user?.let { user -> + authUI.updateAuthState(AuthState.Success(result, user)) + } + } catch (e: Exception) { + errorMessage = "Incorrect password. Please try again." + } finally { + isVerifying = false + } + } + }, + enabled = password.isNotBlank() && !isVerifying, + ) { + if (isVerifying) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + ) + } else { + Text("Verify") + } + } + }, + ) +} + +@Composable +private fun ChangePasswordDialog( + authUI: FirebaseAuthUI, + configuration: com.firebase.ui.auth.configuration.AuthUIConfiguration, + stringProvider: com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider, + context: android.content.Context, + lifecycleOwner: androidx.lifecycle.LifecycleOwner, + onDismiss: () -> Unit, +) { + var newPassword by remember { mutableStateOf("") } + var confirmPassword by remember { mutableStateOf("") } + var isUpdating by remember { mutableStateOf(false) } + var updateError by remember { mutableStateOf(null) } + + val emailProvider = remember(configuration) { + configuration.providers.filterIsInstance() + .firstOrNull() + } + val passwordValidator = remember(emailProvider, stringProvider) { + com.firebase.ui.auth.configuration.validators.PasswordValidator( + stringProvider = stringProvider, + rules = emailProvider?.passwordValidationRules ?: emptyList(), + ) + } + val confirmValidator = remember(stringProvider) { + com.firebase.ui.auth.configuration.validators.PasswordValidator( + stringProvider = stringProvider, + rules = emptyList(), + ) + } + + val passwordsMatch = newPassword == confirmPassword + val isValid = !passwordValidator.hasError && newPassword.isNotBlank() && + passwordsMatch && confirmPassword.isNotBlank() + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Change password") }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + com.firebase.ui.auth.ui.components.AuthTextField( + value = newPassword, + onValueChange = { + newPassword = it + updateError = null + }, + label = { Text("New password") }, + isSecureTextField = true, + validator = passwordValidator, + ) + com.firebase.ui.auth.ui.components.AuthTextField( + value = confirmPassword, + onValueChange = { + confirmPassword = it + updateError = null + }, + label = { Text("Confirm password") }, + isSecureTextField = true, + isError = confirmPassword.isNotEmpty() && !passwordsMatch, + errorMessage = if (confirmPassword.isNotEmpty() && !passwordsMatch) "Passwords do not match" else null, + validator = confirmValidator, + ) + if (updateError != null) { + Text( + updateError!!, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + ) + } + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { Text("Cancel") } + }, + confirmButton = { + Button( + onClick = { + lifecycleOwner.lifecycleScope.launch { + isUpdating = true + updateError = null + try { + authUI.withReauth( + context, + reason = "Verify your identity to change your password", + ) { + authUI.getCurrentUser()?.updatePassword(newPassword)?.await() + Log.d("HighLevelApiDemoActivity", "Password changed successfully") + onDismiss() + } + } catch (e: Exception) { + updateError = "Failed to update password. Please try again." + } finally { + isUpdating = false + } + } + }, + enabled = isValid && !isUpdating, + ) { + if (isUpdating) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + ) + } else { + Text("Update") + } + } + }, + ) +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/PhoneAuthSlotDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/auth/PhoneAuthSlotDemoActivity.kt new file mode 100644 index 0000000000..a36c567b5d --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/PhoneAuthSlotDemoActivity.kt @@ -0,0 +1,338 @@ +package com.firebaseui.android.demo.auth + +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.AuthException +import com.firebase.ui.auth.FirebaseAuthUI +import com.firebase.ui.auth.configuration.AuthUIConfiguration +import com.firebase.ui.auth.configuration.authUIConfiguration +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvider +import com.firebase.ui.auth.ui.screens.phone.PhoneAuthContentState +import com.firebase.ui.auth.ui.screens.phone.PhoneAuthScreen +import com.firebase.ui.auth.ui.screens.phone.PhoneAuthStep +import com.google.firebase.auth.AuthResult + +class PhoneAuthSlotDemoActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val authUI = FirebaseAuthUI.getInstance() + val appContext = applicationContext + + val configuration = authUIConfiguration { + context = appContext + providers { + provider( + AuthProvider.Phone( + defaultNumber = null, + defaultCountryCode = "US", + allowedCountries = emptyList(), + smsCodeLength = 6, + timeout = 60L, + isInstantVerificationEnabled = true + ) + ) + } + } + + setContent { + CustomAuthUITheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + Column( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + ) { + PhoneAuthDemo( + authUI = authUI, + configuration = configuration, + context = appContext + ) + } + } + } + } + } +} + +@Composable +fun PhoneAuthDemo( + authUI: FirebaseAuthUI, + configuration: AuthUIConfiguration, + context: android.content.Context +) { + var currentUser by remember { mutableStateOf(authUI.getCurrentUser()) } + + LaunchedEffect(Unit) { + authUI.authStateFlow().collect { _ -> + currentUser = authUI.getCurrentUser() + } + } + + if (currentUser != null) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Phone Verified!", + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = currentUser?.phoneNumber ?: "Signed in", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(32.dp)) + Button(onClick = { authUI.auth.signOut() }) { + Text("Sign Out") + } + } + } else { + CompositionLocalProvider(LocalAuthUIStringProvider provides configuration.stringProvider) { + PhoneAuthScreen( + context = context, + configuration = configuration, + authUI = authUI, + onSuccess = { result: AuthResult -> + Log.d("PhoneAuthSlotDemo", "Auth success: ${result.user?.uid}") + }, + onError = { exception: AuthException -> + Log.e("PhoneAuthSlotDemo", "Auth error", exception) + }, + onCancel = { + Log.d("PhoneAuthSlotDemo", "Auth cancelled") + } + ) { state: PhoneAuthContentState -> + CustomPhoneAuthUI(state) + } + } + } +} + +@Composable +fun CustomPhoneAuthUI(state: PhoneAuthContentState) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = when (state.step) { + PhoneAuthStep.EnterPhoneNumber -> "Phone Verification" + PhoneAuthStep.EnterVerificationCode -> "Enter Code" + }, + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(8.dp)) + + state.error?.let { errorMessage -> + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ), + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = errorMessage, + modifier = Modifier.padding(12.dp), + color = MaterialTheme.colorScheme.onErrorContainer, + style = MaterialTheme.typography.bodySmall + ) + } + } + + when (state.step) { + PhoneAuthStep.EnterPhoneNumber -> EnterPhoneNumberUI(state) + PhoneAuthStep.EnterVerificationCode -> EnterVerificationCodeUI(state) + } + } +} + +@Composable +fun EnterPhoneNumberUI(state: PhoneAuthContentState) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "Enter your phone number to receive a verification code", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedCard( + onClick = { }, + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "${state.selectedCountry.flagEmoji} ${state.selectedCountry.dialCode}", + style = MaterialTheme.typography.bodyLarge + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = state.selectedCountry.name, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + OutlinedTextField( + value = state.phoneNumber, + onValueChange = state.onPhoneNumberChange, + label = { Text("Phone Number") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !state.isLoading + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = state.onSendCodeClick, + modifier = Modifier.fillMaxWidth(), + enabled = !state.isLoading && state.phoneNumber.isNotBlank() + ) { + if (state.isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = MaterialTheme.colorScheme.onPrimary + ) + } else { + Text("Send Code") + } + } + } +} + +@Composable +fun EnterVerificationCodeUI(state: PhoneAuthContentState) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "We sent a verification code to:", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + + Text( + text = state.fullPhoneNumber, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = state.verificationCode, + onValueChange = state.onVerificationCodeChange, + label = { Text("6-Digit Code") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !state.isLoading + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = state.onVerifyCodeClick, + modifier = Modifier.fillMaxWidth(), + enabled = !state.isLoading && state.verificationCode.length == 6 + ) { + if (state.isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = MaterialTheme.colorScheme.onPrimary + ) + } else { + Text("Verify Code") + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + TextButton(onClick = state.onChangeNumberClick) { + Text("Change Number") + } + + TextButton( + onClick = state.onResendCodeClick, + enabled = state.resendTimer == 0 + ) { + Text( + if (state.resendTimer > 0) "Resend (${state.resendTimer}s)" + else "Resend Code" + ) + } + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/ShapeCustomizationDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/auth/ShapeCustomizationDemoActivity.kt new file mode 100644 index 0000000000..77419b2104 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/ShapeCustomizationDemoActivity.kt @@ -0,0 +1,263 @@ +package com.firebaseui.android.demo.auth + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.configuration.theme.AuthUITheme +import com.firebase.ui.auth.configuration.theme.ProviderStyleDefaults +import com.firebase.ui.auth.ui.components.AuthProviderButton + +class ShapeCustomizationDemoActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + setContent { + MaterialTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + Column( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + ) { + ShapeCustomizationDemo() + } + } + } + } + } +} + +@Composable +fun ShapeCustomizationDemo() { + val context = LocalContext.current + val stringProvider = DefaultAuthUIStringProvider(context) + var selectedPreset by remember { mutableStateOf(ShapePreset.DEFAULT) } + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "Provider Button Shape Customization", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.primary + ) + + Text( + text = "Showcases the shape customization API for provider buttons. " + + "Set a global shape for all buttons or customize individual providers.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + HorizontalDivider() + + Text(text = "Select Shape Preset:", style = MaterialTheme.typography.titleMedium) + + ShapePreset.entries.forEach { preset -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selectedPreset == preset, + onClick = { selectedPreset = preset } + ) + Spacer(modifier = Modifier.width(8.dp)) + Column { + Text(text = preset.displayName, style = MaterialTheme.typography.bodyLarge) + Text( + text = preset.description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + HorizontalDivider() + + Text(text = "Preview:", style = MaterialTheme.typography.titleMedium) + + when (selectedPreset) { + ShapePreset.DEFAULT -> DefaultShapeButtons(stringProvider) + ShapePreset.DEFAULT_COPY -> DefaultCopyShapeButtons(stringProvider) + ShapePreset.DARK_COPY -> DarkCopyShapeButtons(stringProvider) + ShapePreset.FROM_MATERIAL -> FromMaterialThemeButtons(stringProvider) + ShapePreset.PILL -> PillShapeButtons(stringProvider) + ShapePreset.MIXED -> MixedShapeButtons(stringProvider) + } + + HorizontalDivider() + + Text(text = "Code Example:", style = MaterialTheme.typography.titleMedium) + + androidx.compose.material3.Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = selectedPreset.codeExample, + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace + ), + modifier = Modifier.padding(12.dp) + ) + } + } +} + +enum class ShapePreset( + val displayName: String, + val description: String, + val codeExample: String +) { + DEFAULT( + "Default Shapes", + "Uses the standard 4dp rounded corners", + "// No customization needed\nval theme = AuthUITheme.Default" + ), + DEFAULT_COPY( + "Default.copy()", + "Customize default light theme with .copy()", + "val theme = AuthUITheme.Default.copy(\n providerButtonShape = RoundedCornerShape(12.dp)\n)" + ), + DARK_COPY( + "DefaultDark.copy()", + "Customize default dark theme with .copy()", + "val theme = AuthUITheme.DefaultDark.copy(\n providerButtonShape = RoundedCornerShape(16.dp)\n)" + ), + FROM_MATERIAL( + "fromMaterialTheme()", + "Inherit from Material Theme", + "val theme = AuthUITheme.fromMaterialTheme(\n providerButtonShape = RoundedCornerShape(12.dp)\n)" + ), + PILL( + "Pill Shape", + "Creates pill-shaped buttons (Default.copy)", + "val theme = AuthUITheme.Default.copy(\n providerButtonShape = RoundedCornerShape(28.dp)\n)" + ), + MIXED( + "Mixed Shapes", + "Different shapes per provider (Default.copy)", + "val customStyles = mapOf(\n \"google.com\" to ProviderStyleDefaults.Google.copy(\n shape = RoundedCornerShape(24.dp)\n ),\n \"facebook.com\" to ProviderStyleDefaults.Facebook.copy(\n shape = RoundedCornerShape(8.dp)\n )\n)\n\nval theme = AuthUITheme.Default.copy(\n providerButtonShape = RoundedCornerShape(12.dp),\n providerStyles = customStyles\n)" + ) +} + +@Composable +fun DefaultShapeButtons(stringProvider: DefaultAuthUIStringProvider) { + AuthUITheme { ButtonPreviewColumn(stringProvider) } +} + +@Composable +fun DefaultCopyShapeButtons(stringProvider: DefaultAuthUIStringProvider) { + AuthUITheme(theme = AuthUITheme.Default.copy(providerButtonShape = RoundedCornerShape(12.dp))) { + ButtonPreviewColumn(stringProvider) + } +} + +@Composable +fun DarkCopyShapeButtons(stringProvider: DefaultAuthUIStringProvider) { + AuthUITheme(theme = AuthUITheme.DefaultDark.copy(providerButtonShape = RoundedCornerShape(16.dp))) { + ButtonPreviewColumn(stringProvider) + } +} + +@Composable +fun FromMaterialThemeButtons(stringProvider: DefaultAuthUIStringProvider) { + AuthUITheme(theme = AuthUITheme.fromMaterialTheme(providerButtonShape = RoundedCornerShape(12.dp))) { + ButtonPreviewColumn(stringProvider) + } +} + +@Composable +fun PillShapeButtons(stringProvider: DefaultAuthUIStringProvider) { + AuthUITheme(theme = AuthUITheme.Default.copy(providerButtonShape = RoundedCornerShape(28.dp))) { + ButtonPreviewColumn(stringProvider) + } +} + +@Composable +fun MixedShapeButtons(stringProvider: DefaultAuthUIStringProvider) { + val customStyles = mapOf( + "google.com" to ProviderStyleDefaults.Google.copy(shape = RoundedCornerShape(24.dp)), + "facebook.com" to ProviderStyleDefaults.Facebook.copy(shape = RoundedCornerShape(8.dp)) + ) + AuthUITheme( + theme = AuthUITheme.Default.copy( + providerButtonShape = RoundedCornerShape(12.dp), + providerStyles = customStyles + ) + ) { + ButtonPreviewColumn(stringProvider) + } +} + +@Composable +fun ButtonPreviewColumn(stringProvider: DefaultAuthUIStringProvider) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + AuthProviderButton( + provider = AuthProvider.Google(scopes = emptyList(), serverClientId = null), + onClick = { }, + stringProvider = stringProvider, + modifier = Modifier.fillMaxWidth() + ) + AuthProviderButton( + provider = AuthProvider.Facebook(), + onClick = { }, + stringProvider = stringProvider, + modifier = Modifier.fillMaxWidth() + ) + AuthProviderButton( + provider = AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ), + onClick = { }, + stringProvider = stringProvider, + modifier = Modifier.fillMaxWidth() + ) + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/database/DatabaseDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/database/DatabaseDemoActivity.kt new file mode 100644 index 0000000000..4e99cbfd33 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/database/DatabaseDemoActivity.kt @@ -0,0 +1,182 @@ +package com.firebaseui.android.demo.database + +import android.os.Bundle +import android.view.ViewGroup +import android.widget.TextView +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.paging.LoadState +import androidx.paging.PagingConfig +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.firebase.ui.database.paging.DatabasePagingOptions +import com.firebase.ui.database.paging.FirebaseRecyclerPagingAdapter +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.database.DatabaseReference +import com.google.firebase.database.FirebaseDatabase + +class DatabaseDemoActivity : ComponentActivity() { + + private lateinit var adapter: ScoreAdapter + private var currentPage by mutableIntStateOf(1) + private var prevAppendWasLoading = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val ref = FirebaseDatabase.getInstance().reference.child("database_demo") + + val options = DatabasePagingOptions.Builder() + .setLifecycleOwner(this) + .setQuery(ref.orderByChild("score"), PagingConfig(pageSize = 10), ScoreItem::class.java) + .build() + + adapter = ScoreAdapter(options) + + adapter.addLoadStateListener { states -> + if (states.refresh is LoadState.Loading) { + currentPage = 1 + prevAppendWasLoading = false + return@addLoadStateListener + } + val appendLoading = states.append is LoadState.Loading + if (prevAppendWasLoading && !appendLoading) { + currentPage++ + } + prevAppendWasLoading = appendLoading + } + + setContent { + MaterialTheme { + Surface(modifier = Modifier.fillMaxSize()) { + DatabaseDemoScreen( + adapter = adapter, + currentPage = currentPage, + onSeedData = { signInThenSeed(ref) }, + onRefresh = { adapter.refresh() } + ) + } + } + } + } + + private fun signInThenSeed(ref: DatabaseReference) { + val auth = FirebaseAuth.getInstance() + val signIn = if (auth.currentUser != null) { + com.google.android.gms.tasks.Tasks.forResult(null) + } else { + auth.signInAnonymously() + } + signIn.addOnSuccessListener { seedData(ref) } + } + + private fun seedData(ref: DatabaseReference) { + repeat(50) { i -> + ref.push().setValue(ScoreItem("Item ${i + 1}", (1..100).random())) + } + } +} + +data class ScoreItem(var name: String = "", var score: Int = 0) + +class ScoreViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder( + TextView(parent.context).apply { + layoutParams = RecyclerView.LayoutParams( + RecyclerView.LayoutParams.MATCH_PARENT, + RecyclerView.LayoutParams.WRAP_CONTENT + ) + val density = context.resources.displayMetrics.density + val hPadding = (16 * density).toInt() + val vPadding = (8 * density).toInt() + setPadding(hPadding, vPadding, hPadding, vPadding) + textSize = 16f + } +) + +class ScoreAdapter(options: DatabasePagingOptions) : + FirebaseRecyclerPagingAdapter(options) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ScoreViewHolder(parent) + + override fun onBindViewHolder(holder: ScoreViewHolder, position: Int, model: ScoreItem) { + (holder.itemView as TextView).text = "${model.name} — score: ${model.score}" + } +} + +@Composable +fun DatabaseDemoScreen( + adapter: ScoreAdapter, + currentPage: Int, + onSeedData: () -> Unit, + onRefresh: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text("Firebase Database Paging", style = MaterialTheme.typography.headlineSmall) + Text( + "Paginated list using FirebaseRecyclerPagingAdapter with orderByChild(\"score\").", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Button(onClick = onSeedData) { Text("Authenticate & Seed Data") } + OutlinedButton(onClick = onRefresh) { Text("Refresh") } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "Page: $currentPage", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + AndroidView( + factory = { context -> + RecyclerView(context).apply { + layoutManager = LinearLayoutManager(context) + addItemDecoration( + DividerItemDecoration(context, DividerItemDecoration.VERTICAL) + ) + setAdapter(adapter) + } + }, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/firestore/FirestoreDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/firestore/FirestoreDemoActivity.kt new file mode 100644 index 0000000000..91d07abb91 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/firestore/FirestoreDemoActivity.kt @@ -0,0 +1,189 @@ +package com.firebaseui.android.demo.firestore + +import android.os.Bundle +import android.view.ViewGroup +import android.widget.TextView +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.paging.LoadState +import androidx.paging.PagingConfig +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.firebase.ui.firestore.paging.FirestorePagingAdapter +import com.firebase.ui.firestore.paging.FirestorePagingOptions +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.CollectionReference +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.Query + +class FirestoreDemoActivity : ComponentActivity() { + + private lateinit var adapter: ScoreAdapter + private var currentPage by mutableIntStateOf(1) + private var prevAppendWasLoading = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val collection = FirebaseFirestore.getInstance().collection("firestore_demo") + + // Query must only contain where()/orderBy() — the paging library adds limit(). + val query: Query = collection.orderBy("score") + + val options = FirestorePagingOptions.Builder() + .setLifecycleOwner(this) + .setQuery(query, PagingConfig(pageSize = 10), ScoreItem::class.java) + .build() + + adapter = ScoreAdapter(options) + + adapter.addLoadStateListener { states -> + if (states.refresh is LoadState.Loading) { + currentPage = 1 + prevAppendWasLoading = false + return@addLoadStateListener + } + val appendLoading = states.append is LoadState.Loading + if (prevAppendWasLoading && !appendLoading) { + currentPage++ + } + prevAppendWasLoading = appendLoading + } + + setContent { + MaterialTheme { + Surface(modifier = Modifier.fillMaxSize()) { + FirestoreDemoScreen( + adapter = adapter, + currentPage = currentPage, + onSeedData = { signInThenSeed(collection) }, + onRefresh = { adapter.refresh() } + ) + } + } + } + } + + private fun signInThenSeed(collection: CollectionReference) { + val auth = FirebaseAuth.getInstance() + val signIn = if (auth.currentUser != null) { + com.google.android.gms.tasks.Tasks.forResult(null) + } else { + auth.signInAnonymously() + } + signIn.addOnSuccessListener { seedData(collection) } + } + + private fun seedData(collection: CollectionReference) { + val batch = collection.firestore.batch() + for (i in 1..50) { + val docRef = collection.document() + batch.set(docRef, ScoreItem("Item $i", (1..100).random())) + } + batch.commit() + } +} + +data class ScoreItem(var name: String = "", var score: Int = 0) + +class ScoreViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder( + TextView(parent.context).apply { + layoutParams = RecyclerView.LayoutParams( + RecyclerView.LayoutParams.MATCH_PARENT, + RecyclerView.LayoutParams.WRAP_CONTENT + ) + val density = context.resources.displayMetrics.density + val hPadding = (16 * density).toInt() + val vPadding = (8 * density).toInt() + setPadding(hPadding, vPadding, hPadding, vPadding) + textSize = 16f + } +) + +class ScoreAdapter(options: FirestorePagingOptions) : + FirestorePagingAdapter(options) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ScoreViewHolder(parent) + + override fun onBindViewHolder(holder: ScoreViewHolder, position: Int, model: ScoreItem) { + (holder.itemView as TextView).text = "${model.name} — score: ${model.score}" + } +} + +@Composable +fun FirestoreDemoScreen( + adapter: ScoreAdapter, + currentPage: Int, + onSeedData: () -> Unit, + onRefresh: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text("Firebase Firestore Paging", style = MaterialTheme.typography.headlineSmall) + Text( + "Paginated list using FirestorePagingAdapter with orderBy(\"score\").", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Button(onClick = onSeedData) { Text("Authenticate & Seed Data") } + OutlinedButton(onClick = onRefresh) { Text("Refresh") } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "Page: $currentPage", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + AndroidView( + factory = { context -> + RecyclerView(context).apply { + layoutManager = LinearLayoutManager(context) + addItemDecoration( + DividerItemDecoration(context, DividerItemDecoration.VERTICAL) + ) + setAdapter(adapter) + } + }, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/storage/StorageDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/storage/StorageDemoActivity.kt new file mode 100644 index 0000000000..5959a5aa45 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/storage/StorageDemoActivity.kt @@ -0,0 +1,204 @@ +package com.firebaseui.android.demo.storage + +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.widget.ImageView +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import com.google.firebase.storage.FirebaseStorage + +class StorageDemoActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + MaterialTheme { + Surface(modifier = Modifier.fillMaxSize()) { + StorageDemoScreen() + } + } + } + } +} + +@Composable +fun StorageDemoScreen() { + var gsUrl by remember { mutableStateOf("") } + var stringStatus by remember { mutableStateOf("Not loaded") } + var stringUrlToLoad by remember { mutableStateOf("") } + var refStatus by remember { mutableStateOf("Not loaded") } + var refUrlToLoad by remember { mutableStateOf("") } + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .systemBarsPadding() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text("Firebase Storage + Glide", style = MaterialTheme.typography.headlineSmall) + Text( + "Enter a gs:// URL and load it using either approach below.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + OutlinedTextField( + value = gsUrl, + onValueChange = { gsUrl = it }, + label = { Text("gs:// URL") }, + placeholder = { Text("gs://your-project.appspot.com/path/to/image.png") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + + // Approach 1: gs:// string via StringLoader + Card(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text("Via gs:// String", style = MaterialTheme.typography.titleMedium) + Text( + "Uses FirebaseImageLoader.StringLoader, registered in StorageGlideModule. " + + "Pass the gs:// URL string directly to Glide.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Button(onClick = { + if (gsUrl.isNotEmpty()) { + stringStatus = "Loading..." + stringUrlToLoad = gsUrl + } + }) { Text("Load") } + StatusText(stringStatus) + AndroidView( + factory = { ImageView(it) }, + update = { view -> + if (stringUrlToLoad.isNotEmpty()) { + GlideApp.with(view) + .load(stringUrlToLoad) + .listener(glideListener { success, error -> + stringStatus = if (success) "Loaded" else "Error: $error" + }) + .into(view) + } + }, + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + ) + } + } + + // Approach 2: StorageReference + Card(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text("Via StorageReference", style = MaterialTheme.typography.titleMedium) + Text( + "Converts the gs:// URL to a StorageReference first, then passes it to Glide. " + + "Handled by FirebaseImageLoader.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Button(onClick = { + if (gsUrl.isNotEmpty()) { + refStatus = "Loading..." + refUrlToLoad = gsUrl + } + }) { Text("Load") } + StatusText(refStatus) + AndroidView( + factory = { ImageView(it) }, + update = { view -> + if (refUrlToLoad.isNotEmpty()) { + runCatching { + FirebaseStorage.getInstance().getReferenceFromUrl(refUrlToLoad) + }.onSuccess { ref -> + GlideApp.with(view) + .load(ref) + .listener(glideListener { success, error -> + refStatus = if (success) "Loaded" else "Error: $error" + }) + .into(view) + }.onFailure { e -> + refStatus = "Invalid URL: ${e.message}" + } + } + }, + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + ) + } + } + } +} + +@Composable +private fun StatusText(status: String) { + Text( + text = "Status: $status", + style = MaterialTheme.typography.bodySmall, + color = if (status.startsWith("Error") || status.startsWith("Invalid")) + MaterialTheme.colorScheme.error + else + MaterialTheme.colorScheme.onSurface + ) +} + +private fun glideListener(onResult: (success: Boolean, error: String?) -> Unit) = + object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean + ): Boolean { + onResult(false, e?.message ?: "Unknown error") + return false + } + + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + onResult(true, null) + return false + } + } diff --git a/app/src/main/java/com/firebaseui/android/demo/storage/StorageGlideModule.kt b/app/src/main/java/com/firebaseui/android/demo/storage/StorageGlideModule.kt new file mode 100644 index 0000000000..499dfe253a --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/storage/StorageGlideModule.kt @@ -0,0 +1,26 @@ +package com.firebaseui.android.demo.storage + +import android.content.Context +import com.bumptech.glide.Glide +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.module.AppGlideModule +import com.firebase.ui.storage.images.FirebaseImageLoader +import com.google.firebase.storage.StorageReference +import java.io.InputStream + +@GlideModule +class StorageGlideModule : AppGlideModule() { + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + registry.append( + StorageReference::class.java, + InputStream::class.java, + FirebaseImageLoader.Factory() + ) + registry.append( + String::class.java, + InputStream::class.java, + FirebaseImageLoader.StringLoader.Factory() + ) + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/ui/theme/Color.kt b/app/src/main/java/com/firebaseui/android/demo/ui/theme/Color.kt new file mode 100644 index 0000000000..2bf0276ff5 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.firebaseui.android.demo.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/com/firebaseui/android/demo/ui/theme/Theme.kt b/app/src/main/java/com/firebaseui/android/demo/ui/theme/Theme.kt new file mode 100644 index 0000000000..401faae8c3 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.firebaseui.android.demo.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun FirebaseUIAndroidTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/firebaseui/android/demo/ui/theme/Type.kt b/app/src/main/java/com/firebaseui/android/demo/ui/theme/Type.kt new file mode 100644 index 0000000000..0bfbc900ee --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.firebaseui.android.demo.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/anon_user_48dp.png b/app/src/main/res/drawable-hdpi/anon_user_48dp.png deleted file mode 100644 index 54c325af52..0000000000 Binary files a/app/src/main/res/drawable-hdpi/anon_user_48dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/firebase_auth.png b/app/src/main/res/drawable-hdpi/firebase_auth.png new file mode 100644 index 0000000000..fecbcb6dd4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/firebase_auth.png differ diff --git a/app/src/main/res/drawable-hdpi/firebase_auth_120dp.png b/app/src/main/res/drawable-hdpi/firebase_auth_120dp.png deleted file mode 100644 index b03b18b532..0000000000 Binary files a/app/src/main/res/drawable-hdpi/firebase_auth_120dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/logo_googleg_color_144dp.png b/app/src/main/res/drawable-hdpi/logo_googleg_color_144dp.png deleted file mode 100644 index ef410754cc..0000000000 Binary files a/app/src/main/res/drawable-hdpi/logo_googleg_color_144dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/anon_user_48dp.png b/app/src/main/res/drawable-mdpi/anon_user_48dp.png deleted file mode 100644 index 7a643a7e89..0000000000 Binary files a/app/src/main/res/drawable-mdpi/anon_user_48dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/firebase_auth.png b/app/src/main/res/drawable-mdpi/firebase_auth.png new file mode 100644 index 0000000000..bc9af3cc0c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/firebase_auth.png differ diff --git a/app/src/main/res/drawable-mdpi/firebase_auth_120dp.png b/app/src/main/res/drawable-mdpi/firebase_auth_120dp.png deleted file mode 100644 index 820ba76f90..0000000000 Binary files a/app/src/main/res/drawable-mdpi/firebase_auth_120dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/logo_googleg_color_144dp.png b/app/src/main/res/drawable-mdpi/logo_googleg_color_144dp.png deleted file mode 100644 index 55f2f4cd1d..0000000000 Binary files a/app/src/main/res/drawable-mdpi/logo_googleg_color_144dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000000..fde1368fc1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/anon_user_48dp.png b/app/src/main/res/drawable-xhdpi/anon_user_48dp.png deleted file mode 100644 index b89074f508..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/anon_user_48dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/firebase_auth.png b/app/src/main/res/drawable-xhdpi/firebase_auth.png new file mode 100644 index 0000000000..8a93e39a6a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/firebase_auth.png differ diff --git a/app/src/main/res/drawable-xhdpi/firebase_auth_120dp.png b/app/src/main/res/drawable-xhdpi/firebase_auth_120dp.png deleted file mode 100644 index 351fa59fe5..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/firebase_auth_120dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/logo_googleg_color_144dp.png b/app/src/main/res/drawable-xhdpi/logo_googleg_color_144dp.png deleted file mode 100644 index 4358dba153..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/logo_googleg_color_144dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/anon_user_48dp.png b/app/src/main/res/drawable-xxhdpi/anon_user_48dp.png deleted file mode 100644 index 069c1a3e2a..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/anon_user_48dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/firebase_auth.png b/app/src/main/res/drawable-xxhdpi/firebase_auth.png new file mode 100644 index 0000000000..c01b18b144 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/firebase_auth.png differ diff --git a/app/src/main/res/drawable-xxhdpi/firebase_auth_120dp.png b/app/src/main/res/drawable-xxhdpi/firebase_auth_120dp.png deleted file mode 100644 index 6a6374e30e..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/firebase_auth_120dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/logo_googleg_color_144dp.png b/app/src/main/res/drawable-xxhdpi/logo_googleg_color_144dp.png deleted file mode 100644 index 826f092564..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/logo_googleg_color_144dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/anon_user_48dp.png b/app/src/main/res/drawable-xxxhdpi/anon_user_48dp.png deleted file mode 100644 index 37b5211aec..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/anon_user_48dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/firebase_auth.png b/app/src/main/res/drawable-xxxhdpi/firebase_auth.png new file mode 100644 index 0000000000..221da4d3ae Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/firebase_auth.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/firebase_auth_120dp.png b/app/src/main/res/drawable-xxxhdpi/firebase_auth_120dp.png deleted file mode 100644 index 9b68e8ade0..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/firebase_auth_120dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/logo_googleg_color_144dp.png b/app/src/main/res/drawable-xxxhdpi/logo_googleg_color_144dp.png deleted file mode 100644 index eb8a006940..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/logo_googleg_color_144dp.png and /dev/null differ diff --git a/app/src/main/res/drawable/chat_message_arrow.xml b/app/src/main/res/drawable/chat_message_arrow.xml deleted file mode 100644 index 5ee79fae92..0000000000 --- a/app/src/main/res/drawable/chat_message_arrow.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/chat_message_background.xml b/app/src/main/res/drawable/chat_message_background.xml deleted file mode 100644 index bac8495491..0000000000 --- a/app/src/main/res/drawable/chat_message_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_discord_24dp.xml b/app/src/main/res/drawable/ic_discord_24dp.xml new file mode 100644 index 0000000000..6b7ee0dae0 --- /dev/null +++ b/app/src/main/res/drawable/ic_discord_24dp.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..1e4408cae4 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_line_logo_24dp.xml b/app/src/main/res/drawable/ic_line_logo_24dp.xml new file mode 100644 index 0000000000..b5402cc995 --- /dev/null +++ b/app/src/main/res/drawable/ic_line_logo_24dp.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml deleted file mode 100644 index 98639fc10f..0000000000 --- a/app/src/main/res/layout/activity_chat.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - -