diff --git a/packages/firebase_ui_auth/.gitignore b/packages/firebase_ui_auth/.gitignore new file mode 100644 index 000000000000..96486fd93024 --- /dev/null +++ b/packages/firebase_ui_auth/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/firebase_ui_auth/.metadata b/packages/firebase_ui_auth/.metadata new file mode 100644 index 000000000000..e7011f64f39d --- /dev/null +++ b/packages/firebase_ui_auth/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + channel: stable + +project_type: package diff --git a/packages/firebase_ui_auth/CHANGELOG.md b/packages/firebase_ui_auth/CHANGELOG.md new file mode 100644 index 000000000000..e1eaff2ddc91 --- /dev/null +++ b/packages/firebase_ui_auth/CHANGELOG.md @@ -0,0 +1,7 @@ +## 1.0.0-dev.0 + + - Bump "firebase_ui_auth" to `1.0.0-dev.0`. + +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/firebase_ui_auth/LICENSE b/packages/firebase_ui_auth/LICENSE new file mode 100644 index 000000000000..5b8ff6261110 --- /dev/null +++ b/packages/firebase_ui_auth/LICENSE @@ -0,0 +1,26 @@ +Copyright 2017, the Chromium project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/firebase_ui_auth/README.md b/packages/firebase_ui_auth/README.md new file mode 100644 index 000000000000..b2d21e063a4a --- /dev/null +++ b/packages/firebase_ui_auth/README.md @@ -0,0 +1,80 @@ +# Firebase UI Auth + +[![pub package](https://img.shields.io/pub/v/firebase_ui_auth.svg)](https://pub.dev/packages/firebase_ui_auth) + +Firebase UI Auth is a set of Flutter widgets and utilities designed to help you build and integrate your user interface with Firebase Authentication. + +> Please contribute to the [discussion](https://github.com/firebase/flutterfire/discussions/6978) with feedback. + +## Platoform support + +| Feature/platform | Android | iOS | Web | macOS | Windows | Linux | +| ------------------ | ------- | --- | ---------------- | ---------------- | ---------------- | ---------------- | +| Email | ✓ | ✓ | ✓ | ✓ | ✓ (1) | ✓ (1) | +| Phone | ✓ | ✓ | ✓ | ╳ | ╳ | ╳ | +| Email link | ✓ | ✓ | ╳ | ╳ | ╳ | ╳ | +| Email verification | ✓ | ✓ | ✓ (2) | ✓ (2) | ✓ (1) | ✓ (1) | +| Sign in with Apple | ╳ | ✓ | ╳ | ✓ | ╳ | ╳ | +| Google Sign in | ✓ | ✓ | ✓ | ✓ | ✓ (1) | ✓ (1) | +| Twitter Login | ✓ | ✓ | ✓ | ✓ | ✓ (1) | ✓ (1) | +| Facebook Sign in | ✓ | ✓ | ✓ | ✓ | ✓ (1) | ✓ (1) | + +1. Available with [flutterfire_desktop](https://github.com/invertase/flutterfire_desktop) +2. No deep-linking into app, so email verification link opens a web page + +## Installation + +```sh +flutter pub add firebase_ui_auth +``` + +## Getting Started + +Here's a quick example that shows how to build a `SignInScreen` and `ProfileScreen` in your app + +```dart +import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + const providers = [EmailAuthProvider()]; + + return MaterialApp( + initialRoute: FirebaseAuth.instance.currentUser == null ? '/sign-in' : '/profile', + routes: { + '/sign-in': (context) { + return SignInScreen( + providers: providers, + actions: [ + AuthStateChangeAction((context, state) { + Navigator.pushReplacementNamed(context, '/profile'); + }), + ], + ); + }, + '/profile': (context) { + return ProfileScreen( + providers: providers, + actions: [ + SignedOutAction((context) { + Navigator.pushReplacementNamed(context, '/sign-in'); + }), + ], + ); + }, + }, + ); + } +} +``` + +Learn more [here](https://github.com/firebase/flutterfire/packages/firebase_ui_auth/doc/README.md). + +## Roadmap / Features + +- For issues, please create a new [issue on the repository](https://github.com/firebase/flutterfire/issues). +- For feature requests, & questions, please participate on the [discussion](https://github.com/firebase/flutterfire/discussions/6978) thread. +- To contribute a change to this plugin, please review our [contribution guide](https://github.com/firebase/flutterfire/blob/master/CONTRIBUTING.md) and open a [pull request](https://github.com/firebase/flutterfire/pulls). diff --git a/packages/firebase_ui_auth/analysis_options.yaml b/packages/firebase_ui_auth/analysis_options.yaml new file mode 100644 index 000000000000..a5744c1cfbe7 --- /dev/null +++ b/packages/firebase_ui_auth/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/firebase_ui_auth/doc/README.md b/packages/firebase_ui_auth/doc/README.md new file mode 100644 index 000000000000..a3d85b0f71de --- /dev/null +++ b/packages/firebase_ui_auth/doc/README.md @@ -0,0 +1,62 @@ +# Firebase UI for authentication + +Firebase UI for authentication provides a simple and easy way to implement authentication in your Flutter app. +The library provides fully featured UI screens to drop into new or existing applications, along with +lower level abstractions for developers looking for tighter control. + +## Installation + +Activate FlutterFire CLI + +```sh +dart pub global activate flutterfire_cli +``` + +Install dependencies + +```sh +flutter pub add firebase_core +flutter pub add firebase_auth +# required for email link sign in and email verification +flutter pub add firebase_dynamic_links +flutter pub add firebase_ui_auth +``` + +## Configuration + +Configure firebase using cli: + +```sh +flutterfire configure +``` + +Initialize firebase app: + +```dart +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); +} +``` + +## macOS entitlements + +If you're building for macOS, make sure to add necessary entitlements. Learn more [from the official Flutter documentation](https://docs.flutter.dev/development/platform-integration/macos/building). + +## Next steps + +To understand what Firebase UI for authentication offers, the following documentation pages walk you through the various topics on +how to use the package within your Flutter app. + +- Available auth providers: + + - [EmaiAuthProvider](./providers/email.md) - allows registering and signing in using email and password. + - [EmailLinkAuthProvider](./providers/email-link.md) - allows registering and signing in using a link sent to email. + - [PhoneAuthProvider](./providers/phone.md) - allows registering and signing in using a phone number + - [UniversalEmailSignInProvider](./providers/universal-email-sign-in.md) - gets all connected auth providers for a given email. + - [OAuth](./providers/oauth.md) + +- [Localization](../../firebase_ui_localizations/README.md) +- [Theming](./theming.md) +- [Navigation](./navigation.md) diff --git a/packages/firebase_ui_auth/doc/images/ui-apple-provider.jpg b/packages/firebase_ui_auth/doc/images/ui-apple-provider.jpg new file mode 100644 index 000000000000..e3683354e636 Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-apple-provider.jpg differ diff --git a/packages/firebase_ui_auth/doc/images/ui-auth-desktop-side-content.png b/packages/firebase_ui_auth/doc/images/ui-auth-desktop-side-content.png new file mode 100644 index 000000000000..b797e4b0759f Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-auth-desktop-side-content.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-auth-email-google-provider.png b/packages/firebase_ui_auth/doc/images/ui-auth-email-google-provider.png new file mode 100644 index 000000000000..1407a8e09d45 Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-auth-email-google-provider.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-auth-email-provider.png b/packages/firebase_ui_auth/doc/images/ui-auth-email-provider.png new file mode 100644 index 000000000000..40cfb022e4a3 Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-auth-email-provider.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-auth-forgot-password.png b/packages/firebase_ui_auth/doc/images/ui-auth-forgot-password.png new file mode 100644 index 000000000000..5f9d783040e4 Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-auth-forgot-password.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-auth-google-email-provider.png b/packages/firebase_ui_auth/doc/images/ui-auth-google-email-provider.png new file mode 100644 index 000000000000..f6ac0a013d54 Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-auth-google-email-provider.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-auth-no-providers.png b/packages/firebase_ui_auth/doc/images/ui-auth-no-providers.png new file mode 100644 index 000000000000..706c40d472d8 Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-auth-no-providers.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-auth-phone-input-screen.png b/packages/firebase_ui_auth/doc/images/ui-auth-phone-input-screen.png new file mode 100644 index 000000000000..91d431725a6c Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-auth-phone-input-screen.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-auth-profile-screen.png b/packages/firebase_ui_auth/doc/images/ui-auth-profile-screen.png new file mode 100644 index 000000000000..6c464f51b4a9 Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-auth-profile-screen.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-auth-register.png b/packages/firebase_ui_auth/doc/images/ui-auth-register.png new file mode 100644 index 000000000000..4ef43be0b8ee Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-auth-register.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-auth-signin-header.png b/packages/firebase_ui_auth/doc/images/ui-auth-signin-header.png new file mode 100644 index 000000000000..deae17f87340 Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-auth-signin-header.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-auth-signin-subtitle.png b/packages/firebase_ui_auth/doc/images/ui-auth-signin-subtitle.png new file mode 100644 index 000000000000..ef1a64223f1b Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-auth-signin-subtitle.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-auth-theming-button.png b/packages/firebase_ui_auth/doc/images/ui-auth-theming-button.png new file mode 100644 index 000000000000..b0f0c406a5f1 Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-auth-theming-button.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-auth-theming-default.png b/packages/firebase_ui_auth/doc/images/ui-auth-theming-default.png new file mode 100644 index 000000000000..f10bf7ba174a Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-auth-theming-default.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-auth-theming-outline-border.png b/packages/firebase_ui_auth/doc/images/ui-auth-theming-outline-border.png new file mode 100644 index 000000000000..6637fd87a8f9 Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-auth-theming-outline-border.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-email-link-provider copy.png b/packages/firebase_ui_auth/doc/images/ui-email-link-provider copy.png new file mode 100644 index 000000000000..4c6c59f3ff9d Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-email-link-provider copy.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-email-link-provider.png b/packages/firebase_ui_auth/doc/images/ui-email-link-provider.png new file mode 100644 index 000000000000..4c6c59f3ff9d Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-email-link-provider.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-email-provider.jpg b/packages/firebase_ui_auth/doc/images/ui-email-provider.jpg new file mode 100644 index 000000000000..2fc232ea5477 Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-email-provider.jpg differ diff --git a/packages/firebase_ui_auth/doc/images/ui-facebook-client-id copy.png b/packages/firebase_ui_auth/doc/images/ui-facebook-client-id copy.png new file mode 100644 index 000000000000..8deacaad6d9a Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-facebook-client-id copy.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-facebook-client-id.png b/packages/firebase_ui_auth/doc/images/ui-facebook-client-id.png new file mode 100644 index 000000000000..8deacaad6d9a Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-facebook-client-id.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-facebook-provider.jpg b/packages/firebase_ui_auth/doc/images/ui-facebook-provider.jpg new file mode 100644 index 000000000000..6ce0a3b178e6 Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-facebook-provider.jpg differ diff --git a/packages/firebase_ui_auth/doc/images/ui-google-provider-client-id copy.png b/packages/firebase_ui_auth/doc/images/ui-google-provider-client-id copy.png new file mode 100644 index 000000000000..19b17a31735a Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-google-provider-client-id copy.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-google-provider-client-id.png b/packages/firebase_ui_auth/doc/images/ui-google-provider-client-id.png new file mode 100644 index 000000000000..19b17a31735a Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-google-provider-client-id.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-google-provider.jpg b/packages/firebase_ui_auth/doc/images/ui-google-provider.jpg new file mode 100644 index 000000000000..112c8fc4d6ba Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-google-provider.jpg differ diff --git a/packages/firebase_ui_auth/doc/images/ui-phone-provider.jpg b/packages/firebase_ui_auth/doc/images/ui-phone-provider.jpg new file mode 100644 index 000000000000..f420d9266a3b Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-phone-provider.jpg differ diff --git a/packages/firebase_ui_auth/doc/images/ui-twitter-app-id copy.png b/packages/firebase_ui_auth/doc/images/ui-twitter-app-id copy.png new file mode 100644 index 000000000000..6e992de1ee63 Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-twitter-app-id copy.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-twitter-app-id.png b/packages/firebase_ui_auth/doc/images/ui-twitter-app-id.png new file mode 100644 index 000000000000..6e992de1ee63 Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-twitter-app-id.png differ diff --git a/packages/firebase_ui_auth/doc/images/ui-twitter-provider.jpg b/packages/firebase_ui_auth/doc/images/ui-twitter-provider.jpg new file mode 100644 index 000000000000..07fa88b7c3e6 Binary files /dev/null and b/packages/firebase_ui_auth/doc/images/ui-twitter-provider.jpg differ diff --git a/packages/firebase_ui_auth/doc/navigation.md b/packages/firebase_ui_auth/doc/navigation.md new file mode 100644 index 000000000000..fdcb3268aa6c --- /dev/null +++ b/packages/firebase_ui_auth/doc/navigation.md @@ -0,0 +1,109 @@ +# Navigation + +Firebase UI uses Flutter navigation capabilities to navigate between pages. + +By default, it uses "Navigator 1." when a new screen needs to be shown as a result of user interaction (`Navigator.push(context, route)` is used). + +For applications using the standard navigation APIs, navigation will work out of the box and require no intervention. However, for applications using +a custom routing package, you will need to override the default navigation actions to integrate with your routing strategy. + +## Custom routing + +For this example, the application will create [named routes](https://docs.flutter.dev/cookbook/navigation/named-routes). Within the UI logic, we can +override the default actions (e.g. signing in or signing out) the UI performs to instead integrate with those named routes. + +First, we define the root route that checks for authentication state and renders a `SignInScreen` or `ProfileScreen`: + +```dart +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + const providers = [EmailProvider()]; + + return MaterialApp( + initialRoute: FirebaseAuth.instance.currentUser == null ? '/sign-in' : '/profile', + routes: { + '/sign-in': (context) => SignInScreen(providers: providers), + '/profile': (context) => ProfileScreen(providers: providers), + }, + ); + } +} +``` + +By default, when a user triggers a sign-in via the `SignInScreen`, no action default occurs. Since we are not subscribing to the authentication +state (via the `authStateChanges` API), we need to manually force the navigator to push to a new screen (the `/profile` route). + +To do this, add a `AuthStateChangeAction` action to the `actions` property of the widget, for example for a successful sign in: + +```dart +SignInScreen( + actions: [ + AuthStateChangeAction((context, _) { + Navigator.of(context).pushReplacementNamed('/profile'); + }), + ], + // ... +) +``` + +You could also react to the user signing out in a similar manner: + +```dart +ProfileScreen( + actions: [ + SignedOutAction((context, _) { + Navigator.of(context).pushReplacementNamed('/sign-in'); + }), + ], + // ... +) +``` + +Some UI widgets also come with internal actions which triggers navigation to a new screen. For example the `SignInScreen` widget allows users to +reset their password by pressing the "Forgot Password" button, which internally navigates to a `ForgotPasswordScreen`. To override this action and +navigate to a named route, provide the `actions` list with a `ForgotPasswordAction`: + +```dart +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + const providers = [EmailProvider()]; + + return MaterialApp( + initialRoute: FirebaseAuth.instance.currentUser == null ? '/sign-in' : '/profile', + routes: { + '/sign-in': (context) { + return SignInScreen( + providers: providers, + actions: [ + ForgotPasswordAction((context, email) { + Navigator.of(context).pushNamed( + '/forgot-password', + arguments: {'email': email}, + ); + }), + ], + ); + }, + '/profile': (context) => ProfileScreen(providers: providers), + '/forgot-password': (context) => MyCustomForgotPasswordScreen(), + }, + ); + } +} +``` + +To learn more about the available actions, check out the [FirebaseUIActions API reference](https://pub.dev/documentation/firebase_ui_auth/latest/firebase_ui_auth/FirebaseUIActions-class.html). + +## Other topics + +## Other topics + +- [EmaiAuthProvider](./providers/email.md) - allows registering and signing using email and password. +- [EmailLinkAuthProvider](./providers/email-link.md) - allows registering and signing using a link sent to email. +- [PhoneAuthProvider](./providers/phone.md) - allows registering and signing using a phone number +- [UniversalEmailSignInProvider](./providers/universal-email-sign-in.md) - gets all connected auth providers for a given email. +- [OAuth](./providers/oauth.md) +- [Localization](../../firebase_ui_localizations/README.md) +- [Theming](./theming.md) diff --git a/packages/firebase_ui_auth/doc/providers/email-link.md b/packages/firebase_ui_auth/doc/providers/email-link.md new file mode 100644 index 000000000000..39e79bec1ef7 --- /dev/null +++ b/packages/firebase_ui_auth/doc/providers/email-link.md @@ -0,0 +1,258 @@ +# Firebase UI Email provider + +## Configuration + +To support Email link as a provider, first ensure that the "Email link" is enabled under "Email/Password" provider +in the [Firebase Console](https://console.firebase.google.com/project/_/authentication/providers): + +![Enable Email Link Provider](../images/ui-email-link-provider.png) + +Configure email provider: + +```dart +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +import 'firebase_options.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + + FirebaseUIAuth.configureProviders([ + EmailLinkAuthProvider( + actionCodeSettings: ActionCodeSettings( + url: 'https://.page.link', + handleCodeInApp: true, + androidMinimumVersion: '1', + androidPackageName: + 'io.flutter.plugins.firebase_ui.firebase_ui_example', + iOSBundleId: 'io.flutter.plugins.flutterfireui.flutterfireUIExample', + ), + ), + // ... other providers + ]); +} +``` + +See [this doc](https://firebase.google.com/docs/auth/flutter/email-link-auth) for more info about `ActionCodeSettings`. + +## Using screen + +After adding `EmailLinkAuthProvider` to the `FirebaseUIAuth.configureProviders`, `SignInScreen` or `RegisterScren` will have a button that will trigger `EmailLinkSignInAction`, or, if no action provided, will open `EmailLinkSignInScreen` using `Navigator.push`. + +```dart +MaterialApp( + intiialRoute: '/login', + routes: { + '/login': (context) { + return SignInScreen( + actions: [ + EmailLinkSignInAction((context) { + Navigator.pushReplacementNamed(context, '/email-link-sign-in'); + }), + ], + ); + }, + '/email-link-sign-in': (context) => EmailLinkSignInScreen( + actions: [ + AuthStateChangeAction((context, state) { + Navigator.pushReplacementNamed(context, '/profile'); + }), + ], + ), + '/profile': (context) => ProfileScreen(), + } +) +``` + +> Notes: +> +> - see [navigation guide](../navigation.md) to learn how navigation works with Firebase UI. +> - explore [FirebaseUIActions API docs](https://pub.dev/documentation/firebase_ui_auth/latest/firebase_ui_auth/FirebaseUIAction-class.html). + +## Using view + +If the pre-built screen don't suit the app's needs, you could use a `EmailLinkSignInView` to build your custom screen: + +```dart +class MyEmailLinkSignInScreen extends StatelessWidget { + @override + Widget build(BuildContext) { + return Scaffold( + body: Column( + children: [ + MyCustomHeader(), + Expanded( + child: Padding( + padding: const EdgeInsets.all(16), + child: FirebaseUIActions( + actions: [ + AuthStateChangeAction((context, state) { + Navigator.pushReplacementNamed(context, '/profile'); + } + ], + child: EmailLinkSignInView(provider: emailLinkAuthProvider), + ), + ), + ), + ] + ) + ) + } +} +``` + +## Building a custom widget with `AuthFlowBuilder` + +You could also use `AuthFlowBuilder` to facilitate the functionality of the `EmailLinkFlow`: + +```dart +class MyCustomWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AuthFlowBuilder( + provider: emailLinkProvider, + listener: (oldState, newState, ctrl) { + if (newState is SignedIn) { + Navigator.of(context).pushReplacementNamed('/profile'); + } + } + builder: (context, state, ctrl, child) { + if (state is Uninitialized) { + return TextField( + decoration: InputDecoration(label: Text('Email')), + onSubmitted: (email) { + ctrl.sendLink(email); + }, + ); + } else if (state is AwaitingDynamicLink) { + return CircularProgressIndicator(); + } else if (state is AuthFailed) { + return ErrorText(exception: state.exception); + } else { + return Text('Unknown state $state'); + } + }, + ); + } +} +``` + +## Building a custom stateful widget + +For full control over every phase of the authentication lifecycle you could build a stateful widget, which implements `EmailLinkAuthListener`: + +```dart +class CustomEmailLinkSignIn extends StatefulWidget { + const CustomEmailLinkSignIn({Key? key}) : super(key: key); + + @override + State createState() => _CustomEmailLinkSignInState(); +} + +class _CustomEmailLinkSignInState extends State + implements EmailLinkAuthListener { + final auth = FirebaseAuth.instance; + late final EmailLinkAuthProvider provider = + EmailLinkAuthProvider(actionCodeSettings: actionCodeSettings) + ..authListener = this; + + late Widget child = TextField( + decoration: const InputDecoration( + labelText: 'Email', + ), + onSubmitted: provider.sendLink, + ); + + @override + void onBeforeLinkSent(String email) { + setState(() { + child = CircularProgressIndicator(); + }); + } + + @override + void onLinkSent(String email) { + setState(() { + child = Text('Check your email and click the link'); + }); + } + + @override + Widget build(BuildContext context) { + return Center(child: child); + } + + @override + void onBeforeCredentialLinked(AuthCredential credential) { + setState(() { + child = CircularProgressIndicator(); + }); + } + + @override + void onBeforeProvidersForEmailFetch() { + setState(() { + child = CircularProgressIndicator(); + }); + } + + @override + void onBeforeSignIn() { + setState(() { + child = CircularProgressIndicator(); + }); + } + + @override + void onCanceled() { + setState(() { + child = Text('Authenticated cancelled'); + }); + } + + @override + void onCredentialLinked(AuthCredential credential) { + Navigator.of(context).pushReplacementNamed('/profile'); + } + + @override + void onDifferentProvidersFound( + String email, List providers, AuthCredential? credential) { + showDifferentMethodSignInDialog( + context: context, + availableProviders: providers, + providers: FirebaseUIAuth.providersFor(FirebaseAuth.instance.app), + ); + } + + @override + void onError(Object error) { + try { + // tries default recovery strategy + defaultOnAuthError(provider, error); + } catch (err) { + setState(() { + defaultOnAuthError(provider, error); + }); + } + } + + @override + void onSignedIn(UserCredential credential) { + Navigator.of(context).pushReplacementNamed('/profile'); + } +} +``` + +## Other topics + +- [EmaiAuthProvider](./email.md) - allows registering and signing using email and password. +- [Email verification](./email-verification.md) +- [PhoneAuthProvider](./phone.md) - allows registering and signing using a phone number +- [UniversalEmailSignInProvider](./universal-email-sign-in.md) - gets all connected auth providers for a given email. +- [OAuth](./oauth.md) +- [Localization](../../../firebase_ui_localizations/README.md) +- [Theming](../theming.md) +- [Navigation](../navigation.md) diff --git a/packages/firebase_ui_auth/doc/providers/email-verification.md b/packages/firebase_ui_auth/doc/providers/email-verification.md new file mode 100644 index 000000000000..da2e809f4475 --- /dev/null +++ b/packages/firebase_ui_auth/doc/providers/email-verification.md @@ -0,0 +1,113 @@ +# Email verification in Firebase UI + +Firebase UI provides a pre-built `EmailVerificationScreen`: + +```dart +class App extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + initialRoute: FirebaseAuth.instance.currentUser == null + ? '/login' + : '/profile', + routes: { + '/login': (context) { + return SignInScreen( + actions: [ + AuthStateChangeAction((context, state) { + if (!state.user!.emailVerified) { + Navigator.pushNamed(context, '/verify-email'); + } else { + Navigator.pushReplacementNamed(context, '/profile'); + } + }), + ] + ); + }, + '/profile': (context) => ProfileScreen(), + '/verify-email': (context) => EmailVerificationScreen( + actionCodeSettings: ActionCodeSettngs(...), + actions: [ + EmailVerified(() { + Navigator.pushReplacementNamed(context, '/profile'); + }), + Cancel((context) { + FirebaseUIAuth.signOut(context: context); + Navigator.pushReplacementNamed(context, '/'); + }), + ], + ), + } + ) + } +} +``` + +Once opened, it triggers a verification email to be sent and will wait for a dynamic link to be received by the app (on supported platforms). + +## Using `EmailVerificatioController` + +If you want to build a custom email verification screen, you could use `EmailVerificationController`: + +```dart +class MyEmailVerificationScreen extends StatefulWidget { + const MyEmailVerificationScreen({Key? key}) : super(key: key); + + @override + State createState() => + _MyEmailVerificationScreenState(); +} + +class _MyEmailVerificationScreenState extends State { + late final ctrl = EmailVerificationController(FirebaseAuth.instance) + ..addListener(() { + // trigger widget rebuild to reflect new state + setState(() {}); + }); + + @override + void dispose() { + ctrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + switch (ctrl.state) { + case EmailVerificationState.unresolved: + case EmailVerificationState.unverified: + return TextButton( + onPressed: () { + ctrl.sendVerificationEmail( + Theme.of(context).platform, + ActionCodeSettings(...), + ); + }, + child: Text('Send verification email'), + ); + case EmailVerificationState.dismissed: + return Text("Ok, let's verify your email next time"); + case EmailVerificationState.pending: + case EmailVerificationState.sending: + return CircularProgressIndicator(); + case EmailVerificationState.sent: + return Text('Check your email'); + case EmailVerificationState.verified: + return Text('Email verified'); + case EmailVerificationState.failed: + return Text('Failed to verify email'); + } + } +} +``` + +## Other topics + +- [EmaiAuthProvider](./email.md) - allows registering and signing using email and password. +- [EmailLinkAuthProvider](./email-link.md) - allows registering and signing using a link sent to email. +- [PhoneAuthProvider](./phone.md) - allows registering and signing using a phone number +- [UniversalEmailSignInProvider](./universal-email-sign-in.md) - gets all connected auth providers for a given email. +- [OAuth](./oauth.md) +- [Localization](../../../firebase_ui_localizations/README.md) +- [Theming](../theming.md) +- [Navigation](../navigation.md) diff --git a/packages/firebase_ui_auth/doc/providers/email.md b/packages/firebase_ui_auth/doc/providers/email.md new file mode 100644 index 000000000000..dd2266c1fd0b --- /dev/null +++ b/packages/firebase_ui_auth/doc/providers/email.md @@ -0,0 +1,235 @@ +# Firebase UI Email auth provider + +## Configuration + +To support email a provider, first ensure that the "Email/Password" provider is +enabled in the [Firebase Console](https://console.firebase.google.com/project/_/authentication/providers): + +![Enable Email/Password Provider](../images/ui-email-provider.jpg) + +Configure email provider: + +```dart +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +import 'firebase_options.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + + FirebaseUIAuth.configureProviders([ + EmailProvider(), + // ... other providers + ]); +} +``` + +## Using screen + +After adding `EmailProvider` to the `FirebaseUIAuth.configureProviders` email form would be displayed on the `SignInScreen` or `RegisterScren`. + +```dart +SignInScreen( + actions: [ + AuthStateChangeAction((context, state) { + if (!state.user!.emailVerified) { + Navigator.pushNamed(context, '/verify-email'); + } else { + Navigator.pushReplacementNamed(context, '/profile'); + } + }), + ], +); +``` + +> Notes: +> +> - see [navigation guide](../navigation.md) to learn how navigation works with Firebase UI. +> - explore [FirebaseUIActions API docs](https://pub.dev/documentation/firebase_ui_auth/latest/firebase_ui_auth/FirebaseUIAction-class.html). + +## Using view + +If the pre-built screens don't suit the app's needs, you could use a `LoginView` to build your custom screen: + +```dart +class MyLoginScreen extends StatelessWidget { + @override + Widget build(BuildContext) { + return Scaffold( + body: Row( + children: [ + MyCustomSideBar(), + Padding( + padding: const EdgeInsets.all(16), + child: FirebaseUIActions( + actions: [ + AuthStateChangeAction((context, state) { + if (!state.user!.emailVerified) { + Navigator.pushNamed(context, '/verify-email'); + } else { + Navigator.pushReplacementNamed(context, '/profile'); + } + }), + ], + child: LoginView( + action: AuthAction.signUp, + providers: FirebaseUIAuth.providersFor( + FirebaseAuth.instance.app, + ), + ), + ), + ) + ], + ), + ); + } +} +``` + +## Using widget + +If a view is also not flexible enough, there is an `EmailForm`: + +```dart +class MyCustomWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AuthStateListener( + listener: (oldState, newState, controller) { + // perform necessary actions based on previous + // and current auth state. + }, + child: EmailForm(), + ) + } +} +``` + +## Building a custom widget with `AuthFlowBuilder` + +You could also use `AuthFlowBuilder` to facilitate the functionality of the `EmailAuthFlow`: + +```dart +class MyCustomWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AuthFlowBuilder( + builder: (context, state, ctrl, child) { + if (state is AwaitingEmailAndPassword) { + return MyCustomEmailForm(); + } else if (state is SigningIn) { + return CircularProgressIndicator(); + } else if (state is AuthFailed) { + return ErrorText(exception: state.exception); + } else { + return Text('Unknown state $state'); + } + }, + ); + } +} +``` + +## Building a custom stateful widget + +For full control over every phase of the authentication lifecycle, you could build a stateful widget which implements `EmailAuthListener`: + +```dart +class CustomEmailSignIn extends StatefulWidget { + const CustomEmailSignIn({Key? key}) : super(key: key); + + @override + State createState() => _CustomEmailSignInState(); +} + +class _CustomEmailSignInState extends State + implements EmailAuthListener { + final auth = FirebaseAuth.instance; + late final EmailAuthProvider provider = EmailAuthProvider() + ..authListener = this; + + Widget child = MyCustomEmailForm(onSubmit: (email, password) { + provider.authenticate(email, password, AuthAction.signIn); + }); + + @override + Widget build(BuildContext context) { + return Center(child: child); + } + + @override + void onBeforeCredentialLinked(AuthCredential credential) { + setState(() { + child = CircularProgressIndicator(); + }); + } + + @override + void onBeforeProvidersForEmailFetch() { + setState(() { + child = CircularProgressIndicator(); + }); + } + + @override + void onBeforeSignIn() { + setState(() { + child = CircularProgressIndicator(); + }); + } + + @override + void onCanceled() { + setState(() { + child = MyCustomEmailForm(onSubmit: (email, password) { + auth.signInWithEmailAndPassword(email: email, password: password); + }); + }); + } + + @override + void onCredentialLinked(AuthCredential credential) { + Navigator.of(context).pushReplacementNamed('/profile'); + } + + @override + void onDifferentProvidersFound( + String email, List providers, AuthCredential? credential) { + showDifferentMethodSignInDialog( + context: context, + availableProviders: providers, + providers: FirebaseUIAuth.providersFor(FirebaseAuth.instance.app), + ); + } + + @override + void onError(Object error) { + try { + // tries default recovery strategy + defaultOnAuthError(provider, error); + } catch (err) { + setState(() { + defaultOnAuthError(provider, error); + }); + } + } + + @override + void onSignedIn(UserCredential credential) { + Navigator.of(context).pushReplacementNamed('/profile'); + } +} +``` + +## Other topics + +- [Email verification](./email-verification.md) +- [EmailLinkAuthProvider](./email-link.md) - allows registering and signing using a link sent to email. +- [PhoneAuthProvider](./phone.md) - allows registering and signing using a phone number +- [UniversalEmailSignInProvider](./universal-email-sign-in.md) - gets all connected auth providers for a given email. +- [OAuth](./oauth.md) +- [Localization](../../../firebase_ui_localizations/README.md) +- [Theming](../theming.md) +- [Navigation](../navigation.md) diff --git a/packages/firebase_ui_auth/doc/providers/oauth.md b/packages/firebase_ui_auth/doc/providers/oauth.md new file mode 100644 index 000000000000..52248b8677db --- /dev/null +++ b/packages/firebase_ui_auth/doc/providers/oauth.md @@ -0,0 +1,195 @@ +# Firebase UI OAuth + +## Google Sign In + +To support Google as a provider, first install the official [`google_sign_in`](https://pub.dev/packages/google_sign_in) +plugin to your project as described in the README. + +Next, enable the "Google" provider in the Firebase Console: + +![Enable Google Provider](../images/ui-google-provider.jpg) + +> To ensure cross-platform support, please ensure you have followed installation instructions for both the `google_sign_in` package and the provider on the Firebase Console (such as adding a [SHA1 fingerprint](https://developers.google.com/android/guides/client-auth?authuser=0) for Android applications). + +You will also need to install [`firebase_ui_oauth_google`](https://pub.dev/packages/firebase_ui_oauth_google): + +```sh +flutter pub add firebase_ui_oauth_google +``` + +And add a provider to the configuration: + +```dart +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + + FirebaseUIAuth.configureProviders([ + GoogleProvider(clientId: GOOGLE_CLIENT_ID), + ]); +} +``` + +Now all pre-built screens that support multiple providers (such as `RegisterScreen`, `SignInScreen`, `ProfileScreen` and others) will have a themed button. + +The configuration requires the `clientId` property (which can be found in the Firebase Console) to be set for seamless cross-platform support. + +![Google app client ID](../images/ui-google-provider-client-id.png) + +See [Custom screens section](#custom-screens) to learn how to use a button on your custom screen. + +## Sign in with Apple + +To support Apple as a provider, first install the [`sign_in_with_apple`](https://pub.dev/packages/sign_in_with_apple) +plugin to your project. Once added, follow the [Integration](https://pub.dev/packages/sign_in_with_apple#integration) steps +for each platform. + +Next, enable the "Apple" provider in the Firebase Console: + +![Enable Apple Provider](../images/ui-apple-provider.jpg) + +You will also need to install [`firebase_ui_oauth_apple`](https://pub.dev/packages/firebase_ui_oauth_apple): + +```sh +flutter pub add firebase_ui_oauth_apple +``` + +And add a provider to the configuration: + +```dart +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + + FirebaseUIAuth.configureProviders([ + AppleProvider(), + ]); +} +``` + +Now all pre-built screens that support multiple providers (such as `RegisterScreen`, `SignInScreen`, `ProfileScreen` and others) will have a themed button. See [Custom screens section](#custom-screens) to learn how to use a button on your custom screen. + +## Flutter Facebook Auth + +To support Facebook as a provider, first install the [`flutter_facebook_auth`](https://pub.dev/packages/flutter_facebook_auth) +plugin to your project. Each platform requires that you follow the [installation process](https://facebook.meedu.app) as specified +in the documentation. + +Next, enable the "Facebook" provider in the Firebase Console & provide your created Facebook App ID and secret: + +![Enable Facebook Provider](../images/ui-facebook-provider.jpg) + +You will also need to install [`firebase_ui_oauth_facebook`](https://pub.dev/packages/firebase_ui_oauth_facebook): + +```sh +flutter pub add firebase_ui_oauth_facebook +``` + +And add a provider to the configuration: + +```dart +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + + FirebaseUIAuth.configureProviders([ + FacebookProvider(clientId: FACEBOOK_CLIENT_ID), + ]); +} +``` + +Now all pre-built screens that support multiple providers (such as `RegisterScreen`, `SignInScreen`, `ProfileScreen` and others) will have a themed button. + +The configuration requires the `clientId` property (which can be found in the Firebase Console) to be set for seamless cross-platform support. + +![Facebook client id](../images/ui-facebook-client-id.png) + +See [Custom screens section](#custom-screens) to learn how to use a button on your custom screen. + +## Twitter Login + +To support Twitter as a provider, first install the [`twitter_login`](https://pub.dev/packages/twitter_login) +plugin to your project. + +Next, enable the "Twitter" provider in the Firebase Console: + +![Enable Twitter Provider](../images/ui-twitter-provider.jpg) + +You will also need to install [`firebase_ui_oauth_twitter`](https://pub.dev/packages/firebase_ui_oauth_twitter): + +```sh +flutter pub add firebase_ui_oauth_twitter +``` + +And add a provider to the configuration: + +```dart +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + + FirebaseUIAuth.configureProviders([ + TwitterProvider( + apiKey: TWITTER_API_KEY, + apiSecretKey: TWITTER_API_SECRET_KEY, + ), + ]); +} +``` + +Now all pre-built screens that support multiple providers (such as `RegisterScreen`, `SignInScreen`, `ProfileScreen` and others) will have a themed button. + +You can get the `apiKey` and `apiSecretKey` from the Firebase Console or [twitter developer portal](https://developer.twitter.com/en/portal/projects-and-apps). + +![Twitter app id](../images/ui-twitter-app-id.png) + +Providing the `apiSecretKey` directly is not advised if you are building for the web. Instead, you can use "dart-define" to ensure that the value is omitted from web builds: + +```bash +flutter run --dart-define TWITTER_SECRET= +``` + +When building the app on platforms other than the web, the `TWITTER_SECRET` environment variable can be defined using: + +```dart +apiSecretKey: String.fromEnvironment('TWITTER_SECRET', ''), +``` + +See [Custom screens section](#custom-screens) to learn how to use a button on your custom screen. + +## Custom screens + +If you want to use a button on your custom screen, use `OAuthProviderButton`: + +```dart +class MyCustomScreen extends StatelessWidget { + const MyCustomScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return AuthStateListener( + child: OAuthProviderButton( + // or any other OAuthProvider + provider: GoogleProvider(clientId: GOOGLE_CLIENT_ID), + ), + listener: (oldState, newState, ctrl) { + if (newState is SignedIn) { + Navigator.pushReplacementNamed(context, '/profile'); + } + return null; + }, + ); + } +} +``` + +## Other topics + +- [EmaiAuthProvider](./email.md) - allows registering and signing using email and password. +- [Email verification](./email-verification.md) +- [EmailLinkAuthProvider](./email-link.md) - allows registering and signing using a link sent to email. +- [PhoneAuthProvider](./phone.md) - allows registering and signing using a phone number +- [UniversalEmailSignInProvider](./universal-email-sign-in.md) - gets all connected auth providers for a given email. +- [Localization](../../../firebase_ui_localizations/README.md) +- [Theming](../theming.md) +- [Navigation](../navigation.md) diff --git a/packages/firebase_ui_auth/doc/providers/phone.md b/packages/firebase_ui_auth/doc/providers/phone.md new file mode 100644 index 000000000000..1efdbd72e28a --- /dev/null +++ b/packages/firebase_ui_auth/doc/providers/phone.md @@ -0,0 +1,321 @@ +# Firebase UI Email provider + +## Configuration + +To support Phone Numbers as a provider, first ensure that the "Phone" provider is +enabled in the [Firebase Console](https://console.firebase.google.com/project/_/authentication/providers): + +![Enable Phone Provider](../images/ui-phone-provider.jpg) + +Next, follow the [Setup Instructions](https://firebase.google.com/docs/auth/flutter/phone-auth) to configure Phone Authentication for your +platforms. + +Configure email provider: + +```dart +import 'package:firebase_core/firebase_core.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + + FirebaseUIAuth.configureProviders([ + PhoneAuthProvider(),, + // ... other providers + ]); +} +``` + +## Using screen + +After adding `PhoneAuthProvider` to the `FirebaseUIAuth.configureProviders`, a button will be added to the `SignInScreen` and `RegisterScreen`. + +```dart +SignInScreen( + actions: [ + VerifyPhoneAction((context, _) { + Navigator.pushNamed(context, '/phone'); + }), + ], +); +``` + +> Notes: +> +> - see [navigation guide](../navigation.md) to learn how navigation works with Firebase UI. +> - explore [FirebaseUIActions API docs](https://pub.dev/documentation/firebase_ui_auth/latest/firebase_ui_auth/FirebaseUIAction-class.html). + +Configure a `'/phone'` route to render `PhoneInputScreen`: + +```dart +MaterialApp( + routes: { + // ...other routes + '/phone': (context) => PhoneInputScreen( + actions: [ + SMSCodeRequestedAction((context, action, flowKey, phoneNumber) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => SMSCodeInputScreen( + flowKey: flowKey, + ), + ), + ); + }), + ] + ), + } +) +``` + +## Using view + +If the pre-built screens don't suit the app's needs, you could use a `PhoneInputView` to build your custom screen: + +```dart +final _flowKey = Object(); + +class MyLoginScreen extends StatelessWidget { + @override + Widget build(BuildContext) { + return Scaffold( + body: Row( + children: [ + MyCustomSideBar(), + Padding( + padding: const EdgeInsets.all(16), + child: FirebaseUIActions( + actions: [ + SMSCodeRequestedAction((context, action, flowKey, phoneNumber) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => SMSCodeInputScreen( + flowKey: flowKey, + ), + ), + ); + }), + ], + child: PhoneInputView(flowKey: flowKey), + ), + ) + ], + ), + ); + } +} +``` + +## Using widget + +If a view is also not flexible enough, there are `PhoneInput` and `SMSCodeInput` widgets: + +```dart +class MyCustomWidget extends StatefulWidget { + @override + State createState() => _MyCustomWidgetState(); +} + +class _MyCustomWidgetState extends State { + Widget child = PhoneInput(initialCountryCode: 'US'); + + @override + Widget build(BuildContext context) { + return AuthStateListener( + listener: (oldState, newState, controller) { + if (newState is SMSCodeSent) { + setState(() { + child = SMSCodeInput( + onSubmit: (code) { + controller.verifySMSCode( + code, + verificationId: newState.verificationId, + confirmationResult: newState.confirmationResult, + ); + }, + ); + }); + } + return null; + }, + child: child, + ); + } +} +``` + +## Building a custom widget with `AuthFlowBuilder` + +You could also use `AuthFlowBuilder` to facilitate the functionality of the `PhoneAuthFlow`: + +```dart +class MyCustomWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AuthFlowBuilder( + listener: (oldState, newState, controller) { + if (newState is PhoneVerified) { + Navigator.of(context).pushReplacementNamed('/profile'); + } + }, + builder: (context, state, ctrl, child) { + if (state is AwaitingPhoneNumber) { + return PhoneInput( + initialCountryCode: 'US', + onSubmit: (phoneNumber) { + ctrl.acceptPhoneNumber(phoneNumber); + }, + ); + } else if (state is SMSCodeSent) { + return SMSCodeInput(onSubmit: (smsCode) { + ctrl.verifySMSCode( + smsCode, + verificationId: state.verificationId, + confirmationResult: state.confirmationResult, + ); + }); + } else if (state is SigningIn) { + return CircularProgressIndicator(); + } else if (state is AuthFailed) { + return ErrorText(exception: state.exception); + } else { + return Text('Unknown state $state'); + } + }, + ); + } +} +``` + +## Building a custom stateful widget + +For full control over every phase of the authentication lifecycle you could build a stateful widget, which implements `PhoneAuthController`: + +```dart +class CustomPhoneVerification extends StatefulWidget { + const CustomPhoneVerification({Key? key}) : super(key: key); + + @override + State createState() => + _CustomPhoneVerificationState(); +} + +class _CustomPhoneVerificationState extends State + implements PhoneAuthListener { + final auth = FirebaseAuth.instance; + late final PhoneAuthProvider provider = PhoneAuthProvider() + ..authListener = this; + + String? verificationId; + ConfirmationResult? confirmationResult; + + late Widget child = PhoneInput( + initialCountryCode: 'US', + onSubmit: (phoneNumber) { + provider.sendVerificationCode(phoneNumber, AuthAction.signIn); + }, + ); + + @override + void onCodeSent(String verificationId, [int? forceResendToken]) { + this.verificationId = verificationId; + } + + @override + void onConfirmationRequested(ConfirmationResult result) { + this.confirmationResult = result; + } + + @override + void onSMSCodeRequested(String phoneNumber) { + setState(() { + child = SMSCodeInput( + onSubmit: (smsCode) { + provider.verifySMSCode(action: AuthAction.signIn, code: smsCode); + }, + ); + }); + } + + @override + void onVerificationCompleted(PhoneAuthCredential credential) { + provider.onCredentialReceived(credential, AuthAction.signIn); + } + + @override + Widget build(BuildContext context) { + return Center(child: child); + } + + @override + void onBeforeCredentialLinked(AuthCredential credential) { + setState(() { + child = CircularProgressIndicator(); + }); + } + + @override + void onBeforeProvidersForEmailFetch() { + setState(() { + child = CircularProgressIndicator(); + }); + } + + @override + void onBeforeSignIn() { + setState(() { + child = CircularProgressIndicator(); + }); + } + + @override + void onCanceled() { + setState(() { + child = Text("Phone verification cancelled"); + }); + } + + @override + void onCredentialLinked(AuthCredential credential) { + Navigator.of(context).pushReplacementNamed('/profile'); + } + + @override + void onDifferentProvidersFound( + String email, List providers, AuthCredential? credential) { + showDifferentMethodSignInDialog( + context: context, + availableProviders: providers, + providers: FirebaseUIAuth.providersFor(FirebaseAuth.instance.app), + ); + } + + @override + void onError(Object error) { + try { + // tries default recovery strategy + defaultOnAuthError(provider, error); + } catch (err) { + setState(() { + defaultOnAuthError(provider, error); + }); + } + } + + @override + void onSignedIn(UserCredential credential) { + Navigator.of(context).pushReplacementNamed('/profile'); + } +} +``` + +## Other topics + +- [Email verification](./email-verification.md) +- [EmailLinkAuthProvider](./email-link.md) - allows registering and signing using a link sent to email. +- [PhoneAuthProvider](./phone.md) - allows registering and signing using a phone number +- [UniversalEmailSignInProvider](./universal-email-sign-in.md) - gets all connected auth providers for a given email. +- [OAuth](./oauth.md) +- [Localization](../../../firebase_ui_localizations/README.md) +- [Theming](../theming.md) +- [Navigation](../navigation.md) diff --git a/packages/firebase_ui_auth/doc/providers/universal-email-sign-in.md b/packages/firebase_ui_auth/doc/providers/universal-email-sign-in.md new file mode 100644 index 000000000000..42f7d67250d2 --- /dev/null +++ b/packages/firebase_ui_auth/doc/providers/universal-email-sign-in.md @@ -0,0 +1,198 @@ +# Universal email sign in + +Universal email sign in is a flow that will resolve connected auth providers with a given email. +This flow is intended to solve the problem where the user doesn't remember which provider was +previously used to authenticate. + +## Using screen + +Firebase UI provides a pre-built `UniversalEmailSignInScreen`. + +```dart +UniversalEmailSignInScreen( + // optional, shows a dialog with a sign in ui + // with all connected providers. + onProvidersFound: (email, providers) { + // navigate to a custom sign in that provides + // a UI for authentication for received providers. + } +); +``` + +## Using view + +If the pre-built screens don't suit the app's needs, you could use a `FindProvidersForEmailView` to build your custom screen: + +```dart +class MyLoginScreen extends StatelessWidget { + @override + Widget build(BuildContext) { + return Scaffold( + body: Row( + children: [ + MyCustomSideBar(), + Padding( + padding: const EdgeInsets.all(16), + child: FindProvidersForEmailView( + onProvidersFound: (email, providers) { + // navigate to a custom sign in that provides + // a UI for authentication for received providers. + }, + ), + ) + ], + ), + ); + } +} +``` + +## Building a custom widget with `AuthFlowBuilder` + +You could also use `AuthFlowBuilder` to facilitate the functionality of the `UniversalEmailSignInFlow`: + +```dart +class MyCustomWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AuthFlowBuilder( + listener: (oldState, newState, controller) { + if (newState is DifferentSignInMethodsFound) { + showDifferentMethodSignInDialog( + context: context, + availableProviders: newState.methods, + providers: FirebaseUIAuth.providersFor( + FirebaseAuth.instance.app, + ), + ); + } + }, + builder: (context, state, ctrl, child) { + if (state is Uninitialized) { + return TextField( + decoration: InputDecoration( + labelText: 'Email', + ), + onSubmitted: (email) { + ctrl.findProvidersForEmail(email); + }, + ); + } else if (state is FetchingProvidersForEmail) { + return CircularProgressIndicator(); + } else if (state is AuthFailed) { + return ErrorText(exception: state.exception); + } else { + return Text('Unknown state $state'); + } + }, + ); + } +} +``` + +## Building a custom stateful widget + +For full control over every phase of the authentication lifecycle, you could build a stateful widget which implements `UniversalEmailSignInListener`: + +```dart +class CustomUniversalEmailSignIn extends StatefulWidget { + const CustomUniversalEmailSignIn({Key? key}) : super(key: key); + + @override + State createState() => + _CustomUniversalEmailSignInState(); +} + +class _CustomUniversalEmailSignInState extends State + implements UniversalEmailSignInListener { + final auth = FirebaseAuth.instance; + late final UniversalEmailSignInProvider provider = + UniversalEmailSignInProvider()..authListener = this; + + late Widget child = TextField( + decoration: const InputDecoration( + labelText: 'Email', + ), + onSubmitted: provider.findProvidersForEmail, + ); + + @override + void onBeforeProvidersForEmailFetch() { + setState(() { + child = CircularProgressIndicator(); + }); + } + + @override + void onDifferentProvidersFound( + String email, + List providers, + AuthCredential? credential, + ) { + showDifferentMethodSignInDialog( + context: context, + availableProviders: providers, + providers: FirebaseUIAuth.providersFor(FirebaseAuth.instance.app), + ); + } + + @override + Widget build(BuildContext context) { + return Center(child: child); + } + + @override + void onBeforeCredentialLinked(AuthCredential credential) { + setState(() { + child = CircularProgressIndicator(); + }); + } + + @override + void onBeforeSignIn() { + setState(() { + child = CircularProgressIndicator(); + }); + } + + @override + void onCanceled() { + setState(() { + child = Text('Authenticated cancelled'); + }); + } + + @override + void onCredentialLinked(AuthCredential credential) { + Navigator.of(context).pushReplacementNamed('/profile'); + } + + @override + void onError(Object error) { + try { + // tries default recovery strategy + defaultOnAuthError(provider, error); + } catch (err) { + setState(() { + defaultOnAuthError(provider, error); + }); + } + } + + @override + void onSignedIn(UserCredential credential) { + Navigator.of(context).pushReplacementNamed('/profile'); + } +} +``` + +## Other topics + +- [EmaiAuthProvider](./email.md) - allows registering and signing using email and password. +- [Email verification](./email-verification.md) +- [EmailLinkAuthProvider](./email-link.md) - allows registering and signing using a link sent to email. +- [PhoneAuthProvider](./phone.md) - allows registering and signing using a phone number +- [OAuth](./oauth.md) +- [Localization](../../../firebase_ui_localizations/README.md) +- [Theming](../theming.md) +- [Navigation](../navigation.md) diff --git a/packages/firebase_ui_auth/doc/theming.md b/packages/firebase_ui_auth/doc/theming.md new file mode 100644 index 000000000000..773d59462b1f --- /dev/null +++ b/packages/firebase_ui_auth/doc/theming.md @@ -0,0 +1,108 @@ +# Theming + +Firebase UI widgets are built on top of Material and Cupertino design patterns provided by Flutter. + +To provide consistency across your application, the Firebase UI widgets depend on the [`ThemeData`](https://api.flutter.dev/flutter/material/ThemeData-class.html) +or [`CupertinoThemeData`](https://api.flutter.dev/flutter/cupertino/CupertinoThemeData-class.html) instances provided to your `MaterialApp` or `CupertinoApp` widget. + +For example, the `SignInScreen` widget with an email provider wrapped in a `MaterialApp` will use the following widgets: + +- [`TextFormField`](https://api.flutter.dev/flutter/material/TextFormField-class.html) +- [`TextButton`](https://api.flutter.dev/flutter/material/TextButton-class.html) +- [`OutlinedButton`](https://api.flutter.dev/flutter/material/OutlinedButton-class.html) + +```dart +class FirebaseAuthUIExample extends StatelessWidget { + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: SignInScreen( + providers: [ + EmailProvider(), + ], + ), + ); + } +} +``` + +This will render a screen with the default Material style widgets: + +![Firebase UI Auth Theming - default email form style](./images/ui-auth-theming-default.png) + +To update these styles, we can override the `ThemeData` provided to the `MaterialApp`. For example, to apply a border to the input fields, +we can override the `InputDecorationTheme`: + +```dart +class FirebaseAuthUIExample extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData( + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + home: const SignInScreen( + providers: [ + EmailProvider(), + ], + ), + ); + } +} +``` + +The UI widgets will respect the updated theme data, and the UI will be reflected to match: + +![Firebase UI Auth Theming - email form outline border](./images/ui-auth-theming-outline-border.png) + +Furthermore, we can customize the button used in the UI by overriding the `OutlinedButtonThemeData`: + +```dart +class FirebaseAuthUIExample extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData( + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: ButtonStyle( + padding: MaterialStateProperty.all( + const EdgeInsets.all(24), + ), + backgroundColor: MaterialStateProperty.all(Colors.blue), + foregroundColor: MaterialStateProperty.all(Colors.white), + ), + ), + ), + home: const SignInScreen( + providers: [ + EmailProvider(), + ], + ), + ); + } +} +``` + +The button will now respect the updated theme data and display a styled button instead: + +![Firebase UI Auth Theming - email form custom button style](./images/ui-auth-theming-button.png) + +## Other topics + +- [EmaiAuthProvider](./providers/email.md) - allows registering and signing using email and password. +- [EmailLinkAuthProvider](./providers/email-link.md) - allows registering and signing using a link sent to email. +- [PhoneAuthProvider](./providers/phone.md) - allows registering and signing using a phone number +- [UniversalEmailSignInProvider](./providers/universal-email-sign-in.md) - gets all connected auth providers for a given email. +- [OAuth](./providers/oauth.md) + +- [Localization](../../firebase_ui_localizations/README.md) +- [Navigation](./navigation.md) diff --git a/packages/firebase_ui_auth/example/.gitignore b/packages/firebase_ui_auth/example/.gitignore new file mode 100644 index 000000000000..a8e938c08397 --- /dev/null +++ b/packages/firebase_ui_auth/example/.gitignore @@ -0,0 +1,47 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/firebase_ui_auth/example/.metadata b/packages/firebase_ui_auth/example/.metadata new file mode 100644 index 000000000000..39f2501e1faf --- /dev/null +++ b/packages/firebase_ui_auth/example/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + - platform: android + create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + - platform: ios + create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + - platform: linux + create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + - platform: macos + create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + - platform: web + create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + - platform: windows + create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/firebase_ui_auth/example/README.md b/packages/firebase_ui_auth/example/README.md new file mode 100644 index 000000000000..aa8978474d15 --- /dev/null +++ b/packages/firebase_ui_auth/example/README.md @@ -0,0 +1,16 @@ +# firebase_ui_example + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/firebase_ui_auth/example/analysis_options.yaml b/packages/firebase_ui_auth/example/analysis_options.yaml new file mode 100644 index 000000000000..fd16f9219845 --- /dev/null +++ b/packages/firebase_ui_auth/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/firebase_ui_auth/example/android/.gitignore b/packages/firebase_ui_auth/example/android/.gitignore new file mode 100644 index 000000000000..6f568019d3c6 --- /dev/null +++ b/packages/firebase_ui_auth/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/packages/firebase_ui_auth/example/android/app/build.gradle b/packages/firebase_ui_auth/example/android/app/build.gradle new file mode 100644 index 000000000000..e613072eb7fa --- /dev/null +++ b/packages/firebase_ui_auth/example/android/app/build.gradle @@ -0,0 +1,72 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "io.flutter.plugins.firebase_ui_example" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. + minSdkVersion 19 + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + multiDexEnabled true + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/packages/firebase_ui_auth/example/android/app/src/debug/AndroidManifest.xml b/packages/firebase_ui_auth/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..ea82eba30f06 --- /dev/null +++ b/packages/firebase_ui_auth/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/packages/firebase_ui_auth/example/android/app/src/main/AndroidManifest.xml b/packages/firebase_ui_auth/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..36f5b767539a --- /dev/null +++ b/packages/firebase_ui_auth/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/firebase_ui_auth/example/android/app/src/main/kotlin/io/flutter/plugins/firebase_ui_example/MainActivity.kt b/packages/firebase_ui_auth/example/android/app/src/main/kotlin/io/flutter/plugins/firebase_ui_example/MainActivity.kt new file mode 100644 index 000000000000..e998cfce3205 --- /dev/null +++ b/packages/firebase_ui_auth/example/android/app/src/main/kotlin/io/flutter/plugins/firebase_ui_example/MainActivity.kt @@ -0,0 +1,6 @@ +package io.flutter.plugins.firebase_ui_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/packages/firebase_ui_auth/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/firebase_ui_auth/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000000..f74085f3f6a2 --- /dev/null +++ b/packages/firebase_ui_auth/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/firebase_ui_auth/example/android/app/src/main/res/drawable/launch_background.xml b/packages/firebase_ui_auth/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..304732f88420 --- /dev/null +++ b/packages/firebase_ui_auth/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/firebase_ui_auth/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/firebase_ui_auth/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000000..db77bb4b7b09 Binary files /dev/null and b/packages/firebase_ui_auth/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/firebase_ui_auth/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/firebase_ui_auth/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000000..17987b79bb8a Binary files /dev/null and b/packages/firebase_ui_auth/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/firebase_ui_auth/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/firebase_ui_auth/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000000..09d4391482be Binary files /dev/null and b/packages/firebase_ui_auth/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/firebase_ui_auth/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/firebase_ui_auth/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000000..d5f1c8d34e7a Binary files /dev/null and b/packages/firebase_ui_auth/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/firebase_ui_auth/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/firebase_ui_auth/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000000..4d6372eebdb2 Binary files /dev/null and b/packages/firebase_ui_auth/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/firebase_ui_auth/example/android/app/src/main/res/values-night/styles.xml b/packages/firebase_ui_auth/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000000..06952be745f9 --- /dev/null +++ b/packages/firebase_ui_auth/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/firebase_ui_auth/example/android/app/src/main/res/values/strings.xml b/packages/firebase_ui_auth/example/android/app/src/main/res/values/strings.xml new file mode 100644 index 000000000000..4e61b94908a4 --- /dev/null +++ b/packages/firebase_ui_auth/example/android/app/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + 128693022464535 + 16dbbdf0cfb309034a6ad98ac2a21688 + diff --git a/packages/firebase_ui_auth/example/android/app/src/main/res/values/styles.xml b/packages/firebase_ui_auth/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..cb1ef88056ed --- /dev/null +++ b/packages/firebase_ui_auth/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/firebase_ui_auth/example/android/app/src/profile/AndroidManifest.xml b/packages/firebase_ui_auth/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000000..ea82eba30f06 --- /dev/null +++ b/packages/firebase_ui_auth/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/packages/firebase_ui_auth/example/android/build.gradle b/packages/firebase_ui_auth/example/android/build.gradle new file mode 100644 index 000000000000..83ae220041c7 --- /dev/null +++ b/packages/firebase_ui_auth/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/firebase_ui_auth/example/android/gradle.properties b/packages/firebase_ui_auth/example/android/gradle.properties new file mode 100644 index 000000000000..94adc3a3f97a --- /dev/null +++ b/packages/firebase_ui_auth/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/firebase_ui_auth/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/firebase_ui_auth/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..cc5527d781a7 --- /dev/null +++ b/packages/firebase_ui_auth/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/packages/firebase_ui_auth/example/android/settings.gradle b/packages/firebase_ui_auth/example/android/settings.gradle new file mode 100644 index 000000000000..44e62bcf06ae --- /dev/null +++ b/packages/firebase_ui_auth/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/packages/firebase_ui_auth/example/assets/images/firebase_logo.svg b/packages/firebase_ui_auth/example/assets/images/firebase_logo.svg new file mode 100644 index 000000000000..dbf717473387 --- /dev/null +++ b/packages/firebase_ui_auth/example/assets/images/firebase_logo.svg @@ -0,0 +1 @@ + diff --git a/packages/firebase_ui_auth/example/assets/images/flutterfire_logo.png b/packages/firebase_ui_auth/example/assets/images/flutterfire_logo.png new file mode 100644 index 000000000000..604593b8e481 Binary files /dev/null and b/packages/firebase_ui_auth/example/assets/images/flutterfire_logo.png differ diff --git a/packages/firebase_ui_auth/example/ios/.gitignore b/packages/firebase_ui_auth/example/ios/.gitignore new file mode 100644 index 000000000000..7a7f9873ad7d --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/firebase_ui_auth/example/ios/Flutter/AppFrameworkInfo.plist b/packages/firebase_ui_auth/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..8d4492f977ad --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/packages/firebase_ui_auth/example/ios/Flutter/Debug.xcconfig b/packages/firebase_ui_auth/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..ec97fc6f3021 --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/firebase_ui_auth/example/ios/Flutter/Release.xcconfig b/packages/firebase_ui_auth/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..c4855bfe2000 --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/firebase_ui_auth/example/ios/Podfile b/packages/firebase_ui_auth/example/ios/Podfile new file mode 100644 index 000000000000..1e8c3c90a55e --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/firebase_ui_auth/example/ios/Runner.xcodeproj/project.pbxproj b/packages/firebase_ui_auth/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..e673022acc75 --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,484 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ZPF26SRXG5; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebaseUiExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ZPF26SRXG5; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebaseUiExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ZPF26SRXG5; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebaseUiExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/firebase_ui_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/firebase_ui_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/firebase_ui_auth/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/firebase_ui_auth/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/firebase_ui_auth/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/firebase_ui_auth/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/firebase_ui_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/firebase_ui_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..c87d15a33520 --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/firebase_ui_auth/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/firebase_ui_auth/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..1d526a16ed0f --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/firebase_ui_auth/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/firebase_ui_auth/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/firebase_ui_auth/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/firebase_ui_auth/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/firebase_ui_auth/example/ios/Runner/AppDelegate.swift b/packages/firebase_ui_auth/example/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000000..70693e4a8c12 --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d36b1fab2d9d --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 000000000000..dc9ada4725e9 Binary files /dev/null and b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000000..28c6bf03016f Binary files /dev/null and b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000000..f091b6b0bca8 Binary files /dev/null and b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000000..4cde12118dda Binary files /dev/null and b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000000..d0ef06e7edb8 Binary files /dev/null and b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000000..dcdc2306c285 Binary files /dev/null and b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000000..c8f9ed8f5cee Binary files /dev/null and b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000000..75b2d164a5a9 Binary files /dev/null and b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000000..c4df70d39da7 Binary files /dev/null and b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000000..6a84f41e14e2 Binary files /dev/null and b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000000..d0e1f5853602 Binary files /dev/null and b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000000..0bedcf2fd467 --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000000..89c2725b70f1 --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/firebase_ui_auth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/firebase_ui_auth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..f2e259c7c939 --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/firebase_ui_auth/example/ios/Runner/Base.lproj/Main.storyboard b/packages/firebase_ui_auth/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/firebase_ui_auth/example/ios/Runner/Info.plist b/packages/firebase_ui_auth/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..06305e3a1026 --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Firebase Ui Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + firebase_ui_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + + diff --git a/packages/firebase_ui_auth/example/ios/Runner/Runner-Bridging-Header.h b/packages/firebase_ui_auth/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000000..308a2a560b42 --- /dev/null +++ b/packages/firebase_ui_auth/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packages/firebase_ui_auth/example/lib/config.dart b/packages/firebase_ui_auth/example/lib/config.dart new file mode 100644 index 000000000000..c0a1ab37135f --- /dev/null +++ b/packages/firebase_ui_auth/example/lib/config.dart @@ -0,0 +1,12 @@ +// ignore_for_file: do_not_use_environment, constant_identifier_names + +const GOOGLE_CLIENT_ID = + '448618578101-sg12d2qin42cpr00f8b0gehs5s7inm0v.apps.googleusercontent.com'; +const GOOGLE_REDIRECT_URI = + 'https://react-native-firebase-testing.firebaseapp.com/__/auth/handler'; + +const TWITTER_API_KEY = String.fromEnvironment('TWITTER_API_KEY'); +const TWITTER_API_SECRET_KEY = String.fromEnvironment('TWITTER_API_SECRET_KEY'); +const TWITTER_REDIRECT_URI = 'ffire://'; + +const FACEBOOK_CLIENT_ID = '128693022464535'; diff --git a/packages/firebase_ui_auth/example/lib/decorations.dart b/packages/firebase_ui_auth/example/lib/decorations.dart new file mode 100644 index 000000000000..f7a16ecea993 --- /dev/null +++ b/packages/firebase_ui_auth/example/lib/decorations.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +HeaderBuilder headerImage(String assetName) { + return (context, constraints, _) { + return Padding( + padding: const EdgeInsets.all(20), + child: Image.asset(assetName), + ); + }; +} + +HeaderBuilder headerIcon(IconData icon) { + return (context, constraints, shrinkOffset) { + return Padding( + padding: const EdgeInsets.all(20).copyWith(top: 40), + child: Icon( + icon, + color: Colors.blue, + size: constraints.maxWidth / 4 * (1 - shrinkOffset), + ), + ); + }; +} + +SideBuilder sideImage(String assetName) { + return (context, constraints) { + return Center( + child: Padding( + padding: EdgeInsets.all(constraints.maxWidth / 4), + child: Image.asset(assetName), + ), + ); + }; +} + +SideBuilder sideIcon(IconData icon) { + return (context, constraints) { + return Padding( + padding: const EdgeInsets.all(20), + child: Icon( + icon, + color: Colors.blue, + size: constraints.maxWidth / 3, + ), + ); + }; +} diff --git a/packages/firebase_ui_auth/example/lib/firebase_options.dart b/packages/firebase_ui_auth/example/lib/firebase_options.dart new file mode 100644 index 000000000000..a09bac6945fb --- /dev/null +++ b/packages/firebase_ui_auth/example/lib/firebase_options.dart @@ -0,0 +1,97 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyB7wZb2tO1-Fs6GbDADUSTs2Qs3w08Hovw', + appId: '1:406099696497:web:8639aa69bac133513574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + measurementId: 'G-X3614TQ65V', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw', + appId: '1:406099696497:android:899c6485cfce26c13574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyDooSUGSf63Ghq02_iIhtnmwMDs4HlWS6c', + appId: '1:406099696497:ios:24bb8dcaefc434a73574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + androidClientId: + '406099696497-17qn06u8a0dc717u8ul7s49ampk13lul.apps.googleusercontent.com', + iosClientId: + '406099696497-65v1b9ffv6sgfqngfjab5ol5qdikh2rm.apps.googleusercontent.com', + iosBundleId: 'io.flutter.plugins.firebaseUiExample', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyDooSUGSf63Ghq02_iIhtnmwMDs4HlWS6c', + appId: '1:406099696497:ios:24bb8dcaefc434a73574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + androidClientId: + '406099696497-17qn06u8a0dc717u8ul7s49ampk13lul.apps.googleusercontent.com', + iosClientId: + '406099696497-65v1b9ffv6sgfqngfjab5ol5qdikh2rm.apps.googleusercontent.com', + iosBundleId: 'io.flutter.plugins.firebaseUiExample', + ); +} diff --git a/packages/firebase_ui_auth/example/lib/main.dart b/packages/firebase_ui_auth/example/lib/main.dart new file mode 100644 index 000000000000..34ae32c7c9db --- /dev/null +++ b/packages/firebase_ui_auth/example/lib/main.dart @@ -0,0 +1,272 @@ +import 'package:firebase_auth/firebase_auth.dart' + hide PhoneAuthProvider, EmailAuthProvider; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/material.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import 'package:firebase_ui_oauth_apple/firebase_ui_oauth_apple.dart'; +import 'package:firebase_ui_oauth_facebook/firebase_ui_oauth_facebook.dart'; +import 'package:firebase_ui_oauth_google/firebase_ui_oauth_google.dart'; +import 'package:firebase_ui_oauth_twitter/firebase_ui_oauth_twitter.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +import 'firebase_options.dart'; + +import 'config.dart'; +import 'decorations.dart'; + +final actionCodeSettings = ActionCodeSettings( + url: 'https://flutterfire-e2e-tests.firebaseapp.com', + handleCodeInApp: true, + androidMinimumVersion: '1', + androidPackageName: 'io.flutter.plugins.firebase_ui.firebase_ui_example', + iOSBundleId: 'io.flutter.plugins.fireabaseUiExample', +); +final emailLinkProviderConfig = EmailLinkAuthProvider( + actionCodeSettings: actionCodeSettings, +); + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + + FirebaseUIAuth.configureProviders([ + EmailAuthProvider(), + emailLinkProviderConfig, + PhoneAuthProvider(), + GoogleProvider(clientId: GOOGLE_CLIENT_ID), + AppleProvider(), + FacebookProvider(clientId: FACEBOOK_CLIENT_ID), + TwitterProvider( + apiKey: TWITTER_API_KEY, + apiSecretKey: TWITTER_API_SECRET_KEY, + redirectUri: TWITTER_REDIRECT_URI, + ), + ]); + + runApp(const FirebaseAuthUIExample()); +} + +// Overrides a label for en locale +// To add localization for a custom language follow the guide here: +// https://flutter.dev/docs/development/accessibility-and-localization/internationalization#an-alternative-class-for-the-apps-localized-resources +class LabelOverrides extends DefaultLocalizations { + const LabelOverrides(); + + @override + String get emailInputLabel => 'Enter your email'; +} + +class FirebaseAuthUIExample extends StatelessWidget { + const FirebaseAuthUIExample({Key? key}) : super(key: key); + + String get initialRoute { + final auth = FirebaseAuth.instance; + + if (auth.currentUser == null) { + return '/'; + } + + if (!auth.currentUser!.emailVerified && auth.currentUser!.email != null) { + return '/verify-email'; + } + + return '/profile'; + } + + @override + Widget build(BuildContext context) { + final buttonStyle = ButtonStyle( + padding: MaterialStateProperty.all(const EdgeInsets.all(12)), + shape: MaterialStateProperty.all( + RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ); + + final mfaAction = AuthStateChangeAction( + (context, state) async { + final nav = Navigator.of(context); + + await startMFAVerification( + resolver: state.resolver, + context: context, + ); + + nav.pushReplacementNamed('/profile'); + }, + ); + + return MaterialApp( + theme: ThemeData( + brightness: Brightness.light, + visualDensity: VisualDensity.standard, + inputDecorationTheme: const InputDecorationTheme( + border: OutlineInputBorder(), + ), + elevatedButtonTheme: ElevatedButtonThemeData(style: buttonStyle), + textButtonTheme: TextButtonThemeData(style: buttonStyle), + outlinedButtonTheme: OutlinedButtonThemeData(style: buttonStyle), + ), + initialRoute: initialRoute, + routes: { + '/': (context) { + return SignInScreen( + actions: [ + ForgotPasswordAction((context, email) { + Navigator.pushNamed( + context, + '/forgot-password', + arguments: {'email': email}, + ); + }), + VerifyPhoneAction((context, _) { + Navigator.pushNamed(context, '/phone'); + }), + AuthStateChangeAction((context, state) { + if (!state.user!.emailVerified) { + Navigator.pushNamed(context, '/verify-email'); + } else { + Navigator.pushReplacementNamed(context, '/profile'); + } + }), + AuthStateChangeAction((context, state) { + if (!state.credential.user!.emailVerified) { + Navigator.pushNamed(context, '/verify-email'); + } else { + Navigator.pushReplacementNamed(context, '/profile'); + } + }), + mfaAction, + EmailLinkSignInAction((context) { + Navigator.pushReplacementNamed(context, '/email-link-sign-in'); + }), + ], + styles: const { + EmailFormStyle(signInButtonVariant: ButtonVariant.filled), + }, + headerBuilder: headerImage('assets/images/flutterfire_logo.png'), + sideBuilder: sideImage('assets/images/flutterfire_logo.png'), + subtitleBuilder: (context, action) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + action == AuthAction.signIn + ? 'Welcome to Firebase UI! Please sign in to continue.' + : 'Welcome to Firebase UI! Please create an account to continue', + ), + ); + }, + footerBuilder: (context, action) { + return Center( + child: Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + action == AuthAction.signIn + ? 'By signing in, you agree to our terms and conditions.' + : 'By registering, you agree to our terms and conditions.', + style: const TextStyle(color: Colors.grey), + ), + ), + ); + }, + ); + }, + '/verify-email': (context) { + return EmailVerificationScreen( + headerBuilder: headerIcon(Icons.verified), + sideBuilder: sideIcon(Icons.verified), + actionCodeSettings: actionCodeSettings, + actions: [ + EmailVerifiedAction(() { + Navigator.pushReplacementNamed(context, '/profile'); + }), + AuthCancelledAction((context) { + FirebaseUIAuth.signOut(context: context); + Navigator.pushReplacementNamed(context, '/'); + }), + ], + ); + }, + '/phone': (context) { + return PhoneInputScreen( + actions: [ + SMSCodeRequestedAction((context, action, flowKey, phone) { + Navigator.of(context).pushReplacementNamed( + '/sms', + arguments: { + 'action': action, + 'flowKey': flowKey, + 'phone': phone, + }, + ); + }), + ], + headerBuilder: headerIcon(Icons.phone), + sideBuilder: sideIcon(Icons.phone), + ); + }, + '/sms': (context) { + final arguments = ModalRoute.of(context)?.settings.arguments + as Map?; + + return SMSCodeInputScreen( + actions: [ + AuthStateChangeAction((context, state) { + Navigator.of(context).pushReplacementNamed('/profile'); + }) + ], + flowKey: arguments?['flowKey'], + action: arguments?['action'], + headerBuilder: headerIcon(Icons.sms_outlined), + sideBuilder: sideIcon(Icons.sms_outlined), + ); + }, + '/forgot-password': (context) { + final arguments = ModalRoute.of(context)?.settings.arguments + as Map?; + + return ForgotPasswordScreen( + email: arguments?['email'], + headerMaxExtent: 200, + headerBuilder: headerIcon(Icons.lock), + sideBuilder: sideIcon(Icons.lock), + ); + }, + '/email-link-sign-in': (context) { + return EmailLinkSignInScreen( + actions: [ + AuthStateChangeAction((context, state) { + Navigator.pushReplacementNamed(context, '/'); + }), + ], + provider: emailLinkProviderConfig, + headerMaxExtent: 200, + headerBuilder: headerIcon(Icons.link), + sideBuilder: sideIcon(Icons.link), + ); + }, + '/profile': (context) { + return ProfileScreen( + actions: [ + SignedOutAction((context) { + Navigator.pushReplacementNamed(context, '/'); + }), + mfaAction, + ], + actionCodeSettings: actionCodeSettings, + showMFATile: true, + ); + }, + }, + title: 'Firebase UI demo', + debugShowCheckedModeBanner: false, + locale: const Locale('en'), + localizationsDelegates: [ + FirebaseUILocalizations.withDefaultOverrides(const LabelOverrides()), + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + FirebaseUILocalizations.delegate, + ], + ); + } +} diff --git a/packages/firebase_ui_auth/example/linux/.gitignore b/packages/firebase_ui_auth/example/linux/.gitignore new file mode 100644 index 000000000000..d3896c98444f --- /dev/null +++ b/packages/firebase_ui_auth/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/firebase_ui_auth/example/linux/CMakeLists.txt b/packages/firebase_ui_auth/example/linux/CMakeLists.txt new file mode 100644 index 000000000000..adb9eb957b25 --- /dev/null +++ b/packages/firebase_ui_auth/example/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "firebase_ui_example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "io.flutter.plugins.firebase_ui_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/firebase_ui_auth/example/linux/flutter/CMakeLists.txt b/packages/firebase_ui_auth/example/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000000..d5bd01648a96 --- /dev/null +++ b/packages/firebase_ui_auth/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/packages/firebase_ui_auth/example/linux/flutter/generated_plugin_registrant.cc b/packages/firebase_ui_auth/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000000..1c65bab7f8ed --- /dev/null +++ b/packages/firebase_ui_auth/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) desktop_webview_auth_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewAuthPlugin"); + desktop_webview_auth_plugin_register_with_registrar(desktop_webview_auth_registrar); +} diff --git a/packages/firebase_ui_auth/example/linux/flutter/generated_plugin_registrant.h b/packages/firebase_ui_auth/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000000..e0f0a47bc08f --- /dev/null +++ b/packages/firebase_ui_auth/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/firebase_ui_auth/example/linux/flutter/generated_plugins.cmake b/packages/firebase_ui_auth/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..e5bf8b2ecb02 --- /dev/null +++ b/packages/firebase_ui_auth/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + desktop_webview_auth +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/firebase_ui_auth/example/linux/main.cc b/packages/firebase_ui_auth/example/linux/main.cc new file mode 100644 index 000000000000..e7c5c5437037 --- /dev/null +++ b/packages/firebase_ui_auth/example/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/firebase_ui_auth/example/linux/my_application.cc b/packages/firebase_ui_auth/example/linux/my_application.cc new file mode 100644 index 000000000000..d60fbee8c004 --- /dev/null +++ b/packages/firebase_ui_auth/example/linux/my_application.cc @@ -0,0 +1,107 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "firebase_ui_example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "firebase_ui_example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/packages/firebase_ui_auth/example/linux/my_application.h b/packages/firebase_ui_auth/example/linux/my_application.h new file mode 100644 index 000000000000..72271d5e4170 --- /dev/null +++ b/packages/firebase_ui_auth/example/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/firebase_ui_auth/example/macos/.gitignore b/packages/firebase_ui_auth/example/macos/.gitignore new file mode 100644 index 000000000000..746adbb6b9e1 --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/packages/firebase_ui_auth/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/firebase_ui_auth/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000000..4b81f9b2d200 --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/firebase_ui_auth/example/macos/Flutter/Flutter-Release.xcconfig b/packages/firebase_ui_auth/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000000..5caa9d1579e4 --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/firebase_ui_auth/example/macos/Podfile b/packages/firebase_ui_auth/example/macos/Podfile new file mode 100644 index 000000000000..22d9caad2e9d --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.12' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/firebase_ui_auth/example/macos/Runner.xcodeproj/project.pbxproj b/packages/firebase_ui_auth/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..2510f0494b30 --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,637 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 5A1968DEC0E00AD3A7C83ACE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68543B5EFF434AB35E5B519A /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1BCE21F44B33D08989B8AFC1 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* firebase_ui_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = firebase_ui_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 68543B5EFF434AB35E5B519A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + D224B1C435F5909B86D5F172 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + DF9E6F18FCE53B288DFC2F68 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5A1968DEC0E00AD3A7C83ACE /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + C9DF0F4ED19FEFFE79E8E3BE /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* firebase_ui_example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + C9DF0F4ED19FEFFE79E8E3BE /* Pods */ = { + isa = PBXGroup; + children = ( + 1BCE21F44B33D08989B8AFC1 /* Pods-Runner.debug.xcconfig */, + D224B1C435F5909B86D5F172 /* Pods-Runner.release.xcconfig */, + DF9E6F18FCE53B288DFC2F68 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 68543B5EFF434AB35E5B519A /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 736778EA2F52FA14E3F90547 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 29B9D2F5EB6E6BDBA26D3667 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* firebase_ui_example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 29B9D2F5EB6E6BDBA26D3667 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 736778EA2F52FA14E3F90547 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = YYX2P3XVJ7; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = YYX2P3XVJ7; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = YYX2P3XVJ7; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/firebase_ui_auth/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/firebase_ui_auth/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/firebase_ui_auth/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/firebase_ui_auth/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..dd9d0d67a12e --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/firebase_ui_auth/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/firebase_ui_auth/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/firebase_ui_auth/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/firebase_ui_auth/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/firebase_ui_auth/example/macos/Runner/AppDelegate.swift b/packages/firebase_ui_auth/example/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000000..d53ef6437726 --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..a2ec33f19f11 --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000000..3c4935a7ca84 Binary files /dev/null and b/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 000000000000..ed4cc1642168 Binary files /dev/null and b/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 000000000000..483be6138973 Binary files /dev/null and b/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 000000000000..bcbf36df2f2a Binary files /dev/null and b/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 000000000000..9c0a65286476 Binary files /dev/null and b/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 000000000000..e71a726136a4 Binary files /dev/null and b/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 000000000000..8a31fe2dd3f9 Binary files /dev/null and b/packages/firebase_ui_auth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/firebase_ui_auth/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/firebase_ui_auth/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000000..80e867a4e06b --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/firebase_ui_auth/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/firebase_ui_auth/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000000..74be0e33a210 --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = firebase_ui_example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebaseUiExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 io.flutter.plugins. All rights reserved. diff --git a/packages/firebase_ui_auth/example/macos/Runner/Configs/Debug.xcconfig b/packages/firebase_ui_auth/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000000..36b0fd9464f4 --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/firebase_ui_auth/example/macos/Runner/Configs/Release.xcconfig b/packages/firebase_ui_auth/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000000..dff4f49561c8 --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/firebase_ui_auth/example/macos/Runner/Configs/Warnings.xcconfig b/packages/firebase_ui_auth/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000000..42bcbf4780b1 --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/firebase_ui_auth/example/macos/Runner/DebugProfile.entitlements b/packages/firebase_ui_auth/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000000..c34fc0a4e55a --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,18 @@ + + + + + com.apple.developer.applesignin + + Default + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/packages/firebase_ui_auth/example/macos/Runner/Info.plist b/packages/firebase_ui_auth/example/macos/Runner/Info.plist new file mode 100644 index 000000000000..4789daa6a443 --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/firebase_ui_auth/example/macos/Runner/MainFlutterWindow.swift b/packages/firebase_ui_auth/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000000..2722837ec918 --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/packages/firebase_ui_auth/example/macos/Runner/Release.entitlements b/packages/firebase_ui_auth/example/macos/Runner/Release.entitlements new file mode 100644 index 000000000000..cd2171f4278f --- /dev/null +++ b/packages/firebase_ui_auth/example/macos/Runner/Release.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.developer.applesignin + + Default + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/packages/firebase_ui_auth/example/pubspec.yaml b/packages/firebase_ui_auth/example/pubspec.yaml new file mode 100644 index 000000000000..ef65706253d3 --- /dev/null +++ b/packages/firebase_ui_auth/example/pubspec.yaml @@ -0,0 +1,91 @@ +name: firebase_ui_example +description: A new Flutter project. + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 1.0.0+1 + +environment: + sdk: '>=2.16.0 <3.0.0' + +dependencies: + cloud_firestore: ^3.5.1 + crypto: ^3.0.1 + cupertino_icons: ^1.0.2 + firebase_auth: ^3.10.0 + firebase_core: ^1.10.3 + firebase_database: ^9.1.7 + firebase_dynamic_links: ^4.3.11 + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + flutter_svg: ^1.0.0 + firebase_ui_auth: ^1.0.0-dev.0 + firebase_ui_localizations: ^1.0.0-dev.0 + firebase_ui_oauth: ^1.0.0-dev.0 + firebase_ui_oauth_apple: ^1.0.0-dev.0 + firebase_ui_oauth_facebook: ^1.0.0-dev.0 + firebase_ui_oauth_google: ^1.0.0-dev.0 + firebase_ui_oauth_twitter: ^1.0.0-dev.0 +dev_dependencies: + drive: ^1.0.0-1.0.nullsafety.1 + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + google_sign_in: ^5.3.3 + http: ^0.13.4 + integration_test: + sdk: flutter + mockito: ^5.0.0 + test: any + twitter_login: ^4.2.3 + flutter_facebook_auth: ^4.3.4+2 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec +# The following section is specific to Flutter. +flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + # To add assets to your application, add an assets section, like this: + assets: + - assets/images/ + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + fonts: + - family: SocialIcons + fonts: + - asset: packages/firebase_ui_auth/fonts/SocialIcons.ttf + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/firebase_ui_auth/example/test_driver/apple_sign_in_test.dart b/packages/firebase_ui_auth/example/test_driver/apple_sign_in_test.dart new file mode 100644 index 000000000000..0b9ec75cabf5 --- /dev/null +++ b/packages/firebase_ui_auth/example/test_driver/apple_sign_in_test.dart @@ -0,0 +1,157 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; +import 'package:mockito/mockito.dart'; +import 'package:firebase_ui_oauth_apple/src/provider.dart'; + +import 'utils.dart'; + +void main() async { + final provider = AppleProvider(); + late FirebaseAuth auth; + late MockProvider fbProvider; + + const labels = DefaultLocalizations(); + + group( + 'Sign in with Apple button', + () { + setUp(() { + auth = MockAuth(); + fbProvider = MockProvider(); + provider.firebaseAuthProvider = fbProvider; + }); + + testWidgets('has a correct button label', (tester) async { + await render( + tester, + OAuthProviderButton( + provider: provider, + auth: auth, + ), + ); + expect(find.text(labels.signInWithAppleButtonText), findsOneWidget); + }); + + testWidgets( + 'calls sign in when tapped', + (tester) async { + await render( + tester, + OAuthProviderButton( + provider: provider, + auth: auth, + ), + ); + + final button = find.byType(OAuthProviderButtonBase); + await tester.tap(button); + + await tester.pumpAndSettle(); + verify(auth.signInWithProvider(fbProvider)).called(1); + + expect(true, isTrue); + }, + ); + + testWidgets( + 'shows loading indicator when sign in is in progress', + (tester) async { + await render( + tester, + OAuthProviderButton( + provider: provider, + auth: auth, + ), + ); + + final button = find.byType(OAuthProviderButtonBase); + await tester.tap(button); + await tester.pump(); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }, + ); + + testWidgets('signs the user in', (tester) async { + final listener = MockListener(); + + await render( + tester, + AuthStateListener( + listener: (oldState, state, controller) { + listener(state); + return null; + }, + child: OAuthProviderButton( + provider: provider, + auth: auth, + ), + ), + ); + + final button = find.byType(OAuthProviderButtonBase); + await tester.tap(button); + await tester.pumpAndSettle(); + + final result = verify(listener.call(captureAny)); + expect(result.captured[1], isA()); + + final user = (result.captured[1] as SignedIn).user!; + expect(user.displayName, 'Test User'); + expect(user.email, 'test@test.com'); + }); + }, + skip: !provider.supportsPlatform(defaultTargetPlatform), + ); +} + +class MockListener extends Mock { + void call(AuthState? state) { + super.noSuchMethod( + Invocation.method( + #call, + [ + state, + ], + ), + ); + } +} + +class MockUser extends Mock implements User { + @override + String? get displayName => 'Test User'; + + @override + String? get email => 'test@test.com'; +} + +class MockCredential extends Mock implements UserCredential { + @override + User? get user => MockUser(); +} + +class MockProvider extends Mock implements AppleAuthProvider {} + +// ignore: avoid_implementing_value_types +class MockApp extends Mock implements FirebaseApp {} + +class MockAuth extends Mock implements FirebaseAuth { + @override + Future signInWithAuthProvider(Object provider) async { + return super.noSuchMethod( + Invocation.method(#signInWithAuthProvider, [provider]), + returnValue: Future.delayed(const Duration(milliseconds: 50)) + .then((_) => MockCredential()), + returnValueForMissingStub: + Future.delayed(const Duration(milliseconds: 50)) + .then((_) => MockCredential()), + ); + } +} diff --git a/packages/firebase_ui_auth/example/test_driver/email_form_test.dart b/packages/firebase_ui_auth/example/test_driver/email_form_test.dart new file mode 100644 index 000000000000..79efca71aebd --- /dev/null +++ b/packages/firebase_ui_auth/example/test_driver/email_form_test.dart @@ -0,0 +1,231 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; + +import 'utils.dart'; + +void main() { + const labels = DefaultLocalizations(); + + group('EmailForm', () { + testWidgets('validates email', (tester) async { + await render(tester, const EmailForm()); + + final inputs = find.byType(TextFormField); + final emailInput = inputs.first; + + await tester.enterText(emailInput, 'not a vailid email'); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + expect(find.text(labels.isNotAValidEmailErrorText), findsOneWidget); + }); + + testWidgets('requires password', (tester) async { + await render(tester, const EmailForm()); + + final inputs = find.byType(TextFormField); + final emailInput = inputs.first; + final passwordInput = inputs.at(1); + + await tester.enterText(emailInput, 'test@test.com'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.enterText(passwordInput, ''); + await tester.pumpAndSettle(); + + expect(find.text(labels.passwordIsRequiredErrorText), findsOneWidget); + }); + + testWidgets( + 'shows password confirmation if action is sign up', + (tester) async { + await render(tester, const EmailForm(action: AuthAction.signUp)); + + final inputs = find.byType(TextFormField); + expect(inputs, findsNWidgets(3)); + }, + ); + + testWidgets( + 'requires password confirmation', + (tester) async { + await render(tester, const EmailForm(action: AuthAction.signUp)); + + final inputs = find.byType(TextFormField); + + await tester.enterText(inputs.at(0), 'test@test.com'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.enterText(inputs.at(1), 'password'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.enterText(inputs.at(2), ''); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.pumpAndSettle(); + + expect( + find.text(labels.confirmPasswordIsRequiredErrorText), + findsOneWidget, + ); + }, + ); + + testWidgets( + 'verifies that password confirmation matches password', + (tester) async { + await render(tester, const EmailForm(action: AuthAction.signUp)); + + final inputs = find.byType(TextFormField); + + await tester.enterText(inputs.at(0), 'test@test.com'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.enterText(inputs.at(1), 'password'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.enterText(inputs.at(2), 'psasword'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.pumpAndSettle(); + + expect( + find.text(labels.confirmPasswordDoesNotMatchErrorText), + findsOneWidget, + ); + }, + ); + + testWidgets( + 'registers new user', + (tester) async { + await render(tester, const EmailForm(action: AuthAction.signUp)); + + final inputs = find.byType(TextFormField); + + await tester.enterText(inputs.at(0), 'test@test.com'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.enterText(inputs.at(1), 'password'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.enterText(inputs.at(2), 'password'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.pump(); + await Future.delayed(const Duration(milliseconds: 1)); + + expect(find.byType(LoadingIndicator), findsOneWidget); + await tester.pumpAndSettle(); + + expect(FirebaseAuth.instance.currentUser, isNotNull); + }, + ); + + testWidgets('shows wrong password error', (tester) async { + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: 'test@test.com', + password: 'password', + ); + + await FirebaseAuth.instance.signOut(); + + await render(tester, const EmailForm(action: AuthAction.signIn)); + + final inputs = find.byType(TextFormField); + + await tester.enterText(inputs.at(0), 'test@test.com'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.enterText(inputs.at(1), 'wrongpassword'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.pumpAndSettle(); + + expect(find.text(labels.wrongOrNoPasswordErrorText), findsOneWidget); + }); + + testWidgets('signs in the user', (tester) async { + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: 'test@test.com', + password: 'password', + ); + + await FirebaseAuth.instance.signOut(); + + await render( + tester, + FirebaseUIActions( + actions: [ + AuthStateChangeAction((context, state) { + expect(state, isA()); + expect(state.user, isNotNull); + expect(state.user!.email, equals('test@test.com')); + }) + ], + child: const EmailForm(action: AuthAction.signIn), + ), + ); + + final inputs = find.byType(TextFormField); + + await tester.enterText(inputs.at(0), 'test@test.com'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.enterText(inputs.at(1), 'password'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.pumpAndSettle(); + }); + + testWidgets( + 'links email and password when auth action is link', + (tester) async { + await render( + tester, + FirebaseUIActions( + actions: [ + AuthStateChangeAction((context, state) { + expect(state, isA()); + expect(state.credential, isNotNull); + expect(state.credential, isA()); + expect( + (state.credential as EmailAuthCredential).email, + equals('test@test.com'), + ); + expect( + (state.credential as EmailAuthCredential).password, + equals('password'), + ); + + expect( + FirebaseAuth.instance.currentUser!.email, + equals('test@test.com'), + ); + }) + ], + child: const EmailForm(action: AuthAction.link), + ), + ); + + await FirebaseAuth.instance.signInAnonymously(); + + final inputs = find.byType(TextFormField); + + await tester.enterText(inputs.at(0), 'test@test.com'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.enterText(inputs.at(1), 'password'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.enterText(inputs.at(2), 'password'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.pumpAndSettle(); + }, + ); + }); +} diff --git a/packages/firebase_ui_auth/example/test_driver/email_link_sign_in_view_test.dart b/packages/firebase_ui_auth/example/test_driver/email_link_sign_in_view_test.dart new file mode 100644 index 000000000000..cedcda47b86a --- /dev/null +++ b/packages/firebase_ui_auth/example/test_driver/email_link_sign_in_view_test.dart @@ -0,0 +1,57 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; + +import 'utils.dart'; + +final actionCodeSettings = ActionCodeSettings( + url: 'http://$testEmulatorHost:9099', + handleCodeInApp: true, + androidMinimumVersion: '1', + androidPackageName: 'io.flutter.plugins.firebase_ui.firebase_ui_example', + iOSBundleId: 'io.flutter.plugins.flutterfireui.flutterfireUIExample', +); + +final emailLinkProvider = EmailLinkAuthProvider( + actionCodeSettings: actionCodeSettings, +); + +void main() { + const labels = DefaultLocalizations(); + + group('EmailLinkSignInView', () { + testWidgets('validates email', (tester) async { + await render( + tester, + EmailLinkSignInView(provider: emailLinkProvider), + ); + + final input = find.byType(TextFormField); + await tester.enterText(input, 'notanemail'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.pumpAndSettle(); + + expect(find.text(labels.isNotAValidEmailErrorText), findsOneWidget); + }); + + testWidgets('sends a link to an email', (tester) async { + await render( + tester, + EmailLinkSignInView( + provider: emailLinkProvider, + ), + ); + + final input = find.byType(TextFormField); + await tester.enterText(input, 'test@test.com'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.pumpAndSettle(); + + expect(find.text(labels.signInWithEmailLinkSentText), findsOneWidget); + }); + }); +} diff --git a/packages/firebase_ui_auth/example/test_driver/facebook_sign_in_test.dart b/packages/firebase_ui_auth/example/test_driver/facebook_sign_in_test.dart new file mode 100644 index 000000000000..498ec936369e --- /dev/null +++ b/packages/firebase_ui_auth/example/test_driver/facebook_sign_in_test.dart @@ -0,0 +1,129 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_facebook_auth/flutter_facebook_auth.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; +import 'package:firebase_ui_oauth_facebook/firebase_ui_oauth_facebook.dart'; +import 'package:mockito/mockito.dart'; + +import 'utils.dart'; + +void main() async { + late FacebookProvider provider = FacebookProvider(clientId: 'clientId'); + + setUp(() { + provider.provider = MockFacebookAuth(); + }); + + const labels = DefaultLocalizations(); + + group( + 'Sign in with Facebook button', + () { + testWidgets('has a correct button label', (tester) async { + await render(tester, OAuthProviderButton(provider: provider)); + expect(find.text(labels.signInWithFacebookButtonText), findsOneWidget); + }); + + testWidgets( + 'calls sign in when tapped', + (tester) async { + await render( + tester, + OAuthProviderButton(provider: provider), + ); + + final button = find.byType(OAuthProviderButtonBase); + await tester.tap(button); + + await tester.pumpAndSettle(); + verify(provider.provider.login()).called(1); + + expect(true, isTrue); + }, + ); + + testWidgets( + 'shows loading indicator when sign in is in progress', + (tester) async { + await render( + tester, + OAuthProviderButton(provider: provider), + ); + + when(provider.provider.login()).thenAnswer( + (realInvocation) async { + await Future.delayed(const Duration(milliseconds: 50)); + return MockLoginResult(); + }, + ); + + final button = find.byType(OAuthProviderButtonBase); + await tester.tap(button); + await tester.pump(); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }, + ); + + testWidgets('signs the user in', (tester) async { + await render( + tester, + OAuthProviderButton(provider: provider), + ); + + final button = find.byType(OAuthProviderButtonBase); + await tester.tap(button); + await tester.pumpAndSettle(); + + final user = FirebaseAuth.instance.currentUser!; + + expect(user.displayName, 'Test User'); + expect(user.email, 'test@test.com'); + }); + }, + skip: !provider.supportsPlatform(defaultTargetPlatform), + ); +} + +// Mock JWT with the following payload: +// { +// "sub": "1234567890", +// "name": "Test User", +// "email": "test@test.com", +// "iat": 1516239022 +// } +const _jwt = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRlc3QgVXNlciIsImVtYWlsIjoidGVzdEB0ZXN0LmNvbSIsImlhdCI6MTUxNjIzOTAyMn0.m5qYto_Vs5ELTURC8rkD-JAJuoosdQZeuUZ_qFrEiaE'; + +class MockAccessToken extends Mock implements AccessToken { + @override + String get token => _jwt; +} + +class MockLoginResult extends Mock implements LoginResult { + @override + LoginStatus get status => LoginStatus.success; + @override + AccessToken? get accessToken => MockAccessToken(); +} + +class MockFacebookAuth extends Mock implements FacebookAuth { + @override + Future login({ + List? permissions = const ['email', 'public_profile'], + LoginBehavior? loginBehavior = LoginBehavior.dialogOnly, + }) async { + return super.noSuchMethod( + Invocation.method(#signIn, [], { + #permissions: permissions, + #behavior: loginBehavior, + }), + returnValue: MockLoginResult(), + returnValueForMissingStub: MockLoginResult(), + ); + } +} diff --git a/packages/firebase_ui_auth/example/test_driver/firebase_options.dart b/packages/firebase_ui_auth/example/test_driver/firebase_options.dart new file mode 100644 index 000000000000..a09bac6945fb --- /dev/null +++ b/packages/firebase_ui_auth/example/test_driver/firebase_options.dart @@ -0,0 +1,97 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyB7wZb2tO1-Fs6GbDADUSTs2Qs3w08Hovw', + appId: '1:406099696497:web:8639aa69bac133513574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + measurementId: 'G-X3614TQ65V', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw', + appId: '1:406099696497:android:899c6485cfce26c13574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyDooSUGSf63Ghq02_iIhtnmwMDs4HlWS6c', + appId: '1:406099696497:ios:24bb8dcaefc434a73574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + androidClientId: + '406099696497-17qn06u8a0dc717u8ul7s49ampk13lul.apps.googleusercontent.com', + iosClientId: + '406099696497-65v1b9ffv6sgfqngfjab5ol5qdikh2rm.apps.googleusercontent.com', + iosBundleId: 'io.flutter.plugins.firebaseUiExample', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyDooSUGSf63Ghq02_iIhtnmwMDs4HlWS6c', + appId: '1:406099696497:ios:24bb8dcaefc434a73574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + androidClientId: + '406099696497-17qn06u8a0dc717u8ul7s49ampk13lul.apps.googleusercontent.com', + iosClientId: + '406099696497-65v1b9ffv6sgfqngfjab5ol5qdikh2rm.apps.googleusercontent.com', + iosBundleId: 'io.flutter.plugins.firebaseUiExample', + ); +} diff --git a/packages/firebase_ui_auth/example/test_driver/firebase_ui_auth_e2e.dart b/packages/firebase_ui_auth/example/test_driver/firebase_ui_auth_e2e.dart new file mode 100644 index 000000000000..d98b6eb3a1f1 --- /dev/null +++ b/packages/firebase_ui_auth/example/test_driver/firebase_ui_auth_e2e.dart @@ -0,0 +1,41 @@ +// Copyright 2020, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'email_form_test.dart' as email_form; +import 'email_link_sign_in_view_test.dart' as email_link_sign_in_view; +import 'universal_email_sign_in_screen_test.dart' + as universal_email_sign_in_screen; +import 'phone_verification_test.dart' as phone_verification; +import 'google_sign_in_test.dart' as google_sign_in; +import 'twitter_sign_in_test.dart' as twitter_sign_in; +import 'apple_sign_in_test.dart' as apple_sign_in; +import 'facebook_sign_in_test.dart' as facebook_sign_in; + +import 'utils.dart'; + +Future main() async { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + setUpAll(prepare); + + tearDown(() async { + await FirebaseAuth.instance.signOut(); + await deleteAllAccounts(); + }); + + email_form.main(); + email_link_sign_in_view.main(); + universal_email_sign_in_screen.main(); + + if (isMobile) { + phone_verification.main(); + google_sign_in.main(); + twitter_sign_in.main(); + apple_sign_in.main(); + facebook_sign_in.main(); + } +} diff --git a/packages/firebase_ui_auth/example/test_driver/firebase_ui_auth_e2e_test.dart b/packages/firebase_ui_auth/example/test_driver/firebase_ui_auth_e2e_test.dart new file mode 100644 index 000000000000..d44896e41f7c --- /dev/null +++ b/packages/firebase_ui_auth/example/test_driver/firebase_ui_auth_e2e_test.dart @@ -0,0 +1,8 @@ +// @dart=2.9 +// Copyright 2020, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:drive/drive_driver.dart' as drive; + +void main() => drive.main(); diff --git a/packages/firebase_ui_auth/example/test_driver/google_sign_in_test.dart b/packages/firebase_ui_auth/example/test_driver/google_sign_in_test.dart new file mode 100644 index 000000000000..35e91e4e24fd --- /dev/null +++ b/packages/firebase_ui_auth/example/test_driver/google_sign_in_test.dart @@ -0,0 +1,127 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; +import 'package:firebase_ui_oauth_google/firebase_ui_oauth_google.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:mockito/mockito.dart'; + +import 'utils.dart'; + +void main() async { + late GoogleProvider provider = GoogleProvider( + clientId: 'clientId', + redirectUri: 'redirectUri', + scopes: const ['scope1', 'scope2'], + ); + + setUp(() { + provider.provider = MockGoogleSignIn(); + }); + + const labels = DefaultLocalizations(); + + group( + 'Sign in with Google button', + () { + testWidgets('has a correct button label', (tester) async { + await render(tester, OAuthProviderButton(provider: provider)); + expect(find.text(labels.signInWithGoogleButtonText), findsOneWidget); + }); + + testWidgets( + 'calls sign in when tapped', + (tester) async { + await render( + tester, + OAuthProviderButton(provider: provider), + ); + + final button = find.byType(OAuthProviderButtonBase); + await tester.tap(button); + + await tester.pumpAndSettle(); + verify(provider.provider.signIn()).called(1); + + expect(true, isTrue); + }, + ); + + testWidgets( + 'shows loading indicator when sign in is in progress', + (tester) async { + await render( + tester, + OAuthProviderButton(provider: provider), + ); + + when(provider.provider.signIn()).thenAnswer( + (realInvocation) async { + await Future.delayed(const Duration(milliseconds: 50)); + return MockGoogleSignInAccount(); + }, + ); + + final button = find.byType(OAuthProviderButtonBase); + await tester.tap(button); + await tester.pump(); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }, + ); + + testWidgets('signs the user in', (tester) async { + await render( + tester, + OAuthProviderButton(provider: provider), + ); + + final button = find.byType(OAuthProviderButtonBase); + await tester.tap(button); + await tester.pumpAndSettle(); + + final user = FirebaseAuth.instance.currentUser!; + + expect(user.displayName, 'Test User'); + expect(user.email, 'test@test.com'); + }); + }, + skip: !provider.supportsPlatform(defaultTargetPlatform), + ); +} + +// Mock JWT with the following payload: +// { +// "sub": "1234567890", +// "name": "Test User", +// "email": "test@test.com", +// "iat": 1516239022 +// } +const _jwt = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRlc3QgVXNlciIsImVtYWlsIjoidGVzdEB0ZXN0LmNvbSIsImlhdCI6MTUxNjIzOTAyMn0.m5qYto_Vs5ELTURC8rkD-JAJuoosdQZeuUZ_qFrEiaE'; + +class MockAuthentication extends Mock implements GoogleSignInAuthentication { + @override + final String accessToken = _jwt; +} + +// ignore: avoid_implementing_value_types, must_be_immutable +class MockGoogleSignInAccount extends Mock implements GoogleSignInAccount { + @override + Future get authentication async => + MockAuthentication(); +} + +class MockGoogleSignIn extends Mock implements GoogleSignIn { + @override + Future signIn() async { + return super.noSuchMethod( + Invocation.method(#signIn, []), + returnValue: MockGoogleSignInAccount(), + returnValueForMissingStub: MockGoogleSignInAccount(), + ); + } +} diff --git a/packages/firebase_ui_auth/example/test_driver/phone_verification_test.dart b/packages/firebase_ui_auth/example/test_driver/phone_verification_test.dart new file mode 100644 index 000000000000..55aadc8a1228 --- /dev/null +++ b/packages/firebase_ui_auth/example/test_driver/phone_verification_test.dart @@ -0,0 +1,199 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; + +import 'utils.dart'; + +Future sendSMS(WidgetTester tester, String phoneNumber) async { + await tester.pump(); + + final phoneInput = find.byType(TextField).at(1); + await tester.enterText(phoneInput, phoneNumber); + + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); +} + +void main() { + const labels = DefaultLocalizations(); + + group('PhoneInputScreen', () { + testWidgets( + 'pick country code', + (tester) async { + await render( + tester, + const PhoneInputScreen(), + ); + + await tester.pump(); + + final popUpMenu = find.byWidgetPredicate((widget) { + return widget is PopupMenuButton; + }); + + expect(popUpMenu, findsOneWidget); + + await tester.tap(popUpMenu); + await tester.pumpAndSettle(); + + final australia = find.text('Australia (+61)'); + expect(australia, findsOneWidget); + + await tester.tap(australia); + await tester.pumpAndSettle(); + + final inputs = find.byType(TextField); + expect(inputs, findsNWidgets(2)); + + final elements = inputs.evaluate(); + + final codeInput = elements.first.widget as TextField; + + expect(codeInput.decoration!.labelText, labels.countryCode); + expect((codeInput.decoration!.prefix! as Text).data, '+'); + expect(codeInput.controller!.text, '61'); + }, + skip: true, + ); + + testWidgets('validates phone number', (tester) async { + await render( + tester, + const PhoneInputScreen(), + ); + + await tester.pump(); + + final phoneInput = find.byType(TextField).at(1); + await tester.enterText(phoneInput, '12345'); + + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + final errorText = find.text(labels.phoneNumberInvalidErrorText); + expect(errorText, findsOneWidget); + }); + + testWidgets( + 'sends sms verification code when next is clicked', + (tester) async { + final completer = Completer(); + + await render( + tester, + PhoneInputScreen( + actions: [ + AuthStateChangeAction((context, state) { + completer.complete(); + }), + AuthStateChangeAction((context, state) { + fail('should not fail'); + }), + ], + ), + ); + + await sendSMS(tester, '123456789'); + + await completer.future; + + final codes = await getVerificationCodes(); + expect(codes['+1123456789'], isNotEmpty); + }, + ); + + testWidgets( + 'opens sms verification screen when code is requested', + (tester) async { + await render(tester, const PhoneInputScreen()); + await sendSMS(tester, '123456789'); + + expect(find.text(labels.enterSMSCodeText), findsOneWidget); + }, + ); + }); + + group('SMSCodeInputScreen', () { + testWidgets('allows to go back to phone input screen', (tester) async { + await render(tester, const PhoneInputScreen()); + await sendSMS(tester, '123456789'); + + final button = find.text(labels.goBackButtonLabel); + expect(button, findsOneWidget); + await tester.tap(button); + await tester.pumpAndSettle(); + + expect(find.byType(PhoneInputScreen), findsOneWidget); + }); + + testWidgets( + 'shows error message if invalid code was entered', + (tester) async { + await render( + tester, + const PhoneInputScreen(), + ); + await sendSMS(tester, '234567890'); + + final smsCodeInput = find.byType(SMSCodeInput); + expect(smsCodeInput, findsOneWidget); + + final codes = await getVerificationCodes(); + final code = codes['+1234567890']!; + final invalidCode = + code.split('').map(int.parse).map((v) => (v + 1) % 10).join(); + + await tester.tap(smsCodeInput); + + await tester.enterText(smsCodeInput, invalidCode); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + expect(find.byType(ErrorText), findsOneWidget); + }, + ); + + testWidgets( + 'signs in if the code is correct', + (tester) async { + final completer = Completer(); + + await render( + tester, + FirebaseUIActions( + actions: [ + AuthStateChangeAction((context, state) { + completer.complete(state); + }), + AuthStateChangeAction((context, state) { + fail("shouldn't fail"); + }), + ], + child: const PhoneInputScreen(), + ), + ); + await sendSMS(tester, '234567890'); + + final smsCodeInput = find.byType(SMSCodeInput); + expect(smsCodeInput, findsOneWidget); + + final codes = await getVerificationCodes(); + final code = codes['+1234567890']!; + + await tester.tap(smsCodeInput); + + await tester.enterText(smsCodeInput, code); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + final state = await completer.future; + expect(state.user, isNotNull); + expect(state.user!.phoneNumber, '+1234567890'); + }, + ); + }); +} diff --git a/packages/firebase_ui_auth/example/test_driver/twitter_sign_in_test.dart b/packages/firebase_ui_auth/example/test_driver/twitter_sign_in_test.dart new file mode 100644 index 000000000000..b498c6950689 --- /dev/null +++ b/packages/firebase_ui_auth/example/test_driver/twitter_sign_in_test.dart @@ -0,0 +1,127 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; +import 'package:firebase_ui_oauth_twitter/firebase_ui_oauth_twitter.dart'; +import 'package:mockito/mockito.dart'; +import 'package:twitter_login/twitter_login.dart'; +import 'package:twitter_login/entity/auth_result.dart' as twe; + +import 'utils.dart'; + +void main() async { + late TwitterProvider provider = TwitterProvider( + apiKey: 'apiKey', + apiSecretKey: 'apiSecretKey', + ); + + setUp(() { + provider.provider = MockTwitterLogin(); + }); + + const labels = DefaultLocalizations(); + + group( + 'Sign in with Twitter button', + () { + testWidgets('has a correct button label', (tester) async { + await render(tester, OAuthProviderButton(provider: provider)); + expect(find.text(labels.signInWithTwitterButtonText), findsOneWidget); + }); + + testWidgets( + 'calls sign in when tapped', + (tester) async { + await render( + tester, + OAuthProviderButton(provider: provider), + ); + + final button = find.byType(OAuthProviderButtonBase); + await tester.tap(button); + + await tester.pumpAndSettle(); + verify(provider.provider.login()).called(1); + + expect(true, isTrue); + }, + ); + + testWidgets( + 'shows loading indicator when sign in is in progress', + (tester) async { + await render( + tester, + OAuthProviderButton(provider: provider), + ); + + when(provider.provider.login()).thenAnswer( + (realInvocation) async { + await Future.delayed(const Duration(milliseconds: 50)); + return MockAuthResult(); + }, + ); + + final button = find.byType(OAuthProviderButtonBase); + await tester.tap(button); + await tester.pump(); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }, + ); + + testWidgets('signs the user in', (tester) async { + await render( + tester, + OAuthProviderButton(provider: provider), + ); + + final button = find.byType(OAuthProviderButtonBase); + await tester.tap(button); + await tester.pumpAndSettle(); + + final user = FirebaseAuth.instance.currentUser!; + + expect(user.displayName, 'Test User'); + expect(user.email, 'test@test.com'); + }); + }, + skip: !provider.supportsPlatform(defaultTargetPlatform), + ); +} + +// Mock JWT with the following payload: +// { +// "sub": "1234567890", +// "name": "Test User", +// "email": "test@test.com", +// "iat": 1516239022 +// } +const _jwt = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRlc3QgVXNlciIsImVtYWlsIjoidGVzdEB0ZXN0LmNvbSIsImlhdCI6MTUxNjIzOTAyMn0.m5qYto_Vs5ELTURC8rkD-JAJuoosdQZeuUZ_qFrEiaE'; + +class MockAuthResult extends Mock implements twe.AuthResult { + @override + TwitterLoginStatus? get status => TwitterLoginStatus.loggedIn; + @override + String? get authToken => _jwt; + @override + String? get authTokenSecret => 'secret'; +} + +class MockTwitterLogin extends Mock implements TwitterLogin { + @override + Future login({bool? forceLogin}) async { + return super.noSuchMethod( + Invocation.method( + #signIn, + [], + ), + returnValue: MockAuthResult(), + returnValueForMissingStub: MockAuthResult(), + ); + } +} diff --git a/packages/firebase_ui_auth/example/test_driver/universal_email_sign_in_screen_test.dart b/packages/firebase_ui_auth/example/test_driver/universal_email_sign_in_screen_test.dart new file mode 100644 index 000000000000..84acbb076d14 --- /dev/null +++ b/packages/firebase_ui_auth/example/test_driver/universal_email_sign_in_screen_test.dart @@ -0,0 +1,121 @@ +import 'package:firebase_auth/firebase_auth.dart' + hide EmailAuthProvider, PhoneAuthProvider; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import 'package:firebase_ui_oauth_google/firebase_ui_oauth_google.dart'; +import 'package:mockito/mockito.dart'; + +import 'utils.dart'; + +void main() { + const labels = DefaultLocalizations(); + + group('UniversalEmailSignInScreen', () { + testWidgets('validates email', (tester) async { + await render( + tester, + UniversalEmailSignInScreen( + providers: [ + EmailAuthProvider(), + PhoneAuthProvider(), + GoogleProvider(clientId: 'test-client-id'), + ], + ), + ); + + await tester.pump(); + + final input = find.byType(TextField); + expect(input, findsOneWidget); + + await tester.enterText(input, 'notavalidemail'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.pumpAndSettle(); + + expect(find.text(labels.isNotAValidEmailErrorText), findsOneWidget); + }); + + testWidgets('shows RegisterScreen if not providers found', (tester) async { + await render( + tester, + UniversalEmailSignInScreen( + providers: [ + EmailAuthProvider(), + PhoneAuthProvider(), + GoogleProvider(clientId: 'test-client-id'), + ], + ), + ); + + await tester.pump(); + + final input = find.byType(TextField); + expect(input, findsOneWidget); + + await tester.enterText(input, 'test@test.com'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.pumpAndSettle(); + + expect(find.byType(RegisterScreen), findsOneWidget); + }); + + testWidgets('shows SingInScreen with only available providers', + (tester) async { + await render( + tester, + UniversalEmailSignInScreen( + auth: MockAuth(), + providers: [ + EmailAuthProvider(), + PhoneAuthProvider(), + GoogleProvider(clientId: 'test-client-id'), + ], + ), + ); + + await tester.pump(); + + final input = find.byType(TextField); + expect(input, findsOneWidget); + + await tester.enterText(input, 'test@test.com'); + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.pumpAndSettle(); + + expect(find.byType(SignInScreen), findsOneWidget); + + if (PhoneAuthProvider().supportsPlatform(defaultTargetPlatform)) { + expect(find.text(labels.signInWithPhoneButtonText), findsOneWidget); + } + expect(find.text(labels.signInWithGoogleButtonText), findsOneWidget); + expect(find.byType(EmailForm), findsNothing); + }); + }); +} + +// ignore: avoid_implementing_value_types +class MockApp extends Mock implements FirebaseApp {} + +class MockAuth extends Mock implements FirebaseAuth { + @override + FirebaseApp get app => MockApp(); + + @override + Future> fetchSignInMethodsForEmail(String? email) async { + return super.noSuchMethod( + Invocation.method( + #fetchSignInMethodsForEmail, + [email], + ), + returnValue: ['phone', 'google.com'], + returnValueForMissingStub: ['phone', 'google.com'], + ); + } +} diff --git a/packages/firebase_ui_auth/example/test_driver/utils.dart b/packages/firebase_ui_auth/example/test_driver/utils.dart new file mode 100644 index 000000000000..439a52699576 --- /dev/null +++ b/packages/firebase_ui_auth/example/test_driver/utils.dart @@ -0,0 +1,72 @@ +import 'dart:convert'; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; + +import 'firebase_options.dart'; + +String get testEmulatorHost { + if (defaultTargetPlatform == TargetPlatform.android && !kIsWeb) { + return '10.0.2.2'; + } + return 'localhost'; +} + +bool get isMobile { + return !kIsWeb && + (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.android); +} + +Future prepare() async { + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + await FirebaseAuth.instance.useAuthEmulator(testEmulatorHost, 9099); +} + +Future render(WidgetTester tester, Widget widget) async { + await tester.pumpWidget( + MaterialApp( + home: SafeArea( + child: Scaffold( + body: Padding( + padding: const EdgeInsets.all(8), + child: widget, + ), + ), + ), + ), + ); +} + +Future deleteAllAccounts() async { + final id = DefaultFirebaseOptions.currentPlatform.projectId; + final uriString = + 'http://$testEmulatorHost:9099/emulator/v1/projects/$id/accounts'; + final res = await http.delete(Uri.parse(uriString)); + + if (res.statusCode != 200) throw Exception('Delete failed'); +} + +Future> getVerificationCodes() async { + final id = DefaultFirebaseOptions.currentPlatform.projectId; + final uriString = + 'http://$testEmulatorHost:9099/emulator/v1/projects/$id/verificationCodes'; + final res = await http.get(Uri.parse(uriString)); + + final body = json.decode(res.body); + final codes = (body['verificationCodes'] as List).fold>( + {}, + (acc, value) { + return { + ...acc, + value['phoneNumber']: value['code'], + }; + }, + ); + + return codes; +} diff --git a/packages/firebase_ui_auth/example/web/favicon.png b/packages/firebase_ui_auth/example/web/favicon.png new file mode 100644 index 000000000000..8aaa46ac1ae2 Binary files /dev/null and b/packages/firebase_ui_auth/example/web/favicon.png differ diff --git a/packages/firebase_ui_auth/example/web/icons/Icon-192.png b/packages/firebase_ui_auth/example/web/icons/Icon-192.png new file mode 100644 index 000000000000..b749bfef0747 Binary files /dev/null and b/packages/firebase_ui_auth/example/web/icons/Icon-192.png differ diff --git a/packages/firebase_ui_auth/example/web/icons/Icon-512.png b/packages/firebase_ui_auth/example/web/icons/Icon-512.png new file mode 100644 index 000000000000..88cfd48dff11 Binary files /dev/null and b/packages/firebase_ui_auth/example/web/icons/Icon-512.png differ diff --git a/packages/firebase_ui_auth/example/web/icons/Icon-maskable-192.png b/packages/firebase_ui_auth/example/web/icons/Icon-maskable-192.png new file mode 100644 index 000000000000..eb9b4d76e525 Binary files /dev/null and b/packages/firebase_ui_auth/example/web/icons/Icon-maskable-192.png differ diff --git a/packages/firebase_ui_auth/example/web/icons/Icon-maskable-512.png b/packages/firebase_ui_auth/example/web/icons/Icon-maskable-512.png new file mode 100644 index 000000000000..d69c56691fbd Binary files /dev/null and b/packages/firebase_ui_auth/example/web/icons/Icon-maskable-512.png differ diff --git a/packages/firebase_ui_auth/example/web/index.html b/packages/firebase_ui_auth/example/web/index.html new file mode 100644 index 000000000000..1026239505ed --- /dev/null +++ b/packages/firebase_ui_auth/example/web/index.html @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + firebase_ui_example + + + + + + + + + + diff --git a/packages/firebase_ui_auth/example/web/manifest.json b/packages/firebase_ui_auth/example/web/manifest.json new file mode 100644 index 000000000000..99477ce40432 --- /dev/null +++ b/packages/firebase_ui_auth/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "firebase_ui_example", + "short_name": "firebase_ui_example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/packages/firebase_ui_auth/example/windows/.gitignore b/packages/firebase_ui_auth/example/windows/.gitignore new file mode 100644 index 000000000000..d492d0d98c8f --- /dev/null +++ b/packages/firebase_ui_auth/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/packages/firebase_ui_auth/example/windows/CMakeLists.txt b/packages/firebase_ui_auth/example/windows/CMakeLists.txt new file mode 100644 index 000000000000..b2402f63dec9 --- /dev/null +++ b/packages/firebase_ui_auth/example/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(firebase_ui_example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "firebase_ui_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/packages/firebase_ui_auth/example/windows/flutter/CMakeLists.txt b/packages/firebase_ui_auth/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000000..930d2071a324 --- /dev/null +++ b/packages/firebase_ui_auth/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/firebase_ui_auth/example/windows/flutter/generated_plugin_registrant.cc b/packages/firebase_ui_auth/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000000..45e2647eb8a9 --- /dev/null +++ b/packages/firebase_ui_auth/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + DesktopWebviewAuthPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopWebviewAuthPlugin")); +} diff --git a/packages/firebase_ui_auth/example/windows/flutter/generated_plugin_registrant.h b/packages/firebase_ui_auth/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000000..dc139d85a931 --- /dev/null +++ b/packages/firebase_ui_auth/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/firebase_ui_auth/example/windows/flutter/generated_plugins.cmake b/packages/firebase_ui_auth/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..67972319482d --- /dev/null +++ b/packages/firebase_ui_auth/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + desktop_webview_auth +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/firebase_ui_auth/example/windows/runner/CMakeLists.txt b/packages/firebase_ui_auth/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000000..b9e550fba8e1 --- /dev/null +++ b/packages/firebase_ui_auth/example/windows/runner/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/firebase_ui_auth/example/windows/runner/Runner.rc b/packages/firebase_ui_auth/example/windows/runner/Runner.rc new file mode 100644 index 000000000000..acbf6949f047 --- /dev/null +++ b/packages/firebase_ui_auth/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "io.flutter.plugins" "\0" + VALUE "FileDescription", "firebase_ui_example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "firebase_ui_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 io.flutter.plugins. All rights reserved." "\0" + VALUE "OriginalFilename", "firebase_ui_example.exe" "\0" + VALUE "ProductName", "firebase_ui_example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/packages/firebase_ui_auth/example/windows/runner/flutter_window.cpp b/packages/firebase_ui_auth/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000000..b43b9095ea3a --- /dev/null +++ b/packages/firebase_ui_auth/example/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/firebase_ui_auth/example/windows/runner/flutter_window.h b/packages/firebase_ui_auth/example/windows/runner/flutter_window.h new file mode 100644 index 000000000000..6da0652f05f2 --- /dev/null +++ b/packages/firebase_ui_auth/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/firebase_ui_auth/example/windows/runner/main.cpp b/packages/firebase_ui_auth/example/windows/runner/main.cpp new file mode 100644 index 000000000000..2dd4aaa30ab1 --- /dev/null +++ b/packages/firebase_ui_auth/example/windows/runner/main.cpp @@ -0,0 +1,42 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"firebase_ui_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/firebase_ui_auth/example/windows/runner/resource.h b/packages/firebase_ui_auth/example/windows/runner/resource.h new file mode 100644 index 000000000000..d5d958dc4257 --- /dev/null +++ b/packages/firebase_ui_auth/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/firebase_ui_auth/example/windows/runner/resources/app_icon.ico b/packages/firebase_ui_auth/example/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000000..c04e20caf637 Binary files /dev/null and b/packages/firebase_ui_auth/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/firebase_ui_auth/example/windows/runner/runner.exe.manifest b/packages/firebase_ui_auth/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000000..c977c4a42589 --- /dev/null +++ b/packages/firebase_ui_auth/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/firebase_ui_auth/example/windows/runner/utils.cpp b/packages/firebase_ui_auth/example/windows/runner/utils.cpp new file mode 100644 index 000000000000..1e5f5ab33f4b --- /dev/null +++ b/packages/firebase_ui_auth/example/windows/runner/utils.cpp @@ -0,0 +1,63 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE* unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = + ::WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, + nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/packages/firebase_ui_auth/example/windows/runner/utils.h b/packages/firebase_ui_auth/example/windows/runner/utils.h new file mode 100644 index 000000000000..3879d5475579 --- /dev/null +++ b/packages/firebase_ui_auth/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/firebase_ui_auth/example/windows/runner/win32_window.cpp b/packages/firebase_ui_auth/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000000..44091b3f3c91 --- /dev/null +++ b/packages/firebase_ui_auth/example/windows/runner/win32_window.cpp @@ -0,0 +1,237 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { ++g_active_window_count; } + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { return window_handle_; } + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/packages/firebase_ui_auth/example/windows/runner/win32_window.h b/packages/firebase_ui_auth/example/windows/runner/win32_window.h new file mode 100644 index 000000000000..4ae64a12b465 --- /dev/null +++ b/packages/firebase_ui_auth/example/windows/runner/win32_window.h @@ -0,0 +1,95 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/firebase_ui_auth/lib/firebase_ui_auth.dart b/packages/firebase_ui_auth/lib/firebase_ui_auth.dart new file mode 100644 index 000000000000..cdd8f1107ad9 --- /dev/null +++ b/packages/firebase_ui_auth/lib/firebase_ui_auth.dart @@ -0,0 +1,160 @@ +// ignore_for_file: use_build_context_synchronously + +export 'src/loading_indicator.dart'; +export 'src/widgets/auth_flow_builder.dart'; +export 'src/auth_controller.dart' show AuthAction, AuthController; +export 'src/auth_state.dart' + show + Uninitialized, + FetchingProvidersForEmail, + AuthStateListenerCallback, + AuthState, + AuthStateListener, + CredentialLinked, + CredentialReceived, + SignedIn, + SigningIn, + AuthFailed, + DifferentSignInMethodsFound, + MFARequired; + +export 'src/providers/auth_provider.dart'; +export 'src/providers/email_auth_provider.dart'; +export 'src/providers/email_link_auth_provider.dart'; +export 'src/providers/phone_auth_provider.dart'; +export 'src/providers/universal_email_sign_in_provider.dart'; + +export 'src/flows/phone_auth_flow.dart'; +export 'src/flows/email_link_flow.dart'; +export 'src/flows/universal_email_sign_in_flow.dart'; + +export 'src/widgets/phone_input.dart' show PhoneInputState, PhoneInput; + +export 'src/widgets/sms_code_input.dart' show SMSCodeInputState, SMSCodeInput; + +export 'src/auth_flow.dart'; +export 'src/flows/email_flow.dart'; +export 'src/flows/oauth_flow.dart' show OAuthController, OAuthFlow; + +export 'src/oauth/social_icons.dart' show SocialIcons; +export 'src/oauth/provider_resolvers.dart' show providerIcon; +export 'src/oauth_providers.dart' show OAuthHelpers; + +export 'src/widgets/auth_flow_builder.dart'; +export 'src/widgets/email_form.dart' + show EmailForm, ForgotPasswordAction, EmailFormStyle; +export 'src/widgets/error_text.dart' show ErrorText; +export 'src/widgets/phone_verification_button.dart' + show PhoneVerificationButton; + +export 'src/widgets/internal/oauth_provider_button.dart' + show OAuthProviderButton, OAuthButtonVariant; + +export 'src/widgets/sign_out_button.dart'; +export 'src/widgets/user_avatar.dart'; +export 'src/widgets/editable_user_display_name.dart'; +export 'src/widgets/delete_account_button.dart'; +export 'src/widgets/email_input.dart'; +export 'src/widgets/password_input.dart'; +export 'src/widgets/forgot_password_button.dart'; +export 'src/widgets/reauthenticate_dialog.dart'; +export 'src/widgets/different_method_sign_in_dialog.dart'; +export 'src/widgets/email_sign_up_dialog.dart'; +export 'src/widgets/email_link_sign_in_button.dart'; + +export 'src/views/login_view.dart'; +export 'src/views/phone_input_view.dart'; +export 'src/views/sms_code_input_view.dart'; +export 'src/views/reauthenticate_view.dart'; +export 'src/views/forgot_password_view.dart'; +export 'src/views/different_method_sign_in_view.dart'; +export 'src/views/find_providers_for_email_view.dart'; +export 'src/views/email_link_sign_in_view.dart'; + +export 'src/screens/internal/responsive_page.dart' + show HeaderBuilder, SideBuilder; +export 'src/screens/phone_input_screen.dart'; +export 'src/screens/sms_code_input_screen.dart'; +export 'src/screens/sign_in_screen.dart'; +export 'src/screens/register_screen.dart'; +export 'src/screens/profile_screen.dart' show ProfileScreen; +export 'src/screens/forgot_password_screen.dart'; +export 'src/screens/universal_email_sign_in_screen.dart'; +export 'src/screens/email_link_sign_in_screen.dart'; +export 'src/screens/email_verification_screen.dart'; + +export 'src/navigation/phone_verification.dart'; +export 'src/navigation/forgot_password.dart'; +export 'src/navigation/authentication.dart'; +export 'src/actions.dart'; +export 'src/email_verification.dart'; + +export 'src/styling/theme.dart' show FirebaseUITheme; +export 'src/styling/style.dart' show FirebaseUIStyle; +export 'src/widgets/internal/universal_button.dart' show ButtonVariant; + +export 'src/mfa.dart' show startMFAVerification; + +import 'package:firebase_auth/firebase_auth.dart' hide OAuthProvider; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; + +import 'src/actions.dart'; +import 'src/oauth_providers.dart'; +import 'src/providers/auth_provider.dart'; + +class FirebaseUIAuth { + static final _providers = >{}; + static final _configuredApps = {}; + + static List providersFor(FirebaseApp app) { + return _providers[app] ?? []; + } + + static bool isAppConfigured(FirebaseApp app) { + return _providers.containsKey(app); + } + + static void configureProviders( + List configs, { + FirebaseApp? app, + }) { + if (Firebase.apps.isEmpty) { + throw Exception( + 'You must call Firebase.initializeApp() ' + 'before calling configureProviders()', + ); + } + + final resolvedApp = app ?? Firebase.app(); + + if (_configuredApps[resolvedApp] ?? false) { + throw Exception( + 'You can only configure providers once ' + 'for each Firebase App', + ); + } + + _providers[resolvedApp] = configs; + + configs.whereType().forEach((element) { + final auth = FirebaseAuth.instanceFor(app: resolvedApp); + OAuthProviders.register(auth, element); + }); + } + + static Future signOut({ + BuildContext? context, + FirebaseAuth? auth, + }) async { + final resolvedAuth = auth ?? FirebaseAuth.instance; + await OAuthProviders.signOut(resolvedAuth); + await resolvedAuth.signOut(); + + if (context != null) { + final action = FirebaseUIAction.ofType(context); + action?.callback(context); + } + } +} diff --git a/packages/firebase_ui_auth/lib/fonts/SocialIcons.ttf b/packages/firebase_ui_auth/lib/fonts/SocialIcons.ttf new file mode 100644 index 000000000000..b58b0d16285c Binary files /dev/null and b/packages/firebase_ui_auth/lib/fonts/SocialIcons.ttf differ diff --git a/packages/firebase_ui_auth/lib/src/actions.dart b/packages/firebase_ui_auth/lib/src/actions.dart new file mode 100644 index 000000000000..2e881324ffc8 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/actions.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +/// An abstract class that all actions implement. +/// The following actions are available: +/// - [AuthStateChangeAction] +/// - [SignedOutAction] +/// - [AuthCancelledAction] +/// - [EmailLinkSignInAction] +/// - [VerifyPhoneAction] +/// - [SMSCodeRequestedAction] +/// - [EmailVerifiedAction] +/// - [ForgotPasswordAction] +abstract class FirebaseUIAction { + /// Looks up an instance of an action of the type [T] provided + /// via [FirebaseUIActions]. + static T? ofType(BuildContext context) { + final w = FirebaseUIActions.maybeOf(context); + + if (w == null) return null; + + for (final action in w.actions) { + if (action is T) return action; + } + + return null; + } +} + +/// {@template ui.auth.actions.auth_state_change_action} +/// An action that is called when auth state changes. +/// You can capture any type of [AuthState] using this action. +/// +/// For example, you can perform navigation after user has signed in: +/// +/// ```dart +/// SignInScreen( +/// actions: [ +/// AuthStateChangeAction((context, state) { +/// Navigator.pushReplacementNamed(context, '/home'); +/// }), +/// ], +/// ); +/// ``` +/// {@endtemplate} +class AuthStateChangeAction extends FirebaseUIAction { + /// A callback that is being called when underlying auth flow transitioned to + /// a state of type [T]. + final void Function(BuildContext context, T state) callback; + + /// {@macro ui.auth.actions.auth_state_change_action} + AuthStateChangeAction(this.callback); + + /// Verifies that a current [state] is a [T] + bool matches(AuthState state) => state is T; + + /// Invokes the callback with the provided [context] and [state]. + void invoke(BuildContext context, T state) => callback(context, state); +} + +/// {@template ui.auth.actions.signed_out_action} +/// An action that is being called when user has signed out. +/// {@endtemplate} +class SignedOutAction extends FirebaseUIAction { + /// A callback that is being called when user has signed out. + final void Function(BuildContext context) callback; + + /// {@macro ui.auth.actions.signed_out_action} + SignedOutAction(this.callback); +} + +/// {@template ui.auth.actions.cancel} +/// An action that is being called when user has cancelled an auth action. +/// {@endtemplate} +class AuthCancelledAction extends FirebaseUIAction { + /// A callback that is being called when user has cancelled an auth action. + final void Function(BuildContext context) callback; + + /// {@macro ui.auth.actions.cancel} + AuthCancelledAction(this.callback); +} + +/// {@template ui.auth.actions.flutter_fire_ui_actions} +/// An inherited widget that provides a list of actions down the widget tree. +/// {@endtemplate} +class FirebaseUIActions extends InheritedWidget { + /// A list of [FirebaseUIAction]s that will be provided to the descendant + /// widgets. + final List actions; + + /// Looks up an instance of [FirebaseUIActions] in the widget tree. + static FirebaseUIActions? maybeOf(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } + + /// Inherits a list of actions from the context and provides those to the + /// [child]. + static Widget inherit({ + /// A [BuildContext] to inherit from. + required BuildContext from, + + /// A [Widget] to wrap with [FirebaseUIActions]. + required Widget child, + + /// A list of [FirebaseUIAction]s to provide to the [child]. + List actions = const [], + }) { + final w = maybeOf(from); + + if (w != null) { + return FirebaseUIActions( + actions: [...w.actions, ...actions], + child: child, + ); + } + + return child; + } + + /// {@macro ui.auth.actions.flutter_fire_ui_actions} + const FirebaseUIActions({ + Key? key, + required this.actions, + required Widget child, + }) : super(key: key, child: child); + + @override + bool updateShouldNotify(FirebaseUIActions oldWidget) { + return oldWidget.actions != actions; + } + + @override + InheritedElement createElement() { + return _FlutterfireUIAuthActionsElement(this); + } +} + +class _FlutterfireUIAuthActionsElement extends InheritedElement { + _FlutterfireUIAuthActionsElement(InheritedWidget widget) : super(widget); + + @override + FirebaseUIActions get widget => super.widget as FirebaseUIActions; + + @override + Widget build() { + return AuthStateListener( + listener: (oldState, newState, controller) { + for (final action in widget.actions) { + if (action is AuthStateChangeAction && action.matches(newState)) { + action.invoke(this, newState); + } + } + + return null; + }, + child: super.build(), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/auth_controller.dart b/packages/firebase_ui_auth/lib/src/auth_controller.dart new file mode 100644 index 000000000000..edcf38845a0a --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/auth_controller.dart @@ -0,0 +1,86 @@ +import 'package:firebase_auth/firebase_auth.dart' show FirebaseAuth; +import 'package:flutter/widgets.dart'; + +/// {@template ui.auth.auth_action} +/// An authentication action to perform. +/// {@endtemplate} +enum AuthAction { + /// Performs user sign in + signIn, + + /// Creates a new account with for a provided credential + signUp, + + /// Links a provided credential with currently signed in user account + link, + + /// Disables automatic credential handling. + /// It's up to the user to decide what to do with the obtained credential. + none, +} + +/// An abstract class that should be implemented by auth controllers of +/// respective auth providers. +/// +/// See also: +/// * [EmailAuthController] +/// * [EmailLinkAuthController] +/// * [OAuthController] +/// * [PhoneAuthController] +/// * [UniversalEmailSignInController] +abstract class AuthController { + /// Looks up an instance of controller of type T. + /// This method should be called only inside widgets that have + /// an [AuthFlowBuilder] as an ancestor. + static T ofType(BuildContext context) { + final ctrl = context + .dependOnInheritedWidgetOfExactType() + ?.ctrl; + + if (ctrl == null || ctrl is! T) { + throw Exception( + 'No controller of type $T found. ' + 'Make sure to wrap your code with AuthFlowBuilder<$T>', + ); + } + + return ctrl; + } + + /// {@macro ui.auth.auth_action} + AuthAction get action; + + /// {@template ui.auth.auth_controller.auth} + /// The [FirebaseAuth] instance used to perform authentication against. + /// By default, [FirebaseAuth.instance] is used. + /// {@endtemplate} + FirebaseAuth get auth; + + /// {@template ui.auth.auth_controller.reset} + /// Resets the controller to initial state. + /// Usuall called when user cancels the authentication flow. + /// {@endtemplate} + void reset(); +} + +class AuthControllerProvider extends InheritedWidget { + /// {@macro ui.auth.auth_action} + final AuthAction action; + + /// An instance of controller to provide down the widget tree. + final AuthController ctrl; + + const AuthControllerProvider({ + Key? key, + + /// {@macro flutter.widgets.ProxyWidget.child} + required Widget child, + required this.action, + required this.ctrl, + }) : super(key: key, child: child); + + @override + bool updateShouldNotify(AuthControllerProvider oldWidget) { + return ctrl != oldWidget.ctrl || action != oldWidget.action; + } +} diff --git a/packages/firebase_ui_auth/lib/src/auth_flow.dart b/packages/firebase_ui_auth/lib/src/auth_flow.dart new file mode 100644 index 000000000000..a7a145a9e188 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/auth_flow.dart @@ -0,0 +1,172 @@ +// ignore_file: unnecessary_this + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +/// An exception that is being thrown when user cancels the authentication +/// process. +class AuthCancelledException implements Exception { + AuthCancelledException([this.message = 'User has cancelled auth']); + + final String message; +} + +/// {@template ui.auth.auth_flow} +/// A class that provides a current auth state given an [AuthProvider] and +/// implements shared authentication process logic. +/// +/// Should be rarely used directly, use available implementations instead: +/// - [EmailAuthFlow] +/// - [EmailLinkFlow] +/// - [OAuthFlow] +/// - [PhoneAuthFlow] +/// - [UniversalEmailSignInFlow] +/// +/// See [AuthFlowBuilder] docs to learn how to wire up the auth flow with the +/// widget tree. +/// {@endtemplate} +class AuthFlow extends ValueNotifier + implements AuthController, AuthListener { + @override + FirebaseAuth auth; + + /// An initial auth state. Usually [Uninitialized], but varies for different + /// auth flows. + final AuthState initialState; + AuthAction? _action; + final List _onDispose = []; + + final T _provider; + + @override + T get provider => _provider..authListener = this; + + @override + AuthAction get action { + if (_action != null) { + return _action!; + } + + if (auth.currentUser != null) { + return AuthAction.link; + } + + return AuthAction.signIn; + } + + /// Use this setter to override the autoresolved [AuthAction]. + /// Autoresolved action is [AuthAction.signIn] if there is no currently signed + /// in user, [AuthAction.link] otherwise. + set action(AuthAction value) { + _action = value; + } + + /// {@template ui.auth_flow.on_dispose} + /// /// A callback that is being called when auth flow is complete and is being + /// desposed (e.g. when [AuthFlowBuilder] widget is unmounteed from the widget + /// tree). + /// {@endtemplate} + VoidCallback get onDispose { + return () { + for (var callback in _onDispose) { + callback(); + } + }; + } + + /// {@macro ui.auth_flow.on_dispose} + set onDispose(VoidCallback callback) { + _onDispose.add(callback); + } + + /// {@macro ui.auth.auth_flow} + AuthFlow({ + /// An initial [AuthState] of the auth flow + required this.initialState, + + /// {@template ui.auth.auth_flow.ctor.provider} + /// An [AuthProvider] that is used to authenticate the user. + /// {@endtemplate} + required T provider, + + /// {@macro ui.auth.auth_controller.auth} + FirebaseAuth? auth, + + /// {@macro @macro ui.auth.auth_action} + AuthAction? action, + }) : auth = auth ?? FirebaseAuth.instance, + _action = action, + _provider = provider, + super(initialState) { + _provider.authListener = this; + _provider.auth = auth ?? FirebaseAuth.instance; + } + + @override + void onCredentialReceived(AuthCredential credential) { + value = CredentialReceived(credential); + } + + @override + void onBeforeProvidersForEmailFetch() { + value = const FetchingProvidersForEmail(); + } + + @override + void onBeforeSignIn() { + value = const SigningIn(); + } + + @override + void onCredentialLinked(AuthCredential credential) { + value = CredentialLinked(credential); + } + + @override + void onDifferentProvidersFound( + String email, + List providers, + AuthCredential? credential, + ) { + value = DifferentSignInMethodsFound( + email, + providers, + credential, + ); + } + + @override + void onSignedIn(UserCredential credential) { + value = SignedIn(credential.user); + } + + @override + void reset() { + value = initialState; + onDispose(); + } + + @override + void onError(Object error) { + try { + defaultOnAuthError(provider, error); + } on AuthCancelledException { + reset(); + } on Exception catch (err) { + value = AuthFailed(err); + } + } + + @override + void onCanceled() { + value = initialState; + } + + @override + void onMFARequired(MultiFactorResolver resolver) { + value = MFARequired(resolver); + } +} diff --git a/packages/firebase_ui_auth/lib/src/auth_state.dart b/packages/firebase_ui_auth/lib/src/auth_state.dart new file mode 100644 index 000000000000..0f54b2f73ccd --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/auth_state.dart @@ -0,0 +1,275 @@ +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:flutter/widgets.dart'; +import 'package:firebase_auth/firebase_auth.dart' + show AuthCredential, MultiFactorResolver, User; + +/// An abstract class for all auth states. +/// [AuthState] transitions could be captured with an [AuthStateChangeAction]: +/// +/// ```dart +/// SignInScreen( +/// actions: [ +/// AuthStateChangeAction((context, state) { +/// print(state.user!.displayName); +/// print(state.user!.emailVerified); +/// }), +/// ], +/// ); +/// ``` +/// +/// You could also subscribe your widget to auth state transitions with +/// [AuthState.of]: +/// +/// ```dart +/// AuthFlowBuilder( +/// child: MyCustomWidget(), +/// ); +/// +///class MyCustomWidget extends StatelessWidget { +/// @override +/// Widget build(BuildContext context) { +/// final state = AuthState.of(context); +/// +/// if (state is AwaitingEmailAndPassword) { +/// return EmailForm(); +/// } else if (state is AuthFailed) { +/// return ErrorText(state); +/// } else if (state is SignedIn) { +/// return Text(state.user!.displayName); +/// } else { +/// return Text("Unknown state ${state.runtimeType}"); +/// } +/// } +///} +/// ``` +abstract class AuthState { + const AuthState(); + + /// Returns current [AuthState] of the auth flow. + /// Should be used only inside the widget that has an [AuthFlowBuilder] as + /// an ancestor. Use [maybeOf] if there is a chance that the widget is used + /// without [AuthFlowBuilder] as an ancestor. + static AuthState of(BuildContext context) => maybeOf(context)!; + + /// Returns current [AuthState] of the auth flow. + /// Could return null if no [AuthFlowBuilder] was found up the widget tree. + /// + /// See [AuthFlowBuilder] for more examples. + static AuthState? maybeOf(BuildContext context) => + context.dependOnInheritedWidgetOfExactType()?.state; +} + +/// {@template ui.auth.auth_state.uninitialized} +/// A default [AuthState] for many auth flows. +/// {@endtemplate} +class Uninitialized extends AuthState { + /// {@macrp ffui.auth.auth_state.uninitialized} + const Uninitialized(); +} + +/// {@template ui.auth.auth_state.signing_in} +/// Indicates that sign in is in progress. +/// Could be used to reflect the loading state on the ui. +/// +/// See [AuthState] docs for usage examples. +/// {@endtemplate} +class SigningIn extends AuthState { + /// {@macro ui.auth.auth_state.signing_in} + const SigningIn(); +} + +/// {@template ui.auth.auth_state.credential_received} +/// Indicates that the auth credential was successfully received. +/// This is an intermediate state that should transition to either [SignedIn], +/// [CredentialLinked] or [AuthFailed] depending on [AuthAction]. +/// Could be used to reflect the loading state on the ui. +/// +/// See [AuthState] docs for usage examples. +/// {@endtemplate} +class CredentialReceived extends AuthState { + /// A credential that was received during auth flow. + final AuthCredential credential; + + CredentialReceived(this.credential); +} + +/// {@template ui.auth.auth_state.credential_linked} +/// Indicates that the auth credential was successfully linked with the +/// currently signed in user account. +/// +/// See [AuthState] docs for usage examples. +/// {@endtemplate} +class CredentialLinked extends AuthState { + /// A credential that was linked with the currently signed in user account. + final AuthCredential credential; + + /// {@macro ui.auth.auth_state.credential_linked} + CredentialLinked(this.credential); +} + +/// {@template ui.auth.auth_state.auth_failed} +/// An [AuthState] that indicates that something went wrong during +/// authentication. +/// +/// See [AuthState] docs for usage examples. +/// {@endtemplate} +class AuthFailed extends AuthState { + /// The error that occurred during authentication. + /// Often this is an instance of [FirebaseAuthException] that might contain + /// more details about the error. + /// + /// There is an [ErrorText] widget that can be used to display error details + /// in human readable form. + final Exception exception; + + /// {@macro ui.auth.auth_state.auth_failed} + AuthFailed(this.exception); +} + +/// {@template ui.auth.auth_state.signed_in} +/// An [AuthState] that indicates that the user has successfully signed in. +/// +/// See [AuthState] docs for usage examples. +/// {@endtemplate} +class SignedIn extends AuthState { + /// An instance of the [User] that was signed in. + final User? user; + + /// {@macro ui.auth.auth_state.signed_in} + SignedIn(this.user); +} + +/// {@template ui.auth.auth_state.different_sign_in_methods_found} +/// An [AuthState] that indicates that there are different auth providers +/// associated with an email that was used to authenticate. +/// +/// See [AuthState] docs for usage examples. +/// {@endtemplate} +class DifferentSignInMethodsFound extends AuthState { + /// An email that has different auth providers associated with. + final String email; + + /// An instance of the auth credential that was obtained during sign in flow. + /// Could be used to link with the user account after a sign in using on of + /// the available [methods]. + final AuthCredential? credential; + + /// A list of provider ids that were found for the [email]. + final List methods; + + /// {@macro ui.auth.auth_state.different_sign_in_methods_found} + DifferentSignInMethodsFound(this.email, this.methods, this.credential); +} + +/// {@template ui.auth.auth_state.fetching_providers_for_email} +/// An [AuthState] that indicates that there is a lookup of available providers +/// for an email in progress. +/// +/// See [AuthState] docs for usage examples. +/// {@endtemplate} +class FetchingProvidersForEmail extends AuthState { + /// {@macro ui.auth.auth_state.fetching_providers_for_email} + const FetchingProvidersForEmail(); +} + +/// {@template ui.auth.auth_state.mfa_required} +/// An [AuthState] that indicates that multi-factor authentication is required. +/// {@endtemplate} +class MFARequired extends AuthState { + /// A multi-factor resolver that should be used to complete MFA. + final MultiFactorResolver resolver; + + const MFARequired(this.resolver); +} + +class AuthStateProvider extends InheritedWidget { + final AuthState state; + + const AuthStateProvider({ + Key? key, + required this.state, + required Widget child, + }) : super(key: key, child: child); + + @override + bool updateShouldNotify(AuthStateProvider oldWidget) { + return state != oldWidget.state; + } +} + +/// {@template ui.auth.auth_state.auth_state_transition} +/// A sub-type of the [Notification] that is used to notify about auth state +/// transitions. You could use [NotificationListener], but it is recommended +/// to use [AuthStateListener] instead. +/// {@endtemplate} +class AuthStateTransition extends Notification { + /// Previous [AuthState]. + final AuthState from; + + /// Current [AuthState]. + final AuthState to; + + /// An instance of [AuthController] that could be used to perform further + /// actions of the auth flow. + final T controller; + + /// {@macro ui.auth.auth_state.auth_state_transition}} + AuthStateTransition(this.from, this.to, this.controller); +} + +typedef AuthStateListenerCallback = bool? Function( + AuthState oldState, + AuthState state, + T controller, +); + +/// {@template ui.auth.auth_state.auth_state_listener} +/// A [Widget] that could be used to listen auth state transitions. +/// +/// For example, you could show a snackbar when some error occurs: +/// +/// ```dart +/// AuthStateListener( +/// child: LoginView( +/// actions: AuthAction.signIn, +/// providers: [EmailProvider()], +/// ), +/// listener: (oldState, state, controller) { +/// if (state is AuthFailed) { +/// ScaffoldMessenger.of(context).showSnackBar( +/// SnackBar(content: ErrorText(exception: state.exception), +/// ); +/// } +/// } +/// ) +/// ``` +/// {@endtemplate} +class AuthStateListener extends StatelessWidget { + final Widget child; + final AuthStateListenerCallback listener; + + const AuthStateListener({ + Key? key, + required this.child, + required this.listener, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return NotificationListener( + onNotification: (notification) { + if (notification is! AuthStateTransition) { + return false; + } + + return listener( + notification.from, + notification.to, + notification.controller, + ) ?? + false; + }, + child: child, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/configs/countries.dart b/packages/firebase_ui_auth/lib/src/configs/countries.dart new file mode 100644 index 000000000000..574f5d41978c --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/configs/countries.dart @@ -0,0 +1,224 @@ +part of '../widgets/phone_input.dart'; + +final countries = [ + {'name': 'Afghanistan', 'phoneCode': '93', 'countryCode': 'AF'}, + {'name': 'Albania', 'phoneCode': '355', 'countryCode': 'AL'}, + {'name': 'Algeria', 'phoneCode': '213', 'countryCode': 'DZ'}, + {'name': 'American Samoa', 'phoneCode': '1-684', 'countryCode': 'AS'}, + {'name': 'Andorra', 'phoneCode': '376', 'countryCode': 'AD'}, + {'name': 'Angola', 'phoneCode': '244', 'countryCode': 'AO'}, + {'name': 'Antigua and Barbuda', 'phoneCode': '1-268', 'countryCode': 'AG'}, + {'name': 'Argentina', 'phoneCode': '54', 'countryCode': 'AR'}, + {'name': 'Armenia', 'phoneCode': '374', 'countryCode': 'AM'}, + {'name': 'Aruba', 'phoneCode': '297', 'countryCode': 'AW'}, + {'name': 'Australia', 'phoneCode': '61', 'countryCode': 'AU'}, + {'name': 'Austria', 'phoneCode': '43', 'countryCode': 'AT'}, + {'name': 'Azerbaijan', 'phoneCode': '994', 'countryCode': 'AZ'}, + {'name': 'Bahamas', 'phoneCode': '1-242', 'countryCode': 'BS'}, + {'name': 'Bangladesh', 'phoneCode': '880', 'countryCode': 'BD'}, + {'name': 'Barbados', 'phoneCode': '1-246', 'countryCode': 'BB'}, + {'name': 'Belarus', 'phoneCode': '375', 'countryCode': 'BY'}, + {'name': 'Belgium', 'phoneCode': '32', 'countryCode': 'BE'}, + {'name': 'Belize', 'phoneCode': '501', 'countryCode': 'BZ'}, + {'name': 'Benin', 'phoneCode': '229', 'countryCode': 'BJ'}, + {'name': 'Bermuda', 'phoneCode': '1-441', 'countryCode': 'BM'}, + {'name': 'Bhutan', 'phoneCode': '975', 'countryCode': 'BT'}, + {'name': 'Bolivia', 'phoneCode': '591', 'countryCode': 'BO'}, + {'name': 'Bosnia and Herzegovina', 'phoneCode': '387', 'countryCode': 'BA'}, + {'name': 'Botswana', 'phoneCode': '267', 'countryCode': 'BW'}, + {'name': 'Brazil', 'phoneCode': '55', 'countryCode': 'BR'}, + {'name': 'British Virgin Islands', 'phoneCode': '1-284', 'countryCode': 'VG'}, + {'name': 'Brunei', 'phoneCode': '673', 'countryCode': 'BN'}, + {'name': 'Bulgaria', 'phoneCode': '359', 'countryCode': 'BG'}, + {'name': 'Burkina Faso', 'phoneCode': '226', 'countryCode': 'BF'}, + {'name': 'Cambodia', 'phoneCode': '855', 'countryCode': 'KH'}, + {'name': 'Cameroon', 'phoneCode': '237', 'countryCode': 'CM'}, + {'name': 'Canada', 'phoneCode': '1', 'countryCode': 'CA'}, + {'name': 'Cape Verde', 'phoneCode': '238', 'countryCode': 'CV'}, + {'name': 'Cayman Islands', 'phoneCode': '1-345', 'countryCode': 'KY'}, + {'name': 'Central African Republic', 'phoneCode': '236', 'countryCode': 'CF'}, + {'name': 'Chile', 'phoneCode': '56', 'countryCode': 'CL'}, + {'name': 'Colombia', 'phoneCode': '57', 'countryCode': 'CO'}, + {'name': 'Comoros', 'phoneCode': '269', 'countryCode': 'KM'}, + {'name': 'Cook Islands', 'phoneCode': '682', 'countryCode': 'CK'}, + {'name': 'Costa Rica', 'phoneCode': '506', 'countryCode': 'CR'}, + {'name': 'Croatia', 'phoneCode': '385', 'countryCode': 'HR'}, + {'name': 'Curacao', 'phoneCode': '599', 'countryCode': 'CW'}, + {'name': 'Cyprus', 'phoneCode': '357', 'countryCode': 'CY'}, + {'name': 'Czech Republic', 'phoneCode': '420', 'countryCode': 'CZ'}, + { + 'name': 'Democratic Republic of the Congo', + 'phoneCode': '243', + 'countryCode': 'CD' + }, + {'name': 'Denmark', 'phoneCode': '45', 'countryCode': 'DK'}, + {'name': 'Djibouti', 'phoneCode': '253', 'countryCode': 'DJ'}, + {'name': 'Dominica', 'phoneCode': '1-767', 'countryCode': 'DM'}, + {'name': 'Dominican Republic', 'phoneCode': '1-809', 'countryCode': 'DO'}, + {'name': 'Dominican Republic', 'phoneCode': '1-829', 'countryCode': 'DO'}, + {'name': 'Dominican Republic', 'phoneCode': '1-849', 'countryCode': 'DO'}, + {'name': 'East Timor', 'phoneCode': '670', 'countryCode': 'TL'}, + {'name': 'Ecuador', 'phoneCode': '593', 'countryCode': 'EC'}, + {'name': 'Egypt', 'phoneCode': '20', 'countryCode': 'EG'}, + {'name': 'El Salvador', 'phoneCode': '503', 'countryCode': 'SV'}, + {'name': 'Equatorial Guinea', 'phoneCode': '240', 'countryCode': 'GQ'}, + {'name': 'Ethiopia', 'phoneCode': '251', 'countryCode': 'ET'}, + {'name': 'Falkland Islands', 'phoneCode': '500', 'countryCode': 'FK'}, + {'name': 'Faroe Islands', 'phoneCode': '298', 'countryCode': 'FO'}, + {'name': 'Fiji', 'phoneCode': '679', 'countryCode': 'FJ'}, + {'name': 'Finland', 'phoneCode': '358', 'countryCode': 'FI'}, + {'name': 'France', 'phoneCode': '33', 'countryCode': 'FR'}, + {'name': 'French Guiana', 'phoneCode': '594', 'countryCode': 'GF'}, + {'name': 'Gabon', 'phoneCode': '241', 'countryCode': 'GA'}, + {'name': 'Gambia', 'phoneCode': '220', 'countryCode': 'GM'}, + {'name': 'Georgia', 'phoneCode': '995', 'countryCode': 'GE'}, + {'name': 'Germany', 'phoneCode': '49', 'countryCode': 'DE'}, + {'name': 'Ghana', 'phoneCode': '233', 'countryCode': 'GH'}, + {'name': 'Gibraltar', 'phoneCode': '350', 'countryCode': 'GI'}, + {'name': 'Greece', 'phoneCode': '30', 'countryCode': 'GR'}, + {'name': 'Greenland', 'phoneCode': '299', 'countryCode': 'GL'}, + {'name': 'Grenada', 'phoneCode': '1-473', 'countryCode': 'GD'}, + {'name': 'Guadeloupe', 'phoneCode': '590', 'countryCode': 'GP'}, + {'name': 'Guatemala', 'phoneCode': '502', 'countryCode': 'GT'}, + {'name': 'Guernsey', 'phoneCode': '44-1481', 'countryCode': 'GG'}, + {'name': 'Guyana', 'phoneCode': '592', 'countryCode': 'GY'}, + {'name': 'Haiti', 'phoneCode': '509', 'countryCode': 'HT'}, + {'name': 'Honduras', 'phoneCode': '504', 'countryCode': 'HN'}, + {'name': 'Hong Kong', 'phoneCode': '852', 'countryCode': 'HK'}, + {'name': 'Hungary', 'phoneCode': '36', 'countryCode': 'HU'}, + {'name': 'India', 'phoneCode': '91', 'countryCode': 'IN'}, + {'name': 'Indonesia', 'phoneCode': '62', 'countryCode': 'ID'}, + {'name': 'Iraq', 'phoneCode': '964', 'countryCode': 'IQ'}, + {'name': 'Ireland', 'phoneCode': '353', 'countryCode': 'IE'}, + {'name': 'Isle of Man', 'phoneCode': '44-1624', 'countryCode': 'IM'}, + {'name': 'Israel', 'phoneCode': '972', 'countryCode': 'IL'}, + {'name': 'Italy', 'phoneCode': '39', 'countryCode': 'IT'}, + {'name': 'Ivory Coast', 'phoneCode': '225', 'countryCode': 'CI'}, + {'name': 'Jamaica', 'phoneCode': '1-876', 'countryCode': 'JM'}, + {'name': 'Japan', 'phoneCode': '81', 'countryCode': 'JP'}, + {'name': 'Jersey', 'phoneCode': '44-1534', 'countryCode': 'JE'}, + {'name': 'Jordan', 'phoneCode': '962', 'countryCode': 'JO'}, + {'name': 'Kazakhstan', 'phoneCode': '7', 'countryCode': 'KZ'}, + {'name': 'Kenya', 'phoneCode': '254', 'countryCode': 'KE'}, + {'name': 'Kuwait', 'phoneCode': '965', 'countryCode': 'KW'}, + {'name': 'Kyrgyzstan', 'phoneCode': '996', 'countryCode': 'KG'}, + {'name': 'Laos', 'phoneCode': '856', 'countryCode': 'LA'}, + {'name': 'Latvia', 'phoneCode': '371', 'countryCode': 'LV'}, + {'name': 'Lebanon', 'phoneCode': '961', 'countryCode': 'LB'}, + {'name': 'Lesotho', 'phoneCode': '266', 'countryCode': 'LS'}, + {'name': 'Libya', 'phoneCode': '218', 'countryCode': 'LY'}, + {'name': 'Liechtenstein', 'phoneCode': '423', 'countryCode': 'LI'}, + {'name': 'Lithuania', 'phoneCode': '370', 'countryCode': 'LT'}, + {'name': 'Luxembourg', 'phoneCode': '352', 'countryCode': 'LU'}, + {'name': 'Macau', 'phoneCode': '853', 'countryCode': 'MO'}, + {'name': 'Macedonia', 'phoneCode': '389', 'countryCode': 'MK'}, + {'name': 'Madagascar', 'phoneCode': '261', 'countryCode': 'MG'}, + {'name': 'Malawi', 'phoneCode': '265', 'countryCode': 'MW'}, + {'name': 'Malaysia', 'phoneCode': '60', 'countryCode': 'MY'}, + {'name': 'Malta', 'phoneCode': '356', 'countryCode': 'MT'}, + {'name': 'Mauritius', 'phoneCode': '230', 'countryCode': 'MU'}, + {'name': 'Mayotte', 'phoneCode': '262', 'countryCode': 'YT'}, + {'name': 'Mexico', 'phoneCode': '52', 'countryCode': 'MX'}, + {'name': 'Micronesia', 'phoneCode': '691', 'countryCode': 'FM'}, + {'name': 'Moldova', 'phoneCode': '373', 'countryCode': 'MD'}, + {'name': 'Mongolia', 'phoneCode': '976', 'countryCode': 'MN'}, + {'name': 'Montenegro', 'phoneCode': '382', 'countryCode': 'ME'}, + {'name': 'Montserrat', 'phoneCode': '1-664', 'countryCode': 'MS'}, + {'name': 'Morocco', 'phoneCode': '212', 'countryCode': 'MA'}, + {'name': 'Mozambique', 'phoneCode': '258', 'countryCode': 'MZ'}, + {'name': 'Myanmar', 'phoneCode': '95', 'countryCode': 'MM'}, + {'name': 'Namibia', 'phoneCode': '264', 'countryCode': 'NA'}, + {'name': 'Nepal', 'phoneCode': '977', 'countryCode': 'NP'}, + {'name': 'Netherlands', 'phoneCode': '31', 'countryCode': 'NL'}, + {'name': 'New Caledonia', 'phoneCode': '687', 'countryCode': 'NC'}, + {'name': 'New Zealand', 'phoneCode': '64', 'countryCode': 'NZ'}, + {'name': 'Nicaragua', 'phoneCode': '505', 'countryCode': 'NI'}, + {'name': 'Niger', 'phoneCode': '227', 'countryCode': 'NE'}, + {'name': 'Nigeria', 'phoneCode': '234', 'countryCode': 'NG'}, + {'name': 'Norfolk Island', 'phoneCode': '672', 'countryCode': 'NF'}, + {'name': 'Norway', 'phoneCode': '47', 'countryCode': 'NO'}, + {'name': 'Oman', 'phoneCode': '968', 'countryCode': 'OM'}, + {'name': 'Pakistan', 'phoneCode': '92', 'countryCode': 'PK'}, + {'name': 'Palestine', 'phoneCode': '970', 'countryCode': 'PS'}, + {'name': 'Panama', 'phoneCode': '507', 'countryCode': 'PA'}, + {'name': 'Papua New Guinea', 'phoneCode': '675', 'countryCode': 'PG'}, + {'name': 'Paraguay', 'phoneCode': '595', 'countryCode': 'PY'}, + {'name': 'Peru', 'phoneCode': '51', 'countryCode': 'PE'}, + {'name': 'Philippines', 'phoneCode': '63', 'countryCode': 'PH'}, + {'name': 'Poland', 'phoneCode': '48', 'countryCode': 'PL'}, + {'name': 'Portugal', 'phoneCode': '351', 'countryCode': 'PT'}, + {'name': 'Puerto Rico', 'phoneCode': '1-787', 'countryCode': 'PR'}, + {'name': 'Puerto Rico', 'phoneCode': '1-939', 'countryCode': 'PR'}, + {'name': 'Qatar', 'phoneCode': '974', 'countryCode': 'QA'}, + {'name': 'Republic of the Congo', 'phoneCode': '242', 'countryCode': 'CG'}, + {'name': 'Reunion', 'phoneCode': '262', 'countryCode': 'RE'}, + {'name': 'Romania', 'phoneCode': '40', 'countryCode': 'RO'}, + {'name': 'Russia', 'phoneCode': '7', 'countryCode': 'RU'}, + {'name': 'Rwanda', 'phoneCode': '250', 'countryCode': 'RW'}, + {'name': 'Saint Helena', 'phoneCode': '290', 'countryCode': 'SH'}, + {'name': 'Saint Kitts and Nevis', 'phoneCode': '1-869', 'countryCode': 'KN'}, + {'name': 'Saint Lucia', 'phoneCode': '1-758', 'countryCode': 'LC'}, + {'name': 'Saint Martin', 'phoneCode': '590', 'countryCode': 'MF'}, + { + 'name': 'Saint Pierre and Miquelon', + 'phoneCode': '508', + 'countryCode': 'PM' + }, + { + 'name': 'Saint Vincent and the Grenadines', + 'phoneCode': '1-784', + 'countryCode': 'VC' + }, + {'name': 'Samoa', 'phoneCode': '685', 'countryCode': 'WS'}, + {'name': 'Sao Tome and Principe', 'phoneCode': '239', 'countryCode': 'ST'}, + {'name': 'Saudi Arabia', 'phoneCode': '966', 'countryCode': 'SA'}, + {'name': 'Senegal', 'phoneCode': '221', 'countryCode': 'SN'}, + {'name': 'Serbia', 'phoneCode': '381', 'countryCode': 'RS'}, + {'name': 'Seychelles', 'phoneCode': '248', 'countryCode': 'SC'}, + {'name': 'Sierra Leone', 'phoneCode': '232', 'countryCode': 'SL'}, + {'name': 'Singapore', 'phoneCode': '65', 'countryCode': 'SG'}, + {'name': 'Slovakia', 'phoneCode': '421', 'countryCode': 'SK'}, + {'name': 'Slovenia', 'phoneCode': '386', 'countryCode': 'SI'}, + {'name': 'South Africa', 'phoneCode': '27', 'countryCode': 'ZA'}, + {'name': 'South Korea', 'phoneCode': '82', 'countryCode': 'KR'}, + {'name': 'Spain', 'phoneCode': '34', 'countryCode': 'ES'}, + {'name': 'Sri Lanka', 'phoneCode': '94', 'countryCode': 'LK'}, + {'name': 'Suriname', 'phoneCode': '597', 'countryCode': 'SR'}, + {'name': 'Swaziland', 'phoneCode': '268', 'countryCode': 'SZ'}, + {'name': 'Sweden', 'phoneCode': '46', 'countryCode': 'SE'}, + {'name': 'Switzerland', 'phoneCode': '41', 'countryCode': 'CH'}, + {'name': 'Taiwan', 'phoneCode': '886', 'countryCode': 'TW'}, + {'name': 'Tanzania', 'phoneCode': '255', 'countryCode': 'TZ'}, + {'name': 'Thailand', 'phoneCode': '66', 'countryCode': 'TH'}, + {'name': 'Togo', 'phoneCode': '228', 'countryCode': 'TG'}, + {'name': 'Tonga', 'phoneCode': '676', 'countryCode': 'TO'}, + {'name': 'Trinidad and Tobago', 'phoneCode': '1-868', 'countryCode': 'TT'}, + {'name': 'Turkey', 'phoneCode': '90', 'countryCode': 'TR'}, + {'name': 'Turkmenistan', 'phoneCode': '993', 'countryCode': 'TM'}, + { + 'name': 'Turks and Caicos Islands', + 'phoneCode': '1-649', + 'countryCode': 'TC' + }, + {'name': 'U.S. Virgin Islands', 'phoneCode': '1-340', 'countryCode': 'VI'}, + {'name': 'Uganda', 'phoneCode': '256', 'countryCode': 'UG'}, + {'name': 'Ukraine', 'phoneCode': '380', 'countryCode': 'UA'}, + {'name': 'United Arab Emirates', 'phoneCode': '971', 'countryCode': 'AE'}, + {'name': 'United Kingdom', 'phoneCode': '44', 'countryCode': 'GB'}, + {'name': 'United States', 'phoneCode': '1', 'countryCode': 'US'}, + {'name': 'Uruguay', 'phoneCode': '598', 'countryCode': 'UY'}, + {'name': 'Uzbekistan', 'phoneCode': '998', 'countryCode': 'UZ'}, + {'name': 'Venezuela', 'phoneCode': '58', 'countryCode': 'VE'}, + {'name': 'Vietnam', 'phoneCode': '84', 'countryCode': 'VN'}, + {'name': 'Yemen', 'phoneCode': '967', 'countryCode': 'YE'}, + {'name': 'Zambia', 'phoneCode': '260', 'countryCode': 'ZM'}, + {'name': 'Zimbabwe', 'phoneCode': '263', 'countryCode': 'ZW'} +].map(_CountryCodeItem.fromJson); + +final countriesByCountryCode = countries.fold>( + {}, + (previousValue, element) { + previousValue[element.countryCode] = element; + return previousValue; + }, +); diff --git a/packages/firebase_ui_auth/lib/src/email_verification.dart b/packages/firebase_ui_auth/lib/src/email_verification.dart new file mode 100644 index 000000000000..007ec1325e4a --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/email_verification.dart @@ -0,0 +1,126 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; +import 'package:flutter/material.dart'; + +/// All possible states of the email verification process. +enum EmailVerificationState { + /// An initial state of the email verification process. + unresolved, + + /// A state that indicates that the user has not yet verified the email. + unverified, + + /// A state that indicates that the user has cancelled the email verification + /// process. + dismissed, + + /// A state that indicates that email is being sent. + sending, + + /// A state that indicates that user needs to follow the link and the app + /// awaits a valid dynamic link. + pending, + + /// A state that indicates that the verification email was successfully sent. + sent, + + /// A state that indicates that the user has verified its email. + verified, + + /// A state that indiicates that email verification failed. Likely the + /// received link is invalid and verification email should be sent again. + failed, +} + +/// A [ValueNotifier] that manages the email verification process. +class EmailVerificationController extends ValueNotifier + with WidgetsBindingObserver { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth auth; + + EmailVerificationController(this.auth) + : super(EmailVerificationState.unresolved) { + final user = auth.currentUser; + + if (user != null) { + if (user.emailVerified) { + value = EmailVerificationState.verified; + } else { + value = EmailVerificationState.unverified; + } + } + + WidgetsBinding.instance.addObserver(this); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + reload(); + } + } + + /// An instance of user that is currently signed in. + User get user => auth.currentUser!; + + /// Current [EmailVerificationState]. + EmailVerificationState get state => value; + + /// Contains an [Exception] if [state] is [EmailVerificationState.failed]. + Exception? error; + + bool _isMobile(TargetPlatform platform) { + return platform == TargetPlatform.android || platform == TargetPlatform.iOS; + } + + /// Reloads firebase user and updates the [state]. + Future reload() async { + await user.reload(); + + if (user.email == null) { + value = EmailVerificationState.unresolved; + } else if (user.emailVerified) { + value = EmailVerificationState.verified; + } else { + value = EmailVerificationState.unverified; + } + } + + /// Indicates that email verification process was cancelled. + void dismiss() { + value = EmailVerificationState.dismissed; + } + + /// Sends an email with a link to verify the user's email address. + Future sendVerificationEmail( + TargetPlatform platform, + ActionCodeSettings? actionCodeSettings, + ) async { + value = EmailVerificationState.sending; + try { + await user.sendEmailVerification(actionCodeSettings); + } on Exception catch (e) { + error = e; + value = EmailVerificationState.failed; + return; + } + + if (_isMobile(platform)) { + value = EmailVerificationState.pending; + final linkData = await FirebaseDynamicLinks.instance.onLink.first; + + try { + final code = linkData.link.queryParameters['oobCode']!; + await auth.checkActionCode(code); + await auth.applyActionCode(code); + await user.reload(); + value = EmailVerificationState.verified; + } on Exception catch (err) { + error = err; + value = EmailVerificationState.failed; + } + } else { + value = EmailVerificationState.sent; + } + } +} diff --git a/packages/firebase_ui_auth/lib/src/flows/email_flow.dart b/packages/firebase_ui_auth/lib/src/flows/email_flow.dart new file mode 100644 index 000000000000..ef3a9457e637 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/flows/email_flow.dart @@ -0,0 +1,75 @@ +import 'package:firebase_auth/firebase_auth.dart' as fba; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +/// A state that indicates that email flow is not initialized with an email and +/// password. UI should show an [EmailForm] when [EmailAuthFlow]'s current state is +/// [AwaitingEmailAndPassword]. +class AwaitingEmailAndPassword extends AuthState {} + +/// A state that indicates that a new user account was created. +class UserCreated extends AuthState { + /// A [fba.UserCredential] that was obtained during authentication process. + final fba.UserCredential credential; + + UserCreated(this.credential); +} + +/// A state that indicates that user registration is in progress. +/// UIs often reflect this state with a loading indicator. +class SigningUp extends AuthState {} + +/// A controller interface of the [EmailAuthFlow]. +abstract class EmailAuthController extends AuthController { + /// Initializes the flow with an email and password. This method should be + /// called after user submits a form with email and password. + void setEmailAndPassword(String email, String password); +} + +/// {@template ui.auth.flows.email_flow} +/// An auth flow that allows authentication with email and password. +/// {@endtemplate} +class EmailAuthFlow extends AuthFlow + implements EmailAuthController, EmailAuthListener { + @override + final EmailAuthProvider provider; + + /// {@macro ui.auth.flows.email_flow} + EmailAuthFlow({ + /// {@macro ui.auth.auth_flow.ctor.provider} + required this.provider, + + /// {@macro ui.auth.auth_controller.auth} + fba.FirebaseAuth? auth, + + /// {@macro @macro ui.auth.auth_action} + AuthAction? action, + }) : super( + action: action, + initialState: AwaitingEmailAndPassword(), + auth: auth, + provider: provider, + ); + + @override + void onBeforeSignIn() { + if (action == AuthAction.signUp) { + value = SigningUp(); + } else { + super.onBeforeSignIn(); + } + } + + @override + void onSignedIn(fba.UserCredential credential) { + if (action == AuthAction.signUp) { + value = UserCreated(credential); + } else { + super.onSignedIn(credential); + } + } + + @override + void setEmailAndPassword(String email, String password) { + provider.authenticate(email, password, action); + } +} diff --git a/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart b/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart new file mode 100644 index 000000000000..6414f140639a --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart @@ -0,0 +1,90 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +/// A state that indicates that the sign in link is being sent. +/// UIs often reflect this state with a loading indicator. +class SendingLink extends AuthState { + const SendingLink(); +} + +/// A state athat indicates that the sign in link was sent and the user +/// should follow the link from their email to complete the sign in process. +/// UIs often reflect this state with a message that tells the user to follow +/// the link in their email. +class AwaitingDynamicLink extends AuthState { + const AwaitingDynamicLink(); +} + +/// A controller interface of the [EmailLinkFlow]. +abstract class EmailLinkAuthController extends AuthController { + /// Sends a sign in link to the [email]. + void sendLink(String email); +} + +/// {@template ui.auth.flows.email_link_flow} +/// A flow that implements a sign in flow with a link that is sent to the user's +/// email. +/// {@endtemplate} +class EmailLinkFlow extends AuthFlow + implements EmailLinkAuthController, EmailLinkAuthListener { + /// {@macro ui.auth.flows.email_link_flow} + EmailLinkFlow({ + /// {@macro ui.auth.auth_controller.auth} + FirebaseAuth? auth, + + /// {@macro ui.auth.auth_flow.ctor.provider} + required EmailLinkAuthProvider provider, + }) : super( + action: AuthAction.signIn, + auth: auth, + initialState: const Uninitialized(), + provider: provider, + ); + + @override + void sendLink(String email) { + provider.sendLink(email); + } + + @override + void onBeforeLinkSent(String email) { + value = const SendingLink(); + } + + @override + void onLinkSent(String email) { + value = const AwaitingDynamicLink(); + provider.awaitLink(email); + } +} + +/// {@template ui.auth.flows.email_link_flow.email_link_sign_in_action} +/// An action that indicates that email link sign in flow was triggered from +/// the UI. +/// +/// Could be used to show a [EmailLinkSignInScreen] or trigger a custom +/// logic: +/// +/// ```dart +/// SignInScreen( +/// actions: [ +/// EmailLinkSignInAction((context) { +/// Navigator.of(context).push( +/// MaterialPageRoute( +/// builder: (context) => EmailLinkSignInScreen(), +/// ), +/// ); +/// }), +/// ] +/// ); +/// ``` +/// {@endtemplate} +class EmailLinkSignInAction extends FirebaseUIAction { + /// A calback that is being called when a email link sign in flow is triggered + /// from the UI. + final void Function(BuildContext context) callback; + + /// {@macro ui.auth.flows.email_link_flow.email_link_sign_in_action} + EmailLinkSignInAction(this.callback); +} diff --git a/packages/firebase_ui_auth/lib/src/flows/oauth_flow.dart b/packages/firebase_ui_auth/lib/src/flows/oauth_flow.dart new file mode 100644 index 000000000000..c2b26824352b --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/flows/oauth_flow.dart @@ -0,0 +1,38 @@ +import 'package:firebase_auth/firebase_auth.dart' hide OAuthProvider; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:flutter/foundation.dart' show TargetPlatform; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; + +/// A controller interface of the [OAuthFlow]. +abstract class OAuthController extends AuthController { + /// Triggers a sign in. + void signIn(TargetPlatform platform); +} + +/// {@template ui.auth.flows.oauth_flow} +/// A flow that allows to authenticate using OAuth providers. +/// {@endtemplate} +class OAuthFlow extends AuthFlow + implements OAuthController, OAuthListener { + /// {@macro ui.auth.flows.oauth_flow} + OAuthFlow({ + /// {@macro ui.auth.auth_flow.ctor.provider} + required OAuthProvider provider, + + /// {@macro @macro ui.auth.auth_action} + AuthAction? action, + + /// {@macro ui.auth.auth_controller.auth} + FirebaseAuth? auth, + }) : super( + action: action, + auth: auth, + initialState: const Uninitialized(), + provider: provider, + ); + + @override + void signIn(TargetPlatform platform) { + provider.signIn(platform, action); + } +} diff --git a/packages/firebase_ui_auth/lib/src/flows/phone_auth_flow.dart b/packages/firebase_ui_auth/lib/src/flows/phone_auth_flow.dart new file mode 100644 index 000000000000..3eddac2e487d --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/flows/phone_auth_flow.dart @@ -0,0 +1,227 @@ +import 'package:firebase_auth/firebase_auth.dart' as fba; +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +/// An [AuthState] that indicates that [PhoneAuthFlow] is not yet initialized +/// wuth the phone number. UI should provide a way to submit a phone number. +class AwaitingPhoneNumber extends AuthState {} + +/// An [AuthState] that indicates that the phone number was submitted and the SMS +/// code is being sent. UIs often reflect this state with a loading indicator. +class SMSCodeRequested extends AuthState { + /// The phone number that was submitted. + final String phoneNumber; + + const SMSCodeRequested(this.phoneNumber); +} + +/// An [AuthState] that indicates that the SMS code was sucessfully veriied and +/// a [fba.AuthCredential] was obtained. +class PhoneVerified extends AuthState { + /// An [fba.AuthCredential] that was obtained during authentication process. + final fba.AuthCredential credential; + + PhoneVerified(this.credential); +} + +/// Indicates that the phone verification failed. +/// [exception] contains the details describing what exactly went wrong. +class PhoneVerificationFailed extends AuthState { + /// A [fba.FirebaseAuthException] that contains the details about the error. + final fba.FirebaseAuthException exception; + + PhoneVerificationFailed(this.exception); +} + +/// {@template ui.auth.flows.phone_auth_flow} +/// A state that indicates that the SMS code was successfully sent and the user +/// should submit it. UI should provide a way to submit the SMS code. +/// {@endtemplate} +class SMSCodeSent extends AuthState { + /// Verification ID that should be used to verify the phone number. + String? verificationId; + + /// A token that should be used to trigger another send attempt. + final int? resendToken; + + /// Web-only object that is being used to verify the phone number. + fba.ConfirmationResult? confirmationResult; + + /// {@macro ui.auth.flows.phone_auth_flow} + SMSCodeSent({ + this.verificationId, + this.resendToken, + this.confirmationResult, + }); +} + +/// {@template ui.auth.flows.autoresolution_failed_exception} +/// A state that indicates that autoresolution has failed. +/// This doesn't necessarily mean that the code was invalid, sometimes +/// a device doesn't support SMS code autoresolution. +/// {@endtemplate} +class AutoresolutionFailedException implements Exception { + final String message; + + /// {@macro ui.auth.flows.autoresolution_failed_exception} + AutoresolutionFailedException([ + this.message = 'SMS code autoresolution failed', + ]); +} + +/// A controller interface of the [PhoneAuthFlow]. +abstract class PhoneAuthController extends AuthController { + /// Initializes the flow with a phone number. This method should be called + /// after user submits a phone number. + void acceptPhoneNumber( + String phoneNumber, [ + fba.MultiFactorSession? multiFactorSession, + ]); + + /// Triggers an SMS code verification. + void verifySMSCode( + String code, { + String? verificationId, + fba.ConfirmationResult? confirmationResult, + }); +} + +/// {@template ui.auth.flows.phone_auth_flow} +/// An auth flow that allows authentication with a phone number. +/// {@endtemplate} +class PhoneAuthFlow extends AuthFlow + implements PhoneAuthController, PhoneAuthListener { + /// A verification ID that is being used to verify the phone number. + /// Not available on web, [confirmationResult] should be used instead. + String? verificationId; + + /// Web-only object that should be used to verify the phone number. + fba.ConfirmationResult? confirmationResult; + + /// {@macro ui.auth.flows.phone_auth_flow} + PhoneAuthFlow({ + /// {@macro ui.auth.auth_flow.ctor.provider} + required PhoneAuthProvider provider, + + /// {@macro ui.auth.auth_controller.auth} + fba.FirebaseAuth? auth, + + /// {@macro @macro ui.auth.auth_action} + AuthAction? action, + }) : super( + auth: auth, + initialState: AwaitingPhoneNumber(), + action: action, + provider: provider, + ); + + @override + void acceptPhoneNumber( + String phoneNumber, [ + fba.MultiFactorSession? multiFactorSession, + ]) { + provider.sendVerificationCode( + phoneNumber: phoneNumber, + action: action, + multiFactorSession: multiFactorSession, + ); + } + + @override + void verifySMSCode( + String code, { + String? verificationId, + fba.ConfirmationResult? confirmationResult, + fba.MultiFactorSession? multiFactorSession, + }) { + provider.verifySMSCode( + action: action, + code: code, + verificationId: verificationId, + confirmationResult: confirmationResult, + ); + } + + @override + void onCodeSent(String verificationId, [int? forceResendToken]) { + value = SMSCodeSent( + verificationId: verificationId, + resendToken: forceResendToken, + ); + } + + @override + void onSMSCodeRequested(String phoneNumber) { + value = SMSCodeRequested(phoneNumber); + } + + @override + void onVerificationCompleted(fba.PhoneAuthCredential credential) { + value = PhoneVerified(credential); + provider.onCredentialReceived(credential, action); + } + + @override + void onConfirmationRequested(fba.ConfirmationResult result) { + value = SMSCodeSent(confirmationResult: result); + } +} + +/// {@template ui.auth.flows.phone_auth_flow.verify_phone_number} +/// An action that is called when user requests a sign in with the phone number. +/// Could be used to show a [PhoneInputScreen] or trigger a custom +/// logic: +/// +/// ```dart +/// SignInScreen( +/// actions: [ +/// VerifyPhoneAction((context, action) { +/// Navigator.of(context).push( +/// MaterialPageRoute( +/// builder: (context) => PhoneInputScreen(), +/// ), +/// ); +/// }), +/// ] +/// ); +/// ``` +/// {@endtemplate} +class VerifyPhoneAction extends FirebaseUIAction { + /// A callback that is being called when the user requests a sign in with the + /// phone number. + final void Function(BuildContext context, AuthAction? action) callback; + + /// {@macro ui.auth.flows.phone_auth_flow.verify_phone_number} + VerifyPhoneAction(this.callback); +} + +/// {@template ui.auth.flows.phone_auth_flow.sms_code_requested_action} +/// An action that is called when user requests a sign in with the phone number. +/// Could be used to show a [SMSCodeInputScreen] or trigger a custom +/// logic: +/// +/// ```dart +/// SignInScreen( +/// actions: [ +/// SMSCodeRequestedAction((context, action, flowKey, phoneNumber) { +/// Navigator.of(context).push( +/// MaterialPageRoute( +/// builder: (context) => SMSCodeInputScreen(), +/// ), +/// ); +/// }), +/// ] +/// ); +/// ``` +/// {@endtemplate} +class SMSCodeRequestedAction extends FirebaseUIAction { + final void Function( + BuildContext context, + AuthAction? action, + Object flowKey, + String phoneNumber, + ) callback; + + /// {@macro ui.auth.flows.phone_auth_flow.sms_code_requested_action} + SMSCodeRequestedAction(this.callback); +} diff --git a/packages/firebase_ui_auth/lib/src/flows/universal_email_sign_in_flow.dart b/packages/firebase_ui_auth/lib/src/flows/universal_email_sign_in_flow.dart new file mode 100644 index 000000000000..375c0061a485 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/flows/universal_email_sign_in_flow.dart @@ -0,0 +1,42 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +/// A controller interface of the [UniversalEmailSignInFlow]. +abstract class UniversalEmailSignInController extends AuthController { + /// {@template ui.auth.auth_controller.find_providers_for_email} + /// Finds providers that can be used to sign in with a provided email. + /// Calls [AuthListener.onBeforeProvidersForEmailFetch], if request succeded – + /// [AuthListener.onDifferentProvidersFound] is called and + /// [AuthListener.onError] if failed. + /// {@endtemplate} + void findProvidersForEmail(String email); +} + +/// {@template ui.auth.flows.universal_email_sign_in_flow} +/// An auth flow that resolves providers that are accosicatied with the given +/// email. +/// {@endtemplate} +class UniversalEmailSignInFlow extends AuthFlow + implements UniversalEmailSignInController, UniversalEmailSignInListener { + // {@macro ui.auth.flows.universal_email_sign_in_flow} + UniversalEmailSignInFlow({ + /// {@macro ui.auth.auth_flow.ctor.provider} + required UniversalEmailSignInProvider provider, + + /// {@macro ui.auth.auth_controller.auth} + FirebaseAuth? auth, + + /// {@macro @macro ui.auth.auth_action} + AuthAction? action, + }) : super( + initialState: const Uninitialized(), + provider: provider, + auth: auth, + action: action, + ); + + @override + void findProvidersForEmail(String email) { + provider.findProvidersForEmail(email); + } +} diff --git a/packages/firebase_ui_auth/lib/src/loading_indicator.dart b/packages/firebase_ui_auth/lib/src/loading_indicator.dart new file mode 100644 index 000000000000..ba1f689a2191 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/loading_indicator.dart @@ -0,0 +1,43 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import './widgets/internal/platform_widget.dart'; + +class LoadingIndicator extends PlatformWidget { + final double size; + final double borderWidth; + final Color? color; + + const LoadingIndicator({ + Key? key, + required this.size, + required this.borderWidth, + this.color, + }) : super(key: key); + + @override + Widget? buildWrapper(BuildContext context, Widget child) { + return Center( + child: SizedBox( + width: size, + height: size, + child: child, + ), + ); + } + + @override + Widget buildCupertino(BuildContext context) { + return const CupertinoActivityIndicator(); + } + + @override + Widget buildMaterial(BuildContext context) { + final valueColor = color ?? Theme.of(context).colorScheme.secondary; + + return CircularProgressIndicator( + strokeWidth: borderWidth * 2, + valueColor: AlwaysStoppedAnimation(valueColor), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/mfa.dart b/packages/firebase_ui_auth/lib/src/mfa.dart new file mode 100644 index 000000000000..8b9ad9fed2f1 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/mfa.dart @@ -0,0 +1,115 @@ +import 'dart:async'; + +import 'package:firebase_auth/firebase_auth.dart' hide PhoneAuthProvider; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_auth/src/widgets/internal/universal_page_route.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart'; + +typedef SMSCodeInputScreenBuilder = Widget Function( + BuildContext context, + List actions, + Object flowKey, + AuthAction action, +); + +Future startMFAVerification({ + required BuildContext context, + required MultiFactorResolver resolver, + FirebaseAuth? auth, + SMSCodeInputScreenBuilder? smsCodeInputScreenBuilder, +}) async { + if (resolver.hints.first is PhoneMultiFactorInfo) { + return startPhoneMFAVerification( + context: context, + resolver: resolver, + auth: auth, + ); + } else { + throw Exception('Unsupported MFA type'); + } +} + +Future startPhoneMFAVerification({ + required BuildContext context, + required MultiFactorResolver resolver, + FirebaseAuth? auth, + SMSCodeInputScreenBuilder? smsCodeInputScreenBuilder, +}) async { + final session = resolver.session; + final hint = resolver.hints.first; + final completer = Completer(); + final navigator = Navigator.of(context); + + final provider = PhoneAuthProvider(); + provider.auth = auth ?? FirebaseAuth.instance; + + final flow = PhoneAuthFlow( + auth: auth ?? FirebaseAuth.instance, + action: AuthAction.none, + provider: PhoneAuthProvider(), + ); + + provider.authListener = flow; + + final flowKey = Object(); + + final actions = [ + AuthStateChangeAction((context, inner) { + final cred = inner.credential as PhoneAuthCredential; + final assertion = PhoneMultiFactorGenerator.getAssertion(cred); + try { + final cred = resolver.resolveSignIn(assertion); + completer.complete(cred); + } catch (e) { + completer.completeError(e); + } + }), + ]; + + SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + provider.sendVerificationCode( + hint: hint as PhoneMultiFactorInfo, + multiFactorSession: session, + action: AuthAction.none, + ); + }); + + Widget builder(BuildContext context) { + Widget child; + + if (smsCodeInputScreenBuilder != null) { + child = smsCodeInputScreenBuilder.call( + context, + actions, + flowKey, + AuthAction.none, + ); + } else { + child = SMSCodeInputScreen( + flowKey: flowKey, + action: AuthAction.none, + auth: auth, + actions: actions, + ); + } + + return AuthFlowBuilder( + flow: flow, + flowKey: flowKey, + child: child, + ); + } + + final pageRoute = createPageRoute( + context: context, + builder: builder, + ); + + navigator.push(pageRoute); + + final cred = await completer.future; + + navigator.pop(); + return cred; +} diff --git a/packages/firebase_ui_auth/lib/src/navigation/authentication.dart b/packages/firebase_ui_auth/lib/src/navigation/authentication.dart new file mode 100644 index 000000000000..1276a7affaab --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/navigation/authentication.dart @@ -0,0 +1,74 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +/// Shows [ReauthenticateDialog]. +Future showReauthenticateDialog({ + required BuildContext context, + + /// A list of all supported auth providers + required List providers, + + /// {@macro ui.auth.auth_controller.auth} + FirebaseAuth? auth, + + /// A callback that is being called after user has successfully signed in. + VoidCallback? onSignedIn, + + /// A label that would be used for the "Sign in" button. + String? actionButtonLabelOverride, +}) async { + final l = FirebaseUILocalizations.labelsOf(context); + + final reauthenticated = await showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: l.cancelLabel, + pageBuilder: (_, __, ___) => FirebaseUIActions.inherit( + from: context, + child: ReauthenticateDialog( + providers: providers, + auth: auth, + onSignedIn: onSignedIn, + actionButtonLabelOverride: actionButtonLabelOverride, + ), + ), + ); + + if (reauthenticated == null) return false; + return reauthenticated; +} + +/// Shows [DifferentMethodSignInDialog]. +Future showDifferentMethodSignInDialog({ + required BuildContext context, + + /// A list of providers associated with the user account + required List availableProviders, + + /// A list of all supported providers + required List providers, + + /// {@macro ui.auth.auth_controller.auth} + FirebaseAuth? auth, + + /// A callback that is being called after user has successfully signed in. + VoidCallback? onSignedIn, +}) async { + final l = FirebaseUILocalizations.labelsOf(context); + + await showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: l.cancelLabel, + pageBuilder: (context, _, __) => DifferentMethodSignInDialog( + availableProviders: availableProviders, + providers: providers, + auth: auth, + onSignedIn: () { + Navigator.of(context).pop(); + }, + ), + ); +} diff --git a/packages/firebase_ui_auth/lib/src/navigation/forgot_password.dart b/packages/firebase_ui_auth/lib/src/navigation/forgot_password.dart new file mode 100644 index 000000000000..6062dc27b023 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/navigation/forgot_password.dart @@ -0,0 +1,37 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +import '../widgets/internal/universal_page_route.dart'; + +/// Opens a [ForgotPasswordScreen]. +Future showForgotPasswordScreen({ + required BuildContext context, + + /// {@macro ui.auth.auth_controller.auth} + FirebaseAuth? auth, + + /// A email that requires password reset. + String? email, + + /// A returned widget would be placed under the title of the screen. + WidgetBuilder? subtitleBuilder, + + /// A returned widget would be placed at the bottom. + WidgetBuilder? footerBuilder, +}) async { + final route = createPageRoute( + context: context, + builder: (_) => FirebaseUIActions.inherit( + from: context, + child: ForgotPasswordScreen( + auth: auth, + email: email, + footerBuilder: footerBuilder, + subtitleBuilder: subtitleBuilder, + ), + ), + ); + + await Navigator.of(context).push(route); +} diff --git a/packages/firebase_ui_auth/lib/src/navigation/phone_verification.dart b/packages/firebase_ui_auth/lib/src/navigation/phone_verification.dart new file mode 100644 index 000000000000..179dfc65f221 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/navigation/phone_verification.dart @@ -0,0 +1,42 @@ +import 'package:firebase_auth/firebase_auth.dart' + show FirebaseAuth, MultiFactorSession, PhoneMultiFactorInfo; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:flutter/material.dart'; + +import '../widgets/internal/universal_page_route.dart'; + +/// Opens [PhoneInputScreen]. +Future startPhoneVerification({ + required BuildContext context, + + /// {@macro ui.auth.auth_action} + AuthAction? action, + + /// {@macro ui.auth.auth_controller.auth} + FirebaseAuth? auth, + + /// {@macro ui.auth.providers.phone_auth_provider.mfa_session} + MultiFactorSession? multiFactorSession, + + /// {@macro ui.auth.providers.phone_auth_provider.mfa_hint} + PhoneMultiFactorInfo? hint, + + /// Additional actions to pass down to the [PhoneInputScreen]. + List actions = const [], +}) async { + await Navigator.of(context).push( + createPageRoute( + context: context, + builder: (_) => FirebaseUIActions.inherit( + from: context, + actions: actions, + child: PhoneInputScreen( + auth: auth, + action: action, + multiFactorSession: multiFactorSession, + mfaHint: hint, + ), + ), + ), + ); +} diff --git a/packages/firebase_ui_auth/lib/src/oauth/provider_resolvers.dart b/packages/firebase_ui_auth/lib/src/oauth/provider_resolvers.dart new file mode 100644 index 000000000000..3ba14d5e1576 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/oauth/provider_resolvers.dart @@ -0,0 +1,48 @@ +// ignore_for_file: constant_identifier_names + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import 'social_icons.dart'; + +const GOOGLE_PROVIDER_ID = 'google.com'; +const APPLE_PROVIDER_ID = 'apple.com'; +const TWITTER_PROVIDER_ID = 'twitter.com'; +const FACEBOOK_PROVIDER_ID = 'facebook.com'; +const PHONE_PROVIDER_ID = 'phone'; +const PASSWORD_PROVIDER_ID = 'password'; + +/// Resolves an icon given a [providerId]. +/// +/// ```dart +/// final icon = providerIcon(context, 'google.com'); +/// Icon(icon); +/// ``` +IconData providerIcon(BuildContext context, String providerId) { + final isCupertino = CupertinoUserInterfaceLevel.maybeOf(context) != null; + + switch (providerId) { + case GOOGLE_PROVIDER_ID: + return SocialIcons.google; + case APPLE_PROVIDER_ID: + return SocialIcons.apple; + case TWITTER_PROVIDER_ID: + return SocialIcons.twitter; + case FACEBOOK_PROVIDER_ID: + return SocialIcons.facebook; + case PHONE_PROVIDER_ID: + if (isCupertino) { + return CupertinoIcons.phone; + } else { + return Icons.phone; + } + case PASSWORD_PROVIDER_ID: + if (isCupertino) { + return CupertinoIcons.mail; + } else { + return Icons.email_outlined; + } + default: + throw Exception('Unknown provider: $providerId'); + } +} diff --git a/packages/firebase_ui_auth/lib/src/oauth/social_icons.dart b/packages/firebase_ui_auth/lib/src/oauth/social_icons.dart new file mode 100644 index 000000000000..74c91e0b31f6 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/oauth/social_icons.dart @@ -0,0 +1,32 @@ +/// Flutter icons SocialIcons +/// Copyright (C) 2021 by original authors @ fluttericon.com, fontello.com +/// This font was generated by FlutterIcon.com, which is derived from Fontello. +/// +/// To use this font, place it in your fonts/ directory and include the +/// following in your pubspec.yaml +/// +/// flutter: +/// fonts: +/// - family: SocialIcons +/// fonts: +/// - asset: fonts/SocialIcons.ttf +/// +/// +/// * Font Awesome 4, Copyright (C) 2016 by Dave Gandy +/// Author: Dave Gandy +/// License: SIL () +/// Homepage: http://fortawesome.github.com/Font-Awesome/ +/// +import 'package:flutter/widgets.dart'; + +/// Font icons of the social networks. +class SocialIcons { + SocialIcons._(); + + static const _kFontFam = 'SocialIcons'; + + static const IconData twitter = IconData(0xf099, fontFamily: _kFontFam); + static const IconData facebook = IconData(0xf09a, fontFamily: _kFontFam); + static const IconData apple = IconData(0xf179, fontFamily: _kFontFam); + static const IconData google = IconData(0xf1a0, fontFamily: _kFontFam); +} diff --git a/packages/firebase_ui_auth/lib/src/oauth_providers.dart b/packages/firebase_ui_auth/lib/src/oauth_providers.dart new file mode 100644 index 000000000000..3182031f1bce --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/oauth_providers.dart @@ -0,0 +1,64 @@ +import 'package:firebase_auth/firebase_auth.dart' hide OAuthProvider; +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; + +@immutable +class ProviderKey { + final FirebaseAuth auth; + final Type providerType; + + const ProviderKey(this.auth, this.providerType); + + @override + int get hashCode => Object.hash(auth, providerType); + + @override + bool operator ==(Object other) { + return hashCode == other.hashCode; + } +} + +abstract class OAuthProviders { + static final _providers = {}; + + static void register(FirebaseAuth? auth, OAuthProvider provider) { + final resolvedAuth = auth ?? FirebaseAuth.instance; + final key = ProviderKey(resolvedAuth, provider.runtimeType); + + _providers[key] = provider; + } + + static OAuthProvider? resolve(FirebaseAuth? auth, Type providerType) { + final resolvedAuth = auth ?? FirebaseAuth.instance; + final key = ProviderKey(resolvedAuth, providerType); + return _providers[key]; + } + + static Iterable providersFor(FirebaseAuth auth) sync* { + for (final k in _providers.keys) { + if (k.auth == auth) { + yield _providers[k]!; + } + } + } + + static Future signOut([FirebaseAuth? auth]) async { + final resolvedAuth = auth ?? FirebaseAuth.instance; + final providers = providersFor(resolvedAuth); + + for (final p in providers) { + await p.logOutProvider(); + } + } +} + +extension OAuthHelpers on User { + bool isProviderLinked(String providerId) { + try { + providerData.firstWhere((e) => e.providerId == providerId); + return true; + } catch (_) { + return false; + } + } +} diff --git a/packages/firebase_ui_auth/lib/src/providers/auth_provider.dart b/packages/firebase_ui_auth/lib/src/providers/auth_provider.dart new file mode 100644 index 000000000000..f673da088af3 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/providers/auth_provider.dart @@ -0,0 +1,180 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +/// Default error handler that fetches available providers if +/// `account-exists-with-different-credential` was thrown. +/// +/// After succesful execution, auth flow should have +/// [DifferentSignInMethodsFound] state. +void defaultOnAuthError(AuthProvider provider, Object error) { + if (error is! FirebaseAuthException) { + throw error; + } + + if (error is FirebaseAuthMultiFactorException) { + provider.authListener.onMFARequired(error.resolver); + return; + } + + if (error.code == 'account-exists-with-different-credential') { + final email = error.email; + if (email == null) { + throw error; + } + + provider.findProvidersForEmail(email, error.credential); + } + + throw error; +} + +/// An interface that describes authentication process lifecycle. +/// +/// See implementers: +/// - [EmailAuthListener] +/// - [EmailLinkAuthListener] +/// - [PhoneAuthListener] +/// - [UniversalEmailSignInListener] +abstract class AuthListener { + /// Current [AuthProvider] that is being used to authenticate the user. + AuthProvider get provider; + + /// {@macro ui.auth.auth_controller.auth} + FirebaseAuth get auth; + + /// Called if an error occured during the authentication process. + void onError(Object error); + + /// Called right before the authentication process starts. + void onBeforeSignIn(); + + /// Called if the user has successfully signed in. + void onSignedIn(UserCredential credential); + + /// Called before an attempt to link the credential with currently signed in + /// user account. + void onCredentialReceived(AuthCredential credential); + + /// Called if the credential was successfully linked with the user account. + void onCredentialLinked(AuthCredential credential); + + /// Called before an attempt to fetch available providers for the email. + void onBeforeProvidersForEmailFetch(); + + /// Called when available providers for the email were successfully fetched. + void onDifferentProvidersFound( + String email, + List providers, + AuthCredential? credential, + ); + + /// Called when the user cancells the sign in process. + void onCanceled(); + + /// Called when the user has to complete MFA. + void onMFARequired(MultiFactorResolver resolver); +} + +/// {@template ui.auth.auth_provider} +/// An interface that all auth providers should implement. +/// Contains shared authentication logic. +/// {@endtemplate} +abstract class AuthProvider { + /// {@macro ui.auth.auth_controller.auth} + late FirebaseAuth auth; + + /// {@template ui.auth.auth_provider.auth_listener} + /// An instance of the [AuthListener] that is used to notify about the + /// current state of the authentication process. + /// {@endtemplate} + T get authListener; + + /// {@macro ui.auth.auth_provider.auth_listener} + set authListener(T listener); + + /// {@template ui.auth.auth_provider.provider_id} + /// String identifer of the auth provider, for example: `'password'`, + /// `'phone'` or `'google.com'`. + /// {@endtemplate} + String get providerId; + + /// Verifies that an [AuthProvider] is supported on a [platform]. + bool supportsPlatform(TargetPlatform platform); + + /// {@macro ui.auth.auth_provider} + AuthProvider(); + + /// Signs the user in with the provided [AuthCredential]. + void signInWithCredential(AuthCredential credential) { + authListener.onBeforeSignIn(); + auth + .signInWithCredential(credential) + .then(authListener.onSignedIn) + .catchError(authListener.onError); + } + + /// Links a provided [AuthCredential] with the currently signed in user + /// account. + void linkWithCredential(AuthCredential credential) { + authListener.onCredentialReceived(credential); + try { + final user = auth.currentUser!; + user + .linkWithCredential(credential) + .then((_) => authListener.onCredentialLinked(credential)) + .catchError(authListener.onError); + } catch (err) { + authListener.onError(err); + } + } + + /// Fetches available providers for the given [email]. + void findProvidersForEmail( + String email, [ + AuthCredential? credential, + ]) { + authListener.onBeforeProvidersForEmailFetch(); + + auth + .fetchSignInMethodsForEmail(email) + .then( + (methods) => authListener.onDifferentProvidersFound( + email, + methods, + credential, + ), + ) + .catchError(authListener.onError); + } + + /// {@template ui.auth.auth_provider.on_credential_received} + /// A method that is called when the user has successfully completed the + /// authentication process and decides what to do with the obtained + /// [credential]. + /// + /// [linkWithCredential] and respectful lifecycle hooks are called if [action] + /// is [AuthAction.link]. + /// + /// [signInWithCredential] and respectful lifecycle hooks are called + /// if [action] is [AuthAction.signIn]. + /// + /// [FirebaseAuth.createUserWithEmailAndPassword] and respectful lifecycle + /// hooks are called if action is [AuthAction.signUp]. + /// {@endtemplate} + void onCredentialReceived(K credential, AuthAction action) { + switch (action) { + case AuthAction.link: + linkWithCredential(credential); + break; + case AuthAction.signIn: + signInWithCredential(credential); + break; + case AuthAction.none: + authListener.onCredentialReceived(credential); + break; + default: + throw Exception('$runtimeType should handle $action'); + } + } +} diff --git a/packages/firebase_ui_auth/lib/src/providers/email_auth_provider.dart b/packages/firebase_ui_auth/lib/src/providers/email_auth_provider.dart new file mode 100644 index 000000000000..95b495757384 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/providers/email_auth_provider.dart @@ -0,0 +1,60 @@ +import 'package:firebase_auth/firebase_auth.dart' as fba; +import 'package:flutter/material.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +/// A listener of the [EmailAuthFlow] flow lifecycle. +abstract class EmailAuthListener extends AuthListener {} + +/// {@template ui.auth.providers.email_auth_provider} +/// An [AuthProvider] that allows to authenticate using email and password. +/// {@endtemplate} +class EmailAuthProvider + extends AuthProvider { + @override + late EmailAuthListener authListener; + + @override + final providerId = 'password'; + + @override + bool supportsPlatform(TargetPlatform platform) => true; + + /// Tries to create a new user account with the given [EmailAuthCredential]. + void signUpWithCredential(fba.EmailAuthCredential credential) { + authListener.onBeforeSignIn(); + auth + .createUserWithEmailAndPassword( + email: credential.email, + password: credential.password!, + ) + .then(authListener.onSignedIn) + .catchError(authListener.onError); + } + + /// Creates an [EmailAuthCredential] with the given [email] and [password] and + /// performs a corresponding [AuthAction]. + void authenticate( + String email, + String password, [ + AuthAction action = AuthAction.signIn, + ]) { + final credential = fba.EmailAuthProvider.credential( + email: email, + password: password, + ) as fba.EmailAuthCredential; + + onCredentialReceived(credential, action); + } + + @override + void onCredentialReceived( + fba.EmailAuthCredential credential, + AuthAction action, + ) { + if (action == AuthAction.signUp) { + signUpWithCredential(credential); + } else { + super.onCredentialReceived(credential, action); + } + } +} diff --git a/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart b/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart new file mode 100644 index 000000000000..8c04aeb95761 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart @@ -0,0 +1,90 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; +import 'package:flutter/foundation.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +/// A listener of the [EmailLinkFlow] lifecycle. +abstract class EmailLinkAuthListener extends AuthListener { + /// Called when the link being is sent to the user's [email]. + void onBeforeLinkSent(String email); + + /// Called when the link was sucessfully sent to the [email]. + void onLinkSent(String email); +} + +/// {@template ui.auth.providers.email_link_auth_provider} +/// An [AuthProvider] that allows to authenticate using a link that is being +/// sent to the user's email. +/// {@endtemplate} +class EmailLinkAuthProvider + extends AuthProvider { + /// A configuration of the dynamic link. + final ActionCodeSettings actionCodeSettings; + final FirebaseDynamicLinks _dynamicLinks; + + @override + late EmailLinkAuthListener authListener; + + @override + final providerId = 'email_link'; + + @override + bool supportsPlatform(TargetPlatform platform) { + if (kIsWeb) return false; + return platform == TargetPlatform.android || platform == TargetPlatform.iOS; + } + + /// {@macro ui.auth.providers.email_link_auth_provider} + EmailLinkAuthProvider({ + required this.actionCodeSettings, + + /// An instance of the [FirebaseDynamicLinks] that should be used to handle + /// the link. By default [FirebaseDynamicLinks.instance] is used. + FirebaseDynamicLinks? dynamicLinks, + }) : _dynamicLinks = dynamicLinks ?? FirebaseDynamicLinks.instance; + + /// Sends a link to the [email]. + void sendLink(String email) { + authListener.onBeforeLinkSent(email); + + final future = auth.sendSignInLinkToEmail( + email: email, + actionCodeSettings: actionCodeSettings, + ); + + future + .then((_) => authListener.onLinkSent(email)) + .catchError(authListener.onError); + } + + void _onLinkReceived(String email, PendingDynamicLinkData linkData) { + final link = linkData.link.toString(); + + if (auth.isSignInWithEmailLink(link)) { + authListener.onBeforeSignIn(); + _signInWithEmailLink(email, link); + } else { + authListener.onError( + FirebaseAuthException( + code: 'invalid-email-signin-link', + message: 'Invalid email sign in link', + ), + ); + } + } + + /// Calls [FirebaseDynamicLinks] to receive the link and perform a sign in. + /// Should be called after [EmailLinkAuthListener.onLinkSent] was called. + void awaitLink(String email) { + _dynamicLinks.onLink.first + .then((linkData) => _onLinkReceived(email, linkData)) + .catchError(authListener.onError); + } + + void _signInWithEmailLink(String email, String link) { + auth + .signInWithEmailLink(email: email, emailLink: link) + .then(authListener.onSignedIn) + .catchError(authListener.onError); + } +} diff --git a/packages/firebase_ui_auth/lib/src/providers/phone_auth_provider.dart b/packages/firebase_ui_auth/lib/src/providers/phone_auth_provider.dart new file mode 100644 index 000000000000..68f60e5c2446 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/providers/phone_auth_provider.dart @@ -0,0 +1,126 @@ +import 'package:firebase_auth/firebase_auth.dart' as fba; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/foundation.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +/// A listener of the [PhoneAuthFlow] lifecycle. +abstract class PhoneAuthListener extends AuthListener { + /// Caled when the SMS code was requested. + /// UIs usually reflect this state with a loading indicator. + /// Is not supported on web. + void onSMSCodeRequested(String phoneNumber); + + /// Called when the phone number was successfully verified. + void onVerificationCompleted(fba.PhoneAuthCredential credential); + + /// Called when the SMS code was sent. + /// UI should provide a way to enter the code. + void onCodeSent(String verificationId, [int? forceResendToken]); + + /// Caled when the SMS code was requested. + /// UIs usually reflect this state with a loading indicator. + /// Called only on web. + void onConfirmationRequested(fba.ConfirmationResult result); +} + +/// {@template ui.auth.providers.phone_auth_provider} +/// An [AuthProvider] that allows to authenticate using a phone number. +/// {@endtemplate} +class PhoneAuthProvider + extends AuthProvider { + @override + late PhoneAuthListener authListener; + + @override + final providerId = 'phone'; + + @override + bool supportsPlatform(TargetPlatform platform) { + return platform == TargetPlatform.android || + platform == TargetPlatform.iOS || + kIsWeb; + } + + /// Sends an SMS code to the [phoneNumber]. + /// If [action] is [AuthAction.link], an obtained auth credential will be + /// linked with the currently signed in user account. + /// If [action] is [AuthAction.signIn], the user will be created (if doesn't + /// exist) or signed in. + void sendVerificationCode({ + String? phoneNumber, + AuthAction action = AuthAction.signIn, + int? forceResendingToken, + + /// {@template ui.auth.providers.phone_auth_provider.mfa_session} + /// Multi-factor session to use for verification + /// {@endtemplate} + MultiFactorSession? multiFactorSession, + + /// {@template ui.auth.providers.phone_auth_provider.mfa_hint} + /// Multi-factor session info to use for verification + /// {@endtemplate} + final PhoneMultiFactorInfo? hint, + }) { + final phone = phoneNumber ?? hint!.phoneNumber; + authListener.onSMSCodeRequested(phone); + + if (kIsWeb) { + _sendVerficationCodeWeb(phone, action); + } + + auth.verifyPhoneNumber( + forceResendingToken: forceResendingToken, + phoneNumber: hint != null ? null : phoneNumber, + multiFactorInfo: hint, + multiFactorSession: multiFactorSession, + verificationCompleted: authListener.onVerificationCompleted, + verificationFailed: authListener.onError, + codeSent: authListener.onCodeSent, + codeAutoRetrievalTimeout: (_) { + authListener.onError(AutoresolutionFailedException()); + }, + ); + } + + /// Verifies an SMS code using [verificationId] or [confirmationResult] + /// (depending on what is currently available). + void verifySMSCode({ + required AuthAction action, + required String code, + String? verificationId, + fba.ConfirmationResult? confirmationResult, + }) { + if (verificationId != null) { + final credential = fba.PhoneAuthProvider.credential( + verificationId: verificationId, + smsCode: code, + ); + onCredentialReceived(credential, action); + } else { + confirmationResult!.confirm(code).then((userCredential) { + if (action == AuthAction.link) { + authListener.onCredentialLinked(userCredential.credential!); + } else { + authListener.onSignedIn(userCredential); + } + }).catchError((err) { + authListener.onError(err); + }); + } + } + + void _sendVerficationCodeWeb(String phoneNumber, [AuthAction? action]) { + Future result; + bool shouldLink = action == AuthAction.link || auth.currentUser != null; + + if (shouldLink) { + result = auth.currentUser!.linkWithPhoneNumber(phoneNumber); + } else { + result = auth.signInWithPhoneNumber(phoneNumber); + } + + result + .then(authListener.onConfirmationRequested) + .catchError(authListener.onError); + } +} diff --git a/packages/firebase_ui_auth/lib/src/providers/universal_email_sign_in_provider.dart b/packages/firebase_ui_auth/lib/src/providers/universal_email_sign_in_provider.dart new file mode 100644 index 000000000000..6792207e9f76 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/providers/universal_email_sign_in_provider.dart @@ -0,0 +1,32 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/foundation.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +/// A [UniversalEmailSignInFlow] lifecycle listener. +abstract class UniversalEmailSignInListener extends AuthListener { + @override + void onBeforeProvidersForEmailFetch(); + + @override + void onDifferentProvidersFound( + String email, + List providers, + AuthCredential? credential, + ); +} + +/// A provider that resolves available authentication methods for a given +/// email. +class UniversalEmailSignInProvider + extends AuthProvider { + @override + late UniversalEmailSignInListener authListener; + + @override + String get providerId => 'universal_email_sign_in'; + + @override + bool supportsPlatform(TargetPlatform platform) { + return true; + } +} diff --git a/packages/firebase_ui_auth/lib/src/screens/email_link_sign_in_screen.dart b/packages/firebase_ui_auth/lib/src/screens/email_link_sign_in_screen.dart new file mode 100644 index 000000000000..ad84d4c1cfa3 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/screens/email_link_sign_in_screen.dart @@ -0,0 +1,75 @@ +import 'package:firebase_auth/firebase_auth.dart' hide EmailAuthProvider; +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +import 'internal/provider_screen.dart'; +import 'internal/responsive_page.dart'; +import '../widgets/internal/universal_scaffold.dart'; + +/// {@template ui.auth.screens.email_link_sign_in_screen} +/// A screen that provides a UI for authentication using email link. +/// {@endtemplate} +class EmailLinkSignInScreen extends ProviderScreen { + /// {@macro ui.auth.screens.responsive_page.header_builder} + final HeaderBuilder? headerBuilder; + + /// {@macro ui.auth.screens.responsive_page.header_max_extent} + final double? headerMaxExtent; + + /// {@macro ui.auth.screens.responsive_page.side_builder} + final SideBuilder? sideBuilder; + + /// {@macro ui.auth.screens.responsive_page.desktop_layout_direction} + final TextDirection? desktopLayoutDirection; + + /// EmailLinkSignInScreen could invoke these actions: + /// + /// * [AuthStateChangeAction] + /// + /// ```dart + /// EmailLinkSignInScreen( + /// actions: [ + /// AuthStateChangeAction((context, state) { + /// Navigator.pushReplacementNamed(context, '/'); + /// }), + /// ], + /// ); + /// ``` + final List? actions; + + /// {@macro ui.auth.screens.responsive_page.breakpoint} + final double breakpoint; + + const EmailLinkSignInScreen({ + Key? key, + FirebaseAuth? auth, + this.actions, + EmailLinkAuthProvider? provider, + this.headerBuilder, + this.headerMaxExtent, + this.sideBuilder, + this.desktopLayoutDirection, + this.breakpoint = 500, + }) : super(key: key, auth: auth, provider: provider); + + @override + Widget build(BuildContext context) { + return UniversalScaffold( + body: ResponsivePage( + breakpoint: breakpoint, + headerBuilder: headerBuilder, + headerMaxExtent: headerMaxExtent, + maxWidth: 1200, + sideBuilder: sideBuilder, + desktopLayoutDirection: desktopLayoutDirection, + child: Padding( + padding: const EdgeInsets.all(32), + child: EmailLinkSignInView( + auth: auth, + provider: provider, + ), + ), + ), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/screens/email_verification_screen.dart b/packages/firebase_ui_auth/lib/src/screens/email_verification_screen.dart new file mode 100644 index 000000000000..223f2e683b43 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/screens/email_verification_screen.dart @@ -0,0 +1,226 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart' hide Title; +import 'package:flutter/scheduler.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; + +import '../widgets/internal/loading_button.dart'; +import '../widgets/internal/title.dart'; +import '../widgets/internal/universal_button.dart'; +import '../widgets/internal/universal_scaffold.dart'; + +import 'internal/responsive_page.dart'; + +/// An action that is being called when email was successfully verified. +class EmailVerifiedAction extends FirebaseUIAction { + final VoidCallback callback; + + EmailVerifiedAction(this.callback); +} + +/// {@template ui.auth.screens.email_verification_screen} +/// A screen that contains hints of how to verify the email. +/// A verification email is being sent automatically when this screen is opened. +/// {@endtemplate} +class EmailVerificationScreen extends StatelessWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// {@macro ui.auth.screens.responsive_page.header_builder} + final HeaderBuilder? headerBuilder; + + /// EmailVerificationScreen could invoke these actions: + /// + /// * [AuthCancelledAction] + /// * [EmailVerifiedAction] + /// + /// ```dart + /// EmailVerificationScreen( + /// actions: [ + /// EmailVerified(() { + /// Navigator.pushReplacementNamed(context, '/profile'); + /// }), + /// Cancel((context) { + /// Navigator.of(context).pop(); + /// }), + /// ], + /// ); + /// ``` + final List actions; + + /// {@macro ui.auth.screens.responsive_page.header_max_extent} + final double? headerMaxExtent; + + /// {@macro ui.auth.screens.responsive_page.side_builder} + final SideBuilder? sideBuilder; + + /// {@macro ui.auth.screens.responsive_page.desktop_layout_direction} + final TextDirection? desktopLayoutDirection; + + /// {@macro ui.auth.screens.responsive_page.breakpoint} + final double breakpoint; + + /// A configuration object used to construct a dynamic link. + final ActionCodeSettings? actionCodeSettings; + + /// {@macro ui.auth.screens.email_verification_screen} + const EmailVerificationScreen({ + Key? key, + this.auth, + this.actions = const [], + this.headerBuilder, + this.headerMaxExtent, + this.sideBuilder, + this.desktopLayoutDirection, + this.breakpoint = 500, + this.actionCodeSettings, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FirebaseUIActions( + actions: actions, + child: UniversalScaffold( + body: ResponsivePage( + breakpoint: breakpoint, + desktopLayoutDirection: desktopLayoutDirection, + headerBuilder: headerBuilder, + headerMaxExtent: headerMaxExtent, + sideBuilder: sideBuilder, + maxWidth: 1200, + contentFlex: 2, + child: Padding( + padding: const EdgeInsets.all(32), + child: _EmailVerificationScreenContent( + auth: auth, + actionCodeSettings: actionCodeSettings, + ), + ), + ), + ), + ); + } +} + +/// This allows a value of type T or T? +/// to be treated as a value of type T?. +/// +/// We use this so that APIs that have become +/// non-nullable can still be used with `!` and `?` +/// to support older versions of the API as well. +T? _ambiguate(T? value) => value; + +class _EmailVerificationScreenContent extends StatefulWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + final ActionCodeSettings? actionCodeSettings; + + const _EmailVerificationScreenContent({ + Key? key, + required this.auth, + required this.actionCodeSettings, + }) : super(key: key); + + @override + State<_EmailVerificationScreenContent> createState() => + __EmailVerificationScreenContentState(); +} + +class __EmailVerificationScreenContentState + extends State<_EmailVerificationScreenContent> { + late final controller = EmailVerificationController(auth); + FirebaseAuth get auth => widget.auth ?? FirebaseAuth.instance; + User get user => auth.currentUser!; + bool isLoading = false; + + @override + void initState() { + _ambiguate(SchedulerBinding.instance)! + .addPostFrameCallback(_sendEmailVerification); + super.initState(); + } + + void _sendEmailVerification(_) { + controller + ..addListener(() { + setState(() {}); + + if (state == EmailVerificationState.verified) { + final action = FirebaseUIAction.ofType(context); + action?.callback(); + } + }) + ..sendVerificationEmail( + Theme.of(context).platform, + widget.actionCodeSettings, + ); + } + + EmailVerificationState get state => controller.state; + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Align( + child: Title(text: 'Verify your email'), + ), + const SizedBox(height: 32), + const Text( + 'A verification email has been sent to your email address. ' + 'Please check your email and click on the link to verify ' + 'your email address.', + ), + const SizedBox(height: 32), + if (state == EmailVerificationState.pending) + const LoadingIndicator(size: 32, borderWidth: 2) + else if (state == EmailVerificationState.sent) ...[ + LoadingButton( + isLoading: isLoading, + variant: ButtonVariant.filled, + label: l.continueText, + onTap: () async { + await controller.reload(); + }, + ), + ], + if (state == EmailVerificationState.sending) + const LoadingIndicator(size: 32, borderWidth: 2), + if (state == EmailVerificationState.unverified) ...[ + Text( + "We couldn't verify your email address. ", + textAlign: TextAlign.center, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + const SizedBox(height: 16), + UniversalButton( + text: 'Resend verification email', + onPressed: () { + controller.sendVerificationEmail( + Theme.of(context).platform, + widget.actionCodeSettings, + ); + }, + ) + ], + if (state == EmailVerificationState.failed) ...[ + const SizedBox(height: 16), + ErrorText(exception: controller.error!), + ], + const SizedBox(height: 16), + UniversalButton( + variant: ButtonVariant.text, + text: l.goBackButtonLabel, + onPressed: () { + FirebaseUIAction.ofType(context) + ?.callback(context); + }, + ) + ], + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/screens/forgot_password_screen.dart b/packages/firebase_ui_auth/lib/src/screens/forgot_password_screen.dart new file mode 100644 index 000000000000..958ec25933f7 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/screens/forgot_password_screen.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +import '../widgets/internal/universal_scaffold.dart'; +import 'internal/responsive_page.dart'; + +/// A password reset screen. +class ForgotPasswordScreen extends StatelessWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// A returned widget would be placed under the title of the screen. + final WidgetBuilder? subtitleBuilder; + + /// A returned widget would be placed at the bottom. + final WidgetBuilder? footerBuilder; + + /// An email that should be pre-filled. + final String? email; + + /// {@macro ui.auth.screens.responsive_page.header_builder} + final HeaderBuilder? headerBuilder; + + /// {@macro ui.auth.screens.responsive_page.header_max_extent} + final double? headerMaxExtent; + + /// {@macro ui.auth.screens.responsive_page.side_builder} + final SideBuilder? sideBuilder; + + /// {@macro ui.auth.screens.responsive_page.desktop_layout_direction} + final TextDirection? desktopLayoutDirection; + + /// See [Scaffold.resizeToAvoidBottomInset] + final bool? resizeToAvoidBottomInset; + + /// {@macro ui.auth.screens.responsive_page.breakpoint} + final double breakpoint; + + const ForgotPasswordScreen({ + Key? key, + this.auth, + this.email, + this.subtitleBuilder, + this.footerBuilder, + this.headerBuilder, + this.headerMaxExtent, + this.sideBuilder, + this.desktopLayoutDirection, + this.resizeToAvoidBottomInset, + this.breakpoint = 600, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final child = ForgotPasswordView( + auth: auth, + email: email, + footerBuilder: footerBuilder, + subtitleBuilder: subtitleBuilder, + ); + + return UniversalScaffold( + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + body: ResponsivePage( + desktopLayoutDirection: desktopLayoutDirection, + headerBuilder: headerBuilder, + headerMaxExtent: headerMaxExtent, + sideBuilder: sideBuilder, + breakpoint: breakpoint, + maxWidth: 1200, + contentFlex: 1, + child: Padding( + padding: const EdgeInsets.all(32), + child: child, + ), + ), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/screens/internal/login_screen.dart b/packages/firebase_ui_auth/lib/src/screens/internal/login_screen.dart new file mode 100644 index 000000000000..044ded3be8c5 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/screens/internal/login_screen.dart @@ -0,0 +1,108 @@ +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_auth/firebase_auth.dart' show FirebaseAuth; + +import '../../widgets/internal/universal_scaffold.dart'; + +import 'responsive_page.dart'; + +class LoginScreen extends StatelessWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + final AuthAction action; + final List providers; + + /// {@macro ui.auth.screens.responsive_page.header_builder} + final HeaderBuilder? headerBuilder; + + /// {@macro ui.auth.screens.responsive_page.header_max_extent} + final double? headerMaxExtent; + + /// Indicates whether icon-only or icon and text OAuth buttons should be used. + /// Icon-only buttons are placed in a row. + final OAuthButtonVariant? oauthButtonVariant; + + /// {@macro ui.auth.screens.responsive_page.side_builder} + final SideBuilder? sideBuilder; + + /// {@macro ui.auth.screens.responsive_page.desktop_layout_direction} + final TextDirection? desktopLayoutDirection; + final String? email; + + /// Whether the "Login/Register" link should be displayed. The link changes + /// the type of the [AuthAction] from [AuthAction.signIn] + /// and [AuthAction.signUp] and vice versa. + final bool? showAuthActionSwitch; + + /// See [Scaffold.resizeToAvoidBottomInset] + final bool? resizeToAvoidBottomInset; + + /// A returned widget would be placed up the authentication related widgets. + final AuthViewContentBuilder? subtitleBuilder; + + /// A returned widget would be placed down the authentication related widgets. + final AuthViewContentBuilder? footerBuilder; + final Key? loginViewKey; + + /// {@macro ui.auth.screens.responsive_page.breakpoint} + final double breakpoint; + final Set? styles; + + const LoginScreen({ + Key? key, + required this.action, + required this.providers, + this.auth, + this.oauthButtonVariant, + this.headerBuilder, + this.headerMaxExtent = defaultHeaderImageHeight, + this.sideBuilder, + this.desktopLayoutDirection = TextDirection.ltr, + this.email, + this.showAuthActionSwitch, + this.resizeToAvoidBottomInset = false, + this.subtitleBuilder, + this.footerBuilder, + this.loginViewKey, + this.breakpoint = 800, + this.styles, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final loginContent = ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: Padding( + padding: const EdgeInsets.all(30), + child: LoginView( + key: loginViewKey, + action: action, + auth: auth, + providers: providers, + oauthButtonVariant: oauthButtonVariant, + email: email, + showAuthActionSwitch: showAuthActionSwitch, + subtitleBuilder: subtitleBuilder, + footerBuilder: footerBuilder, + ), + ), + ); + + final body = ResponsivePage( + breakpoint: breakpoint, + desktopLayoutDirection: desktopLayoutDirection, + headerBuilder: headerBuilder, + headerMaxExtent: headerMaxExtent, + sideBuilder: sideBuilder, + child: loginContent, + ); + + return FirebaseUITheme( + styles: styles ?? const {}, + child: UniversalScaffold( + body: body, + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + ), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/screens/internal/multi_provider_screen.dart b/packages/firebase_ui_auth/lib/src/screens/internal/multi_provider_screen.dart new file mode 100644 index 000000000000..26daad7d8dba --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/screens/internal/multi_provider_screen.dart @@ -0,0 +1,57 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +abstract class MultiProviderScreen extends Widget { + final List? _providers; + final FirebaseAuth? _auth; + FirebaseAuth get auth { + return _auth ?? FirebaseAuth.instance; + } + + const MultiProviderScreen({ + Key? key, + FirebaseAuth? auth, + List? providers, + }) : _auth = auth, + _providers = providers, + super(key: key); + + List get providers { + if (_providers != null) { + return _providers!; + } else { + return FirebaseUIAuth.providersFor(auth.app); + } + } + + Widget build(BuildContext context); + + @override + ScreenElement createElement() { + return ScreenElement(this); + } +} + +class ScreenElement extends ComponentElement { + ScreenElement(Widget widget) : super(widget); + + @override + MultiProviderScreen get widget => super.widget as MultiProviderScreen; + + @override + void mount(Element? parent, Object? newSlot) { + if (widget._providers != null) { + if (!FirebaseUIAuth.isAppConfigured(widget.auth.app)) { + FirebaseUIAuth.configureProviders(widget._providers!); + } + } + + super.mount(parent, newSlot); + } + + @override + Widget build() { + return widget.build(this); + } +} diff --git a/packages/firebase_ui_auth/lib/src/screens/internal/provider_screen.dart b/packages/firebase_ui_auth/lib/src/screens/internal/provider_screen.dart new file mode 100644 index 000000000000..1505e00b1ee5 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/screens/internal/provider_screen.dart @@ -0,0 +1,30 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +abstract class ProviderScreen extends StatelessWidget { + final T? _provider; + + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + static final _cache = {}; + + /// Current [AuthProvider] that is being used to authenticate the user. + T get provider { + if (_provider != null) return _provider!; + if (_cache.containsKey(T)) { + return _cache[T]! as T; + } + + final auth = this.auth ?? FirebaseAuth.instance; + final configs = FirebaseUIAuth.providersFor(auth.app); + final config = configs.firstWhere((element) => element is T) as T; + _cache[T] = config; + return config; + } + + const ProviderScreen({Key? key, T? provider, this.auth}) + : _provider = provider, + super(key: key); +} diff --git a/packages/firebase_ui_auth/lib/src/screens/internal/responsive_page.dart b/packages/firebase_ui_auth/lib/src/screens/internal/responsive_page.dart new file mode 100644 index 000000000000..a4b1794d506e --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/screens/internal/responsive_page.dart @@ -0,0 +1,228 @@ +import 'package:flutter/widgets.dart'; + +import '../../widgets/internal/keyboard_appearence_listener.dart'; + +/// {@template ui.auth.screens.responsive_page.header_builder} +/// A builder that builds the contents of the header. +/// Used only on mobile platforms. +/// {@endtemplate} +typedef HeaderBuilder = Widget Function( + BuildContext context, + BoxConstraints constraints, + double shrinkOffset, +); + +const defaultHeaderImageHeight = 150.0; + +class HeaderImageSliverDelegate extends SliverPersistentHeaderDelegate { + final HeaderBuilder builder; + @override + final double maxExtent; + + const HeaderImageSliverDelegate({ + required this.builder, + this.maxExtent = defaultHeaderImageHeight, + }) : super(); + + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + return LayoutBuilder( + builder: (context, constraints) => builder( + context, + constraints, + shrinkOffset / maxExtent, + ), + ); + } + + @override + double get minExtent => 0; + + @override + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { + return true; + } +} + +/// {@template ui.auth.screens.responsive_page.side_builder} +/// A builder that builds a contents of a page displayed on a side of +/// of the main authentication related UI. +/// +/// Used only on desktop platforms. +/// {@endtemplate} +typedef SideBuilder = Widget Function( + BuildContext context, + BoxConstraints constraints, +); + +class ResponsivePage extends StatefulWidget { + /// Main content of the page + final Widget child; + + /// {@template ui.auth.screens.responsive_page.desktop_layout_direction} + /// A direction of the desktop layout. + /// [TextDirection.ltr] indicates that side content is built on the left, and + /// the child is placed on the right. The order is reversed when + /// [TextDirection.rtl] is used. + /// {@endtemplate} + final TextDirection? desktopLayoutDirection; + + /// {@macro ui.auth.screens.responsive_page.side_builder} + final SideBuilder? sideBuilder; + + /// {@macro ui.auth.screens.responsive_page.header_builder} + final HeaderBuilder? headerBuilder; + + /// {@template ui.auth.screens.responsive_page.header_max_extent} + /// The maximum height of the header. + /// {@endtemplate} + final double? headerMaxExtent; + + /// {@template ui.auth.screens.responsive_page.breakpoint} + /// Min width of the viewport for desktop layout. If the available width is + /// less than this value, a mobile layout is used. + /// {@endtemplate} + /// {@macro ui.auth.screens.responsive_page.breakpoint} + final double breakpoint; + + /// {@template ui.auth.screens.responsive_page.content_flex} + /// A flex value of the [Expanded] that wraps the child on desktop. + /// {@endtemplate} + final int? contentFlex; + + /// {@template ui.auth.screens.responsive_page.max_width} + /// A max width of the page on desktop. If the available width is greater than + /// this value, the content is centered and horizontal paddings are added. + /// {@endtemplate} + final double? maxWidth; + + const ResponsivePage({ + Key? key, + required this.child, + this.desktopLayoutDirection, + this.sideBuilder, + this.headerBuilder, + this.headerMaxExtent, + this.breakpoint = 800, + this.contentFlex, + this.maxWidth, + }) : super(key: key); + + @override + State createState() => _ResponsivePageState(); +} + +class _ResponsivePageState extends State { + final ctrl = ScrollController(); + final paddingListenable = ValueNotifier(0); + + void _onKeyboardPositionChanged(double position) { + if (!ctrl.hasClients) { + return; + } + + if (widget.headerBuilder == null) return; + + paddingListenable.value = position; + + final max = widget.headerMaxExtent ?? defaultHeaderImageHeight; + final ctrlPosition = position.clamp(0.0, max); + ctrl.jumpTo(ctrlPosition); + } + + @override + Widget build(BuildContext context) { + final breakpoint = widget.breakpoint; + + return LayoutBuilder( + builder: (context, constraints) { + if (constraints.biggest.width > breakpoint) { + return Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: widget.maxWidth ?? constraints.biggest.width, + ), + child: Row( + textDirection: widget.desktopLayoutDirection, + children: [ + if (widget.sideBuilder != null) + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + return widget.sideBuilder!(context, constraints); + }, + ), + ), + Expanded( + flex: widget.contentFlex ?? 1, + child: Center( + child: ListView( + shrinkWrap: true, + children: [ + Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: breakpoint), + child: IntrinsicHeight( + child: widget.child, + ), + ), + ), + ], + ), + ), + ) + ], + ), + ), + ); + } else if (widget.headerBuilder != null) { + return Padding( + padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), + child: KeyboardAppearenceListener( + listener: _onKeyboardPositionChanged, + child: CustomScrollView( + controller: ctrl, + slivers: [ + if (widget.headerBuilder != null) + SliverPersistentHeader( + delegate: HeaderImageSliverDelegate( + maxExtent: + widget.headerMaxExtent ?? defaultHeaderImageHeight, + builder: widget.headerBuilder!, + ), + ), + SliverList( + delegate: SliverChildListDelegate.fixed( + [ + widget.child, + ValueListenableBuilder( + valueListenable: paddingListenable, + builder: (context, value, _) { + return SizedBox(height: value); + }, + ), + ], + ), + ) + ], + ), + ), + ); + } else { + return Center( + child: ListView( + shrinkWrap: true, + children: [ + widget.child, + ], + ), + ); + } + }, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/screens/phone_input_screen.dart b/packages/firebase_ui_auth/lib/src/screens/phone_input_screen.dart new file mode 100644 index 000000000000..4f451b8dd9a5 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/screens/phone_input_screen.dart @@ -0,0 +1,142 @@ +import 'package:firebase_auth/firebase_auth.dart' + show FirebaseAuth, MultiFactorSession, PhoneMultiFactorInfo; +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; + +import '../widgets/internal/universal_button.dart'; +import '../widgets/internal/universal_page_route.dart'; +import '../widgets/internal/universal_scaffold.dart'; + +import 'internal/responsive_page.dart'; + +/// A screen displaying a fully styled phone number entry screen, with a country-code +/// picker. +class PhoneInputScreen extends StatelessWidget { + /// {@macro ui.auth.auth_action} + final AuthAction? action; + + /// PhoneInputScreen could invoke these actions: + /// + /// - [SMSCodeRequestedAction] + /// + /// ```dart + /// PhoneInputScreen( + /// actions: [ + /// SMSCodeRequestedAction((context, action, flowKey, phoneNumber) { + /// Navigator.of(context).push( + /// MaterialPageRoute( + /// builder: (context) => SMSCodeInputScreen( + /// flowKey: flowKey, + /// ), + /// ), + /// ); + /// }), + /// ] + /// ); + /// ``` + final List? actions; + + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// A returned widget would be placed under the title of the screen. + final WidgetBuilder? subtitleBuilder; + + /// A returned widget would be placed at the bottom. + final WidgetBuilder? footerBuilder; + + /// {@macro ui.auth.screens.responsive_page.header_builder} + final HeaderBuilder? headerBuilder; + + /// {@macro ui.auth.screens.responsive_page.header_max_extent} + final double? headerMaxExtent; + + /// {@macro ui.auth.screens.responsive_page.side_builder} + final SideBuilder? sideBuilder; + + /// {@macro ui.auth.screens.responsive_page.desktop_layout_direction} + final TextDirection? desktopLayoutDirection; + + /// {@macro ui.auth.screens.responsive_page.breakpoint} + final double breakpoint; + + /// {@macro ui.auth.providers.phone_auth_provider.mfa_session} + final MultiFactorSession? multiFactorSession; + + /// {@macro ui.auth.providers.phone_auth_provider.mfa_hint} + final PhoneMultiFactorInfo? mfaHint; + + const PhoneInputScreen({ + Key? key, + this.action, + this.actions, + this.auth, + this.subtitleBuilder, + this.footerBuilder, + this.headerBuilder, + this.headerMaxExtent, + this.sideBuilder, + this.desktopLayoutDirection, + this.breakpoint = 500, + this.multiFactorSession, + this.mfaHint, + }) : super(key: key); + + void _next(BuildContext context, AuthAction? action, Object flowKey, _) { + Navigator.of(context).push( + createPageRoute( + context: context, + builder: (_) => FirebaseUIActions.inherit( + from: context, + child: SMSCodeInputScreen( + action: action, + flowKey: flowKey, + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final flowKey = Object(); + final l = FirebaseUILocalizations.labelsOf(context); + + return FirebaseUIActions( + actions: actions ?? [SMSCodeRequestedAction(_next)], + child: UniversalScaffold( + body: ResponsivePage( + desktopLayoutDirection: desktopLayoutDirection, + sideBuilder: sideBuilder, + headerBuilder: headerBuilder, + headerMaxExtent: headerMaxExtent, + breakpoint: breakpoint, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + PhoneInputView( + auth: auth, + action: action, + subtitleBuilder: subtitleBuilder, + footerBuilder: footerBuilder, + flowKey: flowKey, + multiFactorSession: multiFactorSession, + mfaHint: mfaHint, + ), + UniversalButton( + text: l.goBackButtonLabel, + variant: ButtonVariant.text, + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/screens/profile_screen.dart b/packages/firebase_ui_auth/lib/src/screens/profile_screen.dart new file mode 100644 index 000000000000..63a5e9b034b2 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/screens/profile_screen.dart @@ -0,0 +1,931 @@ +import 'package:firebase_auth/firebase_auth.dart' + show + ActionCodeSettings, + FirebaseAuth, + FirebaseAuthException, + MultiFactorInfo, + PhoneAuthCredential, + PhoneMultiFactorGenerator, + User; +import 'package:firebase_ui_auth/src/widgets/internal/universal_icon.dart'; +import 'package:flutter/cupertino.dart' hide Title; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import 'package:flutter/material.dart' hide Title; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart' + hide OAuthProviderButtonBase; +import 'package:flutter/services.dart'; + +import '../widgets/internal/loading_button.dart'; +import '../widgets/internal/universal_button.dart'; +import '../widgets/internal/rebuild_scope.dart'; +import '../widgets/internal/subtitle.dart'; +import '../widgets/internal/universal_icon_button.dart'; + +import 'internal/multi_provider_screen.dart'; + +class _AvailableProvidersRow extends StatefulWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + final List providers; + final VoidCallback onProviderLinked; + + const _AvailableProvidersRow({ + Key? key, + this.auth, + required this.providers, + required this.onProviderLinked, + }) : super(key: key); + + @override + State<_AvailableProvidersRow> createState() => _AvailableProvidersRowState(); +} + +class _AvailableProvidersRowState extends State<_AvailableProvidersRow> { + AuthFailed? error; + + Future connectProvider({ + required BuildContext context, + required AuthProvider provider, + }) async { + setState(() { + error = null; + }); + + switch (provider.providerId) { + case 'phone': + await startPhoneVerification( + context: context, + action: AuthAction.link, + auth: widget.auth, + ); + break; + case 'password': + await showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: '', + pageBuilder: (context, _, __) { + return EmailSignUpDialog( + provider: provider as EmailAuthProvider, + auth: widget.auth, + action: AuthAction.link, + ); + }, + ); + } + + await (widget.auth ?? FirebaseAuth.instance).currentUser!.reload(); + } + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + final isCupertino = CupertinoUserInterfaceLevel.maybeOf(context) != null; + + final providers = widget.providers + .where((provider) => provider is! EmailLinkAuthProvider) + .toList(); + + Widget child = Row( + children: [ + for (var provider in providers) + if (provider is! OAuthProvider) + if (isCupertino) + CupertinoButton( + onPressed: () => connectProvider( + context: context, + provider: provider, + ).then((_) => widget.onProviderLinked()), + child: Icon( + providerIcon(context, provider.providerId), + ), + ) + else + IconButton( + icon: Icon( + providerIcon(context, provider.providerId), + ), + onPressed: () => connectProvider( + context: context, + provider: provider, + ).then((_) => widget.onProviderLinked()), + ) + else + AuthStateListener( + listener: (oldState, newState, controller) { + if (newState is CredentialLinked) { + widget.onProviderLinked(); + } else if (newState is AuthFailed) { + setState(() => error = newState); + } + return null; + }, + child: OAuthProviderButton( + provider: provider, + auth: widget.auth, + action: AuthAction.link, + variant: OAuthButtonVariant.icon, + ), + ), + ], + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Subtitle(text: l.enableMoreSignInMethods), + const SizedBox(height: 16), + child, + if (error != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: ErrorText(exception: error!.exception), + ), + ], + ); + } +} + +class _EditButton extends StatelessWidget { + final bool isEditing; + final VoidCallback? onPressed; + + const _EditButton({ + Key? key, + required this.isEditing, + this.onPressed, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return UniversalIconButton( + materialIcon: isEditing ? Icons.check : Icons.edit, + cupertinoIcon: isEditing ? CupertinoIcons.check_mark : CupertinoIcons.pen, + color: theme.colorScheme.secondary, + onPressed: () { + onPressed?.call(); + }, + ); + } +} + +class _LinkedProvidersRow extends StatefulWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + final List providers; + final VoidCallback onProviderUnlinked; + + const _LinkedProvidersRow({ + Key? key, + this.auth, + required this.providers, + required this.onProviderUnlinked, + }) : super(key: key); + + @override + State<_LinkedProvidersRow> createState() => _LinkedProvidersRowState(); +} + +class _LinkedProvidersRowState extends State<_LinkedProvidersRow> { + bool isEditing = false; + String? unlinkingProvider; + FirebaseAuthException? error; + + final size = 32.0; + + void _toggleEdit() { + setState(() { + isEditing = !isEditing; + error = null; + }); + } + + Future _unlinkProvider(BuildContext context, String providerId) async { + setState(() { + unlinkingProvider = providerId; + error = null; + }); + + try { + final user = widget.auth!.currentUser!; + await user.unlink(providerId); + await user.reload(); + + setState(() { + widget.onProviderUnlinked(); + isEditing = false; + }); + } on FirebaseAuthException catch (e) { + setState(() { + error = e; + }); + } finally { + setState(() { + unlinkingProvider = null; + }); + } + } + + Widget buildProviderIcon(BuildContext context, String providerId) { + final isCupertino = CupertinoUserInterfaceLevel.maybeOf(context) != null; + const animationDuration = Duration(milliseconds: 150); + const curve = Curves.easeOut; + + void unlink() { + _unlinkProvider(context, providerId); + } + + return Stack( + children: [ + SizedBox( + width: size, + height: size, + child: unlinkingProvider == providerId + ? Center( + child: LoadingIndicator( + size: size - (size / 4), + borderWidth: 1, + ), + ) + : Icon(providerIcon(context, providerId)), + ), + if (unlinkingProvider != providerId) + AnimatedOpacity( + duration: animationDuration, + opacity: isEditing ? 1 : 0, + curve: curve, + child: GestureDetector( + onTap: unlink, + child: SizedBox( + width: size, + height: size, + child: Align( + alignment: Alignment.topRight, + child: Transform.translate( + offset: const Offset(14, -12), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: unlink, + child: Padding( + padding: const EdgeInsets.all(8), + child: Icon( + isCupertino + ? CupertinoIcons.minus_circle_fill + : Icons.remove_circle, + size: 20, + color: Theme.of(context).colorScheme.error, + ), + ), + ), + ), + ), + ), + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + Widget child = Row( + children: [ + for (var provider in widget.providers) + buildProviderIcon(context, provider.providerId) + ] + .map((e) => [e, const SizedBox(width: 8)]) + .expand((element) => element) + .toList(), + ); + + if (widget.providers.length > 1) { + child = Row( + children: [ + Expanded(child: child), + const SizedBox(width: 8), + _EditButton( + isEditing: isEditing, + onPressed: _toggleEdit, + ), + ], + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Subtitle(text: l.signInMethods), + const SizedBox(height: 16), + child, + ], + ); + } +} + +class _EmailVerificationBadge extends StatefulWidget { + final FirebaseAuth auth; + final ActionCodeSettings? actionCodeSettings; + const _EmailVerificationBadge({ + Key? key, + required this.auth, + this.actionCodeSettings, + }) : super(key: key); + + @override + State<_EmailVerificationBadge> createState() => + _EmailVerificationBadgeState(); +} + +class _EmailVerificationBadgeState extends State<_EmailVerificationBadge> { + late final service = EmailVerificationController(widget.auth) + ..addListener(() { + setState(() {}); + }) + ..reload(); + + EmailVerificationState get state => service.state; + + User get user { + return widget.auth.currentUser!; + } + + TargetPlatform get platform { + return Theme.of(context).platform; + } + + @override + Widget build(BuildContext context) { + if (state == EmailVerificationState.dismissed || + state == EmailVerificationState.unresolved || + state == EmailVerificationState.verified) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.only(top: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.yellow, + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Subtitle( + text: state == EmailVerificationState.sent || + state == EmailVerificationState.pending + ? 'Verification email sent' + : 'Email is not verified', + fontWeight: FontWeight.bold, + ), + if (state == EmailVerificationState.pending) ...[ + const SizedBox(height: 8), + const Text( + 'Please check your email and click the link to verify your email address.', + ), + ] + ], + ), + ), + ), + const SizedBox(height: 16), + if (state == EmailVerificationState.pending) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + LoadingIndicator(size: 16, borderWidth: 0.5), + SizedBox(width: 16), + Text('Waiting for email verification'), + ], + ) + else + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (state != EmailVerificationState.sent && + state != EmailVerificationState.sending) + UniversalButton( + variant: ButtonVariant.text, + color: Theme.of(context).colorScheme.error, + text: 'Dismiss', + onPressed: () { + setState(service.dismiss); + }, + ), + if (state != EmailVerificationState.sent) + LoadingButton( + isLoading: state == EmailVerificationState.sending, + label: 'Send verification email', + onTap: () { + service.sendVerificationEmail( + platform, + widget.actionCodeSettings, + ); + }, + ) + else + UniversalButton( + variant: ButtonVariant.text, + text: 'Ok', + onPressed: () { + setState(service.dismiss); + }, + ) + ], + ) + ], + ), + ); + } +} + +class _MFABadge extends StatelessWidget { + final bool enrolled; + final FirebaseAuth auth; + final VoidCallback onToggled; + final List providers; + + const _MFABadge({ + Key? key, + required this.enrolled, + required this.auth, + required this.onToggled, + required this.providers, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Subtitle(text: l.mfaTitle), + const SizedBox(height: 8), + _MFAToggle( + enrolled: enrolled, + auth: auth, + onToggled: onToggled, + providers: providers, + ), + ], + ), + ); + } +} + +class _MFAToggle extends StatefulWidget { + final bool enrolled; + final FirebaseAuth auth; + final VoidCallback? onToggled; + final List providers; + + const _MFAToggle({ + Key? key, + required this.enrolled, + required this.auth, + required this.onToggled, + required this.providers, + }) : super(key: key); + + @override + State<_MFAToggle> createState() => _MFAToggleState(); +} + +class _MFAToggleState extends State<_MFAToggle> { + bool isLoading = false; + Exception? exception; + + IconData getCupertinoIcon() { + if (widget.enrolled) { + return CupertinoIcons.check_mark_circled; + } else { + return CupertinoIcons.circle; + } + } + + IconData getMaterialIcon() { + if (widget.enrolled) { + return Icons.check_circle; + } else { + return Icons.remove_circle_sharp; + } + } + + Color getColor() { + if (widget.enrolled) { + return Theme.of(context).colorScheme.primary; + } else { + return Theme.of(context).colorScheme.error; + } + } + + Future _reauthenticate() async { + return await showReauthenticateDialog( + context: context, + providers: widget.providers, + auth: widget.auth, + onSignedIn: () { + Navigator.of(context).pop(true); + }, + ); + } + + Future _disable() async { + setState(() { + exception = null; + isLoading = true; + }); + + final mfa = widget.auth.currentUser!.multiFactor; + final factors = await mfa.getEnrolledFactors(); + + if (factors.isEmpty) { + setState(() { + isLoading = false; + }); + return; + } + + try { + await mfa.unenroll(multiFactorInfo: factors.first); + widget.onToggled?.call(); + } on PlatformException catch (e) { + if (e.code == 'FirebaseAuthRecentLoginRequiredException') { + if (await _reauthenticate()) { + await _disable(); + } + } else { + rethrow; + } + } on Exception catch (e) { + setState(() { + exception = e; + }); + } finally { + setState(() { + isLoading = false; + }); + } + } + + Future _enable() async { + setState(() { + exception = null; + isLoading = true; + }); + + final currentRoute = ModalRoute.of(context); + + final mfa = widget.auth.currentUser!.multiFactor; + final session = await mfa.getSession(); + + await startPhoneVerification( + context: context, + action: AuthAction.none, + multiFactorSession: session, + auth: widget.auth, + actions: [ + AuthStateChangeAction((context, state) async { + final cred = state.credential as PhoneAuthCredential; + final assertion = PhoneMultiFactorGenerator.getAssertion(cred); + + try { + await mfa.enroll(assertion); + widget.onToggled?.call(); + } on Exception catch (e) { + setState(() { + exception = e; + }); + } finally { + setState(() { + isLoading = false; + }); + + Navigator.of(context).popUntil((route) => route == currentRoute); + } + }) + ], + ); + + setState(() { + isLoading = false; + }); + } + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + UniversalIcon( + cupertinoIcon: getCupertinoIcon(), + materialIcon: getMaterialIcon(), + color: getColor(), + ), + const SizedBox(width: 8), + Expanded( + child: Text(widget.enrolled ? l.on : l.off), + ), + LoadingButton( + variant: ButtonVariant.text, + label: widget.enrolled ? l.disable : l.enable, + onTap: widget.enrolled ? _disable : _enable, + isLoading: isLoading, + ) + ], + ), + if (exception != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: ErrorText(exception: exception!), + ) + ], + ); + } +} + +/// {@template ui.auth.screens.profile_screen} +/// A pre-built profile screen that allows to link more auth providers, +/// unlink auth providers, edit user name and delete the account. Could also +/// contain a user-defined content. +/// {@endtemplate} +class ProfileScreen extends MultiProviderScreen { + /// A user-defined content of the screen. + final List children; + + /// {@macro ui.auth.widgets.user_avatar.placeholder_color} + final Color? avatarPlaceholderColor; + + /// {@macro ui.auth.widgets.user_avatar.shape} + final ShapeBorder? avatarShape; + + /// {@macro ui.auth.widgets.user_avatar.size} + final double? avatarSize; + + /// Possible actions that could be triggered: + /// + /// - [SignedOutAction] + /// - [AuthStateChangeAction] + /// + /// ```dart + /// ProfileScreen( + /// actions: [ + /// SignedOutAction((context) { + /// Navigator.of(context).pushReplacementNamed('/sign-in'); + /// }), + /// AuthStateChangeAction((context, state) { + /// ScaffoldMessenger.of(context).showSnackBar( + /// SnackBar( + /// content: Text("Provider sucessfully linked!"), + /// ), + /// ); + /// }), + /// ] + /// ) + /// ``` + final List? actions; + + /// See [Scaffold.appBar]. + final AppBar? appBar; + + /// See [CupertinoPageScaffold.navigationBar]. + final CupertinoNavigationBar? cupertinoNavigationBar; + + /// A configuration object used to construct a dynamic link for email + /// verification. + final ActionCodeSettings? actionCodeSettings; + + final bool showMFATile; + + const ProfileScreen({ + Key? key, + FirebaseAuth? auth, + List? providers, + this.avatarPlaceholderColor, + this.avatarShape, + this.avatarSize, + this.children = const [], + this.actions, + this.appBar, + this.cupertinoNavigationBar, + this.actionCodeSettings, + this.showMFATile = false, + }) : super(key: key, providers: providers, auth: auth); + + Future _reauthenticate(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + + return showReauthenticateDialog( + context: context, + providers: providers, + auth: auth, + onSignedIn: () => Navigator.of(context).pop(true), + actionButtonLabelOverride: l.deleteAccount, + ); + } + + List getLinkedProviders(User user) { + return providers + .where((provider) => user.isProviderLinked(provider.providerId)) + .toList(); + } + + List getAvailableProviders(BuildContext context, User user) { + final platform = Theme.of(context).platform; + + return providers + .where( + (provider) => + !user.isProviderLinked(provider.providerId) && + provider.supportsPlatform(platform), + ) + .toList(); + } + + @override + Widget build(BuildContext context) { + return FirebaseUITheme( + styles: const {}, + child: Builder(builder: buildPage), + ); + } + + Widget buildPage(BuildContext context) { + final isCupertino = CupertinoUserInterfaceLevel.maybeOf(context) != null; + final providersScopeKey = RebuildScopeKey(); + final mfaScopeKey = RebuildScopeKey(); + final emailVerificationScopeKey = RebuildScopeKey(); + + final user = auth.currentUser!; + + final content = Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Align( + child: UserAvatar( + auth: auth, + placeholderColor: avatarPlaceholderColor, + shape: avatarShape, + size: avatarSize, + ), + ), + Align(child: EditableUserDisplayName(auth: auth)), + if (!user.emailVerified) ...[ + RebuildScope( + builder: (context) { + if (user.emailVerified) { + return const SizedBox.shrink(); + } + + return _EmailVerificationBadge( + auth: auth, + actionCodeSettings: actionCodeSettings, + ); + }, + scopeKey: emailVerificationScopeKey, + ), + ], + RebuildScope( + builder: (context) { + final user = auth.currentUser!; + final linkedProviders = getLinkedProviders(user); + + if (linkedProviders.isEmpty) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.only(top: 32), + child: _LinkedProvidersRow( + auth: auth, + providers: linkedProviders, + onProviderUnlinked: providersScopeKey.rebuild, + ), + ); + }, + scopeKey: providersScopeKey, + ), + RebuildScope( + builder: (context) { + final user = auth.currentUser!; + final availableProviders = getAvailableProviders(context, user); + + if (availableProviders.isEmpty) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.only(top: 32), + child: _AvailableProvidersRow( + auth: auth, + providers: availableProviders, + onProviderLinked: providersScopeKey.rebuild, + ), + ); + }, + scopeKey: providersScopeKey, + ), + if (showMFATile) + RebuildScope( + builder: (context) { + final user = auth.currentUser!; + final mfa = user.multiFactor; + + return FutureBuilder>( + future: mfa.getEnrolledFactors(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const SizedBox.shrink(); + } + + final enrolledFactors = snapshot.requireData; + + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: _MFABadge( + providers: providers, + enrolled: enrolledFactors.isNotEmpty, + auth: auth, + onToggled: mfaScopeKey.rebuild, + ), + ); + }, + ); + }, + scopeKey: mfaScopeKey, + ), + ...children, + const SizedBox(height: 16), + SignOutButton( + auth: auth, + variant: ButtonVariant.outlined, + ), + const SizedBox(height: 8), + DeleteAccountButton( + auth: auth, + onSignInRequired: () { + return _reauthenticate(context); + }, + ), + ], + ); + final body = Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > 500) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: content, + ); + } else { + return content; + } + }, + ), + ), + ); + + Widget child = SafeArea(child: SingleChildScrollView(child: body)); + + if (isCupertino) { + child = CupertinoPageScaffold( + navigationBar: cupertinoNavigationBar, + child: SafeArea( + child: SingleChildScrollView(child: child), + ), + ); + } else { + child = Scaffold( + appBar: appBar, + body: SafeArea( + child: SingleChildScrollView(child: body), + ), + ); + } + + return FirebaseUIActions( + actions: actions ?? const [], + child: child, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/screens/register_screen.dart b/packages/firebase_ui_auth/lib/src/screens/register_screen.dart new file mode 100644 index 000000000000..67114394eb6e --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/screens/register_screen.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart' show FirebaseAuth; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +import 'internal/login_screen.dart'; +import 'internal/multi_provider_screen.dart'; + +/// A screen displaying a fully styled Registration flow for Authentication. +/// +/// {@subCategory service:auth} +/// {@subCategory type:screen} +/// {@subCategory description:A screen displaying a fully styled Registration flow for Authentication.} +/// {@subCategory img:https://place-hold.it/400x150} +class RegisterScreen extends MultiProviderScreen { + /// {@macro ui.auth.screens.responsive_page.header_max_extent} + final double? headerMaxExtent; + + /// {@macro ui.auth.screens.responsive_page.header_builder} + final HeaderBuilder? headerBuilder; + + /// {@macro ui.auth.screens.responsive_page.side_builder} + final SideBuilder? sideBuilder; + + /// Indicates whether icon-only or icon and text OAuth buttons should be used. + /// Icon-only buttons are placed in a row. + final OAuthButtonVariant? oauthButtonVariant; + + /// {@macro ui.auth.screens.responsive_page.desktop_layout_direction} + final TextDirection? desktopLayoutDirection; + + /// An email that [EmailForm] should be pre-filled with. + final String? email; + + /// See [Scaffold.resizeToAvoidBottomInset] + final bool? resizeToAvoidBottomInset; + + /// Whether the "Login/Register" link should be displayed. The link changes + /// the type of the [AuthAction] from [AuthAction.signIn] + /// and [AuthAction.signUp] and vice versa. + final bool? showAuthActionSwitch; + + /// {@macro ui.auth.views.login_view.subtitle_builder} + final AuthViewContentBuilder? subtitleBuilder; + + /// {@macro ui.auth.views.login_view.footer_builder} + final AuthViewContentBuilder? footerBuilder; + + /// {@macro ui.auth.screens.responsive_page.breakpoint} + final double breakpoint; + + /// A set of styles that are provided to the descendant widgets. + /// + /// Possible styles are: + /// * [EmailFormStyle] + final Set? styles; + + const RegisterScreen({ + Key? key, + FirebaseAuth? auth, + List? providers, + this.headerMaxExtent, + this.headerBuilder, + this.sideBuilder, + this.oauthButtonVariant = OAuthButtonVariant.icon_and_text, + this.desktopLayoutDirection, + this.email, + this.resizeToAvoidBottomInset = false, + this.showAuthActionSwitch, + this.subtitleBuilder, + this.footerBuilder, + this.breakpoint = 800, + this.styles, + }) : super(key: key, auth: auth, providers: providers); + + @override + Widget build(BuildContext context) { + return LoginScreen( + styles: styles, + action: AuthAction.signUp, + providers: providers, + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + auth: auth, + headerMaxExtent: headerMaxExtent, + headerBuilder: headerBuilder, + sideBuilder: sideBuilder, + desktopLayoutDirection: desktopLayoutDirection, + oauthButtonVariant: oauthButtonVariant, + email: email, + showAuthActionSwitch: showAuthActionSwitch, + subtitleBuilder: subtitleBuilder, + footerBuilder: footerBuilder, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/screens/sign_in_screen.dart b/packages/firebase_ui_auth/lib/src/screens/sign_in_screen.dart new file mode 100644 index 000000000000..99527aeedb8f --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/screens/sign_in_screen.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart' show FirebaseAuth; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +import 'internal/login_screen.dart'; +import 'internal/multi_provider_screen.dart'; + +/// {@template ui.auth.screens.sign_in_screen} +/// A screen displaying a fully styled Sign In flow for Authentication. +/// {@endtemplate} +class SignInScreen extends MultiProviderScreen { + /// {@macro ui.auth.screens.responsive_page.header_max_extent} + final double? headerMaxExtent; + + /// {@macro ui.auth.screens.responsive_page.header_builder} + final HeaderBuilder? headerBuilder; + + /// {@macro ui.auth.screens.responsive_page.side_builder} + final SideBuilder? sideBuilder; + + /// Indicates whether icon-only or icon and text OAuth buttons should be used. + /// Icon-only buttons are placed in a row. + final OAuthButtonVariant? oauthButtonVariant; + + /// {@macro ui.auth.screens.responsive_page.desktop_layout_direction} + final TextDirection? desktopLayoutDirection; + + /// A email that [EmailForm] would be pre-filled with. + final String? email; + + /// See [Scaffold.resizeToAvoidBottomInset] + final bool? resizeToAvoidBottomInset; + + /// Whether the "Login/Register" link should be displayed. The link changes + /// the type of the [AuthAction] from [AuthAction.signIn] + /// and [AuthAction.signUp] and vice versa. + final bool? showAuthActionSwitch; + + /// {@macro ui.auth.views.login_view.subtitle_builder} + final AuthViewContentBuilder? subtitleBuilder; + + /// {@macro ui.auth.views.login_view.subtitle_builder} + final AuthViewContentBuilder? footerBuilder; + + /// A [Key] that would be passed down to the [LoginView]. + final Key? loginViewKey; + + /// [SignInScreen] could invoke these actions: + /// + /// * [EmailLinkSignInAction] + /// * [VerifyPhoneAction] + /// * [ForgotPasswordAction] + /// * [AuthStateChangeAction] + /// + /// These actions could be used to trigger route transtion or display + /// a dialog. + /// + /// ```dart + /// SignInScreen( + /// actions: [ + /// ForgotPasswordAction((context, email) { + /// Navigator.pushNamed( + /// context, + /// '/forgot-password', + /// arguments: {'email': email}, + /// ); + /// }), + /// VerifyPhoneAction((context, _) { + /// Navigator.pushNamed(context, '/phone'); + /// }), + /// AuthStateChangeAction((context, state) { + /// if (!state.user!.emailVerified) { + /// Navigator.pushNamed(context, '/verify-email'); + /// } else { + /// Navigator.pushReplacementNamed(context, '/profile'); + /// } + /// }), + /// EmailLinkSignInAction((context) { + /// Navigator.pushReplacementNamed(context, '/email-link-sign-in'); + /// }), + /// ], + /// ) + /// ``` + final List actions; + + /// {@macro ui.auth.screens.responsive_page.breakpoint} + final double breakpoint; + + /// A set of styles that are provided to the descendant widgets. + /// + /// Possible styles are: + /// * [EmailFormStyle] + final Set? styles; + + /// {@macro ui.auth.screens.sign_in_screen} + const SignInScreen({ + Key? key, + List? providers, + FirebaseAuth? auth, + this.headerMaxExtent, + this.headerBuilder, + this.sideBuilder, + this.oauthButtonVariant = OAuthButtonVariant.icon_and_text, + this.desktopLayoutDirection, + this.resizeToAvoidBottomInset = true, + this.showAuthActionSwitch, + this.email, + this.subtitleBuilder, + this.footerBuilder, + this.loginViewKey, + this.actions = const [], + this.breakpoint = 800, + this.styles, + }) : super(key: key, providers: providers, auth: auth); + + Future _signInWithDifferentProvider( + BuildContext context, + DifferentSignInMethodsFound state, + ) async { + await showDifferentMethodSignInDialog( + availableProviders: state.methods, + providers: providers, + context: context, + auth: auth, + onSignedIn: () { + Navigator.of(context).pop(); + }, + ); + + await auth.currentUser!.linkWithCredential(state.credential!); + } + + @override + Widget build(BuildContext context) { + final handlesDifferentSignInMethod = this + .actions + .whereType>() + .isNotEmpty; + + final actions = [ + ...this.actions, + if (!handlesDifferentSignInMethod) + AuthStateChangeAction(_signInWithDifferentProvider) + ]; + + return FirebaseUIActions( + actions: actions, + child: LoginScreen( + styles: styles, + loginViewKey: loginViewKey, + action: AuthAction.signIn, + providers: providers, + auth: auth, + headerMaxExtent: headerMaxExtent, + headerBuilder: headerBuilder, + sideBuilder: sideBuilder, + desktopLayoutDirection: desktopLayoutDirection, + oauthButtonVariant: oauthButtonVariant, + email: email, + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + showAuthActionSwitch: showAuthActionSwitch, + subtitleBuilder: subtitleBuilder, + footerBuilder: footerBuilder, + breakpoint: breakpoint, + ), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/screens/sms_code_input_screen.dart b/packages/firebase_ui_auth/lib/src/screens/sms_code_input_screen.dart new file mode 100644 index 000000000000..2dce48e9cbad --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/screens/sms_code_input_screen.dart @@ -0,0 +1,140 @@ +import 'package:firebase_auth/firebase_auth.dart' show FirebaseAuth; +import 'package:flutter/material.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; + +import '../widgets/internal/universal_button.dart'; +import '../widgets/internal/universal_scaffold.dart'; +import '../screens/internal/responsive_page.dart'; + +/// A screen displaying a UI which allows users to enter an SMS validation code +/// sent from Firebase. +/// +/// {@subCategory service:auth} +/// {@subCategory type:screen} +/// {@subCategory description:A screen displaying SMS verification UI.} +/// {@subCategory img:https://place-hold.it/400x150} +class SMSCodeInputScreen extends StatelessWidget { + /// {@macro ui.auth.auth_action} + final AuthAction? action; + + /// SMSCodeInputScreen could invoke these actions: + /// + /// * [AuthStateChangeAction] + /// + /// ```dart + /// SMSCodeInputScreen( + /// actions: [ + /// AuthStateChangeAction((context, state) { + /// Navigator.pushReplacementNamed(context, '/'); + /// }), + /// ], + /// ); + /// ``` + final List? actions; + + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// A unique key that could be used to obtain an instance of the + /// [PhoneAuthController]. + /// + /// ```dart + /// final ctrl = AuthFlowBuilder.getController(flowKey); + /// ctrl.acceptPhoneNumber('+1234567890'); + /// ``` + final Object flowKey; + + /// {@macro ui.auth.screens.responsive_page.desktop_layout_direction} + final TextDirection? desktopLayoutDirection; + + /// {@macro ui.auth.screens.responsive_page.side_builder} + final SideBuilder? sideBuilder; + + /// {@macro ui.auth.screens.responsive_page.header_builder} + final HeaderBuilder? headerBuilder; + + /// {@macro ui.auth.screens.responsive_page.header_max_extent} + final double? headerMaxExtent; + + /// {@macro ui.auth.screens.responsive_page.content_flex} + final int? contentFlex; + + /// {@macro ui.auth.screens.responsive_page.max_width} + final double? maxWidth; + + /// {@macro ui.auth.screens.responsive_page.breakpoint} + final double breakpoint; + + const SMSCodeInputScreen({ + Key? key, + this.action, + this.actions, + this.auth, + required this.flowKey, + this.desktopLayoutDirection, + this.sideBuilder, + this.headerBuilder, + this.headerMaxExtent, + this.breakpoint = 500, + this.contentFlex, + this.maxWidth, + }) : super(key: key); + + void _reset() { + final ctrl = AuthFlowBuilder.getController(flowKey); + ctrl?.reset(); + } + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + + return WillPopScope( + onWillPop: () async { + _reset(); + return true; + }, + child: FirebaseUIActions( + actions: actions ?? const [], + child: UniversalScaffold( + body: Center( + child: ResponsivePage( + breakpoint: breakpoint, + maxWidth: maxWidth, + desktopLayoutDirection: desktopLayoutDirection, + sideBuilder: sideBuilder, + headerBuilder: headerBuilder, + headerMaxExtent: headerMaxExtent, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SMSCodeInputView( + auth: auth, + action: action, + flowKey: flowKey, + onCodeVerified: () { + if (actions != null) return; + + Navigator.of(context).popUntil((route) { + return route.isFirst; + }); + }, + ), + UniversalButton( + variant: ButtonVariant.text, + text: l.goBackButtonLabel, + onPressed: () { + _reset(); + Navigator.of(context).pop(); + }, + ) + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/screens/universal_email_sign_in_screen.dart b/packages/firebase_ui_auth/lib/src/screens/universal_email_sign_in_screen.dart new file mode 100644 index 000000000000..1707f393e745 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/screens/universal_email_sign_in_screen.dart @@ -0,0 +1,121 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import '../widgets/internal/universal_page_route.dart'; +import '../widgets/internal/universal_scaffold.dart'; +import 'internal/multi_provider_screen.dart'; + +/// A screen that allows to resolve previously used providers for a given email. +class UniversalEmailSignInScreen extends MultiProviderScreen { + /// A callback that is being called when providers fetch request completed. + final ProvidersFoundCallback? onProvidersFound; + + const UniversalEmailSignInScreen({ + Key? key, + + /// {@macro ui.auth.auth_controller.auth} + FirebaseAuth? auth, + + /// A list of all supported auth providers + List? providers, + this.onProvidersFound, + }) : assert(onProvidersFound != null || providers != null), + super(key: key, auth: auth, providers: providers); + + Widget _wrap(BuildContext context, Widget child) { + return AuthStateListener( + child: FirebaseUIActions.inherit( + from: context, + child: child, + ), + listener: (_, newState, controller) { + if (newState is SignedIn) { + Navigator.of(context).pop(); + } + return null; + }, + ); + } + + void _defaultAction( + BuildContext context, + String email, + List providerIds, + ) { + late Route route; + + if (providerIds.isEmpty) { + route = createPageRoute( + context: context, + builder: (context) => _wrap( + context, + RegisterScreen( + showAuthActionSwitch: false, + providers: providers, + auth: auth, + email: email, + ), + ), + ); + } else { + final providersMap = providers.fold>( + {}, + (acc, element) { + return { + ...acc, + element.providerId: element, + }; + }, + ); + + final authorizedProviders = providerIds + .where(providersMap.containsKey) + .map((id) => providersMap[id]!) + .toList(); + + route = createPageRoute( + context: context, + builder: (context) => _wrap( + context, + SignInScreen( + showAuthActionSwitch: false, + providers: authorizedProviders, + auth: auth, + email: email, + ), + ), + ); + } + + Navigator.of(context).push(route); + } + + @override + Widget build(BuildContext context) { + final content = FindProvidersForEmailView( + auth: auth, + onProvidersFound: onProvidersFound ?? + (email, providers) => _defaultAction(context, email, providers), + ); + + return UniversalScaffold( + body: Center( + child: LayoutBuilder( + builder: (context, constraints) { + if (constraints.biggest.width < 500) { + return Padding( + padding: const EdgeInsets.all(20), + child: content, + ); + } else { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: content, + ); + } + }, + ), + ), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/styling/style.dart b/packages/firebase_ui_auth/lib/src/styling/style.dart new file mode 100644 index 000000000000..e3788e0306ea --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/styling/style.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; + +import 'theme.dart'; + +/// An abstract class that should be implemented by all styling classes. +abstract class FirebaseUIStyle { + const FirebaseUIStyle(); + + /// Resolves the style object via [BuildContext] and provides a [defaultValue] + /// if none was found. + static T ofType( + BuildContext context, + T defaultValue, + ) { + final el = + context.getElementForInheritedWidgetOfExactType(); + if (el == null) return defaultValue; + + context.dependOnInheritedElement(el, aspect: T); + final style = (el as FirebaseUIThemeElement).styles[T]; + + if (style == null) return defaultValue; + return style as T; + } + + /// Could wrap a [child] with a [Theme] to override global styles if + /// necessary. + Widget applyToMaterialTheme(BuildContext context, Widget child) => child; + + /// Could wrap a [child] with a [CupertinoTheme] to override global styles if + /// necessary. + Widget applyToCupertinoTheme(BuildContext context, Widget child) => child; + + /// Wires the style with the widget tree and makes sure it is accessible + /// from the [child] or its descendants. + Widget mount({required Widget child}) { + return FirebaseUITheme( + styles: {this}, + child: child, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/styling/theme.dart b/packages/firebase_ui_auth/lib/src/styling/theme.dart new file mode 100644 index 000000000000..c04e67290db1 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/styling/theme.dart @@ -0,0 +1,81 @@ +import 'package:flutter/widgets.dart'; + +import 'style.dart'; + +typedef StylesMap = Map; + +StylesMap _buildStylesMap(Set styles) { + return styles.fold({}, (acc, el) { + return { + ...acc, + el.runtimeType: el, + }; + }); +} + +/// FirebaseUI styles provider widget. +/// +/// Shouldn't be used if you're using pre-built screens, but could be used +/// if you're building your own and using only widgets from the FirebaseUI. +class FirebaseUITheme extends InheritedModel { + /// A set of styles that need to be provded down the widget tree. + final Set styles; + + const FirebaseUITheme({ + Key? key, + required Widget child, + required this.styles, + }) : super(key: key, child: child); + + @override + bool updateShouldNotify(FirebaseUITheme oldWidget) { + return oldWidget.styles != styles; + } + + @override + bool updateShouldNotifyDependent( + FirebaseUITheme oldWidget, + Set dependencies, + ) { + final oldStyles = _buildStylesMap(oldWidget.styles); + final newStyles = _buildStylesMap(styles); + + return dependencies.any((element) { + final oldStyle = oldStyles[element]; + final newStyle = newStyles[element]; + return oldStyle != newStyle; + }); + } + + @override + InheritedModelElement createElement() { + return FirebaseUIThemeElement(this); + } +} + +class FirebaseUIThemeElement extends InheritedModelElement { + FirebaseUIThemeElement(InheritedModel widget) : super(widget); + + @override + FirebaseUITheme get widget => super.widget as FirebaseUITheme; + + StylesMap styles = {}; + FirebaseUIThemeElement? _parent; + + @override + void mount(Element? parent, Object? newSlot) { + _parent = parent?.getElementForInheritedWidgetOfExactType() + as FirebaseUIThemeElement?; + + if (_parent != null) { + dependOnInheritedElement(_parent!); + } + + styles = { + if (_parent != null) ..._parent!.styles, + ..._buildStylesMap(widget.styles), + }; + + super.mount(parent, newSlot); + } +} diff --git a/packages/firebase_ui_auth/lib/src/validators.dart b/packages/firebase_ui_auth/lib/src/validators.dart new file mode 100644 index 000000000000..621ddc6be192 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/validators.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:email_validator/email_validator.dart' as e; + +/// An abstract class for building composite input validators. +abstract class Validator { + /// Error text that should be displayed if the valie is invalid. + final String errorText; + final List _validators; + + Validator(this.errorText, List children) : _validators = children; + + /// Triggers validation. + String? validate(String? value); + + /// Triggers all children validators. + @protected + String? validateChildren(String? value) { + for (final validator in _validators) { + final error = validator.validate(value); + if (error != null) { + return error; + } + } + return null; + } + + /// Returns a callback that could be used as a [TextFormField.validator]. + static String? Function(String?) validateAll(List validators) { + return CompositeValidator(validators).validate; + } +} + +/// A validator that doesn't have it's own logic and instead delegates +/// the validation to its children. +class CompositeValidator extends Validator { + CompositeValidator(List children) : super('', children); + + @override + String? validate(String? value) { + return validateChildren(value); + } +} + +/// Validates that the input is not a null and not an empty string. +class NotEmpty extends Validator { + NotEmpty(String errorText) : super(errorText, []); + + @override + String? validate(String? value) { + return (value == null || value.isEmpty) ? errorText : null; + } +} + +/// Validates an email. +class EmailValidator extends Validator { + EmailValidator(String errorText) : super(errorText, []); + + @override + String? validate(String? value) { + if (value == null) return errorText; + return e.EmailValidator.validate(value) ? null : errorText; + } +} + +/// Validates that the passwords match. +class ConfirmPasswordValidator extends Validator { + final TextEditingController controller; + + ConfirmPasswordValidator( + this.controller, + String errorText, + ) : super(errorText, []); + + @override + String? validate(String? value) { + return value == controller.text ? null : errorText; + } +} + +/// Validates phone number. +/// Should be used together with the [NotEmpty] and +/// [FilteringTextInputFormatter.digitsOnly]. +class PhoneValidator extends Validator { + PhoneValidator(String errorText) : super(errorText, []); + + @override + String? validate(String? value) { + return value!.length < 7 ? errorText : null; + } +} diff --git a/packages/firebase_ui_auth/lib/src/views/different_method_sign_in_view.dart b/packages/firebase_ui_auth/lib/src/views/different_method_sign_in_view.dart new file mode 100644 index 000000000000..df2b1faf44cc --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/views/different_method_sign_in_view.dart @@ -0,0 +1,68 @@ +import 'package:firebase_auth/firebase_auth.dart' show FirebaseAuth; +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +/// {@template ui.auth.views.different_method_sign_in_view} +/// A view that renders a list of providers that were previously used by the +/// user to authenticate. +/// {@endtemplate} +class DifferentMethodSignInView extends StatelessWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// A list of all providers that were previously used to authenticate. + final List availableProviders; + + /// A list of all supported auth providers. + final List providers; + + /// A callback that is being called when the user has signed in using on of + /// the [availableProviders]. + final VoidCallback? onSignedIn; + + /// {@macro ui.auth.views.different_method_sign_in_view} + const DifferentMethodSignInView({ + Key? key, + required this.availableProviders, + required this.providers, + this.auth, + this.onSignedIn, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final providersMap = this.providers.fold>( + {}, + (map, config) { + return { + ...map, + config.providerId: config, + }; + }, + ); + + List providers = []; + + for (final p in availableProviders) { + final providerConfig = providersMap[p]; + if (providerConfig != null) { + providers.add(providerConfig); + } + } + + return AuthStateListener( + child: LoginView( + action: AuthAction.signIn, + providers: providers, + showTitle: false, + ), + listener: (oldState, newState, ctrl) { + if (newState is SignedIn) { + onSignedIn?.call(); + } + + return false; + }, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart b/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart new file mode 100644 index 000000000000..b2704b6ba7c4 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart @@ -0,0 +1,88 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/widgets.dart' hide Title; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; + +import '../widgets/internal/loading_button.dart'; +import '../widgets/internal/title.dart'; + +/// {@template ui.auth.views.email_link_sign_in_view} +/// A view that could be used to build a custom [EmailLinkSignInScreen]. +/// {@endtemplate} +class EmailLinkSignInView extends StatefulWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// An instance of the [EmailLinkAuthProvider] that should be used to + /// authenticate. + final EmailLinkAuthProvider provider; + + /// A focus node that could be used to control the focus state of the + /// [EmailInput]. + final FocusNode? emailInputFocusNode; + + /// {@macro ui.auth.views.email_link_sign_in_view} + const EmailLinkSignInView({ + Key? key, + this.auth, + required this.provider, + this.emailInputFocusNode, + }) : super(key: key); + + @override + State createState() => _EmailLinkSignInViewState(); +} + +class _EmailLinkSignInViewState extends State { + final emailCtrl = TextEditingController(); + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + final formKey = GlobalKey(); + + return AuthFlowBuilder( + auth: widget.auth, + provider: widget.provider, + builder: (context, state, ctrl, child) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Title(text: l.signInWithEmailLinkViewTitleText), + const SizedBox(height: 16), + if (state is! AwaitingDynamicLink) + Form( + key: formKey, + child: EmailInput( + autofocus: true, + focusNode: widget.emailInputFocusNode, + controller: emailCtrl, + onSubmitted: (v) { + if (formKey.currentState?.validate() ?? false) { + ctrl.sendLink(emailCtrl.text); + } + }, + ), + ) + else ...[ + Text(l.signInWithEmailLinkSentText), + const SizedBox(height: 16), + ], + if (state is! AwaitingDynamicLink) ...[ + const SizedBox(height: 8), + LoadingButton( + isLoading: state is SendingLink, + label: l.sendLinkButtonLabel, + onTap: () { + ctrl.sendLink(emailCtrl.text); + }, + ), + ], + const SizedBox(height: 8), + if (state is AuthFailed) ErrorText(exception: state.exception), + ], + ); + }, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/views/find_providers_for_email_view.dart b/packages/firebase_ui_auth/lib/src/views/find_providers_for_email_view.dart new file mode 100644 index 000000000000..a48348c43478 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/views/find_providers_for_email_view.dart @@ -0,0 +1,96 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart' hide Title; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import '../widgets/internal/loading_button.dart'; + +import '../widgets/internal/title.dart'; + +/// A callback that is being called when providers fetch request is completed. +typedef ProvidersFoundCallback = void Function( + String email, + List providers, +); + +/// {@template ui.auth.views.find_providers_for_email_view} +/// A view that could be used to build a custom [UniversalEmailSignInScreen]. +/// {@endtemplate} +class FindProvidersForEmailView extends StatefulWidget { + final ProvidersFoundCallback? onProvidersFound; + + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// {@macro ui.auth.views.find_providers_for_email_view} + const FindProvidersForEmailView({ + Key? key, + this.onProvidersFound, + this.auth, + }) : super(key: key); + + @override + State createState() => + _FindProvidersForEmailViewState(); +} + +class _FindProvidersForEmailViewState extends State { + final formKey = GlobalKey(); + final emailCtrl = TextEditingController(); + + late final flow = UniversalEmailSignInFlow( + provider: UniversalEmailSignInProvider(), + auth: widget.auth, + ); + + void _submit(UniversalEmailSignInController ctrl, String email) { + if (formKey.currentState!.validate()) { + ctrl.findProvidersForEmail(email); + } + } + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + const spacer = SizedBox(height: 24); + + return AuthFlowBuilder( + auth: widget.auth, + flow: flow, + listener: (oldState, newState, controller) { + if (newState is DifferentSignInMethodsFound) { + widget.onProvidersFound?.call( + emailCtrl.text, + newState.methods, + ); + } + }, + builder: (context, state, ctrl, child) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Title( + text: l.findProviderForEmailTitleText, + ), + spacer, + Form( + key: formKey, + child: EmailInput( + controller: emailCtrl, + onSubmitted: (_) { + _submit(ctrl, emailCtrl.text); + }, + ), + ), + spacer, + LoadingButton( + isLoading: state is FetchingProvidersForEmail, + label: l.continueText, + onTap: () { + _submit(ctrl, emailCtrl.text); + }, + ) + ], + ), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/views/forgot_password_view.dart b/packages/firebase_ui_auth/lib/src/views/forgot_password_view.dart new file mode 100644 index 000000000000..772e2aa382eb --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/views/forgot_password_view.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart' hide Title; + +import 'package:firebase_auth/firebase_auth.dart' + show ActionCodeSettings, FirebaseAuth, FirebaseAuthException; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import '../widgets/internal/universal_button.dart'; + +import '../widgets/internal/loading_button.dart'; +import '../widgets/internal/title.dart'; + +/// {@template ui.auth.views.forgot_password_view} +/// A view that could be used to build a custom [ForgotPasswordScreen]. +/// {@endtemplate} +class ForgotPasswordView extends StatefulWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// A configuration object that is used to construct a dynamic link. + final ActionCodeSettings? actionCodeSettings; + + /// Returned widget would be placed under the title. + final WidgetBuilder? subtitleBuilder; + + /// Returned widget would be placed at the bottom of the view. + final WidgetBuilder? footerBuilder; + + /// An email that [EmailInput] should be pre-filled with. + final String? email; + + /// {@macro ui.auth.views.forgot_password_view} + const ForgotPasswordView({ + Key? key, + this.auth, + this.email, + this.actionCodeSettings, + this.subtitleBuilder, + this.footerBuilder, + }) : super(key: key); + + @override + // ignore: library_private_types_in_public_api + _ForgotPasswordViewState createState() => _ForgotPasswordViewState(); +} + +class _ForgotPasswordViewState extends State { + late final emailCtrl = TextEditingController(text: widget.email ?? ''); + final formKey = GlobalKey(); + bool emailSent = false; + + FirebaseAuth get auth => widget.auth ?? FirebaseAuth.instance; + bool isLoading = false; + FirebaseAuthException? exception; + + Future _submit(String email) async { + setState(() => isLoading = true); + try { + await auth.sendPasswordResetEmail( + email: email, + actionCodeSettings: widget.actionCodeSettings, + ); + + emailSent = true; + } on FirebaseAuthException catch (e) { + exception = e; + } finally { + setState(() => isLoading = false); + } + } + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + const spacer = SizedBox(height: 32); + + return Form( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Title(text: l.forgotPasswordViewTitle), + if (!emailSent) ...[ + spacer, + widget.subtitleBuilder?.call(context) ?? + Text(l.forgotPasswordHintText), + ], + spacer, + if (!emailSent) ...[ + EmailInput( + autofocus: false, + controller: emailCtrl, + onSubmitted: _submit, + ), + spacer, + ] else ...[ + Text(l.passwordResetEmailSentText), + spacer, + ], + if (exception != null) ...[ + const SizedBox(height: 16), + ErrorText(exception: exception!), + const SizedBox(height: 16), + ], + if (!emailSent) + LoadingButton( + isLoading: isLoading, + label: l.resetPasswordButtonLabel, + onTap: () { + if (formKey.currentState!.validate()) { + _submit(emailCtrl.text); + } + }, + ), + const SizedBox(height: 8), + UniversalButton( + variant: ButtonVariant.text, + text: l.goBackButtonLabel, + onPressed: () => Navigator.pop(context), + ), + if (widget.footerBuilder != null) widget.footerBuilder!(context), + ], + ), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/views/login_view.dart b/packages/firebase_ui_auth/lib/src/views/login_view.dart new file mode 100644 index 000000000000..10ee399b236a --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/views/login_view.dart @@ -0,0 +1,248 @@ +import 'package:flutter/cupertino.dart' hide Title; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart' hide Title; + +import 'package:firebase_auth/firebase_auth.dart' show FirebaseAuth; + +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart' + hide OAuthProviderButtonBase; + +import '../widgets/internal/title.dart'; + +typedef AuthViewContentBuilder = Widget Function( + BuildContext context, + AuthAction action, +); + +/// {@template ui.auth.views.login_view} +/// A view that could be used to build a custom [SignInScreen] or +/// [RegisterScreen]. +/// {@endtemplate} +class LoginView extends StatefulWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// {@macro ui.auth.auth_action} + final AuthAction action; + + /// Indicates whether icon-only or icon and text OAuth buttons should be used. + /// Icon-only buttons are placed in a row. + final OAuthButtonVariant? oauthButtonVariant; + final bool? showTitle; + final String? email; + + /// Whether the "Login/Register" link should be displayed. The link changes + /// the type of the [AuthAction] from [AuthAction.signIn] + /// and [AuthAction.signUp] and vice versa. + final bool? showAuthActionSwitch; + + /// {@template ui.auth.views.login_view.footer_builder} + /// A returned widget would be placed down the authentication related widgets. + /// {@endtemplate} + final AuthViewContentBuilder? footerBuilder; + + /// {@template ui.auth.views.login_view.subtitle_builder} + /// A returned widget would be placed up the authentication related widgets. + /// {@endtemplate} + final AuthViewContentBuilder? subtitleBuilder; + + final List providers; + + /// A label that would be used for the "Sign in" button. + final String? actionButtonLabelOverride; + + /// {@macro ui.auth.views.login_view} + const LoginView({ + Key? key, + required this.action, + required this.providers, + this.oauthButtonVariant = OAuthButtonVariant.icon_and_text, + this.auth, + this.showTitle = true, + this.email, + this.showAuthActionSwitch, + this.footerBuilder, + this.subtitleBuilder, + this.actionButtonLabelOverride, + }) : super(key: key); + + @override + State createState() => _LoginViewState(); +} + +class _LoginViewState extends State { + late AuthAction _action = widget.action; + bool get _showTitle => widget.showTitle ?? true; + bool get _showAuthActionSwitch => widget.showAuthActionSwitch ?? true; + bool _buttonsBuilt = false; + + void setAction(AuthAction action) { + setState(() { + _action = action; + }); + } + + Widget _buildOAuthButtons(TargetPlatform platform) { + final oauthProviders = widget.providers + .whereType() + .where((element) => element.supportsPlatform(platform)); + + _buttonsBuilt = true; + + final oauthButtonsList = oauthProviders.map((provider) { + return OAuthProviderButton( + provider: provider, + auth: widget.auth, + action: _action, + ); + }).toList(); + + if (widget.oauthButtonVariant == OAuthButtonVariant.icon_and_text) { + return Column( + mainAxisSize: MainAxisSize.min, + children: oauthButtonsList, + ); + } else { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: oauthButtonsList, + ); + } + } + + void _handleDifferentAuthAction(BuildContext context) { + if (_action == AuthAction.signIn) { + setState(() { + _action = AuthAction.signUp; + }); + } else { + setState(() { + _action = AuthAction.signIn; + }); + } + } + + List _buildHeader(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + + late String title; + late String hint; + late String actionText; + + if (_action == AuthAction.signIn) { + title = l.signInText; + hint = l.registerHintText; + actionText = l.registerText; + } else if (_action == AuthAction.signUp) { + title = l.registerText; + hint = l.signInHintText; + actionText = l.signInText; + } + + final isCupertino = CupertinoUserInterfaceLevel.maybeOf(context) != null; + TextStyle? hintStyle; + late Color registerTextColor; + + if (isCupertino) { + final theme = CupertinoTheme.of(context); + registerTextColor = theme.primaryColor; + hintStyle = theme.textTheme.textStyle.copyWith(fontSize: 12); + } else { + final theme = Theme.of(context); + hintStyle = Theme.of(context).textTheme.caption; + registerTextColor = theme.colorScheme.primary; + } + + return [ + Title(text: title), + const SizedBox(height: 16), + if (widget.subtitleBuilder != null) + widget.subtitleBuilder!( + context, + _action, + ), + if (_showAuthActionSwitch) ...[ + Text.rich( + TextSpan( + children: [ + TextSpan( + text: '$hint ', + style: hintStyle, + ), + TextSpan( + text: actionText, + style: Theme.of(context).textTheme.button?.copyWith( + color: registerTextColor, + ), + mouseCursor: SystemMouseCursors.click, + recognizer: TapGestureRecognizer() + ..onTap = () => _handleDifferentAuthAction(context), + ), + ], + ), + ), + const SizedBox(height: 16), + ] + ]; + } + + @override + void didUpdateWidget(covariant LoginView oldWidget) { + if (oldWidget.action != widget.action) { + _action = widget.action; + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + final platform = Theme.of(context).platform; + _buttonsBuilt = false; + + return IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (_showTitle) ..._buildHeader(context), + for (var provider in widget.providers) + if (provider.supportsPlatform(platform)) + if (provider is EmailAuthProvider) ...[ + const SizedBox(height: 8), + EmailForm( + key: ValueKey(_action), + auth: widget.auth, + action: _action, + provider: provider, + email: widget.email, + actionButtonLabelOverride: widget.actionButtonLabelOverride, + ) + ] else if (provider is PhoneAuthProvider) ...[ + const SizedBox(height: 8), + PhoneVerificationButton( + label: l.signInWithPhoneButtonText, + action: _action, + auth: widget.auth, + ), + const SizedBox(height: 8), + ] else if (provider is EmailLinkAuthProvider) ...[ + const SizedBox(height: 8), + EmailLinkSignInButton( + auth: widget.auth, + provider: provider, + ), + ] else if (provider is OAuthProvider && !_buttonsBuilt) + _buildOAuthButtons(platform), + if (widget.footerBuilder != null) + widget.footerBuilder!( + context, + widget.action, + ), + ], + ), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/views/phone_input_view.dart b/packages/firebase_ui_auth/lib/src/views/phone_input_view.dart new file mode 100644 index 000000000000..6d1ca2c4e598 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/views/phone_input_view.dart @@ -0,0 +1,145 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/widgets.dart' hide Title; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import '../widgets/internal/universal_button.dart'; + +import '../widgets/internal/title.dart'; + +typedef SMSCodeRequestedCallback = void Function( + BuildContext context, + AuthAction? action, + Object flowKey, + String phoneNumber, +); + +typedef PhoneNumberSubmitCallback = void Function(String phoneNumber); + +/// {@template ui.auth.views.phone_input_view} +/// A view that could be used to build a custom [PhoneInputScreen]. +/// {@endtemplate} +class PhoneInputView extends StatefulWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// {@macro ui.auth.auth_action} + final AuthAction? action; + + /// A unique object that could be used to obtain an instance of the + /// [PhoneAuthController]. + final Object flowKey; + + /// A callback that is being called when the SMS code was requested. + final SMSCodeRequestedCallback? onSMSCodeRequested; + + /// A callback that is being called when the user submits a phone number. + final PhoneNumberSubmitCallback? onSubmit; + + /// Returned widget would be placed under the title. + final WidgetBuilder? subtitleBuilder; + + /// Returned widget would be placed at the bottom. + final WidgetBuilder? footerBuilder; + + /// {@macro ui.auth.providers.phone_auth_provider.mfa_session} + final MultiFactorSession? multiFactorSession; + + /// {@macro ui.auth.providers.phone_auth_provider.mfa_hint} + final PhoneMultiFactorInfo? mfaHint; + + /// {@macro ui.auth.views.phone_input_view} + const PhoneInputView({ + Key? key, + required this.flowKey, + this.onSMSCodeRequested, + this.auth, + this.action, + this.onSubmit, + this.subtitleBuilder, + this.footerBuilder, + this.multiFactorSession, + this.mfaHint, + }) : super(key: key); + + @override + State createState() => _PhoneInputViewState(); +} + +class _PhoneInputViewState extends State { + final phoneInputKey = GlobalKey(); + + PhoneNumberSubmitCallback onSubmit(PhoneAuthController ctrl) => + (String phoneNumber) { + if (widget.onSubmit != null) { + widget.onSubmit!(phoneNumber); + } else { + ctrl.acceptPhoneNumber( + phoneNumber, + widget.multiFactorSession, + ); + } + }; + + void _next(PhoneAuthController ctrl) { + final number = PhoneInput.getPhoneNumber(phoneInputKey); + if (number != null) { + onSubmit(ctrl)(number); + } + } + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + final countryCode = Localizations.localeOf(context).countryCode; + + return AuthFlowBuilder( + flowKey: widget.flowKey, + action: widget.action, + auth: widget.auth, + listener: (oldState, newState, controller) { + if (newState is SMSCodeRequested) { + final cb = widget.onSMSCodeRequested ?? + FirebaseUIAction.ofType(context) + ?.callback; + + cb?.call( + context, + widget.action, + widget.flowKey, + PhoneInput.getPhoneNumber(phoneInputKey)!, + ); + } + }, + builder: (context, state, ctrl, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Title(text: l.phoneVerificationViewTitleText), + const SizedBox(height: 32), + if (widget.subtitleBuilder != null) + widget.subtitleBuilder!(context), + if (state is AwaitingPhoneNumber || state is SMSCodeRequested) ...[ + PhoneInput( + initialCountryCode: countryCode!, + onSubmit: onSubmit(ctrl), + key: phoneInputKey, + ), + const SizedBox(height: 16), + UniversalButton( + text: l.verifyPhoneNumberButtonText, + onPressed: () => _next(ctrl), + ), + ], + if (state is AuthFailed) ...[ + const SizedBox(height: 8), + ErrorText(exception: state.exception), + const SizedBox(height: 8), + ], + if (widget.footerBuilder != null) widget.footerBuilder!(context), + ], + ); + }, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/views/reauthenticate_view.dart b/packages/firebase_ui_auth/lib/src/views/reauthenticate_view.dart new file mode 100644 index 000000000000..aa8bdacedb2f --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/views/reauthenticate_view.dart @@ -0,0 +1,72 @@ +import 'package:firebase_auth/firebase_auth.dart' show FirebaseAuth; +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +/// {@template ui.auth.views.reauthenticate_view} +/// A view that could be used to build a custom [ReauthenticateDialog]. +/// {@endtemplate} +class ReauthenticateView extends StatelessWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// A list of all supported auth providers. + final List providers; + + /// A callback that is being called when the user has successfuly signed in. + final VoidCallback? onSignedIn; + + /// A label that would be used for the "Sign in" button. + final String? actionButtonLabelOverride; + + /// {@macro ui.auth.views.reauthenticate_view} + const ReauthenticateView({ + Key? key, + required this.providers, + this.auth, + this.onSignedIn, + this.actionButtonLabelOverride, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final linkedProviders = + (auth ?? FirebaseAuth.instance).currentUser!.providerData; + + final providersMap = this.providers.fold>( + {}, + (map, provider) { + return { + ...map, + provider.providerId: provider, + }; + }, + ); + + List providers = []; + + for (final p in linkedProviders) { + final provider = providersMap[p.providerId]; + + if (provider != null) { + providers.add(provider); + } + } + + return AuthStateListener( + child: LoginView( + action: AuthAction.signIn, + providers: providers, + showTitle: false, + showAuthActionSwitch: false, + actionButtonLabelOverride: actionButtonLabelOverride, + ), + listener: (oldState, newState, ctrl) { + if (newState is SignedIn) { + onSignedIn?.call(); + } + + return false; + }, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/views/sms_code_input_view.dart b/packages/firebase_ui_auth/lib/src/views/sms_code_input_view.dart new file mode 100644 index 000000000000..b0b5f2df1da0 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/views/sms_code_input_view.dart @@ -0,0 +1,121 @@ +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import 'package:flutter/material.dart'; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +import '../widgets/internal/universal_button.dart'; + +typedef SMSCodeSubmitCallback = void Function(String smsCode); + +/// {@template ui.auth.views.sms_code_input_view} +/// A view that could be used to build a custom [SMSCodeInputScreen]. +/// {@endtemplate} +class SMSCodeInputView extends StatefulWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// {@macro ui.auth.auth_action} + final AuthAction? action; + + /// A unique object that could be used to obtain an instance of the + /// [PhoneAuthController]. + final Object flowKey; + + /// A callback that is being called when the code was successfully verified. + final VoidCallback? onCodeVerified; + + /// A callback that is being called when the user submits a SMS code. + final SMSCodeSubmitCallback? onSubmit; + + /// {@macro ui.auth.views.sms_code_input_view} + const SMSCodeInputView({ + Key? key, + required this.flowKey, + this.onCodeVerified, + this.auth, + this.action, + this.onSubmit, + }) : super(key: key); + + @override + State createState() => _SMSCodeInputViewState(); +} + +class _SMSCodeInputViewState extends State { + final columnKey = GlobalKey(); + final key = GlobalKey(); + + @override + void initState() { + super.initState(); + + final state = AuthFlowBuilder.getState(widget.flowKey); + + if (state != null && state is SMSCodeSent) { + _codeSentState = state; + } + } + + SMSCodeSent? _codeSentState; + + void submit(String code, PhoneAuthController ctrl) { + if (widget.onSubmit != null) { + widget.onSubmit!(code); + } else if (_codeSentState != null) { + ctrl.verifySMSCode( + code, + confirmationResult: _codeSentState!.confirmationResult, + verificationId: _codeSentState!.verificationId, + ); + } + } + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + + return AuthFlowBuilder( + auth: widget.auth, + action: widget.action, + flowKey: widget.flowKey, + listener: (oldState, newState, controller) { + if (newState is SignedIn || newState is CredentialLinked) { + widget.onCodeVerified?.call(); + } + + if (newState is SMSCodeSent) { + _codeSentState = newState; + } + }, + builder: (context, state, ctrl, child) { + return IntrinsicWidth( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SMSCodeInput( + key: key, + onSubmit: (smsCode) { + submit(smsCode, ctrl); + }, + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.all(8), + child: UniversalButton( + onPressed: () { + final code = key.currentState!.code; + if (code.length < 6) return; + submit(code, ctrl); + }, + text: l.verifyCodeButtonText, + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/auth_flow_builder.dart b/packages/firebase_ui_auth/lib/src/widgets/auth_flow_builder.dart new file mode 100644 index 000000000000..14ff0f8d665d --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/auth_flow_builder.dart @@ -0,0 +1,322 @@ +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:flutter/widgets.dart'; +import 'package:firebase_auth/firebase_auth.dart' + show AuthCredential, FirebaseAuth; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; + +import '../auth_controller.dart'; +import '../auth_state.dart'; + +/// {@template ui.auth.widgets.auth_flow_builder.auth_flow_builder_callback} +/// A callback that is being called every time the [AuthFlow] changes it's +/// state. Returned widget is rendered as a child of [AuthFlowBuilder]. +/// {@endtemplate} +typedef AuthFlowBuilderCallback = Widget Function( + BuildContext context, + + /// Current [AuthState] of the [AuthFlow]. + AuthState state, + + /// An instance of [AuthController] that could be used to control the + /// [AuthFlow]. + T ctrl, + + /// A [Widget] that was provided to the [AuthFlowBuilder]. + Widget? child, +); + +/// {@template ui.auth.widgets.auth_flow_builder.state_transition_listener} +/// A callback that is being called when [AuthFlow] changes it's state. +/// +/// Invoked before the widget is built. +/// {@endtemplate} +typedef StateTransitionListener = void Function( + /// Previous state of the [AuthFlow]. + AuthState oldState, + + /// Current state of the [AuthFlow]. + AuthState newState, + + /// An instance of the [AuthController] that could be used to manipulate the + /// [AuthFlow]. + T controller, +); + +/// {@template ui.auth.widgets.auth_flow_builder} +/// A widget that is used to wire up the [AuthFlow]s with the widget tree. +/// +/// Could be used to build a custom UI and facilitate the built-in functionality +/// of the all available [AuthFlow]s: +/// +/// * [EmailAuthFlow] +/// * [EmailLinkFlow] +/// * [OAuthFlow] +/// * [PhoneAuthFlow] +/// * [UniversalEmailSignInFlow]. +/// +/// An example of how to build a custom email sign up form using +/// [AuthFlowBuilder]: +/// +/// ```dart +/// final emailCtrl = TextEditingController(); +/// final passwordCtrl = TextEditingController(); +/// +/// AuthFlowBuilder( +/// auth: FirebaseAuth.instance, +/// action: AuthAction.signUp, +/// listener: (oldState, newState, ctrl) { +/// if (newState is UserCreated) { +/// Navigator.of(context).pushReplacementNamed('/profile'); +/// } +/// }, +/// builder: (context, state, ctrl, child) { +/// if (state is AwaitingEmailAndPassword) { +/// return Column( +/// children: [ +/// TextField( +/// decoration: InputDecoration(labelText: 'Email'), +/// controller: emailCtrl, +/// ), +/// TextField( +/// decoration: InputDecoration(labelText: 'Password'), +/// controller: passwordCtrl, +/// ), +/// OutlinedButton( +/// child: Text('Sign Up'), +/// onPressed: () { +/// ctrl.setEmailAndPassword(emailCtrl.text, passwordCtrl.text); +/// } +/// ), +/// ] +/// ); +/// } else if (state is SigningIn) { +/// return Center(child: CircularProgressIndicator()); +/// } else if (state is AuthFailed) { +/// return ErrorText(exception: state.exception); +/// } +/// } +/// ) +/// ``` +/// {@endtemplate} +class AuthFlowBuilder extends StatefulWidget { + static final _flows = {}; + + /// Resolves an [AuthController] by the [flowKey]. + static T? getController(Object flowKey) { + final flow = _flows[flowKey]; + if (flow == null) return null; + return flow as T; + } + + /// Returns a current [AuthState] of the [AuthFlow] given the [flowKey]. + static AuthState? getState(Object flowKey) { + final flow = _flows[flowKey]; + if (flow == null) return null; + return flow.value; + } + + /// A unique object that is used as a key for an [AuthFlow]. + /// Could be used to obtain a controller via [getController] or + /// to read a current state using [getState]. + final Object? flowKey; + + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// {@macro ui.auth.auth_action} + final AuthAction? action; + + /// An optional instance of the [AuthProvider] that should be used to + /// authenticate. If not provided, a default instance of the [AuthProvider] + /// will be created. A type of provider is resolved by the type of the + /// [AuthController] provided to the [AuthFlowBuilder]. + /// + /// The following providers are optional to provide: + /// * [EmailAuthController] + /// * [PhoneAuthController] + /// * [UniversalEmailSignInController] + final AuthProvider? provider; + + /// An optional instance of the [AuthFlow]. + /// Should be rarely provided, as the [AuthFlow] is created automatically, + /// based on [provider]. + final AuthFlow? flow; + + /// {@macro ui.auth.widgets.auth_flow_builder.auth_flow_builder_callback} + final AuthFlowBuilderCallback? builder; + + /// A pre-built child that will be provided as an argument of the [builder]. + final Widget? child; + + /// A callback that is being called when the auth flow completes. + final Function(AuthCredential credential)? onComplete; + + /// {@macro ui.auth.widgets.auth_flow_builder.state_transition_listener} + final StateTransitionListener? listener; + + /// {@macro ui.auth.widgets.auth_flow_builder} + const AuthFlowBuilder({ + Key? key, + this.flowKey, + this.action, + this.builder, + this.onComplete, + this.child, + this.listener, + this.provider, + this.auth, + this.flow, + }) : assert( + builder != null || child != null, + 'Either child or builder should be provided', + ), + super(key: key); + + @override + // ignore: library_private_types_in_public_api + _AuthFlowBuilderState createState() => _AuthFlowBuilderState(); +} + +class _AuthFlowBuilderState + extends State { + @override + AuthFlowBuilder get widget => super.widget as AuthFlowBuilder; + AuthFlowBuilderCallback get builder => widget.builder ?? _defaultBuilder; + + AuthState? prevState; + + late AuthFlow flow; + late AuthAction action; + + bool initialized = false; + + late AuthProvider provider = widget.provider ?? _createDefaultProvider(); + + Widget _defaultBuilder(_, __, ___, ____) { + return widget.child!; + } + + @override + void initState() { + super.initState(); + provider.auth = widget.auth ?? FirebaseAuth.instance; + + flow = widget.flow ?? createFlow(); + + if (widget.flowKey != null) { + AuthFlowBuilder._flows[widget.flowKey!] = flow; + + flow.onDispose = () { + AuthFlowBuilder._flows.remove(widget.flowKey); + }; + } + + action = widget.action ?? + (flow.auth.currentUser != null ? AuthAction.link : AuthAction.signIn); + + flow.addListener(onFlowStateChanged); + prevState = flow.value; + initialized = true; + } + + AuthProvider _createDefaultProvider() { + switch (T) { + case EmailAuthController: + return EmailAuthProvider(); + case PhoneAuthController: + return PhoneAuthProvider(); + case UniversalEmailSignInController: + return UniversalEmailSignInProvider(); + default: + throw Exception("Can't create $T provider"); + } + } + + AuthFlow createFlow() { + if (widget.flowKey != null) { + final existingFlow = AuthFlowBuilder._flows[widget.flowKey!]; + if (existingFlow != null) { + return existingFlow; + } + } + + final provider = this.provider; + + if (provider is EmailAuthProvider) { + return EmailAuthFlow( + provider: provider, + action: widget.action, + auth: widget.auth, + ); + } else if (provider is EmailLinkAuthProvider) { + return EmailLinkFlow( + provider: provider, + auth: widget.auth, + ); + } else if (provider is OAuthProvider) { + return OAuthFlow( + provider: provider, + action: widget.action, + auth: widget.auth, + ); + } else if (provider is PhoneAuthProvider) { + return PhoneAuthFlow( + provider: provider, + action: widget.action, + auth: widget.auth, + ); + } else if (provider is UniversalEmailSignInProvider) { + return UniversalEmailSignInFlow( + provider: provider, + action: widget.action, + auth: widget.auth, + ); + } else { + throw Exception('Unknown provider $provider'); + } + } + + void onFlowStateChanged() { + AuthStateTransition(prevState!, flow.value, flow as T).dispatch(context); + widget.listener?.call(prevState!, flow.value, flow as T); + prevState = flow.value; + } + + @override + void didUpdateWidget(covariant AuthFlowBuilder oldWidget) { + flow.action = widget.action ?? action; + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + return AuthControllerProvider( + action: flow.action, + ctrl: flow, + child: ValueListenableBuilder( + valueListenable: flow, + builder: (context, value, _) { + final child = builder( + context, + value, + flow as T, + widget.child, + ); + + return AuthStateProvider(state: value, child: child); + }, + ), + ); + } + + @override + void dispose() { + flow.removeListener(onFlowStateChanged); + + if (widget.flowKey == null && widget.flow == null) { + flow.reset(); + } + + super.dispose(); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/delete_account_button.dart b/packages/firebase_ui_auth/lib/src/widgets/delete_account_button.dart new file mode 100644 index 000000000000..8633fd23c032 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/delete_account_button.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart' + show FirebaseAuth, FirebaseAuthException; +import 'package:flutter/cupertino.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import '../widgets/internal/loading_button.dart'; + +typedef DeleteFailedCallback = void Function(Exception exception); +typedef SignInRequiredCallback = Future Function(); + +/// {@template ui.auth.widgets.delete_account_button} +/// A button that triggers the deletion of the user's account. +/// {@endtemplate} +class DeleteAccountButton extends StatefulWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// A callback tha is called if the [FirebaseAuth] requires the user to + /// re-authenticate and approve the account deletion. By default, + /// [ReauthenticateDialog] is being shown. + final SignInRequiredCallback? onSignInRequired; + + /// A callback that is called if the account deletion fails. + final DeleteFailedCallback? onDeleteFailed; + + /// {@macro ui.auth.widgets.button_variant} + final ButtonVariant? variant; + + /// {@macro ui.auth.widgets.delete_account_button} + const DeleteAccountButton({ + Key? key, + this.auth, + this.onSignInRequired, + this.onDeleteFailed, + this.variant, + }) : super(key: key); + + @override + // ignore: library_private_types_in_public_api + _DeleteAccountButtonState createState() => _DeleteAccountButtonState(); +} + +class _DeleteAccountButtonState extends State { + FirebaseAuth get auth => widget.auth ?? FirebaseAuth.instance; + bool _isLoading = false; + + Future _deleteAccount() async { + setState(() { + _isLoading = true; + }); + + try { + await auth.currentUser?.delete(); + await FirebaseUIAuth.signOut(context: context, auth: auth); + } on FirebaseAuthException catch (err) { + if (err.code == 'requires-recent-login') { + if (widget.onSignInRequired != null) { + final signedIn = await widget.onSignInRequired!(); + if (signedIn) { + await _deleteAccount(); + } + } + } + } on Exception catch (e) { + widget.onDeleteFailed?.call(e); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + bool isCupertino = CupertinoUserInterfaceLevel.maybeOf(context) != null; + + return LoadingButton( + isLoading: _isLoading, + color: isCupertino ? CupertinoColors.destructiveRed : Colors.red, + icon: isCupertino ? CupertinoIcons.delete : Icons.delete, + label: l.deleteAccount, + onTap: _deleteAccount, + variant: widget.variant, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/different_method_sign_in_dialog.dart b/packages/firebase_ui_auth/lib/src/widgets/different_method_sign_in_dialog.dart new file mode 100644 index 000000000000..dd7635b6c077 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/different_method_sign_in_dialog.dart @@ -0,0 +1,69 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart' hide Title; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; + +import '../widgets/internal/title.dart'; +import 'internal/universal_button.dart'; + +/// {@template ui.auth.widgets.different_method_sign_in_dialog} +/// A dialog that is shown when the user tries to sign in with a provider that +/// wasn't previously used, but there are other providers for a given email. +/// {@endtemplate} +class DifferentMethodSignInDialog extends StatelessWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// A list of all providers that were previously used to authenticate. + final List availableProviders; + + /// A list of all supported auth providers. + final List providers; + + /// A callback that is being called when the user has signed in using on of + /// the [availableProviders]. + final VoidCallback? onSignedIn; + + /// {@macro ui.auth.widgets.different_method_sign_in_dialog} + const DifferentMethodSignInDialog({ + Key? key, + required this.availableProviders, + required this.providers, + this.auth, + this.onSignedIn, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Dialog( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Title(text: l.differentMethodsSignInTitleText), + const SizedBox(height: 32), + DifferentMethodSignInView( + auth: auth, + providers: providers, + availableProviders: availableProviders, + onSignedIn: onSignedIn, + ), + UniversalButton( + text: l.cancelLabel, + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/editable_user_display_name.dart b/packages/firebase_ui_auth/lib/src/widgets/editable_user_display_name.dart new file mode 100644 index 000000000000..5f7f6bf9a0dd --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/editable_user_display_name.dart @@ -0,0 +1,147 @@ +import 'package:firebase_auth/firebase_auth.dart' show FirebaseAuth; +import 'package:flutter/cupertino.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import 'package:flutter/material.dart'; + +import 'internal/subtitle.dart'; + +/// {@template ui.auth.widgets.editable_user_display_name} +/// A widget that displays user name and allows to edit it. +/// If the user name is not provided by neither of the providers, +/// a text field is being shown. Otherwise, a user name is rendered with the +/// edit icon. +/// {@endtemplate} +class EditableUserDisplayName extends StatefulWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// {@macro ui.auth.widgets.editable_user_display_name} + const EditableUserDisplayName({ + Key? key, + this.auth, + }) : super(key: key); + + @override + // ignore: library_private_types_in_public_api + _EditableUserDisplayNameState createState() => + _EditableUserDisplayNameState(); +} + +class _EditableUserDisplayNameState extends State { + FirebaseAuth get auth => widget.auth ?? FirebaseAuth.instance; + String? get displayName => auth.currentUser?.displayName; + + late final ctrl = TextEditingController(text: displayName ?? ''); + + late bool _editing = displayName == null; + bool _isLoading = false; + + void _onEdit() { + setState(() { + _editing = true; + }); + } + + Future _finishEditing() async { + try { + if (displayName == ctrl.text) return; + + setState(() { + _isLoading = true; + }); + + await auth.currentUser?.updateDisplayName(ctrl.text); + await auth.currentUser?.reload(); + } finally { + setState(() { + _editing = false; + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l = FirebaseUILocalizations.labelsOf(context); + final isCupertino = CupertinoUserInterfaceLevel.maybeOf(context) != null; + + late Widget iconButton; + + if (isCupertino) { + iconButton = Transform.translate( + offset: Offset(0, _editing ? -12 : 0), + child: CupertinoButton( + onPressed: _editing ? _finishEditing : _onEdit, + child: Icon( + _editing ? CupertinoIcons.check_mark_circled : CupertinoIcons.pen, + ), + ), + ); + } else { + iconButton = IconButton( + icon: Icon(_editing ? Icons.check : Icons.edit), + color: theme.colorScheme.secondary, + onPressed: _editing ? _finishEditing : _onEdit, + ); + } + + if (!_editing) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5.5), + child: IntrinsicWidth( + child: Row( + children: [ + Subtitle(text: displayName ?? 'Unknown'), + iconButton, + ], + ), + ), + ); + } + + late Widget textField; + + if (isCupertino) { + textField = Padding( + padding: const EdgeInsets.symmetric(vertical: 17.5), + child: CupertinoTextField( + autofocus: true, + controller: ctrl, + placeholder: l.name, + onSubmitted: (_) => _finishEditing(), + ), + ); + } else { + textField = TextField( + autofocus: true, + controller: ctrl, + decoration: InputDecoration(hintText: l.name, labelText: l.name), + onSubmitted: (_) => _finishEditing(), + ); + } + + return Row( + children: [ + Expanded(child: textField), + const SizedBox(width: 8), + SizedBox( + width: 50, + height: 32, + child: Stack( + children: [ + if (_isLoading) + const LoadingIndicator(size: 24, borderWidth: 1) + else + Align( + alignment: Alignment.topLeft, + child: iconButton, + ), + ], + ), + ), + ], + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/email_form.dart b/packages/firebase_ui_auth/lib/src/widgets/email_form.dart new file mode 100644 index 000000000000..19a71799c790 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/email_form.dart @@ -0,0 +1,308 @@ +import 'package:firebase_auth/firebase_auth.dart' show FirebaseAuth; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import 'package:flutter/material.dart'; + +import '../widgets/internal/loading_button.dart'; +import '../validators.dart'; + +/// {@template ui.auth.widgets.email_form.forgot_password_action} +/// An action that indicates that password recovery was triggered from the UI. +/// +/// Could be used to show a [ForgotPasswordScreen] or trigger a custom +/// logic: +/// +/// ```dart +/// SignInScreen( +/// actions: [ +/// ForgotPasswordAction((context, email) { +/// Navigator.of(context).push( +/// MaterialPageRoute( +/// builder: (context) => ForgotPasswordScreen(), +/// ), +/// ); +/// }), +/// ] +/// ); +/// ``` +/// {@endtemplate} +class ForgotPasswordAction extends FirebaseUIAction { + /// A callback that is being called when a password recovery flow was + /// triggered. + final void Function(BuildContext context, String? email) callback; + + /// {@macro ui.auth.widgets.email_form.forgot_password_action} + ForgotPasswordAction(this.callback); +} + +typedef EmailFormSubmitCallback = void Function(String email, String password); + +/// {@template ui.auth.widgets.email_form.email_form_style} +/// An object that is being used to apply styles to the email form. +/// +/// For example: +/// +/// ```dart +/// EmailForm( +/// style: EmailFormStyle( +/// signInButtonVariant: ButtonVariant.text, +/// ), +/// ); +/// ``` +/// {@endtemplate} +class EmailFormStyle extends FirebaseUIStyle { + /// A [ButtonVariant] that should be used for the sign in button. + final ButtonVariant? signInButtonVariant; + + /// An override of the global [ThemeData.inputDecorationTheme]. + final InputDecorationTheme? inputDecorationTheme; + + /// {@macro ui.auth.widgets.email_form.email_form_style} + const EmailFormStyle({ + this.signInButtonVariant = ButtonVariant.outlined, + this.inputDecorationTheme, + }); + + @override + Widget applyToMaterialTheme(BuildContext context, Widget child) { + return Theme( + data: Theme.of(context).copyWith( + inputDecorationTheme: inputDecorationTheme, + ), + child: child, + ); + } +} + +/// {@template ui.auth.widgets.email_form} +/// An email form widget. +/// {@endtemplate} +class EmailForm extends StatelessWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// {@macro ui.auth.auth_action} + final AuthAction? action; + + /// An instance of the [EmailAuthProvider] that is being used to authenticate. + final EmailAuthProvider? provider; + + /// A callback that is being called when the form was submitted. + final EmailFormSubmitCallback? onSubmit; + + /// An email that should be pre-filled in the form. + final String? email; + + /// A label that would be used for the "Sign in" button. + final String? actionButtonLabelOverride; + + /// {@macro ui.auth.widgets.email_form} + const EmailForm({ + Key? key, + this.action, + this.auth, + this.provider, + this.onSubmit, + this.email, + this.actionButtonLabelOverride, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final child = _SignInFormContent( + action: action ?? AuthAction.signIn, + auth: auth, + provider: provider, + email: email, + onSubmit: onSubmit, + actionButtonLabelOverride: actionButtonLabelOverride, + ); + + return AuthFlowBuilder( + auth: auth, + action: action, + provider: provider, + child: child, + ); + } +} + +class _SignInFormContent extends StatefulWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + final EmailFormSubmitCallback? onSubmit; + + /// {@macro ui.auth.auth_action} + final AuthAction? action; + final String? email; + final EmailAuthProvider? provider; + + final String? actionButtonLabelOverride; + + const _SignInFormContent({ + Key? key, + this.auth, + this.onSubmit, + this.action, + this.email, + this.provider, + this.actionButtonLabelOverride, + }) : super(key: key); + + @override + _SignInFormContentState createState() => _SignInFormContentState(); +} + +class _SignInFormContentState extends State<_SignInFormContent> { + final emailCtrl = TextEditingController(); + final passwordCtrl = TextEditingController(); + final confirmPasswordCtrl = TextEditingController(); + final formKey = GlobalKey(); + + final emailFocusNode = FocusNode(); + final passwordFocusNode = FocusNode(); + final confirmPasswordFocusNode = FocusNode(); + + String _chooseButtonLabel() { + final ctrl = AuthController.ofType(context); + final l = FirebaseUILocalizations.labelsOf(context); + + switch (ctrl.action) { + case AuthAction.signIn: + return widget.actionButtonLabelOverride ?? l.signInActionText; + case AuthAction.signUp: + return l.registerActionText; + case AuthAction.link: + return l.linkEmailButtonText; + default: + throw Exception('Invalid auth action: ${ctrl.action}'); + } + } + + void _submit([String? password]) { + final ctrl = AuthController.ofType(context); + final email = (widget.email ?? emailCtrl.text).trim(); + + if (formKey.currentState!.validate()) { + if (widget.onSubmit != null) { + widget.onSubmit!(email, passwordCtrl.text); + } else { + ctrl.setEmailAndPassword( + email, + password ?? passwordCtrl.text, + ); + } + } + } + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + const spacer = SizedBox(height: 16); + + final children = [ + if (widget.email == null) ...[ + EmailInput( + focusNode: emailFocusNode, + controller: emailCtrl, + onSubmitted: (v) { + formKey.currentState?.validate(); + FocusScope.of(context).requestFocus(passwordFocusNode); + }, + ), + spacer, + ], + PasswordInput( + focusNode: passwordFocusNode, + controller: passwordCtrl, + onSubmit: _submit, + placeholder: l.passwordInputLabel, + ), + if (widget.action == AuthAction.signIn) ...[ + const SizedBox(height: 8), + Align( + alignment: Alignment.centerRight, + child: ForgotPasswordButton( + onPressed: () { + final navAction = + FirebaseUIAction.ofType(context); + + if (navAction != null) { + navAction.callback(context, emailCtrl.text); + } else { + showForgotPasswordScreen( + context: context, + email: emailCtrl.text, + auth: widget.auth, + ); + } + }, + ), + ), + ], + if (widget.action == AuthAction.signUp || + widget.action == AuthAction.link) ...[ + const SizedBox(height: 8), + PasswordInput( + autofillHints: const [AutofillHints.newPassword], + focusNode: confirmPasswordFocusNode, + controller: confirmPasswordCtrl, + onSubmit: _submit, + validator: Validator.validateAll([ + NotEmpty(l.confirmPasswordIsRequiredErrorText), + ConfirmPasswordValidator( + passwordCtrl, + l.confirmPasswordDoesNotMatchErrorText, + ) + ]), + placeholder: l.confirmPasswordInputLabel, + ), + const SizedBox(height: 8), + ], + const SizedBox(height: 8), + Builder( + builder: (context) { + final state = AuthState.of(context); + final style = FirebaseUIStyle.ofType( + context, + const EmailFormStyle(), + ); + + return LoadingButton( + variant: style.signInButtonVariant, + label: _chooseButtonLabel(), + isLoading: state is SigningIn || state is SigningUp, + onTap: _submit, + ); + }, + ), + Builder( + builder: (context) { + final authState = AuthState.of(context); + if (authState is AuthFailed) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: ErrorText( + textAlign: TextAlign.center, + exception: authState.exception, + ), + ); + } + + return const SizedBox.shrink(); + }, + ), + ]; + + return AutofillGroup( + child: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: children, + ), + ), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/email_input.dart b/packages/firebase_ui_auth/lib/src/widgets/email_input.dart new file mode 100644 index 000000000000..d414581a84b7 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/email_input.dart @@ -0,0 +1,63 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import '../widgets/internal/universal_text_form_field.dart'; + +import '../validators.dart'; + +final _whitespaceRegExp = RegExp(r'\s\b|\b\s'); + +/// {@template ui.auth.widget.email_input} +/// An input that allows to enter an email address. +/// +/// Takes care of email validation. +/// {@macro ui.auth.widgets.internal.universal_text_form_field} +/// {@endtemplate} +class EmailInput extends StatelessWidget { + /// A focus node that might be used to control the focus of the input. + final FocusNode? focusNode; + + /// Whether the input should have a focus when rendered. + final bool? autofocus; + + /// A [TextEditingController] that might be used to track input's value + /// changes. + final TextEditingController controller; + + /// An initial value that input should be pre-filled with. + final String? initialValue; + + /// A callback that is being called when the input is submitted. + final void Function(String value) onSubmitted; + + /// {@macro ui.auth.widget.email_input} + const EmailInput({ + Key? key, + required this.controller, + required this.onSubmitted, + this.focusNode, + this.autofocus, + this.initialValue, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + + return UniversalTextFormField( + autofillHints: const [AutofillHints.email], + autofocus: autofocus ?? false, + focusNode: focusNode, + controller: controller, + placeholder: l.emailInputLabel, + keyboardType: TextInputType.emailAddress, + inputFormatters: [FilteringTextInputFormatter.deny(_whitespaceRegExp)], + validator: Validator.validateAll([ + NotEmpty(l.emailIsRequiredErrorText), + EmailValidator(l.isNotAValidEmailErrorText), + ]), + onSubmitted: (v) => onSubmitted(v!), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/email_link_sign_in_button.dart b/packages/firebase_ui_auth/lib/src/widgets/email_link_sign_in_button.dart new file mode 100644 index 000000000000..94cfe6db07d9 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/email_link_sign_in_button.dart @@ -0,0 +1,63 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; + +import 'internal/universal_button.dart'; +import 'internal/universal_page_route.dart'; + +/// {@template ui.auth.widget.email_link_sign_in_button} +/// A button that starts an email link sign in flow. +/// +/// Triggers an [EmailLinkSignInAction] if provided, otherwise +/// opens an [EmailLinkSignInScreen]. +/// +/// {@endtemplate} +class EmailLinkSignInButton extends StatelessWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// An instance of [EmailLinkAuthProvider] that should be used to + /// authenticate. + final EmailLinkAuthProvider provider; + + /// {@macro ui.auth.widget.email_link_sign_in_button} + const EmailLinkSignInButton({ + Key? key, + required this.provider, + this.auth, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final isCupertino = CupertinoUserInterfaceLevel.maybeOf(context) != null; + final l = FirebaseUILocalizations.labelsOf(context); + + return UniversalButton( + text: l.emailLinkSignInButtonLabel, + icon: isCupertino ? CupertinoIcons.link : Icons.link, + onPressed: () { + final action = FirebaseUIAction.ofType(context); + if (action != null) { + action.callback(context); + } else { + Navigator.of(context).push( + createPageRoute( + context: context, + builder: (_) { + return FirebaseUIActions.inherit( + from: context, + child: EmailLinkSignInScreen( + auth: auth, + provider: provider, + ), + ); + }, + ), + ); + } + }, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/email_sign_up_dialog.dart b/packages/firebase_ui_auth/lib/src/widgets/email_sign_up_dialog.dart new file mode 100644 index 000000000000..ae68e11890c7 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/email_sign_up_dialog.dart @@ -0,0 +1,70 @@ +import 'package:firebase_auth/firebase_auth.dart' hide EmailAuthProvider; +import 'package:flutter/material.dart' hide Title; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; + +import 'internal/title.dart'; + +/// {@template ui.auth.widget.email_sign_up_dialog} +/// A dialog [Widget] that allows to create a new account using email and +/// password or to link current account with an email. +/// {@endtemplate} +class EmailSignUpDialog extends StatelessWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// {@macro ui.auth.auth_action} + final AuthAction? action; + + /// An instance of [EmailAuthProvider] that should be used to authenticate. + final EmailAuthProvider provider; + + /// {@macro ui.auth.widget.email_sign_up_dialog} + const EmailSignUpDialog({ + Key? key, + this.auth, + required this.provider, + required this.action, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + + return Center( + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: Dialog( + child: AuthStateListener( + listener: (oldState, newState, ctrl) { + if (newState is CredentialLinked) { + Navigator.of(context).pop(); + } + + return null; + }, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 16), + Title(text: l.provideEmail), + const SizedBox(height: 32), + EmailForm( + auth: auth, + action: action, + provider: provider, + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/error_text.dart b/packages/firebase_ui_auth/lib/src/widgets/error_text.dart new file mode 100644 index 000000000000..71fc01d87364 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/error_text.dart @@ -0,0 +1,82 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart' show FirebaseAuthException; + +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; + +import '../flows/phone_auth_flow.dart'; + +String? localizedErrorText( + String? errorCode, + FirebaseUILocalizationLabels labels, +) { + switch (errorCode) { + case 'user-not-found': + return labels.userNotFoundErrorText; + case 'email-already-in-use': + return labels.emailTakenErrorText; + case 'too-many-requests': + return labels.accessDisabledErrorText; + case 'wrong-password': + return labels.wrongOrNoPasswordErrorText; + case 'credential-already-in-use': + return labels.credentialAlreadyInUseErrorText; + + default: + return null; + } +} + +/// {@template ui.auth.widgets.error_text} +/// A widget which displays error text for a given Firebase error code. +/// {@endtemplate} +class ErrorText extends StatelessWidget { + /// An exception that contains error details. + /// Often this is a [FirebaseAuthException]. + final Exception exception; + + /// How the text should be aligned horizontally. + final TextAlign? textAlign; + + /// {@macro ui.auth.widgets.error_text} + const ErrorText({ + Key? key, + required this.exception, + this.textAlign, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + late Color color; + final isCupertino = CupertinoUserInterfaceLevel.maybeOf(context) != null; + + if (isCupertino) { + color = CupertinoColors.destructiveRed; + } else { + color = Theme.of(context).errorColor; + } + + final l = FirebaseUILocalizations.labelsOf(context); + String text = l.unknownError; + + if (exception is AutoresolutionFailedException) { + text = l.smsAutoresolutionFailedError; + } + + if (exception is FirebaseAuthException) { + final e = exception as FirebaseAuthException; + final code = e.code; + final newText = localizedErrorText(code, l) ?? e.message; + + if (newText != null) { + text = newText; + } + } + + return Text( + text, + textAlign: textAlign, + style: TextStyle(color: color), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/forgot_password_button.dart b/packages/firebase_ui_auth/lib/src/widgets/forgot_password_button.dart new file mode 100644 index 000000000000..293a46880f20 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/forgot_password_button.dart @@ -0,0 +1,28 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; + +import 'internal/universal_button.dart'; + +/// {@template ui.auth.widget.forgot_password_button} +/// A button that has a localized "Forgot password" label. +/// {@endtemplate} +class ForgotPasswordButton extends StatelessWidget { + /// A callback that is called when the button is pressed. + final VoidCallback onPressed; + + /// {@macro ui.auth.widget.forgot_password_button} + const ForgotPasswordButton({Key? key, required this.onPressed}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + + return UniversalButton( + variant: ButtonVariant.text, + text: l.forgotPasswordButtonLabel, + onPressed: onPressed, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/internal/keyboard_appearence_listener.dart b/packages/firebase_ui_auth/lib/src/widgets/internal/keyboard_appearence_listener.dart new file mode 100644 index 000000000000..2b8499b45eb0 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/internal/keyboard_appearence_listener.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +typedef KeyboardPositionListener = void Function(double position); + +class KeyboardAppearenceListener extends StatefulWidget { + final Widget child; + final KeyboardPositionListener listener; + const KeyboardAppearenceListener({ + Key? key, + required this.child, + required this.listener, + }) : super(key: key); + + @override + State createState() => + _KeyboardAppearenceListenerState(); +} + +class _KeyboardAppearenceListenerState + extends State { + @override + Widget build(BuildContext context) { + return widget.child; + } + + @override + void didChangeDependencies() { + final bottom = MediaQuery.of(context).viewInsets.bottom; + widget.listener(bottom); + super.didChangeDependencies(); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/internal/loading_button.dart b/packages/firebase_ui_auth/lib/src/widgets/internal/loading_button.dart new file mode 100644 index 000000000000..cc9b656383d5 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/internal/loading_button.dart @@ -0,0 +1,72 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +import 'universal_button.dart'; + +class _LoadingButtonContent extends StatelessWidget { + final String label; + final bool isLoading; + final Color? color; + const _LoadingButtonContent({ + Key? key, + required this.label, + required this.isLoading, + required this.color, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + bool isCupertino = CupertinoUserInterfaceLevel.maybeOf(context) != null; + + Widget child = Text(label); + + if (isLoading) { + child = LoadingIndicator( + size: isCupertino ? 20 : 16, + borderWidth: 1, + color: color, + ); + } + + return child; + } +} + +class LoadingButton extends StatelessWidget { + final bool isLoading; + final String label; + final IconData? icon; + final Color? color; + final VoidCallback onTap; + final ButtonVariant? variant; + + const LoadingButton({ + Key? key, + required this.label, + required this.onTap, + this.isLoading = false, + this.icon, + this.color, + this.variant = ButtonVariant.outlined, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final content = _LoadingButtonContent( + label: label, + isLoading: isLoading, + color: variant == ButtonVariant.filled + ? Theme.of(context).colorScheme.onPrimary + : null, + ); + + return UniversalButton( + color: color, + icon: icon, + onPressed: onTap, + variant: variant, + child: content, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/internal/oauth_provider_button.dart b/packages/firebase_ui_auth/lib/src/widgets/internal/oauth_provider_button.dart new file mode 100644 index 000000000000..5f737e8e18cb --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/internal/oauth_provider_button.dart @@ -0,0 +1,130 @@ +import 'package:firebase_auth/firebase_auth.dart' hide OAuthProvider; +import 'package:flutter/material.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart' as ffui_oauth; + +typedef ErrorCallback = void Function(Exception e); + +/// {@template ui.auth.widgets.oauth_provider_button.oauth_button_variant} +/// Either button should display icon and text or only icon. +/// {@endtemplate} +enum OAuthButtonVariant { + // ignore: constant_identifier_names + icon_and_text, + icon, +} + +class _ErrorListener extends StatelessWidget { + const _ErrorListener({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final state = AuthState.of(context); + if (state is AuthFailed) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: ErrorText(exception: state.exception), + ); + } + + return const SizedBox.shrink(); + } +} + +/// {@template ui.auth.widgets.oauth_provider_button} +/// A button that is used to sign in with an OAuth provider. +/// {@endtemplate} +class OAuthProviderButton extends StatelessWidget { + /// {@template ui.auth.widgets.oauth_provider_button.provider} + /// An instance of [ffui_oauth.OAuthProvider] that should be used to + /// authenticate. + /// {@endtemplate} + final ffui_oauth.OAuthProvider provider; + + /// {@macro ui.auth.auth_action} + final AuthAction? action; + + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// {@macro ui.auth.widgets.oauth_provider_button.oauth_button_variant} + final OAuthButtonVariant? variant; + + /// Returns a text that should be displayed on the button. + static String resolveProviderButtonLabel( + String providerId, + FirebaseUILocalizationLabels labels, + ) { + switch (providerId) { + case 'google.com': + return labels.signInWithGoogleButtonText; + case 'facebook.com': + return labels.signInWithFacebookButtonText; + case 'twitter.com': + return labels.signInWithTwitterButtonText; + case 'apple.com': + return labels.signInWithAppleButtonText; + default: + throw Exception('Unknown providerId $providerId'); + } + } + + /// {@macro ui.auth.widgets.oauth_provider_button} + const OAuthProviderButton({ + Key? key, + + /// {@macro ui.auth.widgets.oauth_provider_button.provider} + required this.provider, + + /// {@macro ui.auth.widgets.oauth_provider_button.oauth_button_variant} + this.variant = OAuthButtonVariant.icon_and_text, + + /// {@macro ui.auth.auth_action} + this.action, + + /// {@macro ui.auth.auth_controller.auth} + this.auth, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final labels = FirebaseUILocalizations.labelsOf(context); + final brightness = Theme.of(context).brightness; + + return AuthFlowBuilder( + provider: provider, + action: action, + auth: auth, + builder: (context, state, ctrl, child) { + final button = ffui_oauth.OAuthProviderButtonBase( + provider: provider, + action: action, + isLoading: state is SigningIn || state is CredentialReceived, + onTap: () => ctrl.signIn(Theme.of(context).platform), + overrideDefaultTapAction: true, + loadingIndicator: LoadingIndicator( + size: 19, + borderWidth: 1, + color: provider.style.color.getValue(brightness), + ), + label: variant == OAuthButtonVariant.icon + ? '' + : resolveProviderButtonLabel(provider.providerId, labels), + auth: auth, + ); + + if (variant == OAuthButtonVariant.icon) { + return button; + } + + return Column( + children: [ + button, + const _ErrorListener(), + ], + ); + }, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/internal/platform_widget.dart b/packages/firebase_ui_auth/lib/src/widgets/internal/platform_widget.dart new file mode 100644 index 000000000000..9feeb94a74ae --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/internal/platform_widget.dart @@ -0,0 +1,32 @@ +import 'package:flutter/cupertino.dart'; + +abstract class PlatformWidget extends StatelessWidget { + const PlatformWidget({Key? key}) : super(key: key); + + Widget buildCupertino(BuildContext context); + Widget buildMaterial(BuildContext context); + + Widget? buildWrapper(BuildContext context, Widget child) { + return null; + } + + @override + Widget build(BuildContext context) { + final isCupertino = CupertinoUserInterfaceLevel.maybeOf(context) != null; + late Widget child; + + if (isCupertino) { + child = buildCupertino(context); + } else { + child = buildMaterial(context); + } + + final wrapper = buildWrapper(context, child); + + if (wrapper == null) { + return child; + } else { + return wrapper; + } + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/internal/rebuild_scope.dart b/packages/firebase_ui_auth/lib/src/widgets/internal/rebuild_scope.dart new file mode 100644 index 000000000000..4fcbb69bb497 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/internal/rebuild_scope.dart @@ -0,0 +1,63 @@ +import 'package:flutter/widgets.dart'; + +class RebuildScopeKey { + RebuildScopeKey(); + + final _elements = []; + + void rebuild() { + for (var element in _elements) { + element.markNeedsBuild(); + } + } +} + +class RebuildScope extends Widget { + final WidgetBuilder builder; + final RebuildScopeKey scopeKey; + + const RebuildScope({ + Key? key, + required this.builder, + required this.scopeKey, + }) : super(key: key); + + @override + RebuildScopeElement createElement() { + return RebuildScopeElement(this); + } +} + +class RebuildScopeElement extends ComponentElement { + RebuildScopeElement(RebuildScope widget) : super(widget); + + @override + RebuildScope get widget => super.widget as RebuildScope; + + late RebuildScopeKey scopeKey; + + void _registerElement(RebuildScope widget) { + scopeKey = widget.scopeKey; + scopeKey._elements.add(this); + } + + @override + void mount(Element? parent, Object? newSlot) { + _registerElement(widget); + super.mount(parent, newSlot); + } + + @override + void update(RebuildScope newWidget) { + scopeKey._elements.clear(); + _registerElement(widget); + + super.update(newWidget); + markNeedsBuild(); + } + + @override + Widget build() { + return widget.builder(this); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/internal/subtitle.dart b/packages/firebase_ui_auth/lib/src/widgets/internal/subtitle.dart new file mode 100644 index 000000000000..18527f3a0f50 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/internal/subtitle.dart @@ -0,0 +1,33 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'platform_widget.dart'; + +class Subtitle extends PlatformWidget { + final String text; + final FontWeight? fontWeight; + + const Subtitle({ + Key? key, + required this.text, + this.fontWeight, + }) : super(key: key); + + @override + Widget buildCupertino(BuildContext context) { + return Text( + text, + style: CupertinoTheme.of(context).textTheme.navTitleTextStyle, + ); + } + + @override + Widget buildMaterial(BuildContext context) { + return Text( + text, + style: Theme.of(context) + .textTheme + .subtitle1! + .copyWith(fontWeight: fontWeight), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/internal/title.dart b/packages/firebase_ui_auth/lib/src/widgets/internal/title.dart new file mode 100644 index 000000000000..c830fc9ea8a0 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/internal/title.dart @@ -0,0 +1,25 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import 'platform_widget.dart'; + +class Title extends PlatformWidget { + final String text; + const Title({Key? key, required this.text}) : super(key: key); + + @override + Widget buildCupertino(BuildContext context) { + return Text( + text, + style: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle, + ); + } + + @override + Widget buildMaterial(BuildContext context) { + return Text( + text, + style: Theme.of(context).textTheme.headline5, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/internal/universal_button.dart b/packages/firebase_ui_auth/lib/src/widgets/internal/universal_button.dart new file mode 100644 index 000000000000..c03ce415bd44 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/internal/universal_button.dart @@ -0,0 +1,157 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import 'platform_widget.dart'; + +/// {@template ui.auth.widgets.button_variant} +/// An enumeration of the possible button variants. +/// {@endtemplate} +enum ButtonVariant { + /// button variant that is rendered as a text without a background or border. + text, + + /// button variant that has a background. + filled, + + /// button variant that has a border. + outlined, +} + +class UniversalButton extends PlatformWidget { + final VoidCallback? onPressed; + final String? text; + final Widget? child; + final IconData? icon; + final TextDirection? direction; + final ButtonVariant? variant; + final Color? color; + + const UniversalButton({ + Key? key, + this.text, + this.child, + this.onPressed, + this.icon, + this.direction = TextDirection.ltr, + this.variant, + this.color, + }) : assert(text != null || child != null), + super(key: key); + + ButtonVariant get _variant { + return variant ?? ButtonVariant.filled; + } + + @override + Widget buildCupertino(BuildContext context) { + late Widget button; + + final child = Row( + textDirection: direction, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) ...[ + if (direction == TextDirection.rtl) const SizedBox(width: 8), + Icon(icon, size: 20), + if (direction == TextDirection.ltr) const SizedBox(width: 8), + ], + this.child ?? Text(text!), + ], + ); + + if (_variant == ButtonVariant.text || _variant == ButtonVariant.outlined) { + button = CupertinoButton( + padding: EdgeInsets.zero, + onPressed: onPressed, + child: child, + ); + } else { + button = CupertinoButton.filled( + onPressed: onPressed, + child: child, + ); + } + + if (color != null) { + return CupertinoTheme( + data: CupertinoTheme.of(context).copyWith(primaryColor: color), + child: button, + ); + } else { + return button; + } + } + + @override + Widget buildMaterial(BuildContext context) { + final child = this.child ?? Text(text!); + + ButtonStyle? style; + + if (color != null) { + MaterialStateColor? foregroundColor; + MaterialStateColor? backgroundColor; + + if (variant == ButtonVariant.text) { + foregroundColor = MaterialStateColor.resolveWith((_) => color!); + } else { + backgroundColor = MaterialStateColor.resolveWith((_) => color!); + } + + style = ButtonStyle( + foregroundColor: foregroundColor, + backgroundColor: backgroundColor, + overlayColor: MaterialStateColor.resolveWith( + (states) => color!.withAlpha(20), + ), + ); + } + + if (icon != null) { + switch (_variant) { + case ButtonVariant.text: + return TextButton.icon( + icon: Icon(icon), + onPressed: onPressed, + label: child, + style: style, + ); + case ButtonVariant.filled: + return ElevatedButton.icon( + onPressed: onPressed, + icon: Icon(icon), + label: child, + style: style, + ); + case ButtonVariant.outlined: + return OutlinedButton.icon( + onPressed: onPressed, + icon: Icon(icon), + label: child, + style: style, + ); + } + } else { + switch (_variant) { + case ButtonVariant.text: + return TextButton( + onPressed: onPressed, + style: style, + child: child, + ); + case ButtonVariant.filled: + return ElevatedButton( + onPressed: onPressed, + style: style, + child: child, + ); + case ButtonVariant.outlined: + return OutlinedButton( + onPressed: onPressed, + style: style, + child: child, + ); + } + } + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/internal/universal_icon.dart b/packages/firebase_ui_auth/lib/src/widgets/internal/universal_icon.dart new file mode 100644 index 000000000000..557e619c7762 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/internal/universal_icon.dart @@ -0,0 +1,35 @@ +import 'package:firebase_ui_auth/src/widgets/internal/platform_widget.dart'; +import 'package:flutter/widgets.dart'; + +class UniversalIcon extends PlatformWidget { + final IconData cupertinoIcon; + final IconData materialIcon; + final Color? color; + final double? size; + + const UniversalIcon({ + Key? key, + required this.cupertinoIcon, + required this.materialIcon, + this.color, + this.size, + }) : super(key: key); + + @override + Widget buildCupertino(BuildContext context) { + return Icon( + cupertinoIcon, + color: color, + size: size, + ); + } + + @override + Widget buildMaterial(BuildContext context) { + return Icon( + materialIcon, + color: color, + size: size, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/internal/universal_icon_button.dart b/packages/firebase_ui_auth/lib/src/widgets/internal/universal_icon_button.dart new file mode 100644 index 000000000000..c584c40f6e91 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/internal/universal_icon_button.dart @@ -0,0 +1,39 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import 'platform_widget.dart'; + +class UniversalIconButton extends PlatformWidget { + final IconData cupertinoIcon; + final IconData materialIcon; + final VoidCallback? onPressed; + final double? size; + final Color? color; + + const UniversalIconButton({ + Key? key, + this.onPressed, + required this.cupertinoIcon, + required this.materialIcon, + this.size, + this.color, + }) : super(key: key); + + @override + Widget buildCupertino(BuildContext context) { + return CupertinoButton( + onPressed: onPressed, + child: Icon(cupertinoIcon, size: size), + ); + } + + @override + Widget buildMaterial(BuildContext context) { + return IconButton( + color: color, + iconSize: size, + onPressed: onPressed, + icon: Icon(materialIcon), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/internal/universal_page_route.dart b/packages/firebase_ui_auth/lib/src/widgets/internal/universal_page_route.dart new file mode 100644 index 000000000000..e240bbc87983 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/internal/universal_page_route.dart @@ -0,0 +1,15 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +Route createPageRoute({ + required BuildContext context, + required WidgetBuilder builder, +}) { + final isCupertino = CupertinoUserInterfaceLevel.maybeOf(context) != null; + + if (isCupertino) { + return CupertinoPageRoute(builder: builder); + } else { + return MaterialPageRoute(builder: builder); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/internal/universal_scaffold.dart b/packages/firebase_ui_auth/lib/src/widgets/internal/universal_scaffold.dart new file mode 100644 index 000000000000..e5398d4c2861 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/internal/universal_scaffold.dart @@ -0,0 +1,33 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import 'platform_widget.dart'; + +class UniversalScaffold extends PlatformWidget { + final Widget body; + + /// See [Scaffold.resizeToAvoidBottomInset] + final bool? resizeToAvoidBottomInset; + + const UniversalScaffold({ + Key? key, + required this.body, + this.resizeToAvoidBottomInset, + }) : super(key: key); + + @override + Widget buildCupertino(BuildContext context) { + return CupertinoPageScaffold( + resizeToAvoidBottomInset: resizeToAvoidBottomInset ?? true, + child: body, + ); + } + + @override + Widget buildMaterial(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: resizeToAvoidBottomInset ?? true, + body: body, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/internal/universal_text_form_field.dart b/packages/firebase_ui_auth/lib/src/widgets/internal/universal_text_form_field.dart new file mode 100644 index 000000000000..2a69ea974f09 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/internal/universal_text_form_field.dart @@ -0,0 +1,89 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'platform_widget.dart'; + +/// {@template ui.auth.widgets.internal.universal_text_form_field} +/// Uses [TextFormField] under mateiral library and [CupertinoTextFormFieldRow] +/// under cupertion. +class UniversalTextFormField extends PlatformWidget { + final TextEditingController? controller; + final String? placeholder; + final String? Function(String?)? validator; + final void Function(String?)? onSubmitted; + final List? inputFormatters; + final TextInputType? keyboardType; + final bool autofocus; + final bool obscureText; + final FocusNode? focusNode; + final bool? enableSuggestions; + final bool autocorrect; + final Widget? prefix; + final Iterable? autofillHints; + + const UniversalTextFormField({ + Key? key, + this.controller, + this.prefix, + this.placeholder, + this.validator, + this.onSubmitted, + this.inputFormatters, + this.keyboardType, + this.autofocus = false, + this.obscureText = false, + this.focusNode, + this.enableSuggestions, + this.autocorrect = false, + this.autofillHints, + }) : super(key: key); + + @override + Widget buildCupertino(BuildContext context) { + return Container( + padding: const EdgeInsets.only(bottom: 8), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: CupertinoColors.inactiveGray, + ), + ), + ), + child: CupertinoTextFormFieldRow( + autocorrect: autocorrect, + autofillHints: autofillHints, + focusNode: focusNode, + padding: EdgeInsets.zero, + controller: controller, + placeholder: placeholder, + validator: validator, + onFieldSubmitted: onSubmitted, + autofocus: autofocus, + inputFormatters: inputFormatters, + keyboardType: keyboardType, + obscureText: obscureText, + prefix: prefix, + ), + ); + } + + @override + Widget buildMaterial(BuildContext context) { + return TextFormField( + autocorrect: autocorrect, + autofillHints: autofillHints, + autofocus: autofocus, + focusNode: focusNode, + controller: controller, + decoration: InputDecoration( + labelText: placeholder, + prefix: prefix, + ), + validator: validator, + onFieldSubmitted: onSubmitted, + inputFormatters: inputFormatters, + keyboardType: keyboardType, + obscureText: obscureText, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/password_input.dart b/packages/firebase_ui_auth/lib/src/widgets/password_input.dart new file mode 100644 index 000000000000..a17068b1c799 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/password_input.dart @@ -0,0 +1,60 @@ +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; + +import '../validators.dart'; +import 'internal/universal_text_form_field.dart'; + +/// {@template ui.auth.widgets.password_input} +/// An input that allows to enter a password. +/// +/// {@macro ui.auth.widgets.internal.universal_text_form_field} +/// {@endtemplate} +class PasswordInput extends StatelessWidget { + /// Allows to control the focus state of the input. + final FocusNode focusNode; + + /// Allows to respond to changes in the input's value. + final TextEditingController controller; + + /// A callback that is being called when the input is submitted. + final void Function(String value) onSubmit; + + /// A placeholder of the input's value. + final String placeholder; + + /// Used to validate the input's value. + /// + /// Returned string will be shown as an error message. + final String? Function(String? value)? validator; + + /// {@macro flutter.widgets.editableText.autofillHints} + /// {@macro flutter.services.AutofillConfiguration.autofillHints} + final Iterable autofillHints; + + /// {@macro ui.auth.widgets.password_input} + const PasswordInput({ + Key? key, + required this.focusNode, + required this.controller, + required this.onSubmit, + required this.placeholder, + this.autofillHints = const [AutofillHints.password], + this.validator, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + + return UniversalTextFormField( + autofillHints: autofillHints, + focusNode: focusNode, + controller: controller, + obscureText: true, + enableSuggestions: false, + validator: validator ?? NotEmpty(l.passwordIsRequiredErrorText).validate, + onSubmitted: (v) => onSubmit(v!), + placeholder: placeholder, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/phone_input.dart b/packages/firebase_ui_auth/lib/src/widgets/phone_input.dart new file mode 100644 index 000000000000..d52bccaa7e28 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/phone_input.dart @@ -0,0 +1,335 @@ +import 'package:flutter/cupertino.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../validators.dart'; + +import '../widgets/internal/universal_text_form_field.dart'; + +part '../configs/countries.dart'; + +class _CountryCodeItem { + final String countryCode; + final String phoneCode; + final String name; + + _CountryCodeItem({ + required this.countryCode, + required this.phoneCode, + required this.name, + }); + + static _CountryCodeItem fromJson(Map data) { + return _CountryCodeItem( + countryCode: data['countryCode']!, + phoneCode: data['phoneCode']!, + name: data['name']!, + ); + } +} + +typedef SubmitCallback = void Function(String value); + +class _CountryPicker extends StatefulWidget { + const _CountryPicker({Key? key}) : super(key: key); + + @override + // ignore: library_private_types_in_public_api + _CountryPickerState createState() => _CountryPickerState(); +} + +class _CountryPickerState extends State<_CountryPicker> { + String? _countryCode; + String get countryCode => _countryCode!; + String get phoneCode => countriesByCountryCode[countryCode]!.phoneCode; + + @override + Widget build(BuildContext context) { + _countryCode ??= Localizations.localeOf(context).countryCode; + final item = countriesByCountryCode[_countryCode]!; + + return PopupMenuButton<_CountryCodeItem>( + onSelected: (selected) => setState(() { + _countryCode = selected.countryCode; + }), + itemBuilder: (context) { + return countries.map((e) { + return PopupMenuItem( + value: e, + child: Text('${e.name} (+${e.phoneCode})'), + ); + }).toList(); + }, + child: Container( + padding: const EdgeInsets.all(16).copyWith(left: 0), + child: Row( + children: [ + const Icon(Icons.arrow_drop_down), + Text( + '${item.countryCode} (+${item.phoneCode})', + style: const TextStyle(fontSize: 16), + ), + ], + ), + ), + ); + } +} + +/// {@template ui.auth.widgets.phone_input} +/// An input that allows to enter a phone number and select a country code. +/// {@endtemplate} +class PhoneInput extends StatefulWidget { + /// A callback that is being called when the input is submitted. + final SubmitCallback? onSubmit; + + /// An initial country code that should be selected in the country code + /// picker. + final String initialCountryCode; + + /// Returns a phone number from the [PhoneInput] that was provided a [key]. + static String? getPhoneNumber(GlobalKey key) { + final state = key.currentState!; + + if (state.formKey.currentState!.validate()) { + return state.phoneNumber; + } + + return null; + } + + /// {@macro ui.auth.widgets.phone_input} + const PhoneInput({ + Key? key, + required this.initialCountryCode, + this.onSubmit, + }) : super(key: key); + + @override + PhoneInputState createState() => PhoneInputState(); +} + +/// A state of the [PhoneInput]. +/// +/// Shouldn't be used directly. +/// Should be used only to construct a key for phone input. +/// +/// ```dart +/// final key = GlobalKey(); +/// return PhoneInput(key: key); +/// ``` +class PhoneInputState extends State { + late final countryController = TextEditingController() + ..addListener(_onCountryChanged); + final numberController = TextEditingController(); + final formKey = GlobalKey(); + final numberFocusNode = FocusNode(); + + String get phoneNumber => + '+${countryController.text}${numberController.text}'; + + String? country; + bool isValidCountryCode = true; + + // ignore: library_private_types_in_public_api + _CountryCodeItem? countryCodeItem; + + void _onSubmitted(_) { + if (formKey.currentState!.validate()) { + widget.onSubmit?.call(phoneNumber); + } + } + + @override + void initState() { + _setCountry(countryCode: widget.initialCountryCode); + super.initState(); + } + + void _setCountry({ + String? phoneCode, + String? countryCode, + bool updateCountryInput = true, + }) { + try { + final newItem = countries.firstWhere( + (element) => + element.countryCode == countryCode || + element.phoneCode == phoneCode, + ); + + if (phoneCode != null && + newItem.phoneCode == countryCodeItem?.phoneCode) { + return; + } + + countryCodeItem = newItem; + isValidCountryCode = true; + } catch (_) { + countryCodeItem = null; + isValidCountryCode = false; + } + + if (updateCountryInput) { + countryController.text = countryCodeItem?.phoneCode ?? ''; + } + } + + void _onCountryChanged() { + setState(() { + _setCountry( + phoneCode: countryController.text, + updateCountryInput: false, + ); + }); + } + + void _showCountryPicker(BuildContext context) { + showCupertinoModalPopup( + context: context, + builder: (context) { + return Container( + color: CupertinoTheme.of(context).scaffoldBackgroundColor, + height: 300, + child: Column( + children: [ + Expanded( + child: CupertinoPicker.builder( + useMagnifier: true, + itemExtent: 40, + childCount: countries.length, + onSelectedItemChanged: (i) { + setState(() { + _setCountry( + countryCode: countries.elementAt(i).countryCode, + ); + }); + }, + itemBuilder: (context, index) { + final item = countries.elementAt(index); + return Center( + child: Text( + '${item.name} (+${item.phoneCode})', + style: const TextStyle(fontSize: 16), + ), + ); + }, + ), + ), + CupertinoButton( + child: const Text('Done'), + onPressed: () => Navigator.pop(context), + ), + ], + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + final isCupertino = CupertinoUserInterfaceLevel.maybeOf(context) != null; + + return Form( + key: formKey, + child: Column( + children: [ + if (isCupertino) + GestureDetector( + onTap: () { + _showCountryPicker(context); + }, + child: Row( + children: [ + const Icon(Icons.arrow_drop_down), + Text( + countryController.text.isNotEmpty && !isValidCountryCode + ? l.invalidCountryCode + : countryCodeItem?.name ?? l.chooseACountry, + ), + ], + ), + ) + else + PopupMenuButton<_CountryCodeItem>( + child: Container( + padding: const EdgeInsets.all(16).copyWith(left: 0), + child: Row( + children: [ + const Icon(Icons.arrow_drop_down), + Text( + countryController.text.isNotEmpty && !isValidCountryCode + ? l.invalidCountryCode + : countryCodeItem?.name ?? l.chooseACountry, + style: const TextStyle(fontSize: 16), + ), + ], + ), + ), + itemBuilder: (context) { + return countries.map((e) { + return PopupMenuItem( + value: e, + child: Text('${e.name} (+${e.phoneCode})'), + ); + }).toList(); + }, + onSelected: (selected) => _setCountry( + countryCode: selected.countryCode, + ), + ), + const SizedBox(height: 16), + Directionality( + textDirection: TextDirection.ltr, + child: Row( + children: [ + SizedBox( + width: 90, + child: UniversalTextFormField( + autofillHints: const [ + AutofillHints.telephoneNumberCountryCode + ], + controller: countryController, + prefix: const Text('+'), + placeholder: l.countryCode, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + keyboardType: TextInputType.phone, + validator: NotEmpty('').validate, + onSubmitted: (_) { + numberFocusNode.requestFocus(); + }, + ), + ), + const SizedBox(width: 8), + Expanded( + child: UniversalTextFormField( + autofillHints: const [ + AutofillHints.telephoneNumberNational + ], + autofocus: true, + focusNode: numberFocusNode, + controller: numberController, + placeholder: l.phoneInputLabel, + validator: Validator.validateAll([ + NotEmpty(l.phoneNumberIsRequiredErrorText), + PhoneValidator(l.phoneNumberInvalidErrorText), + ]), + onSubmitted: _onSubmitted, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + keyboardType: TextInputType.phone, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/phone_verification_button.dart b/packages/firebase_ui_auth/lib/src/widgets/phone_verification_button.dart new file mode 100644 index 000000000000..c774f8337a2a --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/phone_verification_button.dart @@ -0,0 +1,49 @@ +import 'package:firebase_auth/firebase_auth.dart' show FirebaseAuth; +import 'package:flutter/cupertino.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:flutter/material.dart'; +import '../widgets/internal/universal_button.dart'; + +/// {@template ui.auth.widgets.phone_verification_button} +/// A button that triggers phone verification flow. +/// +/// Triggers a [VerifyPhoneAction] action if provided, otherwise +/// uses [startPhoneVerification]. +/// {@endtemplate} +class PhoneVerificationButton extends StatelessWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// {@macro ui.auth.auth_action} + final AuthAction? action; + + /// A text that should be displayed on the button. + final String label; + + /// {@macro ui.auth.widgets.phone_verification_button} + const PhoneVerificationButton({ + Key? key, + required this.label, + this.action, + this.auth, + }) : super(key: key); + + void _onPressed(BuildContext context) { + final a = FirebaseUIAction.ofType(context); + + if (a != null) { + a.callback(context, action); + } else { + startPhoneVerification(context: context, action: action, auth: auth); + } + } + + @override + Widget build(BuildContext context) { + return UniversalButton( + variant: ButtonVariant.text, + text: label, + onPressed: () => _onPressed(context), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/reauthenticate_dialog.dart b/packages/firebase_ui_auth/lib/src/widgets/reauthenticate_dialog.dart new file mode 100644 index 000000000000..bd7b935b4c45 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/reauthenticate_dialog.dart @@ -0,0 +1,69 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart' hide Title; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; + +import 'internal/title.dart'; +import 'internal/universal_button.dart'; + +/// {@template ui.auth.widgets.reauthenticate_dialog} +/// A dialog that prompts the user to re-authenticate their account +/// Used to confirm destructive actions (like account deletion or disabling MFA). +/// {@endtemplate} +class ReauthenticateDialog extends StatelessWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// A list of all supported auth providers. + final List providers; + + /// A callback that is being called when the user has successfully signed in. + final VoidCallback? onSignedIn; + + /// A label that would be used for the "Sign in" button. + final String? actionButtonLabelOverride; + + /// {@macro ui.auth.widgets.reauthenticate_dialog} + const ReauthenticateDialog({ + Key? key, + required this.providers, + this.auth, + this.onSignedIn, + this.actionButtonLabelOverride, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Dialog( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Title(text: l.verifyItsYouText), + const SizedBox(height: 16), + ReauthenticateView( + auth: auth, + providers: providers, + onSignedIn: onSignedIn, + ), + const SizedBox(height: 16), + UniversalButton( + text: l.cancelLabel, + variant: ButtonVariant.text, + onPressed: () => Navigator.of(context).pop(), + ) + ], + ), + ), + ), + ), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/sign_out_button.dart b/packages/firebase_ui_auth/lib/src/widgets/sign_out_button.dart new file mode 100644 index 000000000000..169bb662e51a --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/sign_out_button.dart @@ -0,0 +1,41 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import 'package:flutter/material.dart'; + +import '../widgets/internal/universal_button.dart'; + +/// {@template ui.auth.widgets.sign_out_button} +/// A button that signs out the user when pressed. +/// {@endtemplate} +class SignOutButton extends StatelessWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// {@macro ui.auth.widgets.button_variant} + final ButtonVariant? variant; + + /// {@macro ui.auth.widgets.sign_out_button} + const SignOutButton({ + Key? key, + this.auth, + this.variant, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final l = FirebaseUILocalizations.labelsOf(context); + final isCupertino = CupertinoUserInterfaceLevel.maybeOf(context) != null; + + return UniversalButton( + text: l.signOutButtonText, + onPressed: () => FirebaseUIAuth.signOut( + context: context, + auth: auth, + ), + icon: isCupertino ? CupertinoIcons.arrow_right_circle : Icons.logout, + variant: variant, + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/sms_code_input.dart b/packages/firebase_ui_auth/lib/src/widgets/sms_code_input.dart new file mode 100644 index 000000000000..6c255260f9ad --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/sms_code_input.dart @@ -0,0 +1,258 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:firebase_auth/firebase_auth.dart' show PhoneAuthCredential; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import '../widgets/internal/universal_text_form_field.dart'; + +class _NumberDecorationPainter extends BoxPainter { + final InputBorder inputBorder; + final Color color; + + _NumberDecorationPainter({ + VoidCallback? onChanged, + required this.inputBorder, + required this.color, + }) : super(onChanged); + + final rect = const Rect.fromLTWH(0, 0, _numberSlotWidth, _numberSlotHeight); + + @override + void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { + canvas.save(); + canvas.translate(offset.dx, offset.dy); + inputBorder + .copyWith(borderSide: BorderSide(color: color, width: 2)) + .paint(canvas, rect); + canvas.restore(); + } +} + +class _NumberSlotDecoration extends Decoration { + final InputBorder inputBorder; + final Color color; + + const _NumberSlotDecoration({ + required this.inputBorder, + required this.color, + }); + + @override + BoxPainter createBoxPainter([VoidCallback? onChanged]) { + return _NumberDecorationPainter( + onChanged: onChanged, + inputBorder: inputBorder, + color: color, + ); + } +} + +const _numberSlotWidth = 44.0; +const _numberSlotHeight = 55.0; +const _numberSlotMargin = 5.5; + +class _NumberSlot extends StatefulWidget { + final String number; + + const _NumberSlot({Key? key, this.number = ''}) : super(key: key); + + @override + _NumberSlotState createState() => _NumberSlotState(); +} + +class _NumberSlotState extends State<_NumberSlot> + with SingleTickerProviderStateMixin { + bool hasError = false; + + late final controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 150), + ); + + @override + void didUpdateWidget(covariant _NumberSlot oldWidget) { + if (oldWidget.number.isEmpty && widget.number.isNotEmpty) { + controller.animateTo(1); + } + + if (oldWidget.number.isNotEmpty && widget.number.isEmpty) { + controller.animateBack(0); + } + + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + final inputBorder = Theme.of(context).inputDecorationTheme.border; + final primaryColor = Theme.of(context).colorScheme.primary; + final errorColor = Theme.of(context).errorColor; + + final color = hasError ? errorColor : primaryColor; + + return Container( + width: _numberSlotWidth, + height: _numberSlotHeight, + decoration: _NumberSlotDecoration( + inputBorder: inputBorder ?? const UnderlineInputBorder(), + color: color, + ), + margin: const EdgeInsets.all(_numberSlotMargin), + child: Center( + child: AnimatedBuilder( + animation: controller, + builder: (context, child) { + return Transform.scale( + scale: controller.value, + child: child, + ); + }, + child: Text( + widget.number, + style: const TextStyle(fontSize: 24), + ), + ), + ), + ); + } +} + +typedef SMSCodeSubmitCallback = void Function(String smsCode); + +/// {@template ui.auth.widgets.sms_code_input} +/// +/// A widget that allows the user to enter the SMS code sent to the user's +/// phone. +/// +/// This input is autofilled if SMS autoresolution is supported. +/// {@endtemplate} +class SMSCodeInput extends StatefulWidget { + /// Whether the input should have a focus by default. + final bool autofocus; + + /// Rendered under the input. + final Widget? text; + + /// A callback that is being called when SMS code is submitted. + final SMSCodeSubmitCallback? onSubmit; + + /// {@macro ui.auth.widgets.sms_code_input} + const SMSCodeInput({ + Key? key, + this.autofocus = true, + this.text, + this.onSubmit, + }) : super(key: key); + + @override + SMSCodeInputState createState() => SMSCodeInputState(); +} + +class SMSCodeInputState extends State { + String code = ''; + late final controller = TextEditingController()..addListener(onChange); + final focusNode = FocusNode(); + + void onChange() { + setState(() { + code = controller.text; + }); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + void didChangeDependencies() { + final authState = AuthState.maybeOf(context); + + if (authState is PhoneVerified) { + if (authState.credential is PhoneAuthCredential) { + controller.text = + (authState.credential as PhoneAuthCredential).smsCode!; + } + } + + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + final primaryColor = Theme.of(context).colorScheme.primary; + final l = FirebaseUILocalizations.labelsOf(context); + + final state = AuthState.maybeOf(context); + + Widget? text; + if (state is CredentialReceived || + state is SigningIn || + state is SignedIn) { + text = Text(l.verifyingSMSCodeText); + } + + if (state is AuthFailed) { + text = ErrorText(exception: state.exception); + } + + return ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: _numberSlotWidth * 6 + _numberSlotMargin * 12, + ), + child: Stack( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(_numberSlotMargin), + child: Text( + l.enterSMSCodeText, + style: TextStyle(color: primaryColor), + ), + ), + Directionality( + textDirection: TextDirection.ltr, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (int i = 0; i < 6; i++) + _NumberSlot(number: code.length > i ? code[i] : ''), + ], + ), + ), + if (widget.text != null || text != null) + Padding( + padding: const EdgeInsets.all(6), + child: widget.text ?? text, + ), + ], + ), + Opacity( + opacity: 0, + child: Padding( + padding: const EdgeInsets.only(top: 30), + child: UniversalTextFormField( + autofillHints: const [AutofillHints.oneTimeCode], + autofocus: true, + focusNode: focusNode, + controller: controller, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + onSubmitted: (v) { + if (v == null) return; + if (v.length < 6) return; + widget.onSubmit?.call(v); + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/packages/firebase_ui_auth/lib/src/widgets/user_avatar.dart b/packages/firebase_ui_auth/lib/src/widgets/user_avatar.dart new file mode 100644 index 000000000000..8981f9be5b82 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/widgets/user_avatar.dart @@ -0,0 +1,92 @@ +import 'package:firebase_auth/firebase_auth.dart' show FirebaseAuth; +import 'package:flutter/material.dart'; + +/// {@template ui.auth.widgets.user_avatar} +/// +/// A widget that displays the user's avatar. +/// +/// Shows a placeholder if user doesn't have a profile photo. +/// {@endtemplate} +class UserAvatar extends StatefulWidget { + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// {@template ui.auth.widgets.user_avatar.size} + /// A size of the avatar. + /// {@endtemplate} + final double? size; + + /// {@template ui.auth.widgets.user_avatar.shape} + /// A shape of the avatar. + /// A [CircleBorder] is used by default. + /// {@endtemplate} + final ShapeBorder? shape; + + /// {@template ui.auth.widgets.user_avatar.placeholder_color} + /// A color of the avatar placeholder. + /// {@endtemplate} + final Color? placeholderColor; + + /// {@macro ui.auth.widgets.user_avatar} + const UserAvatar({ + Key? key, + this.auth, + this.size, + this.shape, + this.placeholderColor, + }) : super(key: key); + + @override + State createState() => _UserAvatarState(); +} + +class _UserAvatarState extends State { + FirebaseAuth get auth => widget.auth ?? FirebaseAuth.instance; + ShapeBorder get shape => widget.shape ?? const CircleBorder(); + Color get placeholderColor => widget.placeholderColor ?? Colors.grey; + double get size => widget.size ?? 120; + + late String? photoUrl = auth.currentUser?.photoURL; + + Widget _imageFrameBuilder( + BuildContext context, + Widget? child, + int? frame, + bool? _, + ) { + if (frame == null) { + return Container(color: placeholderColor); + } + + return child!; + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: size, + width: size, + child: ClipPath( + clipper: ShapeBorderClipper(shape: shape), + clipBehavior: Clip.hardEdge, + child: photoUrl != null + ? Image.network( + photoUrl!, + width: size, + height: size, + cacheWidth: size.toInt(), + cacheHeight: size.toInt(), + fit: BoxFit.cover, + frameBuilder: _imageFrameBuilder, + ) + : Center( + child: Icon( + Icons.account_circle, + size: size, + color: placeholderColor, + ), + ), + ), + ); + } +} diff --git a/packages/firebase_ui_auth/pubspec.yaml b/packages/firebase_ui_auth/pubspec.yaml new file mode 100644 index 000000000000..613f00332fad --- /dev/null +++ b/packages/firebase_ui_auth/pubspec.yaml @@ -0,0 +1,74 @@ +name: firebase_ui_auth +description: Pre-built widgets library that are integrated with the variety of the Firebase Auth providers. +version: 1.0.0-dev.0 +repository: https://github.com/firebase/flutterfire/tree/master/packages/firebase_ui_auth +homepage: https://github.com/firebase/flutterfire/tree/master/packages/firebase_ui_auth + +environment: + sdk: '>=2.16.0 <3.0.0' + flutter: '>=1.17.0' + +dependencies: + cloud_firestore: ^3.5.1 + collection: ^1.15.0 + crypto: ^3.0.1 + desktop_webview_auth: ^0.0.9 + email_validator: ^2.0.1 + firebase_auth: ^3.10.0 + firebase_core: ^1.10.2 + firebase_database: ^9.1.7 + firebase_dynamic_links: ^4.3.11 + firebase_ui_localizations: ^1.0.0-dev.0 + firebase_ui_oauth: ^1.0.0-dev.0 + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + flutter_svg: ^1.1.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + mockito: ^5.2.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec +# The following section is specific to Flutter. + +false_secrets: + - '/example/**/google-services.json' + - '/example/**/firebase_options.dart' + - '/example/**/GoogleService-Info.plist' + - 'example/lib/config.dart' + +flutter: +# To add assets to your package, add an assets section, like this: +# assets: +# - images/a_dot_burr.jpeg +# - images/a_dot_ham.jpeg +# +# For details regarding assets in packages, see +# https://flutter.dev/assets-and-images/#from-packages +# +# An image asset can refer to one or more resolution-specific "variants", see +# https://flutter.dev/assets-and-images/#resolution-aware. +# To add custom fonts to your package, add a fonts section here, +# in this "flutter" section. Each entry in this list should have a +# "family" key with the font family name, and a "fonts" key with a +# list giving the asset and other descriptors for the font. For +# example: +# fonts: +# - family: Schyler +# fonts: +# - asset: fonts/Schyler-Regular.ttf +# - asset: fonts/Schyler-Italic.ttf +# style: italic +# - family: Trajan Pro +# fonts: +# - asset: fonts/TrajanPro.ttf +# - asset: fonts/TrajanPro_Bold.ttf +# weight: 700 +# +# For details regarding fonts in packages, see +# https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/firebase_ui_auth/test/firebase_ui_test.dart b/packages/firebase_ui_auth/test/firebase_ui_test.dart new file mode 100644 index 000000000000..0e72490ef115 --- /dev/null +++ b/packages/firebase_ui_auth/test/firebase_ui_test.dart @@ -0,0 +1,14 @@ +import 'flows/email_auth_flow_test.dart' as email_auth_flow; +import 'flows/email_link_flow_test.dart' as email_link_flow; +import 'flows/universal_email_sign_in_flow_test.dart' + as universal_email_sign_in_flow; +import 'flows/phone_auth_flow_test.dart' as phone_auth_flow; +import 'widgets/email_form_test.dart' as email_form; + +void main() { + email_auth_flow.main(); + email_link_flow.main(); + universal_email_sign_in_flow.main(); + phone_auth_flow.main(); + email_form.main(); +} diff --git a/packages/firebase_ui_auth/test/flows/email_auth_flow_test.dart b/packages/firebase_ui_auth/test/flows/email_auth_flow_test.dart new file mode 100644 index 000000000000..cab5b352aaf1 --- /dev/null +++ b/packages/firebase_ui_auth/test/flows/email_auth_flow_test.dart @@ -0,0 +1,279 @@ +import 'package:firebase_auth/firebase_auth.dart' hide EmailAuthProvider; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:mockito/mockito.dart'; + +import '../test_utils.dart'; + +void main() { + late EmailAuthFlow flow; + late MockAuth auth; + late MockAuthListener mockListener; + + late EmailAuthProvider provider; + + setUp(() { + auth = MockAuth(); + provider = EmailAuthProvider(); + + flow = EmailAuthFlow( + action: AuthAction.signIn, + auth: auth, + provider: provider, + ); + + mockListener = MockAuthListener(); + }); + + tearDown(() { + // simulate sign out + auth.user = null; + }); + + group('EmailAuthProvider', () { + test('has correct provider id', () { + expect(flow.provider.providerId, 'password'); + }); + + group('#authenticate', () { + test('calls signInWithCredential', () { + flow.provider.authenticate('email', 'password'); + final result = verify(auth.signInWithCredential(captureAny)); + + result.called(1); + + expect(result.captured[0], isA()); + expect(result.captured[0].email, 'email'); + expect(result.captured[0].password, 'password'); + }); + + test('calls createUserWithEmailAndPassword if action is signUp', () { + provider.authenticate('email', 'password', AuthAction.signUp); + provider.auth = MockAuth(); + + final result = verify( + auth.createUserWithEmailAndPassword( + email: captureAnyNamed('email'), + password: captureAnyNamed('password'), + ), + )..called(1); + + expect(result.captured, ['email', 'password']); + }); + + test('calls linkWithCredential if action is link', () { + final user = MockUser(); + auth.user = user; + + provider.authenticate('email', 'password', AuthAction.link); + verify(user.linkWithCredential(any)).called(1); + }); + + test('calls onBeforeSignIn', () { + flow.provider.authListener = mockListener; + flow.provider.authenticate('email', 'password'); + + verify(mockListener.onBeforeSignIn()).called(1); + }); + + test('calls onBeforeCredentialLinked if action is link', () { + flow.provider.authListener = mockListener; + flow.provider.authenticate('email', 'password', AuthAction.link); + + verify(mockListener.onCredentialReceived(any)).called(1); + }); + + test('calls onSignedIn', () async { + flow.provider.authListener = mockListener; + + final cred = MockCredential(); + when(auth.signInWithCredential(any)).thenAnswer((_) async { + return cred; + }); + + flow.provider.authenticate('email', 'password'); + + await untilCalled(auth.signInWithCredential(any)); + final result = verify(mockListener.onSignedIn(captureAny))..called(1); + + expect(result.captured[0], cred); + }); + + test('calls onCredentialLinked if action is link', () async { + flow.provider.authListener = mockListener; + + final user = MockUser(); + auth.user = user; + + flow.provider.authenticate('email', 'password', AuthAction.link); + + await untilCalled(user.linkWithCredential(any)); + final result = verify(mockListener.onCredentialLinked(captureAny)) + ..called(1); + + expect(result.captured[0], isA()); + expect(result.captured[0].email, 'email'); + expect(result.captured[0].password, 'password'); + }); + + test('calls onError if error occured', () async { + final exception = TestException(); + when(auth.signInWithCredential(any)).thenThrow(exception); + + flow.provider.authListener = mockListener; + flow.provider.authenticate('email', 'password'); + + await untilCalled(auth.signInWithCredential(any)); + final result = verify(mockListener.onError(captureAny))..called(1); + + expect(result.captured[0], exception); + }); + }); + }); + + group('EmailAuthController', () { + group('#setEmailAndPassword', () { + test('calls EmailAuthProvider#signIn', () { + final provider = MockProvider(); + final ctrl = EmailAuthFlow(provider: provider, auth: MockAuth()); + + ctrl.setEmailAndPassword('email', 'password'); + + final result = verify( + provider.authenticate( + captureAny, + captureAny, + captureAny, + ), + )..called(1); + + expect(result.captured[0], 'email'); + expect(result.captured[1], 'password'); + expect(result.captured[2], AuthAction.signIn); + }); + }); + }); + + group('AuthFlowBuilder', () { + testWidgets('emits correct states during sign in', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: AuthFlowBuilder( + auth: auth, + listener: (prevState, state, ctrl) { + if (prevState is AwaitingEmailAndPassword) { + expect(state, isA()); + } + + if (prevState is SigningIn) { + expect(state, isA()); + } + }, + builder: (context, state, ctrl, _) { + return ElevatedButton( + child: const Text('Sign in'), + onPressed: () => ctrl.setEmailAndPassword( + 'email', + 'password', + ), + ); + }, + ), + ), + ), + ); + + final button = find.byType(ElevatedButton); + await tester.tap(button); + await tester.pump(); + }); + + testWidgets('emits AuthFailed if error occured', (tester) async { + final exception = TestException(); + when(auth.signInWithCredential(any)).thenThrow(exception); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: AuthFlowBuilder( + auth: auth, + listener: (prevState, state, ctrl) { + if (prevState is AwaitingEmailAndPassword) { + expect(state, isA()); + } + + if (prevState is SigningIn) { + expect(state, isA()); + expect((state as AuthFailed).exception, exception); + } + }, + builder: (context, state, ctrl, _) { + return ElevatedButton( + child: const Text('Sign in'), + onPressed: () => ctrl.setEmailAndPassword( + 'email', + 'password', + ), + ); + }, + ), + ), + ), + ); + + final button = find.byType(ElevatedButton); + await tester.tap(button); + await tester.pump(); + }); + }); +} + +class MockProvider extends Mock implements EmailAuthProvider { + @override + void authenticate( + String? email, + String? password, [ + AuthAction? action = AuthAction.signIn, + ]) { + super.noSuchMethod(Invocation.method(#signIn, [email, password, action])); + } +} + +class MockAuthListener extends Mock implements EmailAuthListener { + @override + void onCredentialReceived(AuthCredential? credential) { + super.noSuchMethod( + Invocation.method( + #onBeforeCredentialLinked, + [credential], + ), + ); + } + + @override + void onCredentialLinked(AuthCredential? credential) { + super.noSuchMethod( + Invocation.method( + #onCredentialLinked, + [credential], + ), + ); + } + + @override + void onSignedIn(UserCredential? credential) { + super.noSuchMethod( + Invocation.method( + #onSignedIn, + [credential], + ), + ); + } + + @override + void onError(Object? error) { + super.noSuchMethod(Invocation.method(#onError, [error])); + } +} diff --git a/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart b/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart new file mode 100644 index 000000000000..397afcc9f88f --- /dev/null +++ b/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart @@ -0,0 +1,352 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:mockito/mockito.dart'; + +import '../test_utils.dart'; + +void main() { + late EmailLinkAuthProvider provider; + late MockListener listener; + late MockAuth auth; + late MockDynamicLinks dynamicLinks; + late EmailLinkFlow flow; + late EmailLinkAuthController ctrl; + + final actionCodeSettings = ActionCodeSettings( + url: 'https://example.com', + handleCodeInApp: true, + androidPackageName: 'com.test.app', + ); + + setUp(() { + auth = MockAuth(); + listener = MockListener(); + dynamicLinks = MockDynamicLinks(); + + provider = EmailLinkAuthProvider( + actionCodeSettings: actionCodeSettings, + dynamicLinks: dynamicLinks, + ); + + flow = EmailLinkFlow( + provider: provider, + auth: auth, + ); + + ctrl = flow; + }); + + group('EmailLinkAuthProvider', () { + test('has correct provider id', () { + expect(provider.providerId, 'email_link'); + }); + + group('#sendLink', () { + test('calls onBeforeLinkSent', () { + provider.authListener = listener; + + provider.sendLink('test@test.com'); + + final result = verify(listener.onBeforeLinkSent(captureAny)); + result.called(1); + expect(result.captured, ['test@test.com']); + }); + + test('calls FirebaseAuth#sendSignInLinkToEmail', () { + provider.authListener = listener; + provider.sendLink('test@test.com'); + + final result = verify( + auth.sendSignInLinkToEmail( + actionCodeSettings: captureAnyNamed('actionCodeSettings'), + email: captureAnyNamed('email'), + ), + ); + + result.called(1); + expect(result.captured[0], actionCodeSettings); + expect(result.captured[1], 'test@test.com'); + }); + + test('calls onLinkSent', () async { + provider.authListener = listener; + + provider.sendLink('test@test.com'); + + await untilCalled( + auth.sendSignInLinkToEmail( + email: anyNamed('email'), + actionCodeSettings: anyNamed('actionCodeSettings'), + ), + ); + + final result = verify(listener.onLinkSent(captureAny)); + result.called(1); + expect(result.captured, ['test@test.com']); + }); + + test('calls onError if an error occured', () async { + provider.authListener = listener; + final exception = TestException(); + + when( + auth.sendSignInLinkToEmail( + email: anyNamed('email'), + actionCodeSettings: anyNamed('actionCodeSettings'), + ), + ).thenThrow(exception); + + provider.sendLink('test@test.com'); + + await untilCalled(listener.onBeforeLinkSent(any)); + final result = verify(listener.onError(captureAny)); + + result.called(1); + expect(result.captured, [exception]); + }); + }); + + group('#awaitLink', () { + test( + 'waits for a link from dynamic links and calls onBeforeSignIn', + () async { + provider.authListener = listener; + provider.awaitLink('test@test.com'); + + await untilCalled(listener.onBeforeSignIn()); + + verify(listener.onBeforeSignIn()).called(1); + }, + ); + + test('calls onError if got not a valid sign in link', () async { + provider.authListener = listener; + provider.awaitLink('test@test.com'); + + when(auth.isSignInWithEmailLink(any)).thenReturn(false); + + await untilCalled(listener.onError(any)); + + final result = verify(listener.onError(captureAny)); + result.called(1); + expect(result.captured[0], isA()); + expect(result.captured[0].code, 'invalid-email-signin-link'); + }); + + test( + 'calls FirebaseAuth#signInWithEmailLink when got a valid sign in link', + () async { + provider.authListener = listener; + provider.awaitLink('test@test.com'); + + await untilCalled(listener.onBeforeSignIn()); + + final result = verify( + auth.signInWithEmailLink( + email: captureAnyNamed('email'), + emailLink: captureAnyNamed('emailLink'), + ), + ); + + result.called(1); + + expect(result.captured[0], 'test@test.com'); + expect(result.captured[1], 'https://test.com'); + }, + ); + + test('calls onSignedIn when sign in succeded', () async { + provider.authListener = listener; + provider.awaitLink('test@test.com'); + + await untilCalled(listener.onSignedIn(any)); + final result = verify(listener.onSignedIn(captureAny)); + + result.called(1); + expect(result.captured[0], isA()); + }); + + test('calls onError if sing in failed', () async { + provider.authListener = listener; + final exception = TestException(); + + when( + auth.signInWithEmailLink( + email: anyNamed('email'), + emailLink: anyNamed('emailLink'), + ), + ).thenThrow(exception); + + provider.awaitLink('test@test.com'); + + await untilCalled(listener.onError(any)); + final result = verify(listener.onError(captureAny)); + + result.called(1); + expect(result.captured, [exception]); + }); + }); + }); + + group('EmailLinkFlowController', () { + test('#sendLink calls EmailLinkAuthProvider#sendLink', () { + final provider = MockProvider(); + ctrl = EmailLinkFlow(provider: provider, auth: auth); + + ctrl.sendLink('test@test.com'); + + final result = verify(provider.sendLink(captureAny)); + + result.called(1); + expect(result.captured, ['test@test.com']); + }); + }); + + group('EmailLinkFlow', () { + test('#onLinkSent calls EmailLinkAuthProvider#awaitLink', () { + final provider = MockProvider(); + flow = EmailLinkFlow(provider: provider, auth: auth); + + flow.onLinkSent('test@test.com'); + + final result = verify(provider.awaitLink(captureAny)); + + result.called(1); + expect(result.captured, ['test@test.com']); + }); + }); + + group('AuthFlowBuilder', () { + testWidgets('emits correct states', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: AuthFlowBuilder( + auth: auth, + provider: provider, + listener: (prevState, state, ctrl) { + if (prevState is Uninitialized) { + expect(state, isA()); + } + + if (prevState is SendingLink) { + expect(state, isA()); + } + + if (prevState is AwaitingDynamicLink) { + expect(state, isA()); + } + + if (prevState is SignedIn) { + expect(state, isA()); + } + }, + builder: (context, state, ctrl, _) { + return ElevatedButton( + child: const Text('Sign in'), + onPressed: () => ctrl.sendLink('tesT@test.com'), + ); + }, + ), + ), + ), + ); + + final button = find.byType(ElevatedButton); + await tester.tap(button); + await tester.pump(); + }); + + testWidgets('emits AuthFailed if an error occured', (tester) async { + final exception = TestException(); + + when( + auth.signInWithEmailLink( + email: anyNamed('email'), + emailLink: anyNamed('emailLink'), + ), + ).thenThrow(exception); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: AuthFlowBuilder( + auth: auth, + provider: provider, + listener: (prevState, state, ctrl) { + if (prevState is Uninitialized) { + expect(state, isA()); + } + + if (prevState is SendingLink) { + expect(state, isA()); + } + + if (prevState is AwaitingDynamicLink) { + expect(state, isA()); + } + + if (prevState is SigningIn) { + expect(state, isA()); + expect((state as AuthFailed).exception, exception); + } + }, + builder: (context, state, ctrl, _) { + return ElevatedButton( + child: const Text('Sign in'), + onPressed: () => ctrl.sendLink('tesT@test.com'), + ); + }, + ), + ), + ), + ); + + final button = find.byType(ElevatedButton); + await tester.tap(button); + await tester.pump(); + }); + }); +} + +class MockProvider extends Mock implements EmailLinkAuthProvider { + @override + void sendLink(String? email) { + super.noSuchMethod(Invocation.method(#sendLink, [email])); + } + + @override + void awaitLink(String? email) { + super.noSuchMethod(Invocation.method(#awaitLink, [email])); + } +} + +class MockListener extends Mock implements EmailLinkAuthListener { + @override + void onSignedIn(UserCredential? credential) { + super.noSuchMethod(Invocation.method(#onSignedIn, [credential])); + } + + @override + void onBeforeLinkSent(String? email) { + super.noSuchMethod( + Invocation.method(#onBeforeLinkSent, [email]), + ); + } + + @override + void onLinkSent(String? email) { + super.noSuchMethod( + Invocation.method(#onLinkSent, [email]), + ); + } + + @override + void onError(Object? error) { + super.noSuchMethod( + Invocation.method(#onError, [error]), + ); + } +} diff --git a/packages/firebase_ui_auth/test/flows/phone_auth_flow_test.dart b/packages/firebase_ui_auth/test/flows/phone_auth_flow_test.dart new file mode 100644 index 000000000000..6e08e888d6cd --- /dev/null +++ b/packages/firebase_ui_auth/test/flows/phone_auth_flow_test.dart @@ -0,0 +1,482 @@ +import 'package:firebase_auth/firebase_auth.dart' hide PhoneAuthProvider; +import 'package:flutter_test/flutter_test.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:mockito/mockito.dart'; + +import '../test_utils.dart'; + +void main() { + late PhoneAuthProvider provider; + late MockAuth auth; + late MockListener listener; + + setUp(() { + auth = MockAuth(); + listener = MockListener(); + + provider = PhoneAuthProvider(); + provider.auth = auth; + provider.authListener = listener; + }); + + group('PhoneAuthProvider', () { + test('has correct provider id', () { + expect(provider.providerId, 'phone'); + }); + + group('#sendVerificationCode', () { + test('calls onSMSCodeRequested', () { + provider.sendVerificationCode( + phoneNumber: '+123456789', + action: AuthAction.signIn, + ); + + final invocation = verify(listener.onSMSCodeRequested('+123456789')); + expect(invocation.callCount, 1); + }); + + test('calls FirebaseAuth#verifyPhoneNumber', () async { + provider.sendVerificationCode( + phoneNumber: '+123456789', + action: AuthAction.signIn, + ); + + final invocation = verify( + auth.verifyPhoneNumber( + phoneNumber: captureAnyNamed('phoneNumber'), + verificationCompleted: anyNamed('verificationCompleted'), + verificationFailed: anyNamed('verificationFailed'), + codeSent: anyNamed('codeSent'), + codeAutoRetrievalTimeout: anyNamed('codeAutoRetrievalTimeout'), + ), + ); + + await untilCalled(listener.onSMSCodeRequested(any)); + + expect(invocation.callCount, 1); + expect(invocation.captured.first, '+123456789'); + }); + + test('calls onCodeSent when code is sent', () async { + provider.sendVerificationCode( + phoneNumber: '+123456789', + action: AuthAction.signIn, + ); + + final invocation = verify( + auth.verifyPhoneNumber( + phoneNumber: anyNamed('phoneNumber'), + verificationCompleted: anyNamed('verificationCompleted'), + verificationFailed: anyNamed('verificationFailed'), + codeSent: captureAnyNamed('codeSent'), + codeAutoRetrievalTimeout: anyNamed('codeAutoRetrievalTimeout'), + ), + ); + + await untilCalled(listener.onSMSCodeRequested(any)); + + final onCodeSent = invocation.captured[0]; + onCodeSent('verificationId'); + + final onCodeSendInvocation = verify(listener.onCodeSent(captureAny)); + + expect(onCodeSendInvocation.callCount, 1); + expect(onCodeSendInvocation.captured, ['verificationId']); + }); + + test( + 'calls onVerificationCompleted when verification is complete', + () async { + provider.sendVerificationCode( + phoneNumber: '+123456789', + action: AuthAction.signIn, + ); + + final credential = MockPhoneCredential(); + + final invocation = verify( + auth.verifyPhoneNumber( + phoneNumber: anyNamed('phoneNumber'), + verificationCompleted: captureAnyNamed('verificationCompleted'), + verificationFailed: anyNamed('verificationFailed'), + codeSent: anyNamed('codeSent'), + codeAutoRetrievalTimeout: anyNamed('codeAutoRetrievalTimeout'), + ), + ); + + await untilCalled(listener.onSMSCodeRequested(any)); + + final onVerificationCompleted = invocation.captured[0]; + onVerificationCompleted(credential); + + final onVerificationCompletedInvocation = verify( + listener.onVerificationCompleted(captureAny), + ); + + expect(onVerificationCompletedInvocation.callCount, 1); + expect(onVerificationCompletedInvocation.captured, [credential]); + }, + ); + + test('calls onError if autoresolution timed out', () async { + provider.sendVerificationCode( + phoneNumber: '+123456789', + action: AuthAction.signIn, + ); + + final invocation = verify( + auth.verifyPhoneNumber( + phoneNumber: anyNamed('phoneNumber'), + verificationCompleted: anyNamed('verificationCompleted'), + verificationFailed: anyNamed('verificationFailed'), + codeSent: anyNamed('codeSent'), + codeAutoRetrievalTimeout: captureAnyNamed( + 'codeAutoRetrievalTimeout', + ), + ), + ); + + await untilCalled(listener.onSMSCodeRequested(any)); + + final timeout = invocation.captured[0]; + timeout('+123456789'); + + final onErrorInvocation = verify(listener.onError(captureAny)); + + expect(onErrorInvocation.callCount, 1); + expect( + onErrorInvocation.captured.first, + isA(), + ); + }); + + test('calls onError if verification failed', () async { + provider.sendVerificationCode( + phoneNumber: '+123456789', + action: AuthAction.signIn, + ); + + final exception = TestException(); + + final invocation = verify( + auth.verifyPhoneNumber( + phoneNumber: anyNamed('phoneNumber'), + verificationCompleted: anyNamed('verificationCompleted'), + verificationFailed: captureAnyNamed('verificationFailed'), + codeSent: anyNamed('codeSent'), + codeAutoRetrievalTimeout: anyNamed('codeAutoRetrievalTimeout'), + ), + ); + + await untilCalled(listener.onSMSCodeRequested(any)); + + final onError = invocation.captured[0]; + onError(exception); + + final onErrorInvocation = verify(listener.onError(captureAny)); + + expect(onErrorInvocation.callCount, 1); + expect(onErrorInvocation.captured, [exception]); + }); + }); + + group('#verifySMSCode', () { + test( + 'calls FirebaseAuth#signInWithCredential if action is sign in', + () { + provider.verifySMSCode( + action: AuthAction.signIn, + code: '123456', + verificationId: 'verificationId', + ); + + final invocation = verify(auth.signInWithCredential(captureAny)); + + expect(invocation.callCount, 1); + expect(invocation.captured.first, isA()); + + final cred = invocation.captured.first as PhoneAuthCredential; + + expect(cred.smsCode, '123456'); + expect(cred.verificationId, 'verificationId'); + }, + ); + + test('calls onBeforeSignIn if action is sign in', () { + provider.verifySMSCode( + action: AuthAction.signIn, + code: '123456', + verificationId: 'verificationId', + ); + + final invocation = verify(listener.onBeforeSignIn()); + expect(invocation.callCount, 1); + }); + + test('calls onSignedIn if sign in succeded', () async { + final cred = MockUserCredential(); + when(auth.signInWithCredential(any)).thenAnswer((_) async => cred); + + provider.verifySMSCode( + action: AuthAction.signIn, + code: '123456', + verificationId: 'verificationId', + ); + + await untilCalled(listener.onBeforeSignIn()); + + final invocation = verify(listener.onSignedIn(captureAny)); + + expect(invocation.callCount, 1); + expect(invocation.captured, [cred]); + }); + + test('calls onError if sign in failed', () async { + final exception = TestException(); + + when(auth.signInWithCredential(any)).thenThrow(exception); + + provider.verifySMSCode( + action: AuthAction.signIn, + code: '123456', + verificationId: 'verificationId', + ); + + await untilCalled(listener.onBeforeSignIn()); + + final onErrorInvocation = verify(listener.onError(captureAny)); + + expect(onErrorInvocation.callCount, 1); + expect(onErrorInvocation.captured, [exception]); + }); + + test('calls linkWithCredential if action is link', () { + final user = MockUser(); + auth.user = user; + + provider.verifySMSCode( + action: AuthAction.link, + code: '123456', + verificationId: 'verificationId', + ); + + verify(user.linkWithCredential(any)).called(1); + }); + + test('calls onBeforeCredentialLinked if action is link', () { + provider.verifySMSCode( + action: AuthAction.link, + code: '123456', + verificationId: 'verificationId', + ); + + final invocation = verify( + listener.onCredentialReceived(captureAny), + ); + + expect(invocation.callCount, 1); + expect(invocation.captured.first, isA()); + + final cred = invocation.captured.first as PhoneAuthCredential; + + expect(cred.smsCode, '123456'); + expect(cred.verificationId, 'verificationId'); + }); + + test( + 'calls onCredentialLinked if credential linking succeded', + () async { + final user = MockUser(); + auth.user = user; + + provider.verifySMSCode( + action: AuthAction.link, + code: '123456', + verificationId: 'verificationId', + ); + + await untilCalled(user.linkWithCredential(any)); + + final invocation = verify(listener.onCredentialLinked(captureAny)); + + expect(invocation.callCount, 1); + expect(invocation.captured.first, isA()); + + final cred = invocation.captured.first as PhoneAuthCredential; + + expect(cred.smsCode, '123456'); + expect(cred.verificationId, 'verificationId'); + }, + ); + }); + + group('PhoneAuthController', () { + group('#acceptPhoneNumber', () { + test('calls PhoneAuthProvider#sendVerificationCode', () { + final provider = MockProvider(); + final ctrl = PhoneAuthFlow(provider: provider, auth: MockAuth()); + + ctrl.acceptPhoneNumber('+123456789'); + + final invocation = verify( + provider.sendVerificationCode( + phoneNumber: captureAnyNamed('phoneNumber'), + action: anyNamed('action'), + forceResendingToken: anyNamed('forceResendingToken'), + hint: anyNamed('hint'), + multiFactorSession: anyNamed('multiFactorSession'), + ), + )..called(1); + + expect(invocation.callCount, 1); + expect(invocation.captured, ['+123456789']); + }); + }); + + group('#verifySMSCode', () { + test('calls PhoneAuthProvider#verifySMSCode', () { + final provider = MockProvider(); + final ctrl = PhoneAuthFlow(provider: provider, auth: MockAuth()); + + ctrl.verifySMSCode('123456', verificationId: 'verificationId'); + + final invocation = verify( + provider.verifySMSCode( + action: anyNamed('action'), + code: captureAnyNamed('code'), + verificationId: captureAnyNamed('verificationId'), + ), + ); + + expect(invocation.callCount, 1); + expect(invocation.captured, ['123456', 'verificationId']); + }); + }); + }); + }); +} + +class MockProvider extends Mock implements PhoneAuthProvider { + @override + void sendVerificationCode({ + String? phoneNumber, + AuthAction? action, + int? forceResendingToken, + MultiFactorSession? multiFactorSession, + PhoneMultiFactorInfo? hint, + }) { + super.noSuchMethod( + Invocation.method( + #sendVerificationCode, + null, + { + #phoneNumber: phoneNumber, + #action: action, + #forceResendingToken: forceResendingToken, + #multiFactorSession: multiFactorSession, + #hint: hint, + }, + ), + ); + } + + @override + void verifySMSCode({ + AuthAction? action, + String? code, + String? verificationId, + ConfirmationResult? confirmationResult, + }) { + super.noSuchMethod( + Invocation.method( + #verifySMSCode, + null, + { + #action: action, + #code: code, + #verificationId: verificationId, + #confirmationResult: confirmationResult, + }, + ), + ); + } +} + +class MockUserCredential extends Mock implements UserCredential {} + +class MockPhoneCredential extends Mock implements PhoneAuthCredential {} + +class MockListener extends Mock implements PhoneAuthListener { + @override + void onCodeSent(String? verificationId, [int? forceResendToken]) { + super.noSuchMethod( + Invocation.method( + #onCodeSent, + [ + verificationId, + forceResendToken, + ], + ), + ); + } + + @override + void onSMSCodeRequested(String? phoneNumber) { + super.noSuchMethod( + Invocation.method( + #onSMSCodeRequested, + [phoneNumber], + ), + ); + } + + @override + void onCredentialLinked(AuthCredential? credential) { + super.noSuchMethod( + Invocation.method( + #onCredentialLinked, + [credential], + ), + ); + } + + @override + void onVerificationCompleted(PhoneAuthCredential? credential) { + super.noSuchMethod( + Invocation.method( + #onVerificationCompleted, + [credential], + ), + ); + } + + @override + void onError(Object? error) { + super.noSuchMethod( + Invocation.method( + #onError, + [error], + ), + ); + } + + @override + void onCredentialReceived(AuthCredential? credential) { + super.noSuchMethod( + Invocation.method( + #onBeforeCredentialLinked, + [credential], + ), + ); + } + + @override + void onSignedIn(UserCredential? credential) { + super.noSuchMethod( + Invocation.method( + #onSignedIn, + [credential], + ), + ); + } +} diff --git a/packages/firebase_ui_auth/test/flows/universal_email_sign_in_flow_test.dart b/packages/firebase_ui_auth/test/flows/universal_email_sign_in_flow_test.dart new file mode 100644 index 000000000000..97cebd81339d --- /dev/null +++ b/packages/firebase_ui_auth/test/flows/universal_email_sign_in_flow_test.dart @@ -0,0 +1,148 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:mockito/mockito.dart'; + +import '../test_utils.dart'; + +void main() { + late UniversalEmailSignInProvider provider; + late MockAuth auth; + late MockListener listener; + + setUp(() { + auth = MockAuth(); + listener = MockListener(); + + provider = UniversalEmailSignInProvider(); + provider.auth = auth; + provider.authListener = listener; + }); + + group('UniversalEmailSignInProvider', () { + test('has correct provider id', () { + expect(provider.providerId, 'universal_email_sign_in'); + }); + + group('#findProvidersForEmail', () { + test('calls FirebaseAuth#fetchSignInMethodsForEmail', () { + provider.findProvidersForEmail('test@test.com'); + final invocation = verify(auth.fetchSignInMethodsForEmail(captureAny)); + + expect(invocation.callCount, 1); + expect(invocation.captured, ['test@test.com']); + }); + + test( + 'calls onBeforeProvidersForEmailFetch', + () { + provider.findProvidersForEmail('test@test.com'); + verify(listener.onBeforeProvidersForEmailFetch()).called(1); + }, + ); + + test('calls onDifferentProvidersFound', () async { + provider.findProvidersForEmail('test@test.com'); + await untilCalled(listener.onBeforeProvidersForEmailFetch()); + + final invocation = verify( + listener.onDifferentProvidersFound( + captureAny, + captureAny, + captureAny, + ), + ); + + invocation.called(1); + + expect(invocation.captured, [ + 'test@test.com', + ['phone'], + null, + ]); + }); + + test('calls onError if an error occured', () async { + final exception = TestException(); + when(auth.fetchSignInMethodsForEmail(any)).thenThrow(exception); + + provider.findProvidersForEmail('test@test.com'); + await untilCalled(listener.onError(any)); + + final invocation = verify(listener.onError(captureAny)); + + expect(invocation.callCount, 1); + expect(invocation.captured, [exception]); + }); + }); + + group('UniversalEmailSignInController', () { + group('#findProvidersForEmail', () { + test( + 'calls UniversalEmailSignInProvider#findProvidersForEmail', + () async { + final provider = MockProvider(); + + UniversalEmailSignInController ctrl = UniversalEmailSignInFlow( + provider: provider, + auth: auth, + ); + + ctrl.findProvidersForEmail('test@test.com'); + final invocation = verify( + provider.findProvidersForEmail(captureAny), + ); + + expect(invocation.callCount, 1); + expect(invocation.captured, ['test@test.com']); + }, + ); + }); + }); + }); +} + +class MockListener extends Mock implements UniversalEmailSignInListener { + @override + void onBeforeProvidersForEmailFetch() { + super.noSuchMethod( + Invocation.method(#onBeforeProvidersForEmailFetch, null), + ); + } + + @override + void onDifferentProvidersFound( + String? email, + List? providers, + AuthCredential? credential, + ) { + super.noSuchMethod( + Invocation.method( + #onDifferentProvidersFound, + [email, providers, credential], + ), + ); + } + + @override + void onError(Object? error) { + super.noSuchMethod( + Invocation.method(#onError, [error]), + ); + } +} + +class MockProvider extends Mock implements UniversalEmailSignInProvider { + @override + void findProvidersForEmail( + String? email, [ + AuthCredential? credential, + ]) { + super.noSuchMethod( + Invocation.method( + #findProvidersForEmail, + [email, credential], + ), + ); + } +} diff --git a/packages/firebase_ui_auth/test/test_utils.dart b/packages/firebase_ui_auth/test/test_utils.dart new file mode 100644 index 000000000000..125d81670b59 --- /dev/null +++ b/packages/firebase_ui_auth/test/test_utils.dart @@ -0,0 +1,187 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; +import 'package:flutter/material.dart'; +import 'package:mockito/mockito.dart'; + +class TestMaterialApp extends StatelessWidget { + final Widget child; + + const TestMaterialApp({Key? key, required this.child}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: child, + ), + ); + } +} + +class MockCredential extends Mock implements UserCredential {} + +class MockUser extends Mock implements User { + @override + Future linkWithCredential(AuthCredential? credential) async { + return super.noSuchMethod( + Invocation.method( + #linkWithCredential, + [credential], + ), + returnValue: MockCredential(), + returnValueForMissingStub: MockCredential(), + ); + } +} + +class MockLinksStream extends Mock implements Stream { + @override + Future get first async { + return super.noSuchMethod( + Invocation.getter(#first), + returnValue: PendingDynamicLinkData( + link: Uri.parse('https://test.com'), + ), + returnValueForMissingStub: PendingDynamicLinkData( + link: Uri.parse('https://test.com'), + ), + ); + } +} + +class MockDynamicLinks extends Mock implements FirebaseDynamicLinks { + static final _linkStream = MockLinksStream(); + + @override + Stream get onLink => _linkStream; +} + +class MockAuth extends Mock implements FirebaseAuth { + MockUser? user; + + @override + User? get currentUser => user; + + @override + Future signInWithCredential( + AuthCredential? credential, + ) async { + return super.noSuchMethod( + Invocation.method( + #signInWithCredential, + [credential], + ), + returnValue: MockCredential(), + returnValueForMissingStub: MockCredential(), + ); + } + + @override + Future createUserWithEmailAndPassword({ + String? email, + String? password, + }) async { + return super.noSuchMethod( + Invocation.method(#createUserWithEmailAndPassword, null, { + #email: email, + #password: password, + }), + returnValue: MockCredential(), + returnValueForMissingStub: MockCredential(), + ); + } + + @override + Future sendSignInLinkToEmail({ + required String? email, + required ActionCodeSettings? actionCodeSettings, + }) async { + return super.noSuchMethod( + Invocation.method( + #sendSignInLinkToEmail, + null, + { + #email: email, + #actionCodeSettings: actionCodeSettings, + }, + ), + returnValueForMissingStub: null, + ); + } + + @override + bool isSignInWithEmailLink(String? emailLink) { + return super.noSuchMethod( + Invocation.method( + #isSignInWithEmailLink, + [emailLink], + ), + returnValue: true, + returnValueForMissingStub: true, + ); + } + + @override + Future signInWithEmailLink({ + required String? email, + required String? emailLink, + }) async { + return super.noSuchMethod( + Invocation.method( + #signInWithEmailLink, + null, + { + #email: email, + #emailLink: emailLink, + }, + ), + returnValue: MockCredential(), + returnValueForMissingStub: MockCredential(), + ); + } + + @override + Future> fetchSignInMethodsForEmail(String? email) async { + return super.noSuchMethod( + Invocation.method( + #fetchSignInMethodsForEmail, + [email], + ), + returnValue: ['phone'], + returnValueForMissingStub: ['phone'], + ); + } + + @override + Future verifyPhoneNumber({ + String? phoneNumber, + PhoneMultiFactorInfo? multiFactorInfo, + PhoneVerificationCompleted? verificationCompleted, + PhoneVerificationFailed? verificationFailed, + PhoneCodeSent? codeSent, + PhoneCodeAutoRetrievalTimeout? codeAutoRetrievalTimeout, + String? autoRetrievedSmsCodeForTesting, + Duration timeout = const Duration(seconds: 30), + int? forceResendingToken, + MultiFactorSession? multiFactorSession, + }) async { + super.noSuchMethod( + Invocation.method( + #verifyPhoneNumber, + null, + { + #phoneNumber: phoneNumber, + #verificationCompleted: verificationCompleted, + #verificationFailed: verificationFailed, + #codeSent: codeSent, + #codeAutoRetrievalTimeout: codeAutoRetrievalTimeout, + #autoRetrievedSmsCodeForTesting: autoRetrievedSmsCodeForTesting, + #timeout: timeout, + #forceResendingToken: forceResendingToken, + }, + ), + ); + } +} + +class TestException implements Exception {} diff --git a/packages/firebase_ui_auth/test/widgets/email_form_test.dart b/packages/firebase_ui_auth/test/widgets/email_form_test.dart new file mode 100644 index 000000000000..1548ae180bf8 --- /dev/null +++ b/packages/firebase_ui_auth/test/widgets/email_form_test.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:mockito/mockito.dart'; + +import '../test_utils.dart'; + +class MockFirebaseAuth extends Mock implements FirebaseAuth {} + +void main() { + group('EmailForm', () { + late Widget widget; + + setUp(() { + widget = TestMaterialApp( + child: EmailForm( + auth: MockAuth(), + action: AuthAction.signIn, + ), + ); + }); + + testWidgets('has a Sign in button of outlined variant', (tester) async { + await tester.pumpWidget(widget); + final button = find.byType(OutlinedButton); + + expect(button, findsOneWidget); + }); + + testWidgets('has a Forgot password button of text variant', (tester) async { + await tester.pumpWidget(widget); + final button = find.byType(TextButton); + + expect( + button, + findsOneWidget, + ); + }); + + testWidgets('respects the EmailFormStyle', (tester) async { + await tester.pumpWidget( + FirebaseUITheme( + styles: const { + EmailFormStyle(signInButtonVariant: ButtonVariant.filled) + }, + child: widget, + ), + ); + + final button = find.byType(ElevatedButton); + expect(button, findsOneWidget); + }); + }); +} diff --git a/packages/firebase_ui_oauth/.gitignore b/packages/firebase_ui_oauth/.gitignore new file mode 100644 index 000000000000..96486fd93024 --- /dev/null +++ b/packages/firebase_ui_oauth/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/firebase_ui_oauth/.metadata b/packages/firebase_ui_oauth/.metadata new file mode 100644 index 000000000000..e7011f64f39d --- /dev/null +++ b/packages/firebase_ui_oauth/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + channel: stable + +project_type: package diff --git a/packages/firebase_ui_oauth/CHANGELOG.md b/packages/firebase_ui_oauth/CHANGELOG.md new file mode 100644 index 000000000000..a5c39d636c33 --- /dev/null +++ b/packages/firebase_ui_oauth/CHANGELOG.md @@ -0,0 +1,5 @@ +## 1.0.0-dev.0 + +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/firebase_ui_oauth/LICENSE b/packages/firebase_ui_oauth/LICENSE new file mode 100644 index 000000000000..5b8ff6261110 --- /dev/null +++ b/packages/firebase_ui_oauth/LICENSE @@ -0,0 +1,26 @@ +Copyright 2017, the Chromium project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/firebase_ui_oauth/README.md b/packages/firebase_ui_oauth/README.md new file mode 100644 index 000000000000..e869258d6add --- /dev/null +++ b/packages/firebase_ui_oauth/README.md @@ -0,0 +1,18 @@ +# Firebase UI OAuth + +[![pub package](https://img.shields.io/pub/v/firebase_ui_oauth.svg)](https://pub.dev/packages/firebase_ui_oauth) + +Base package for Firebase UI OAuth providers: + +- [Apple Sign In](https://pub.dev/packages/firebase_ui_oauth_apple) +- [Google Sign In](https://pub.dev/packages/firebase_ui_oauth_google) +- [Facebook Sign In](https://pub.dev/packages/firebase_ui_oauth_facebook) +- [Twitter Sign In](https://pub.dev/packages/firebase_ui_oauth_twitter) + +For issues, please create a new [issue on the repository](https://github.com/firebase/flutterfire/issues). + +For feature requests, & questions, please participate on the [discussion](https://github.com/firebase/flutterfire/discussions/6978) thread. + +To contribute a change to this plugin, please review our [contribution guide](https://github.com/firebase/flutterfire/blob/master/CONTRIBUTING.md) and open a [pull request](https://github.com/firebase/flutterfire/pulls). + +Please contribute to the [discussion](https://github.com/firebase/flutterfire/discussions/6978) with feedback. diff --git a/packages/firebase_ui_oauth/analysis_options.yaml b/packages/firebase_ui_oauth/analysis_options.yaml new file mode 100644 index 000000000000..a5744c1cfbe7 --- /dev/null +++ b/packages/firebase_ui_oauth/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/firebase_ui_oauth/example/.gitignore b/packages/firebase_ui_oauth/example/.gitignore new file mode 100644 index 000000000000..0fa6b675c0a5 --- /dev/null +++ b/packages/firebase_ui_oauth/example/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/firebase_ui_oauth/example/.metadata b/packages/firebase_ui_oauth/example/.metadata new file mode 100644 index 000000000000..ce13b42014bd --- /dev/null +++ b/packages/firebase_ui_oauth/example/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: android + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: ios + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: linux + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: macos + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: web + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: windows + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/firebase_ui_oauth/example/README.md b/packages/firebase_ui_oauth/example/README.md new file mode 100644 index 000000000000..3db21501dcb1 --- /dev/null +++ b/packages/firebase_ui_oauth/example/README.md @@ -0,0 +1,16 @@ +# firebase_ui_oauth_example + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/firebase_ui_oauth/example/analysis_options.yaml b/packages/firebase_ui_oauth/example/analysis_options.yaml new file mode 100644 index 000000000000..61b6c4de17c9 --- /dev/null +++ b/packages/firebase_ui_oauth/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/firebase_ui_oauth/example/android/.gitignore b/packages/firebase_ui_oauth/example/android/.gitignore new file mode 100644 index 000000000000..6f568019d3c6 --- /dev/null +++ b/packages/firebase_ui_oauth/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/packages/firebase_ui_oauth/example/android/app/build.gradle b/packages/firebase_ui_oauth/example/android/app/build.gradle new file mode 100644 index 000000000000..84a4bbb655e2 --- /dev/null +++ b/packages/firebase_ui_oauth/example/android/app/build.gradle @@ -0,0 +1,71 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +// START: FlutterFire Configuration +apply plugin: 'com.google.gms.google-services' +// END: FlutterFire Configuration +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "io.flutter.plugins.firebase_ui_oauth_example" + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/packages/firebase_ui_oauth/example/android/app/google-services.json b/packages/firebase_ui_oauth/example/android/app/google-services.json new file mode 100644 index 000000000000..d26d5328d243 --- /dev/null +++ b/packages/firebase_ui_oauth/example/android/app/google-services.json @@ -0,0 +1,343 @@ +{ + "project_info": { + "project_number": "406099696497", + "firebase_url": "https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app", + "project_id": "flutterfire-e2e-tests", + "storage_bucket": "flutterfire-e2e-tests.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:406099696497:android:3ef965ff044efc0b3574d0", + "android_client_info": { + "package_name": "io.flutter.plugins.firebase.database.example" + } + }, + "oauth_client": [ + { + "client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "406099696497-1ugbsqv8nkfn788ep0k233e750aupb7u.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "io.flutter.plugins.firebaseDatabaseExample" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:406099696497:android:40da41183cb3d3ff3574d0", + "android_client_info": { + "package_name": "io.flutter.plugins.firebase.dynamiclinksexample" + } + }, + "oauth_client": [ + { + "client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "406099696497-1ugbsqv8nkfn788ep0k233e750aupb7u.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "io.flutter.plugins.firebaseDatabaseExample" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:406099696497:android:7ca3394493cc601a3574d0", + "android_client_info": { + "package_name": "io.flutter.plugins.firebase.functions.example" + } + }, + "oauth_client": [ + { + "client_id": "406099696497-tvtvuiqogct1gs1s6lh114jeps7hpjm5.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "io.flutter.plugins.firebase.functions.example", + "certificate_hash": "909ca1482ef022bbae45a2db6b6d05d807a4c4aa" + } + }, + { + "client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "406099696497-1ugbsqv8nkfn788ep0k233e750aupb7u.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "io.flutter.plugins.firebaseDatabaseExample" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:406099696497:android:0d4ed619c031c0ac3574d0", + "android_client_info": { + "package_name": "io.flutter.plugins.firebase.tests" + } + }, + "oauth_client": [ + { + "client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "406099696497-1ugbsqv8nkfn788ep0k233e750aupb7u.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "io.flutter.plugins.firebaseDatabaseExample" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:406099696497:android:b7a347ba65ca3b803574d0", + "android_client_info": { + "package_name": "io.flutter.plugins.firebaseanalyticsexample" + } + }, + "oauth_client": [ + { + "client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "406099696497-1ugbsqv8nkfn788ep0k233e750aupb7u.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "io.flutter.plugins.firebaseDatabaseExample" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:406099696497:android:bc0b12b0605df8633574d0", + "android_client_info": { + "package_name": "io.flutter.plugins.firebasecoreexample" + } + }, + "oauth_client": [ + { + "client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "406099696497-1ugbsqv8nkfn788ep0k233e750aupb7u.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "io.flutter.plugins.firebaseDatabaseExample" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:406099696497:android:0f3f7bfe78b8b7103574d0", + "android_client_info": { + "package_name": "io.flutter.plugins.firebasecrashlyticsexample" + } + }, + "oauth_client": [ + { + "client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "406099696497-1ugbsqv8nkfn788ep0k233e750aupb7u.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "io.flutter.plugins.firebaseDatabaseExample" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:406099696497:android:2751af6868a69f073574d0", + "android_client_info": { + "package_name": "io.flutter.plugins.firebasestorageexample" + } + }, + "oauth_client": [ + { + "client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "406099696497-1ugbsqv8nkfn788ep0k233e750aupb7u.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "io.flutter.plugins.firebaseDatabaseExample" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:406099696497:android:3f845d318614da1e3574d0", + "android_client_info": { + "package_name": "io.flutter.plugins.firebase_ui_oauth_example" + } + }, + "oauth_client": [ + { + "client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "406099696497-1ugbsqv8nkfn788ep0k233e750aupb7u.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "io.flutter.plugins.firebaseDatabaseExample" + } + } + ] + } + } + } + ], + "configuration_version": "1" +} diff --git a/packages/firebase_ui_oauth/example/android/app/src/debug/AndroidManifest.xml b/packages/firebase_ui_oauth/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..b86746f98ce5 --- /dev/null +++ b/packages/firebase_ui_oauth/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/firebase_ui_oauth/example/android/app/src/main/AndroidManifest.xml b/packages/firebase_ui_oauth/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..531fc6e1fd99 --- /dev/null +++ b/packages/firebase_ui_oauth/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/packages/firebase_ui_oauth/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/firebase_ui_oauth/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000000..f74085f3f6a2 --- /dev/null +++ b/packages/firebase_ui_oauth/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/firebase_ui_oauth/example/android/app/src/main/res/drawable/launch_background.xml b/packages/firebase_ui_oauth/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..304732f88420 --- /dev/null +++ b/packages/firebase_ui_oauth/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/firebase_ui_oauth/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/firebase_ui_oauth/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000000..db77bb4b7b09 Binary files /dev/null and b/packages/firebase_ui_oauth/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/firebase_ui_oauth/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/firebase_ui_oauth/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000000..17987b79bb8a Binary files /dev/null and b/packages/firebase_ui_oauth/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/firebase_ui_oauth/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/firebase_ui_oauth/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000000..09d4391482be Binary files /dev/null and b/packages/firebase_ui_oauth/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/firebase_ui_oauth/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/firebase_ui_oauth/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000000..d5f1c8d34e7a Binary files /dev/null and b/packages/firebase_ui_oauth/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/firebase_ui_oauth/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/firebase_ui_oauth/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000000..4d6372eebdb2 Binary files /dev/null and b/packages/firebase_ui_oauth/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/firebase_ui_oauth/example/android/app/src/main/res/values-night/styles.xml b/packages/firebase_ui_oauth/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000000..3db14bb5391f --- /dev/null +++ b/packages/firebase_ui_oauth/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/firebase_ui_oauth/example/android/app/src/main/res/values/styles.xml b/packages/firebase_ui_oauth/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..d460d1e9215b --- /dev/null +++ b/packages/firebase_ui_oauth/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/firebase_ui_oauth/example/android/app/src/profile/AndroidManifest.xml b/packages/firebase_ui_oauth/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000000..b86746f98ce5 --- /dev/null +++ b/packages/firebase_ui_oauth/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/firebase_ui_oauth/example/android/build.gradle b/packages/firebase_ui_oauth/example/android/build.gradle new file mode 100644 index 000000000000..111a07244817 --- /dev/null +++ b/packages/firebase_ui_oauth/example/android/build.gradle @@ -0,0 +1,34 @@ +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + // START: FlutterFire Configuration + classpath 'com.google.gms:google-services:4.3.10' + // END: FlutterFire Configuration + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/firebase_ui_oauth/example/android/gradle.properties b/packages/firebase_ui_oauth/example/android/gradle.properties new file mode 100644 index 000000000000..94adc3a3f97a --- /dev/null +++ b/packages/firebase_ui_oauth/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/firebase_ui_oauth/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/firebase_ui_oauth/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..bc6a58afdda2 --- /dev/null +++ b/packages/firebase_ui_oauth/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/packages/firebase_ui_oauth/example/android/settings.gradle b/packages/firebase_ui_oauth/example/android/settings.gradle new file mode 100644 index 000000000000..44e62bcf06ae --- /dev/null +++ b/packages/firebase_ui_oauth/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/packages/firebase_ui_oauth/example/ios/.gitignore b/packages/firebase_ui_oauth/example/ios/.gitignore new file mode 100644 index 000000000000..7a7f9873ad7d --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/firebase_ui_oauth/example/ios/Flutter/AppFrameworkInfo.plist b/packages/firebase_ui_oauth/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..8d4492f977ad --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/packages/firebase_ui_oauth/example/ios/Flutter/Debug.xcconfig b/packages/firebase_ui_oauth/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..ec97fc6f3021 --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/firebase_ui_oauth/example/ios/Flutter/Release.xcconfig b/packages/firebase_ui_oauth/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..c4855bfe2000 --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/firebase_ui_oauth/example/ios/Podfile b/packages/firebase_ui_oauth/example/ios/Podfile new file mode 100644 index 000000000000..1e8c3c90a55e --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/firebase_ui_oauth/example/ios/Runner.xcodeproj/project.pbxproj b/packages/firebase_ui_oauth/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..2efd9cce3f2e --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,484 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ZPF26SRXG5; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.flutterfireUiOauthExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ZPF26SRXG5; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.flutterfireUiOauthExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ZPF26SRXG5; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.flutterfireUiOauthExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/firebase_ui_oauth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/firebase_ui_oauth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/firebase_ui_oauth/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/firebase_ui_oauth/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/firebase_ui_oauth/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/firebase_ui_oauth/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/firebase_ui_oauth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/firebase_ui_oauth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..c87d15a33520 --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/firebase_ui_oauth/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/firebase_ui_oauth/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..1d526a16ed0f --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/firebase_ui_oauth/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/firebase_ui_oauth/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/firebase_ui_oauth/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/firebase_ui_oauth/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/firebase_ui_oauth/example/ios/Runner/AppDelegate.swift b/packages/firebase_ui_oauth/example/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000000..70693e4a8c12 --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d36b1fab2d9d --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 000000000000..dc9ada4725e9 Binary files /dev/null and b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000000..28c6bf03016f Binary files /dev/null and b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000000..f091b6b0bca8 Binary files /dev/null and b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000000..4cde12118dda Binary files /dev/null and b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000000..d0ef06e7edb8 Binary files /dev/null and b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000000..dcdc2306c285 Binary files /dev/null and b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000000..c8f9ed8f5cee Binary files /dev/null and b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000000..75b2d164a5a9 Binary files /dev/null and b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000000..c4df70d39da7 Binary files /dev/null and b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000000..6a84f41e14e2 Binary files /dev/null and b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000000..d0e1f5853602 Binary files /dev/null and b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000000..0bedcf2fd467 --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000000..89c2725b70f1 --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/firebase_ui_oauth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..f2e259c7c939 --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Base.lproj/Main.storyboard b/packages/firebase_ui_oauth/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Info.plist b/packages/firebase_ui_oauth/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..b465263d1fe2 --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Runner/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Flutterfire Ui Oauth Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + firebase_ui_oauth_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/firebase_ui_oauth/example/ios/Runner/Runner-Bridging-Header.h b/packages/firebase_ui_oauth/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000000..308a2a560b42 --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packages/firebase_ui_oauth/example/ios/firebase_app_id_file.json b/packages/firebase_ui_oauth/example/ios/firebase_app_id_file.json new file mode 100644 index 000000000000..2867ab34b2ec --- /dev/null +++ b/packages/firebase_ui_oauth/example/ios/firebase_app_id_file.json @@ -0,0 +1,7 @@ +{ + "file_generated_by": "FlutterFire CLI", + "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", + "GOOGLE_APP_ID": "1:406099696497:ios:a6d9ef75e6da86563574d0", + "FIREBASE_PROJECT_ID": "flutterfire-e2e-tests", + "GCM_SENDER_ID": "406099696497" +} \ No newline at end of file diff --git a/packages/firebase_ui_oauth/example/lib/firebase_options.dart b/packages/firebase_ui_oauth/example/lib/firebase_options.dart new file mode 100644 index 000000000000..a09bac6945fb --- /dev/null +++ b/packages/firebase_ui_oauth/example/lib/firebase_options.dart @@ -0,0 +1,97 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyB7wZb2tO1-Fs6GbDADUSTs2Qs3w08Hovw', + appId: '1:406099696497:web:8639aa69bac133513574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + measurementId: 'G-X3614TQ65V', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw', + appId: '1:406099696497:android:899c6485cfce26c13574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyDooSUGSf63Ghq02_iIhtnmwMDs4HlWS6c', + appId: '1:406099696497:ios:24bb8dcaefc434a73574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + androidClientId: + '406099696497-17qn06u8a0dc717u8ul7s49ampk13lul.apps.googleusercontent.com', + iosClientId: + '406099696497-65v1b9ffv6sgfqngfjab5ol5qdikh2rm.apps.googleusercontent.com', + iosBundleId: 'io.flutter.plugins.firebaseUiExample', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyDooSUGSf63Ghq02_iIhtnmwMDs4HlWS6c', + appId: '1:406099696497:ios:24bb8dcaefc434a73574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + androidClientId: + '406099696497-17qn06u8a0dc717u8ul7s49ampk13lul.apps.googleusercontent.com', + iosClientId: + '406099696497-65v1b9ffv6sgfqngfjab5ol5qdikh2rm.apps.googleusercontent.com', + iosBundleId: 'io.flutter.plugins.firebaseUiExample', + ); +} diff --git a/packages/firebase_ui_oauth/example/lib/main.dart b/packages/firebase_ui_oauth/example/lib/main.dart new file mode 100644 index 000000000000..e417cdb95866 --- /dev/null +++ b/packages/firebase_ui_oauth/example/lib/main.dart @@ -0,0 +1,122 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; +import 'package:firebase_ui_oauth_apple/firebase_ui_oauth_apple.dart'; +import 'package:firebase_ui_oauth_facebook/firebase_ui_oauth_facebook.dart'; +import 'package:firebase_ui_oauth_google/firebase_ui_oauth_google.dart'; +import 'package:firebase_ui_oauth_twitter/firebase_ui_oauth_twitter.dart'; + +import 'src/settings.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + runApp(const OAuthProviderButtonExample()); +} + +class OAuthProviderButtonExample extends StatelessWidget { + const OAuthProviderButtonExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Settings(builder: (context, library, brightness, buttonVariant) { + if (library == DesignLibrary.material) { + return MaterialApp( + theme: ThemeData(brightness: brightness), + home: Scaffold( + body: Content( + designLibrary: library, + buttonVariant: buttonVariant, + ), + ), + ); + } else { + return CupertinoApp( + theme: CupertinoThemeData(brightness: brightness), + home: CupertinoPageScaffold( + child: Content( + designLibrary: library, + buttonVariant: buttonVariant, + ), + ), + ); + } + }); + } +} + +class Content extends StatefulWidget { + final DesignLibrary designLibrary; + final ButtonVariant buttonVariant; + + const Content({ + Key? key, + required this.designLibrary, + required this.buttonVariant, + }) : super(key: key); + + @override + State createState() => _ContentState(); +} + +class _ContentState extends State { + String? loadingProvider; + + void Function() _onTap(String provider) { + return () { + setState(() { + loadingProvider = provider; + }); + }; + } + + Widget _button(OAuthProvider provider, String label) { + final loadingIndicator = widget.designLibrary == DesignLibrary.material + ? const CircularProgressIndicator() + : const CupertinoActivityIndicator(); + + return OAuthProviderButtonBase( + provider: provider, + label: label, + onTap: _onTap(provider.providerId), + isLoading: loadingProvider == provider.providerId, + loadingIndicator: loadingIndicator, + overrideDefaultTapAction: true, + ); + } + + @override + Widget build(BuildContext context) { + return Center( + child: SizedBox( + width: 350, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _button(AppleProvider(), 'Sign in with Apple'), + _button( + FacebookProvider(clientId: '', redirectUri: ''), + 'Sign in with Facebook', + ), + _button( + GoogleProvider( + clientId: '', + redirectUri: '', + scopes: [], + ), + 'Sign in with Google', + ), + _button( + TwitterProvider( + apiKey: '', + apiSecretKey: '', + redirectUri: '', + ), + 'Sign in with Twitter', + ), + ], + ), + ), + ); + } +} diff --git a/packages/firebase_ui_oauth/example/lib/src/settings.dart b/packages/firebase_ui_oauth/example/lib/src/settings.dart new file mode 100644 index 000000000000..5a04825ee9cc --- /dev/null +++ b/packages/firebase_ui_oauth/example/lib/src/settings.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; + +enum DesignLibrary { + cupertino, + material, +} + +enum ButtonVariant { + icon, + full, +} + +class SettingsChip extends StatelessWidget { + final VoidCallback onTap; + final String label; + final bool isActive; + + const SettingsChip({ + Key? key, + required this.onTap, + required this.label, + required this.isActive, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTapDown: (_) => onTap(), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + child: Text( + label, + style: TextStyle(color: isActive ? Colors.white : Colors.black), + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: isActive ? Colors.blue : Colors.grey[200], + ), + ), + ), + ); + } +} + +class Settings extends StatefulWidget { + final DesignLibrary library; + final Brightness brightness; + final ButtonVariant buttonVariant; + + final Widget Function( + BuildContext context, + DesignLibrary library, + Brightness brightness, + ButtonVariant buttonVariant, + ) builder; + + const Settings({ + Key? key, + required this.builder, + this.brightness = Brightness.light, + this.library = DesignLibrary.material, + this.buttonVariant = ButtonVariant.full, + }) : super(key: key); + + @override + State createState() => _SettingsState(); +} + +class _SettingsState extends State { + late DesignLibrary library = widget.library; + late Brightness brightness = widget.brightness; + late ButtonVariant buttonVariant = widget.buttonVariant; + + VoidCallback setValue(Object value) { + return () { + setState(() { + if (value is DesignLibrary) { + library = value; + } else if (value is Brightness) { + brightness = value; + } else if (value is ButtonVariant) { + buttonVariant = value; + } + }); + }; + } + + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + Material( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + SettingsChip( + onTap: setValue(DesignLibrary.material), + label: 'Material', + isActive: library == DesignLibrary.material, + ), + SettingsChip( + onTap: setValue(DesignLibrary.cupertino), + label: 'Cupertino', + isActive: library == DesignLibrary.cupertino, + ), + SettingsChip( + onTap: setValue(Brightness.light), + label: 'Light mode', + isActive: brightness == Brightness.light, + ), + SettingsChip( + onTap: setValue(Brightness.dark), + label: 'Dark mode', + isActive: brightness == Brightness.dark, + ), + SettingsChip( + onTap: setValue(ButtonVariant.full), + label: 'Full button', + isActive: buttonVariant == ButtonVariant.full, + ), + SettingsChip( + onTap: setValue(ButtonVariant.icon), + label: 'Icon button', + isActive: buttonVariant == ButtonVariant.icon, + ), + ], + ), + ), + ), + Expanded( + child: widget.builder(context, library, brightness, buttonVariant), + ), + ], + ), + ); + } +} diff --git a/packages/firebase_ui_oauth/example/macos/.gitignore b/packages/firebase_ui_oauth/example/macos/.gitignore new file mode 100644 index 000000000000..d2fd3772308c --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/.gitignore @@ -0,0 +1,6 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/xcuserdata/ diff --git a/packages/firebase_ui_oauth/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/firebase_ui_oauth/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000000..4b81f9b2d200 --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/firebase_ui_oauth/example/macos/Flutter/Flutter-Release.xcconfig b/packages/firebase_ui_oauth/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000000..5caa9d1579e4 --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/firebase_ui_oauth/example/macos/Podfile b/packages/firebase_ui_oauth/example/macos/Podfile new file mode 100644 index 000000000000..22d9caad2e9d --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.12' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/firebase_ui_oauth/example/macos/Runner.xcodeproj/project.pbxproj b/packages/firebase_ui_oauth/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..1c6535df9a78 --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,647 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 2674AEA5ED29158EEDEDFCD3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB0C88E6F0927DC3B5F48BDC /* Pods_Runner.framework */; }; + 2F61F08126EBA08300C15711 /* GoogleSignInMacOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F61F08026EBA08300C15711 /* GoogleSignInMacOS.swift */; }; + 2F6439FA276CCE3900F6B03D /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2F6439F9276CCE3900F6B03D /* GoogleService-Info.plist */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 037A2C29BF0A48E4F7A15C1F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 1AF28F01426F339B8D1D36C3 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 2F61F08026EBA08300C15711 /* GoogleSignInMacOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleSignInMacOS.swift; sourceTree = ""; }; + 2F6439F9276CCE3900F6B03D /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* firebase_ui_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = firebase_ui_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + DB0C88E6F0927DC3B5F48BDC /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FC64926DAC277941BE8446E4 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2674AEA5ED29158EEDEDFCD3 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + F853E572004F0A83C4510D04 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* firebase_ui_example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 2F6439F9276CCE3900F6B03D /* GoogleService-Info.plist */, + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + 2F61F08026EBA08300C15711 /* GoogleSignInMacOS.swift */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + DB0C88E6F0927DC3B5F48BDC /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + F853E572004F0A83C4510D04 /* Pods */ = { + isa = PBXGroup; + children = ( + FC64926DAC277941BE8446E4 /* Pods-Runner.debug.xcconfig */, + 037A2C29BF0A48E4F7A15C1F /* Pods-Runner.release.xcconfig */, + 1AF28F01426F339B8D1D36C3 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + ECC87CC12E85159E86B8BE00 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + E0C2B307D722D81814E34EEE /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* firebase_ui_example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 0930; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + 2F6439FA276CCE3900F6B03D /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + E0C2B307D722D81814E34EEE /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + ECC87CC12E85159E86B8BE00 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 2F61F08126EBA08300C15711 /* GoogleSignInMacOS.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = YYX2P3XVJ7; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.12; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.12; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = YYX2P3XVJ7; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.12; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = YYX2P3XVJ7; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.12; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/firebase_ui_oauth/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/firebase_ui_oauth/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/firebase_ui_oauth/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/firebase_ui_oauth/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..2bc0d3ace819 --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/firebase_ui_oauth/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/firebase_ui_oauth/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/firebase_ui_oauth/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/firebase_ui_oauth/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/firebase_ui_oauth/example/macos/Runner/AppDelegate.swift b/packages/firebase_ui_oauth/example/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000000..5640f7be9f99 --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,17 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + + } + + override func application(_ application:NSApplication, open urls: [URL]) { + var data: [String: URL] = [:] + data["link"] = urls[0] + + NotificationCenter.default.post(Notification(name: Notification.Name(rawValue: "linkReceived"), object: nil, userInfo: data)); + } +} diff --git a/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..a2ec33f19f11 --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000000..3c4935a7ca84 Binary files /dev/null and b/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 000000000000..ed4cc1642168 Binary files /dev/null and b/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 000000000000..483be6138973 Binary files /dev/null and b/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 000000000000..bcbf36df2f2a Binary files /dev/null and b/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 000000000000..9c0a65286476 Binary files /dev/null and b/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 000000000000..e71a726136a4 Binary files /dev/null and b/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 000000000000..8a31fe2dd3f9 Binary files /dev/null and b/packages/firebase_ui_oauth/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/firebase_ui_oauth/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/firebase_ui_oauth/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000000..537341abf994 --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,339 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/firebase_ui_oauth/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/firebase_ui_oauth/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000000..27b0cb5e9a0f --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = firebase_ui_example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.flutterfireui.flutterfireUIExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2021 io.flutter.plugins.firebase_ui. All rights reserved. diff --git a/packages/firebase_ui_oauth/example/macos/Runner/Configs/Debug.xcconfig b/packages/firebase_ui_oauth/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000000..36b0fd9464f4 --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/firebase_ui_oauth/example/macos/Runner/Configs/Release.xcconfig b/packages/firebase_ui_oauth/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000000..dff4f49561c8 --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/firebase_ui_oauth/example/macos/Runner/Configs/Warnings.xcconfig b/packages/firebase_ui_oauth/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000000..42bcbf4780b1 --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/firebase_ui_oauth/example/macos/Runner/DebugProfile.entitlements b/packages/firebase_ui_oauth/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000000..dc506c80f025 --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,18 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.developer.applesignin + + Default + + com.apple.security.network.client + + + diff --git a/packages/firebase_ui_oauth/example/macos/Runner/GoogleService-Info.plist b/packages/firebase_ui_oauth/example/macos/Runner/GoogleService-Info.plist new file mode 100644 index 000000000000..c362e0039fa4 --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Runner/GoogleService-Info.plist @@ -0,0 +1,38 @@ + + + + + CLIENT_ID + 448618578101-mcfaooqa343utlqtd0rbsnhcpqc0r17h.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.448618578101-mcfaooqa343utlqtd0rbsnhcpqc0r17h + ANDROID_CLIENT_ID + 448618578101-a9p7bj5jlakabp22fo3cbkj7nsmag24e.apps.googleusercontent.com + API_KEY + AIzaSyAHAsf51D0A407EklG1bs-5wA7EbyfNFg0 + GCM_SENDER_ID + 448618578101 + PLIST_VERSION + 1 + BUNDLE_ID + io.flutter.plugins.flutterfireui.flutterfireUIExample + PROJECT_ID + react-native-firebase-testing + STORAGE_BUCKET + react-native-firebase-testing.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:448618578101:ios:9847ca0fdc2a4ee7ac3efc + DATABASE_URL + https://react-native-firebase-testing.firebaseio.com + + diff --git a/packages/firebase_ui_oauth/example/macos/Runner/GoogleSignInMacOS.swift b/packages/firebase_ui_oauth/example/macos/Runner/GoogleSignInMacOS.swift new file mode 100644 index 000000000000..1d0922a28e4f --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Runner/GoogleSignInMacOS.swift @@ -0,0 +1,115 @@ +// +// GoogleSignInMacOS.swift +// Runner +// +// Created by Andrei Lesnitsky on 10.09.21. +// + +import Foundation + +import Cocoa +import FlutterMacOS +import WebKit + +public class WebviewController: NSViewController, WKNavigationDelegate { + var width: CGFloat? + var height: CGFloat? + var redirectUri: String? + var result: FlutterResult? + var onComplete: ((String?) -> Void)? + + override public func loadView() { + let webView = WKWebView(frame: NSMakeRect(0, 0, width ?? 980, height ?? 720)) + + webView.navigationDelegate = self + webView.allowsBackForwardNavigationGestures = true + + view = webView + } + + func loadUrl(_ url: String) { + clearCookies() + + let url = URL(string: url)! + let request = URLRequest(url: url) + (view as! WKWebView).load(request) + } + + func clearCookies() { + HTTPCookieStorage.shared.removeCookies(since: Date.distantPast) + + WKWebsiteDataStore.default() + .fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in + records.forEach { record in + WKWebsiteDataStore.default() + .removeData(ofTypes: record.dataTypes, for: [record], completionHandler: {}) + } + } + } + + public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + guard let url = navigationAction.request.url else { + decisionHandler(.allow) + return + } + + let uriString = url.absoluteString + + print("Opening \(uriString)") + + if uriString.starts(with: redirectUri!) { + decisionHandler(.cancel) + onComplete!(uriString) + dismiss(self) + } else { + decisionHandler(.allow) + } + } + + override public func viewDidDisappear() { + onComplete!(nil) + } +} + +public class GoogleSignInMacOSPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel( + name: "google_sign_in_desktop", + binaryMessenger: registrar.messenger + ) + let instance = GoogleSignInMacOSPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "signIn": + let args = call.arguments as! NSDictionary + signIn( + clientId: args["clientId"] as! String, + redirectUri: args["redirectUri"] as! String, + result: result + ) + default: + result(FlutterMethodNotImplemented) + } + } + + func signIn(clientId: String, redirectUri: String, result: @escaping FlutterResult) { + let appWindow = NSApplication.shared.windows.first! + let webviewController = WebviewController() + + webviewController.redirectUri = redirectUri + webviewController.onComplete = { callbackUrl in + result(callbackUrl) + } + + webviewController + .loadUrl( + "https://accounts.google.com/o/oauth2/auth?client_id=\(clientId)&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fplus.login&immediate=false&response_type=token&redirect_uri=\(redirectUri)" + ) + + appWindow.contentViewController?.presentAsModalWindow(webviewController) + } +} diff --git a/packages/firebase_ui_oauth/example/macos/Runner/Info.plist b/packages/firebase_ui_oauth/example/macos/Runner/Info.plist new file mode 100644 index 000000000000..85792a6caefb --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Runner/Info.plist @@ -0,0 +1,46 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + CFBundleURLTypes + + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + com.googleusercontent.apps.448618578101-mcfaooqa343utlqtd0rbsnhcpqc0r17h + fb128693022464535 + ffire + + + + + diff --git a/packages/firebase_ui_oauth/example/macos/Runner/MainFlutterWindow.swift b/packages/firebase_ui_oauth/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000000..257ae0cd8164 --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,16 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + GoogleSignInMacOSPlugin.register(with: flutterViewController.registrar(forPlugin: "GoogleSignInMacOSPlugin")) + + super.awakeFromNib() + } +} diff --git a/packages/firebase_ui_oauth/example/macos/Runner/Release.entitlements b/packages/firebase_ui_oauth/example/macos/Runner/Release.entitlements new file mode 100644 index 000000000000..82b05aa351b7 --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/Runner/Release.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.developer.applesignin + + Default + + com.apple.security.network.client + + + diff --git a/packages/firebase_ui_oauth/example/macos/firebase_app_id_file.json b/packages/firebase_ui_oauth/example/macos/firebase_app_id_file.json new file mode 100644 index 000000000000..d8d33f9e600c --- /dev/null +++ b/packages/firebase_ui_oauth/example/macos/firebase_app_id_file.json @@ -0,0 +1,7 @@ +{ + "file_generated_by": "FlutterFire CLI", + "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", + "GOOGLE_APP_ID": "1:448618578101:ios:9847ca0fdc2a4ee7ac3efc", + "FIREBASE_PROJECT_ID": "react-native-firebase-testing", + "GCM_SENDER_ID": "448618578101" +} \ No newline at end of file diff --git a/packages/firebase_ui_oauth/example/pubspec.yaml b/packages/firebase_ui_oauth/example/pubspec.yaml new file mode 100644 index 000000000000..c006e02c62d5 --- /dev/null +++ b/packages/firebase_ui_oauth/example/pubspec.yaml @@ -0,0 +1,89 @@ +name: firebase_ui_oauth_example +description: A new Flutter project. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 1.0.0+1 + +environment: + sdk: '>=2.16.2 <3.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + firebase_auth: ^3.3.17 + firebase_core: ^1.16.0 + firebase_ui_oauth: ^1.0.0-dev.0 + firebase_ui_oauth_apple: ^1.0.0-dev.0 + firebase_ui_oauth_facebook: ^1.0.0-dev.0 + firebase_ui_oauth_google: ^1.0.0-dev.0 + firebase_ui_oauth_twitter: ^1.0.0-dev.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^1.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/firebase_ui_oauth/example/test/widget_test.dart b/packages/firebase_ui_oauth/example/test/widget_test.dart new file mode 100644 index 000000000000..570e0e4768dc --- /dev/null +++ b/packages/firebase_ui_oauth/example/test/widget_test.dart @@ -0,0 +1,8 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +void main() {} diff --git a/packages/firebase_ui_oauth/example/web/favicon.png b/packages/firebase_ui_oauth/example/web/favicon.png new file mode 100644 index 000000000000..8aaa46ac1ae2 Binary files /dev/null and b/packages/firebase_ui_oauth/example/web/favicon.png differ diff --git a/packages/firebase_ui_oauth/example/web/icons/Icon-192.png b/packages/firebase_ui_oauth/example/web/icons/Icon-192.png new file mode 100644 index 000000000000..b749bfef0747 Binary files /dev/null and b/packages/firebase_ui_oauth/example/web/icons/Icon-192.png differ diff --git a/packages/firebase_ui_oauth/example/web/icons/Icon-512.png b/packages/firebase_ui_oauth/example/web/icons/Icon-512.png new file mode 100644 index 000000000000..88cfd48dff11 Binary files /dev/null and b/packages/firebase_ui_oauth/example/web/icons/Icon-512.png differ diff --git a/packages/firebase_ui_oauth/example/web/icons/Icon-maskable-192.png b/packages/firebase_ui_oauth/example/web/icons/Icon-maskable-192.png new file mode 100644 index 000000000000..eb9b4d76e525 Binary files /dev/null and b/packages/firebase_ui_oauth/example/web/icons/Icon-maskable-192.png differ diff --git a/packages/firebase_ui_oauth/example/web/icons/Icon-maskable-512.png b/packages/firebase_ui_oauth/example/web/icons/Icon-maskable-512.png new file mode 100644 index 000000000000..d69c56691fbd Binary files /dev/null and b/packages/firebase_ui_oauth/example/web/icons/Icon-maskable-512.png differ diff --git a/packages/firebase_ui_oauth/example/web/index.html b/packages/firebase_ui_oauth/example/web/index.html new file mode 100644 index 000000000000..31f6836ace36 --- /dev/null +++ b/packages/firebase_ui_oauth/example/web/index.html @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + firebase_ui_oauth_example + + + + + + + diff --git a/packages/firebase_ui_oauth/example/web/manifest.json b/packages/firebase_ui_oauth/example/web/manifest.json new file mode 100644 index 000000000000..a5fc09e3cc7d --- /dev/null +++ b/packages/firebase_ui_oauth/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "firebase_ui_oauth_example", + "short_name": "firebase_ui_oauth_example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/packages/firebase_ui_oauth/example/windows/.gitignore b/packages/firebase_ui_oauth/example/windows/.gitignore new file mode 100644 index 000000000000..d492d0d98c8f --- /dev/null +++ b/packages/firebase_ui_oauth/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/packages/firebase_ui_oauth/example/windows/CMakeLists.txt b/packages/firebase_ui_oauth/example/windows/CMakeLists.txt new file mode 100644 index 000000000000..e4d9b1fd3455 --- /dev/null +++ b/packages/firebase_ui_oauth/example/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.14) +project(firebase_ui_oauth_example LANGUAGES CXX) + +set(BINARY_NAME "firebase_ui_oauth_example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() + +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/packages/firebase_ui_oauth/example/windows/flutter/CMakeLists.txt b/packages/firebase_ui_oauth/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000000..b2e4bd8d658b --- /dev/null +++ b/packages/firebase_ui_oauth/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,103 @@ +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/firebase_ui_oauth/example/windows/flutter/generated_plugin_registrant.cc b/packages/firebase_ui_oauth/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000000..45e2647eb8a9 --- /dev/null +++ b/packages/firebase_ui_oauth/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + DesktopWebviewAuthPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopWebviewAuthPlugin")); +} diff --git a/packages/firebase_ui_oauth/example/windows/flutter/generated_plugin_registrant.h b/packages/firebase_ui_oauth/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000000..dc139d85a931 --- /dev/null +++ b/packages/firebase_ui_oauth/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/firebase_ui_oauth/example/windows/flutter/generated_plugins.cmake b/packages/firebase_ui_oauth/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..67972319482d --- /dev/null +++ b/packages/firebase_ui_oauth/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + desktop_webview_auth +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/firebase_ui_oauth/example/windows/runner/CMakeLists.txt b/packages/firebase_ui_oauth/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000000..de2d8916b72b --- /dev/null +++ b/packages/firebase_ui_oauth/example/windows/runner/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +apply_standard_settings(${BINARY_NAME}) +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/firebase_ui_oauth/example/windows/runner/Runner.rc b/packages/firebase_ui_oauth/example/windows/runner/Runner.rc new file mode 100644 index 000000000000..e9168e69883b --- /dev/null +++ b/packages/firebase_ui_oauth/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "io.flutter.plugins" "\0" + VALUE "FileDescription", "firebase_ui_oauth_example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "firebase_ui_oauth_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 io.flutter.plugins. All rights reserved." "\0" + VALUE "OriginalFilename", "firebase_ui_oauth_example.exe" "\0" + VALUE "ProductName", "firebase_ui_oauth_example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/packages/firebase_ui_oauth/example/windows/runner/flutter_window.cpp b/packages/firebase_ui_oauth/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000000..b43b9095ea3a --- /dev/null +++ b/packages/firebase_ui_oauth/example/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/firebase_ui_oauth/example/windows/runner/flutter_window.h b/packages/firebase_ui_oauth/example/windows/runner/flutter_window.h new file mode 100644 index 000000000000..6da0652f05f2 --- /dev/null +++ b/packages/firebase_ui_oauth/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/firebase_ui_oauth/example/windows/runner/main.cpp b/packages/firebase_ui_oauth/example/windows/runner/main.cpp new file mode 100644 index 000000000000..5a75cf26042b --- /dev/null +++ b/packages/firebase_ui_oauth/example/windows/runner/main.cpp @@ -0,0 +1,42 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"firebase_ui_oauth_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/firebase_ui_oauth/example/windows/runner/resource.h b/packages/firebase_ui_oauth/example/windows/runner/resource.h new file mode 100644 index 000000000000..d5d958dc4257 --- /dev/null +++ b/packages/firebase_ui_oauth/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/firebase_ui_oauth/example/windows/runner/resources/app_icon.ico b/packages/firebase_ui_oauth/example/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000000..c04e20caf637 Binary files /dev/null and b/packages/firebase_ui_oauth/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/firebase_ui_oauth/example/windows/runner/runner.exe.manifest b/packages/firebase_ui_oauth/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000000..c977c4a42589 --- /dev/null +++ b/packages/firebase_ui_oauth/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/firebase_ui_oauth/example/windows/runner/utils.cpp b/packages/firebase_ui_oauth/example/windows/runner/utils.cpp new file mode 100644 index 000000000000..afa363b236e3 --- /dev/null +++ b/packages/firebase_ui_oauth/example/windows/runner/utils.cpp @@ -0,0 +1,63 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE* unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = + ::WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, + nullptr, 0, nullptr, nullptr); + if (target_length == 0) { + return std::string(); + } + std::string utf8_string; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/packages/firebase_ui_oauth/example/windows/runner/utils.h b/packages/firebase_ui_oauth/example/windows/runner/utils.h new file mode 100644 index 000000000000..3879d5475579 --- /dev/null +++ b/packages/firebase_ui_oauth/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/firebase_ui_oauth/example/windows/runner/win32_window.cpp b/packages/firebase_ui_oauth/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000000..44091b3f3c91 --- /dev/null +++ b/packages/firebase_ui_oauth/example/windows/runner/win32_window.cpp @@ -0,0 +1,237 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { ++g_active_window_count; } + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { return window_handle_; } + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/packages/firebase_ui_oauth/example/windows/runner/win32_window.h b/packages/firebase_ui_oauth/example/windows/runner/win32_window.h new file mode 100644 index 000000000000..4ae64a12b465 --- /dev/null +++ b/packages/firebase_ui_oauth/example/windows/runner/win32_window.h @@ -0,0 +1,95 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/firebase_ui_oauth/lib/firebase_ui_oauth.dart b/packages/firebase_ui_oauth/lib/firebase_ui_oauth.dart new file mode 100644 index 000000000000..904dc8a2f03d --- /dev/null +++ b/packages/firebase_ui_oauth/lib/firebase_ui_oauth.dart @@ -0,0 +1,13 @@ +export 'package:firebase_auth/firebase_auth.dart' show OAuthCredential; +export 'package:desktop_webview_auth/desktop_webview_auth.dart' + show AuthResult, ProviderArgs; +export 'package:desktop_webview_auth/google.dart'; +export 'package:desktop_webview_auth/facebook.dart'; +export 'package:desktop_webview_auth/twitter.dart'; + +export './src/oauth_provider.dart'; +export './src/oauth_provider_button_base.dart'; +export './src/oauth_provider_button_style.dart'; + +export 'package:firebase_ui_auth/firebase_ui_auth.dart' + show AuthAction, AuthCancelledException; diff --git a/packages/firebase_ui_oauth/lib/src/oauth_provider.dart b/packages/firebase_ui_oauth/lib/src/oauth_provider.dart new file mode 100644 index 000000000000..f5f6fa87e6c6 --- /dev/null +++ b/packages/firebase_ui_oauth/lib/src/oauth_provider.dart @@ -0,0 +1,37 @@ +import 'package:flutter/foundation.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; + +import 'platform_oauth_sign_in.dart' + if (dart.library.html) 'platform_oauth_sign_in_web.dart'; + +/// A listener of the [OAuthProvider]. +/// See also: +/// * [OAuthFlow] +abstract class OAuthListener extends AuthListener {} + +/// {@template ui.oauth.oauth_provider} +/// An [AuthProvider] that allows to authenticate using OAuth. +/// {@endtemplate} +abstract class OAuthProvider + extends AuthProvider + with PlatformSignInMixin { + @override + late OAuthListener authListener; + + /// {@macro ui.oauth.themed_oauth_provider_button_style} + ThemedOAuthProviderButtonStyle get style; + + String get defaultRedirectUri => + 'https://${auth.app.options.projectId}.firebaseapp.com/__/auth/handler'; + + void signIn(TargetPlatform platform, AuthAction action) { + authListener.onBeforeSignIn(); + platformSignIn(platform, action); + } + + @override + void platformSignIn(TargetPlatform platform, AuthAction action); + + Future logOutProvider(); +} diff --git a/packages/firebase_ui_oauth/lib/src/oauth_provider_button_base.dart b/packages/firebase_ui_oauth/lib/src/oauth_provider_button_base.dart new file mode 100644 index 000000000000..c38785c8ef58 --- /dev/null +++ b/packages/firebase_ui_oauth/lib/src/oauth_provider_button_base.dart @@ -0,0 +1,493 @@ +import 'package:firebase_auth/firebase_auth.dart' hide OAuthProvider; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; + +/// {@template ui.oauth.oauth_provider_button_base.error_builder} +/// A builder that is invoked to build a widget that indicates an error. +/// {@endtemplate} +typedef ErrorBuilder = Widget Function(Exception e); + +/// {@template ui.oauth.oauth_provider_button_base.different_providers_found_callback} +/// A callback that is being called when there are different oauth providers +/// associated with the same email. +/// {@endtemplate} +typedef DifferentProvidersFoundCallback = void Function( + List providers, + AuthCredential? credential, +); + +/// {@template ui.oauth.oauth_provider_button_base.signed_in_callback} +/// A callback that is being called when the user signs in. +/// {@endtemplate} +typedef SignedInCallback = void Function(UserCredential credential); + +/// {@template ui.oauth.oauth_provider_button_base} +/// A base widget that allows authentication using OAuth providers. +/// {@endtemplate} +class OAuthProviderButtonBase extends StatefulWidget { + /// {@template ui.oauth.oauth_provider_button.label} + /// Text that would be displayed on the button. + /// {@endtemplate} + final String label; + + /// {@template ui.oauth.oauth_provider_button.size} + /// Font size of the button label. Padding of the buttons is calculated to + /// meet the provider design requirements. + /// {@endtemplate} + final double size; + final double _padding; + + /// {@template ui.oauth.oauth_provider_button.loading_indicator} + /// A widget that would be displayed while the button is in loading state. + /// {@endtemplate} + final Widget loadingIndicator; + + /// {@macro ui.auth.auth_action} + final AuthAction? action; + + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + + /// {@template ui.oauth.oauth_provider_button.on_tap} + /// A callback that is being called when the button is tapped. + /// {@endtemplate} + final void Function()? onTap; + + /// {@template ui.oauth.oauth_provider} + final OAuthProvider provider; + + /// {@macro ui.oauth.oauth_provider_button_base.different_providers_found_callback} + final DifferentProvidersFoundCallback? onDifferentProvidersFound; + + /// {@macro ui.oauth.oauth_provider_button_base.signed_in_callback} + final SignedInCallback? onSignedIn; + + /// {@macro ui.oauth.oauth_provider_button_base.on_error} + /// A callback that is being called when an error occurs. + /// {@endtemplate} + final void Function(Exception exception)? onError; + + /// {@macro ui.oauth.oauth_provider_button_base.on_cancelled} + /// A callback that is being called when the user cancels the sign in. + /// {@endtemplate} + final VoidCallback? onCancelled; + + /// {@template ui.oauth.oauth_provider_button_base.override_default_tap_action} + /// Indicates whether the default tap action should be overridden. + /// If set to `true`, authentcation logic is not executed and should be + /// handled by the user. + /// {@endtemplate} + final bool overrideDefaultTapAction; + + /// {@template ui.oauth.oauth_provider_button_base.is_loading} + /// Indicates whether the sign in process is in progress. + /// {@endtemplate} + final bool isLoading; + + const OAuthProviderButtonBase({ + Key? key, + + /// {@macro ui.oauth.oauth_provider_button.label} + required this.label, + + /// {@macro ui.oauth.oauth_provider_button.loading_indicator} + required this.loadingIndicator, + + /// {@macro ui.oauth.oauth_provider} + required this.provider, + + /// {@macro ui.oauth.oauth_provider_button.on_tap} + this.onTap, + + /// {@macro ui.auth.auth_controller.auth} + this.auth, + + /// {@macro ui.auth.auth_action} + this.action, + + /// {@macro ui.oauth.oauth_provider_button_base.different_providers_found_callback} + this.onDifferentProvidersFound, + + /// {@macro ui.oauth.oauth_provider_button_base.signed_in_callback} + this.onSignedIn, + + /// {@macro ui.oauth.oauth_provider_button_base.override_default_tap_action} + this.overrideDefaultTapAction = false, + + /// {@macro ui.oauth.oauth_provider_button.size} + this.size = 19, + + /// {@macro ui.oauth.oauth_provider_button_base.is_loading} + this.isLoading = false, + + /// {@macro ui.oauth.oauth_provider_button_base.on_error} + this.onError, + + /// {@macro ui.oauth.oauth_provider_button_base.on_cancelled} + this.onCancelled, + }) : assert(!overrideDefaultTapAction || onTap != null), + _padding = size * 1.33 / 2, + super(key: key); + + @override + State createState() => + _OAuthProviderButtonBaseState(); +} + +class _OAuthProviderButtonBaseState extends State + implements OAuthListener { + double get _height => widget.size + widget._padding * 2; + late bool isLoading = widget.isLoading; + + void _signIn() { + final platform = Theme.of(context).platform; + + if (widget.overrideDefaultTapAction) { + widget.onTap!.call(); + } else { + provider.signIn(platform, widget.action ?? AuthAction.signIn); + } + } + + Widget _buildCupertino( + BuildContext context, + OAuthProviderButtonStyle style, + double margin, + double borderRadius, + double iconBorderRadius, + ) { + final br = BorderRadius.circular(borderRadius); + + return Padding( + padding: EdgeInsets.all(margin), + child: CupertinoTheme( + data: CupertinoThemeData( + primaryColor: widget.label.isEmpty + ? style.iconBackgroundColor + : style.backgroundColor, + ), + child: Material( + elevation: 1, + borderRadius: br, + child: CupertinoButton.filled( + padding: const EdgeInsets.all(0), + borderRadius: br, + onPressed: _signIn, + child: ClipRRect( + borderRadius: br, + child: _ButtonContent( + assetsPackage: style.assetsPackage, + iconSrc: style.iconSrc, + isLoading: isLoading, + label: widget.label, + height: _height, + fontSize: widget.size, + textColor: style.color, + loadingIndicator: widget.loadingIndicator, + borderRadius: br, + borderColor: style.borderColor, + iconBackgroundColor: style.iconBackgroundColor, + ), + ), + ), + ), + ), + ); + } + + Widget _buildMaterial( + BuildContext context, + OAuthProviderButtonStyle style, + double margin, + double borderRadius, + double iconBorderRadius, + ) { + final br = BorderRadius.circular(borderRadius); + + return _ButtonContainer( + borderRadius: br, + color: widget.label.isEmpty + ? style.iconBackgroundColor + : style.backgroundColor, + height: _height, + width: widget.label.isEmpty ? _height : null, + margin: margin, + child: Stack( + children: [ + _ButtonContent( + assetsPackage: style.assetsPackage, + iconSrc: style.iconSrc, + isLoading: isLoading, + label: widget.label, + height: _height, + fontSize: widget.size, + textColor: style.color, + loadingIndicator: widget.loadingIndicator, + borderRadius: br, + borderColor: style.borderColor, + iconBackgroundColor: style.iconBackgroundColor, + ), + _MaterialForeground(onTap: () => _signIn()), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final isCupertino = CupertinoUserInterfaceLevel.maybeOf(context) != null; + final brightness = + CupertinoTheme.of(context).brightness ?? Theme.of(context).brightness; + + final style = provider.style.withBrightness(brightness); + final margin = (widget.size + widget._padding * 2) / 10; + final borderRadius = widget.size / 3; + const borderWidth = 1.0; + final iconBorderRadius = borderRadius - borderWidth; + + if (isCupertino) { + return _buildCupertino( + context, + style, + margin, + borderRadius, + iconBorderRadius, + ); + } else { + return _buildMaterial( + context, + style, + margin, + borderRadius, + iconBorderRadius, + ); + } + } + + @override + FirebaseAuth get auth => widget.auth ?? FirebaseAuth.instance; + + @override + void onCredentialReceived(AuthCredential credential) { + setState(() { + isLoading = true; + }); + } + + @override + void onMFARequired(MultiFactorResolver resolver) { + startMFAVerification(context: context, resolver: resolver); + } + + @override + void onBeforeProvidersForEmailFetch() { + setState(() { + isLoading = true; + }); + } + + @override + void onBeforeSignIn() { + setState(() { + isLoading = true; + }); + } + + @override + void onCredentialLinked(AuthCredential credential) { + setState(() { + isLoading = false; + }); + } + + @override + void onDifferentProvidersFound( + String email, + List providers, + AuthCredential? credential, + ) { + widget.onDifferentProvidersFound?.call(providers, credential); + } + + @override + void onSignedIn(UserCredential credential) { + setState(() { + isLoading = false; + }); + + widget.onSignedIn?.call(credential); + } + + @override + void onError(Object error) { + try { + defaultOnAuthError(provider, error); + } on Exception catch (err) { + widget.onError?.call(err); + } + } + + @override + void onCanceled() { + setState(() { + isLoading = false; + }); + + widget.onCancelled?.call(); + } + + @override + void didUpdateWidget(covariant OAuthProviderButtonBase oldWidget) { + if (oldWidget.isLoading != widget.isLoading) { + isLoading = widget.isLoading; + } + + super.didUpdateWidget(oldWidget); + } + + @override + OAuthProvider get provider => widget.provider; +} + +class _ButtonContent extends StatelessWidget { + final double height; + final String iconSrc; + final String assetsPackage; + final String label; + final bool isLoading; + final Color textColor; + final double fontSize; + final Widget loadingIndicator; + final BorderRadius borderRadius; + final Color borderColor; + final Color iconBackgroundColor; + + const _ButtonContent({ + Key? key, + required this.height, + required this.iconSrc, + required this.assetsPackage, + required this.label, + required this.isLoading, + required this.fontSize, + required this.textColor, + required this.loadingIndicator, + required this.borderRadius, + required this.borderColor, + required this.iconBackgroundColor, + }) : super(key: key); + + Widget _buildLoadingIndicator() { + return SizedBox( + height: fontSize, + width: fontSize, + child: loadingIndicator, + ); + } + + @override + Widget build(BuildContext context) { + Widget child = SvgPicture.string( + iconSrc, + width: height, + height: height, + ); + + if (label.isNotEmpty) { + final content = isLoading + ? _buildLoadingIndicator() + : Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + height: 1.1, + color: textColor, + fontSize: fontSize, + ), + ); + + final isCupertino = CupertinoUserInterfaceLevel.maybeOf(context) != null; + final topMargin = isCupertino ? (height - fontSize) / 2 : 0.0; + + child = Stack( + children: [ + child, + Align( + alignment: AlignmentDirectional.center, + child: Padding( + padding: EdgeInsets.only(top: topMargin), + child: content, + ), + ), + ], + ); + } else if (isLoading) { + child = _buildLoadingIndicator(); + } + + return child; + } +} + +class _MaterialForeground extends StatelessWidget { + final VoidCallback onTap; + + const _MaterialForeground({ + Key? key, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Positioned.fill( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + ), + ), + ); + } +} + +class _ButtonContainer extends StatelessWidget { + final double margin; + final double height; + final double? width; + final Color color; + final BorderRadius borderRadius; + final Widget child; + + const _ButtonContainer({ + Key? key, + required this.margin, + required this.height, + required this.color, + required this.borderRadius, + required this.child, + this.width, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(margin), + child: SizedBox( + height: height, + width: width, + child: Material( + color: color, + elevation: 1, + shape: RoundedRectangleBorder(borderRadius: borderRadius), + child: ClipRRect( + borderRadius: borderRadius, + child: Center(child: child), + ), + ), + ), + ); + } +} diff --git a/packages/firebase_ui_oauth/lib/src/oauth_provider_button_style.dart b/packages/firebase_ui_oauth/lib/src/oauth_provider_button_style.dart new file mode 100644 index 000000000000..f01ab565e459 --- /dev/null +++ b/packages/firebase_ui_oauth/lib/src/oauth_provider_button_style.dart @@ -0,0 +1,80 @@ +import 'package:flutter/services.dart'; + +/// {@template ui.oauth.themed_value} +/// An object that is used to resolve the value base on the current theme +/// brightness. +/// {@endtemplate} +class ThemedValue { + /// The value that should be used to when the dark theme is used. + final T dark; + + /// The value that should be used to when the light theme is used. + final T light; + + const ThemedValue(this.dark, this.light); + + T getValue(Brightness brightness) { + switch (brightness) { + case Brightness.dark: + return dark; + case Brightness.light: + return light; + } + } +} + +class ThemedColor extends ThemedValue { + const ThemedColor(Color dark, Color light) : super(dark, light); +} + +class ThemedIconSrc extends ThemedValue { + const ThemedIconSrc( + String dark, + String light, + ) : super(dark, light); +} + +/// {@template ui.oauth.themed_oauth_provider_button_style} +/// An object that is being used to resolve a style of the button. +/// {@endtemplate} +abstract class ThemedOAuthProviderButtonStyle { + ThemedIconSrc get iconSrc; + ThemedColor get backgroundColor; + ThemedColor get color; + ThemedColor get iconBackgroundColor => backgroundColor; + ThemedColor get borderColor => backgroundColor; + double get iconPadding => 0; + String get assetsPackage; + + /// {@macro ui.oauth.themed_oauth_provider_button_style} + const ThemedOAuthProviderButtonStyle(); + + OAuthProviderButtonStyle withBrightness(Brightness brightness) { + return OAuthProviderButtonStyle( + iconSrc: iconSrc.getValue(brightness), + backgroundColor: backgroundColor.getValue(brightness), + color: color.getValue(brightness), + borderColor: borderColor.getValue(brightness), + assetsPackage: assetsPackage, + iconBackgroundColor: iconBackgroundColor.getValue(brightness), + ); + } +} + +class OAuthProviderButtonStyle { + final String iconSrc; + final Color backgroundColor; + final Color color; + final Color borderColor; + final String assetsPackage; + final Color iconBackgroundColor; + + OAuthProviderButtonStyle({ + required this.iconSrc, + required this.backgroundColor, + required this.color, + required this.borderColor, + required this.assetsPackage, + required this.iconBackgroundColor, + }); +} diff --git a/packages/firebase_ui_oauth/lib/src/platform_oauth_sign_in.dart b/packages/firebase_ui_oauth/lib/src/platform_oauth_sign_in.dart new file mode 100644 index 000000000000..483935097b21 --- /dev/null +++ b/packages/firebase_ui_oauth/lib/src/platform_oauth_sign_in.dart @@ -0,0 +1,53 @@ +import 'package:desktop_webview_auth/desktop_webview_auth.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +import 'oauth_provider.dart'; + +/// {@template ui.oauth.platform_sign_in_mixin} +/// A helper mixin that implements the platform-specific sign-in logic. +/// {@endtemplate} +mixin PlatformSignInMixin { + OAuthListener get authListener; + ProviderArgs get desktopSignInArgs; + dynamic get firebaseAuthProvider; + + /// Creates [OAuthCredential] based on [AuthResult]. + OAuthCredential fromDesktopAuthResult(AuthResult result); + + /// {@macro ui.auth.auth_provider.on_credential_received} + void onCredentialReceived(OAuthCredential credential, AuthAction action); + + /// {@template ui.oauth.platform_sign_in_mixin.platform_sign_in} + /// Redirects the flow to the [mobileSignIn] or [desktopSignIn] based + /// on current platform. + /// {@endtemplate} + void platformSignIn(TargetPlatform platform, AuthAction action) { + if (platform == TargetPlatform.android || platform == TargetPlatform.iOS) { + mobileSignIn(action); + } else { + desktopSignIn(action); + } + } + + /// Handles authentication logic on desktop platforms + void desktopSignIn(AuthAction action) { + DesktopWebviewAuth.signIn(desktopSignInArgs).then((value) { + if (value == null) throw AuthCancelledException(); + + final oauthCredential = fromDesktopAuthResult(value); + onCredentialReceived(oauthCredential, action); + }).catchError((err) { + if (err is AuthCancelledException) { + authListener.onCanceled(); + return; + } + + authListener.onError(err); + }); + } + + /// Handles authentication logic on mobile platforms. + void mobileSignIn(AuthAction action); +} diff --git a/packages/firebase_ui_oauth/lib/src/platform_oauth_sign_in_web.dart b/packages/firebase_ui_oauth/lib/src/platform_oauth_sign_in_web.dart new file mode 100644 index 000000000000..ac0e84c19909 --- /dev/null +++ b/packages/firebase_ui_oauth/lib/src/platform_oauth_sign_in_web.dart @@ -0,0 +1,27 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; + +import 'oauth_provider.dart'; + +/// {@macro ui.oauth.platform_sign_in_mixin} +mixin PlatformSignInMixin { + FirebaseAuth get auth; + OAuthListener get authListener; + dynamic get firebaseAuthProvider; + + /// {@macro ui.oauth.platform_sign_in_mixin.platform_sign_in} + void platformSignIn(TargetPlatform platform, AuthAction action) { + Future credentialFuture; + + if (action == AuthAction.link) { + credentialFuture = auth.currentUser!.linkWithPopup(firebaseAuthProvider); + } else { + credentialFuture = auth.signInWithPopup(firebaseAuthProvider); + } + + credentialFuture + .then(authListener.onSignedIn) + .catchError(authListener.onError); + } +} diff --git a/packages/firebase_ui_oauth/pubspec.yaml b/packages/firebase_ui_oauth/pubspec.yaml new file mode 100644 index 000000000000..735271a3509d --- /dev/null +++ b/packages/firebase_ui_oauth/pubspec.yaml @@ -0,0 +1,64 @@ +name: firebase_ui_oauth +description: Firebase UI widgets for authentication & OAuth +version: 1.0.0-dev.0 +homepage: https://github.com/firebase/flutterfire/tree/master/packages/firebase_ui_oauth + +environment: + sdk: '>=2.17.6 <3.0.0' + flutter: '>=1.17.0' + +dependencies: + desktop_webview_auth: ^0.0.9 + firebase_auth: ^3.6.2 + firebase_ui_auth: ^1.0.0-dev.0 + flutter_svg: ^1.1.2 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + firebase_ui_oauth_google: ^1.0.0-dev.0 +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +false_secrets: + - '/example/**/google-services.json' + - '/example/**/firebase_options.dart' + - '/example/**/GoogleService-Info.plist' + - '/example/lib/config.dart' + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/firebase_ui_oauth/test/flutterfire_ui_oauth_test.dart b/packages/firebase_ui_oauth/test/flutterfire_ui_oauth_test.dart new file mode 100644 index 000000000000..7135cb153f8b --- /dev/null +++ b/packages/firebase_ui_oauth/test/flutterfire_ui_oauth_test.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; +import 'package:firebase_ui_oauth_google/firebase_ui_oauth_google.dart'; + +class FakeAssetBundle extends Fake implements AssetBundle { + final String svgStr = ''''''; + + @override + Future loadString(String key, {bool cache = true}) async => svgStr; +} + +class FakeOAuthProvider extends OAuthProvider { + @override + ProviderArgs get desktopSignInArgs => throw UnimplementedError(); + + @override + get firebaseAuthProvider => throw UnimplementedError(); + + @override + OAuthCredential fromDesktopAuthResult(AuthResult result) { + throw UnimplementedError(); + } + + @override + Future logOutProvider() { + throw UnimplementedError(); + } + + @override + void mobileSignIn(AuthAction action) {} + + @override + String get providerId => 'fake'; + + @override + ThemedOAuthProviderButtonStyle get style => const GoogleProviderButtonStyle(); + + @override + bool supportsPlatform(TargetPlatform platform) { + return true; + } +} + +void main() { + final provider = FakeOAuthProvider(); + + const style = GoogleProviderButtonStyle(); + late Widget button; + + Widget renderMaterialButton([Brightness brightness = Brightness.dark]) { + button = OAuthProviderButtonBase( + provider: provider, + label: 'Sign in with Google', + loadingIndicator: const CircularProgressIndicator(), + ); + + return DefaultAssetBundle( + bundle: FakeAssetBundle(), + child: MaterialApp( + theme: ThemeData(brightness: brightness), + home: Scaffold(body: button), + ), + ); + } + + group('OAuthProviderButton', () { + testWidgets('renders label', (tester) async { + await tester.pumpWidget(renderMaterialButton()); + + final textFinder = find.text('Sign in with Google'); + expect(textFinder, findsOneWidget); + }); + + testWidgets('applies background color from style', (tester) async { + await tester.pumpWidget(renderMaterialButton()); + + final expectedColor = style.backgroundColor.getValue(Brightness.dark); + + final containerFinder = find.byWidgetPredicate((widget) { + return widget is Material && widget.color!.value == expectedColor.value; + }); + + expect(containerFinder, findsOneWidget); + }); + + testWidgets('applies text color from style', (tester) async { + await tester.pumpWidget(renderMaterialButton()); + + final textFinder = find.byWidgetPredicate( + (widget) => + widget is Text && + widget.style!.color!.value == + style.color.getValue(Brightness.dark).value, + ); + + expect(textFinder, findsOneWidget); + }); + + testWidgets('applies dark theme background color from style', + (tester) async { + await tester.pumpWidget(renderMaterialButton(Brightness.dark)); + + final containerFinder = find.byWidgetPredicate( + (widget) => + widget is Material && + widget.color.toString() == + style.backgroundColor.getValue(Brightness.dark).toString(), + ); + + expect(containerFinder, findsOneWidget); + }); + + testWidgets('applies dark theme text color from style', (tester) async { + await tester.pumpWidget(renderMaterialButton(Brightness.dark)); + + final textFinder = find.byWidgetPredicate( + (widget) => + widget is Text && + widget.style!.color.toString() == + style.color.getValue(Brightness.dark).toString(), + ); + + expect(textFinder, findsOneWidget); + }); + + testWidgets('renders an icon', (tester) async { + await tester.pumpWidget(renderMaterialButton()); + + final iconFinder = find.byWidgetPredicate( + (widget) => widget is SvgPicture, + ); + + expect(iconFinder, findsOneWidget); + }); + }); +} diff --git a/packages/firebase_ui_oauth_apple/.gitignore b/packages/firebase_ui_oauth_apple/.gitignore new file mode 100644 index 000000000000..96486fd93024 --- /dev/null +++ b/packages/firebase_ui_oauth_apple/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/firebase_ui_oauth_apple/.metadata b/packages/firebase_ui_oauth_apple/.metadata new file mode 100644 index 000000000000..e7011f64f39d --- /dev/null +++ b/packages/firebase_ui_oauth_apple/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + channel: stable + +project_type: package diff --git a/packages/firebase_ui_oauth_apple/CHANGELOG.md b/packages/firebase_ui_oauth_apple/CHANGELOG.md new file mode 100644 index 000000000000..a5c39d636c33 --- /dev/null +++ b/packages/firebase_ui_oauth_apple/CHANGELOG.md @@ -0,0 +1,5 @@ +## 1.0.0-dev.0 + +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/firebase_ui_oauth_apple/LICENSE b/packages/firebase_ui_oauth_apple/LICENSE new file mode 100644 index 000000000000..5b8ff6261110 --- /dev/null +++ b/packages/firebase_ui_oauth_apple/LICENSE @@ -0,0 +1,26 @@ +Copyright 2017, the Chromium project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/firebase_ui_oauth_apple/README.md b/packages/firebase_ui_oauth_apple/README.md new file mode 100644 index 000000000000..a4d557a01a72 --- /dev/null +++ b/packages/firebase_ui_oauth_apple/README.md @@ -0,0 +1,99 @@ +# Firebase UI OAuth Apple + +[![pub package](https://img.shields.io/pub/v/firebase_ui_oauth_apple.svg)](https://pub.dev/packages/firebase_ui_oauth_apple) + +Apple Sign In for [Firebase UI](https://pub.dev/packages/firebase_ui_auth) + +## Installation + +Add dependency + +```sh +flutter pub add firebase_ui +flutter pub add firebase_ui_oauth_apple + +flutter pub global activate flutterfire_cli +flutterfire configure +``` + +Enable Apple provider on [firebase console](https://console.firebase.google.com/). + +## Usage + +```dart +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_oauth_apple/firebase_ui_oauth_apple.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + + FirebaseUIAuth.configureProviders([ + AppleProvider(), + ]); + + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: SignInScreen( + actions: [ + AuthStateChangeAction((context, state) { + // redirect to other screen + }) + ], + ), + ); + } +} +``` + +Alternatively you could use the `OAuthProviderButton` + +```dart +class MyScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AuthStateListener( + listener: (oldState, newState, controller) { + if (newState is SignedIn) { + // navigate to other screen. + } + }, + child: OAuthProviderButton( + provider: AppleProvider(), + ), + ); + } +} +``` + +Also there is a standalone version of the `AppleSignInButton` + +```dart +class MyScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AppleSignInButton( + loadingIndicator: CircularProgressIndicator(), + onSignedIn: (UserCredential credential) { + // perform navigation. + } + ); + } +} +``` + +For issues, please create a new [issue on the repository](https://github.com/firebase/flutterfire/issues). + +For feature requests, & questions, please participate on the [discussion](https://github.com/firebase/flutterfire/discussions/6978) thread. + +To contribute a change to this plugin, please review our [contribution guide](https://github.com/firebase/flutterfire/blob/master/CONTRIBUTING.md) and open a [pull request](https://github.com/firebase/flutterfire/pulls). + +Please contribute to the [discussion](https://github.com/firebase/flutterfire/discussions/6978) with feedback. diff --git a/packages/firebase_ui_oauth_apple/analysis_options.yaml b/packages/firebase_ui_oauth_apple/analysis_options.yaml new file mode 100644 index 000000000000..a5744c1cfbe7 --- /dev/null +++ b/packages/firebase_ui_oauth_apple/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/firebase_ui_oauth_apple/lib/firebase_ui_oauth_apple.dart b/packages/firebase_ui_oauth_apple/lib/firebase_ui_oauth_apple.dart new file mode 100644 index 000000000000..5fa7030b7628 --- /dev/null +++ b/packages/firebase_ui_oauth_apple/lib/firebase_ui_oauth_apple.dart @@ -0,0 +1,130 @@ +export 'src/provider.dart' show AppleProvider; +export 'src/theme.dart' show AppleProviderButtonStyle; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; + +import 'src/provider.dart'; + +class AppleSignInButton extends _AppleSignInButton { + const AppleSignInButton({ + Key? key, + required Widget loadingIndicator, + AuthAction? action, + FirebaseAuth? auth, + bool? isLoading, + String? label, + DifferentProvidersFoundCallback? onDifferentProvidersFound, + SignedInCallback? onSignedIn, + void Function()? onTap, + bool? overrideDefaultTapAction, + double? size, + void Function(Exception exception)? onError, + VoidCallback? onCanceled, + }) : super( + key: key, + action: action, + auth: auth, + isLoading: isLoading ?? false, + loadingIndicator: loadingIndicator, + label: label, + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + onTap: onTap, + overrideDefaultTapAction: overrideDefaultTapAction, + size: size, + onError: onError, + onCanceled: onCanceled, + ); +} + +class AppleSignInIconButton extends _AppleSignInButton { + const AppleSignInIconButton({ + Key? key, + required Widget loadingIndicator, + AuthAction? action, + FirebaseAuth? auth, + bool? isLoading, + DifferentProvidersFoundCallback? onDifferentProvidersFound, + SignedInCallback? onSignedIn, + void Function()? onTap, + bool? overrideDefaultTapAction, + double? size, + void Function(Exception exception)? onError, + VoidCallback? onCanceled, + }) : super( + key: key, + action: action, + auth: auth, + isLoading: isLoading ?? false, + loadingIndicator: loadingIndicator, + label: '', + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + onTap: onTap, + overrideDefaultTapAction: overrideDefaultTapAction, + size: size, + onError: onError, + onCanceled: onCanceled, + ); +} + +class _AppleSignInButton extends StatelessWidget { + final String label; + final Widget loadingIndicator; + final void Function()? onTap; + final bool overrideDefaultTapAction; + final bool isLoading; + + /// {@macro ui.auth.auth_action} + final AuthAction? action; + + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + final DifferentProvidersFoundCallback? onDifferentProvidersFound; + final SignedInCallback? onSignedIn; + final double size; + final void Function(Exception exception)? onError; + final VoidCallback? onCanceled; + + const _AppleSignInButton({ + Key? key, + required this.loadingIndicator, + String? label, + bool? overrideDefaultTapAction, + this.onTap, + this.isLoading = false, + this.action = AuthAction.signIn, + this.auth, + this.onDifferentProvidersFound, + this.onSignedIn, + double? size, + this.onError, + this.onCanceled, + }) : label = label ?? 'Sign in with Apple', + overrideDefaultTapAction = overrideDefaultTapAction ?? false, + size = size ?? 19, + super(key: key); + + AppleProvider get provider => AppleProvider(); + + @override + Widget build(BuildContext context) { + return OAuthProviderButtonBase( + provider: provider, + label: label, + onTap: onTap, + loadingIndicator: loadingIndicator, + isLoading: isLoading, + action: action, + auth: auth ?? FirebaseAuth.instance, + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + overrideDefaultTapAction: overrideDefaultTapAction, + size: size, + onError: onError, + onCancelled: onCanceled, + ); + } +} diff --git a/packages/firebase_ui_oauth_apple/lib/src/provider.dart b/packages/firebase_ui_oauth_apple/lib/src/provider.dart new file mode 100644 index 000000000000..14aa175b11c8 --- /dev/null +++ b/packages/firebase_ui_oauth_apple/lib/src/provider.dart @@ -0,0 +1,56 @@ +import 'package:firebase_auth/firebase_auth.dart' as fba; +import 'package:flutter/foundation.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; + +import 'theme.dart'; + +class AppleProvider extends OAuthProvider { + @override + final providerId = 'apple.com'; + + @override + final style = const AppleProviderButtonStyle(); + + @override + fba.AppleAuthProvider firebaseAuthProvider = fba.AppleAuthProvider(); + + @override + void mobileSignIn(AuthAction action) { + authListener.onBeforeSignIn(); + + auth.signInWithProvider(firebaseAuthProvider).then((userCred) { + if (action == AuthAction.signIn) { + authListener.onSignedIn(userCred); + } else { + authListener.onCredentialLinked(userCred.credential!); + } + }).catchError((err) { + authListener.onError(err); + }); + } + + @override + void desktopSignIn(AuthAction action) { + mobileSignIn(action); + } + + @override + ProviderArgs get desktopSignInArgs => throw UnimplementedError(); + + @override + fba.OAuthCredential fromDesktopAuthResult(AuthResult result) { + throw UnimplementedError(); + } + + @override + Future logOutProvider() { + return SynchronousFuture(null); + } + + @override + bool supportsPlatform(TargetPlatform platform) { + return kIsWeb || + platform == TargetPlatform.iOS || + platform == TargetPlatform.macOS; + } +} diff --git a/packages/firebase_ui_oauth_apple/lib/src/theme.dart b/packages/firebase_ui_oauth_apple/lib/src/theme.dart new file mode 100644 index 000000000000..7d96533a6f9e --- /dev/null +++ b/packages/firebase_ui_oauth_apple/lib/src/theme.dart @@ -0,0 +1,51 @@ +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; + +const _appleBlack = Color(0xff060708); +const _appleWhite = Color(0xffffffff); + +const _backgroundColor = ThemedColor(_appleWhite, _appleBlack); +const _color = ThemedColor(_appleBlack, _appleWhite); + +const _iconSvgLight = ''' + + + + + + + + +'''; + +const _iconSvgDark = ''' + + + + + + + + +'''; + +const _iconSrc = ThemedIconSrc( + _iconSvgLight, + _iconSvgDark, +); + +class AppleProviderButtonStyle extends ThemedOAuthProviderButtonStyle { + const AppleProviderButtonStyle(); + + @override + ThemedColor get backgroundColor => _backgroundColor; + + @override + ThemedColor get color => _color; + + @override + ThemedIconSrc get iconSrc => _iconSrc; + + @override + String get assetsPackage => 'firebase_ui_oauth_apple'; +} diff --git a/packages/firebase_ui_oauth_apple/pubspec.yaml b/packages/firebase_ui_oauth_apple/pubspec.yaml new file mode 100644 index 000000000000..7c3c8b49fae6 --- /dev/null +++ b/packages/firebase_ui_oauth_apple/pubspec.yaml @@ -0,0 +1,61 @@ +name: firebase_ui_oauth_apple +description: Firebase UI widgets for authentication & OAuth. +version: 1.0.0-dev.0 +homepage: https://github.com/firebase/flutterfire/tree/master/packages/firebase_ui_oauth_apple + +environment: + sdk: '>=2.17.6 <3.0.0' + flutter: '>=1.17.0' + +dependencies: + firebase_auth: ^3.6.2 + firebase_ui_oauth: ^1.0.0-dev.0 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +false_secrets: + - '/example/**/google-services.json' + - '/example/**/firebase_options.dart' + - '/example/**/GoogleService-Info.plist' + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/firebase_ui_oauth_facebook/.gitignore b/packages/firebase_ui_oauth_facebook/.gitignore new file mode 100644 index 000000000000..96486fd93024 --- /dev/null +++ b/packages/firebase_ui_oauth_facebook/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/firebase_ui_oauth_facebook/.metadata b/packages/firebase_ui_oauth_facebook/.metadata new file mode 100644 index 000000000000..e7011f64f39d --- /dev/null +++ b/packages/firebase_ui_oauth_facebook/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + channel: stable + +project_type: package diff --git a/packages/firebase_ui_oauth_facebook/CHANGELOG.md b/packages/firebase_ui_oauth_facebook/CHANGELOG.md new file mode 100644 index 000000000000..a5c39d636c33 --- /dev/null +++ b/packages/firebase_ui_oauth_facebook/CHANGELOG.md @@ -0,0 +1,5 @@ +## 1.0.0-dev.0 + +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/firebase_ui_oauth_facebook/LICENSE b/packages/firebase_ui_oauth_facebook/LICENSE new file mode 100644 index 000000000000..5b8ff6261110 --- /dev/null +++ b/packages/firebase_ui_oauth_facebook/LICENSE @@ -0,0 +1,26 @@ +Copyright 2017, the Chromium project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/firebase_ui_oauth_facebook/README.md b/packages/firebase_ui_oauth_facebook/README.md new file mode 100644 index 000000000000..695681105fde --- /dev/null +++ b/packages/firebase_ui_oauth_facebook/README.md @@ -0,0 +1,100 @@ +# Firebase UI OAuth Facebook + +[![pub package](https://img.shields.io/pub/v/firebase_ui_oauth_facebook.svg)](https://pub.dev/packages/firebase_ui_oauth_facebook) + +Facebook Sign In for [Firebase UI](https://pub.dev/packages/firebase_ui) + +## Installation + +Add dependencies + +```sh +flutter pub add firebase_ui +flutter pub add firebase_ui_oauth_facebook + +flutter pub global activate flutterfire_cli +flutterfire configure +``` + +Enable Facebook provider on [firebase console](https://console.firebase.google.com/). + +## Usage + +```dart +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_oauth_facebook/firebase_ui_oauth_facebook.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + + FirebaseUIAuth.configureProviders([ + FacebookProvider(clientId: 'clientId'), + ]); + + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: SignInScreen( + actions: [ + AuthStateChangeAction((context, state) { + // redirect to other screen + }) + ], + ), + ); + } +} +``` + +Alternatively you could use the `OAuthProviderButton` + +```dart +class MyScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AuthStateListener( + listener: (oldState, newState, controller) { + if (newState is SignedIn) { + // navigate to other screen. + } + }, + child: OAuthProviderButton( + provider: FacebookProvider(clientId: 'clientId'), + ), + ); + } +} +``` + +Also there is a standalone version of the `FacebookSignInButton` + +```dart +class MyScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return FacebookSignInButton( + clientId: 'clientId', + loadingIndicator: CircularProgressIndicator(), + onSignedIn: (UserCredential credential) { + // perform navigation. + } + ); + } +} +``` + +For issues, please create a new [issue on the repository](https://github.com/firebase/flutterfire/issues). + +For feature requests, & questions, please participate on the [discussion](https://github.com/firebase/flutterfire/discussions/6978) thread. + +To contribute a change to this plugin, please review our [contribution guide](https://github.com/firebase/flutterfire/blob/master/CONTRIBUTING.md) and open a [pull request](https://github.com/firebase/flutterfire/pulls). + +Please contribute to the [discussion](https://github.com/firebase/flutterfire/discussions/6978) with feedback. diff --git a/packages/firebase_ui_oauth_facebook/analysis_options.yaml b/packages/firebase_ui_oauth_facebook/analysis_options.yaml new file mode 100644 index 000000000000..a5744c1cfbe7 --- /dev/null +++ b/packages/firebase_ui_oauth_facebook/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/firebase_ui_oauth_facebook/lib/firebase_ui_oauth_facebook.dart b/packages/firebase_ui_oauth_facebook/lib/firebase_ui_oauth_facebook.dart new file mode 100644 index 000000000000..d74cc98ce993 --- /dev/null +++ b/packages/firebase_ui_oauth_facebook/lib/firebase_ui_oauth_facebook.dart @@ -0,0 +1,146 @@ +export 'src/provider.dart' show FacebookProvider; +export 'src/theme.dart' show FacebookProviderButtonStyle; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; + +import 'src/provider.dart'; + +class FacebookSignInButton extends _FacebookSignInButton { + const FacebookSignInButton({ + Key? key, + required Widget loadingIndicator, + required String clientId, + String? redirectUri, + AuthAction? action, + FirebaseAuth? auth, + bool? isLoading, + String? label, + DifferentProvidersFoundCallback? onDifferentProvidersFound, + SignedInCallback? onSignedIn, + void Function()? onTap, + bool? overrideDefaultTapAction, + double? size, + void Function(Exception exception)? onError, + VoidCallback? onCanceled, + }) : super( + key: key, + clientId: clientId, + action: action, + auth: auth, + isLoading: isLoading ?? false, + loadingIndicator: loadingIndicator, + label: label, + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + onTap: onTap, + overrideDefaultTapAction: overrideDefaultTapAction, + size: size, + redirectUri: redirectUri, + onError: onError, + onCanceled: onCanceled, + ); +} + +class FacebookSignInIconButton extends _FacebookSignInButton { + const FacebookSignInIconButton({ + Key? key, + required String clientId, + required Widget loadingIndicator, + AuthAction? action, + FirebaseAuth? auth, + bool? isLoading, + DifferentProvidersFoundCallback? onDifferentProvidersFound, + SignedInCallback? onSignedIn, + void Function()? onTap, + bool? overrideDefaultTapAction, + double? size, + String? redirectUri, + void Function(Exception exception)? onError, + VoidCallback? onCanceled, + }) : super( + key: key, + action: action, + clientId: clientId, + auth: auth, + isLoading: isLoading ?? false, + loadingIndicator: loadingIndicator, + label: '', + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + onTap: onTap, + overrideDefaultTapAction: overrideDefaultTapAction, + size: size, + redirectUri: redirectUri, + onError: onError, + onCanceled: onCanceled, + ); +} + +class _FacebookSignInButton extends StatelessWidget { + final String label; + final Widget loadingIndicator; + final void Function()? onTap; + final bool overrideDefaultTapAction; + final bool isLoading; + + /// {@macro ui.auth.auth_action} + final AuthAction? action; + + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + final DifferentProvidersFoundCallback? onDifferentProvidersFound; + final SignedInCallback? onSignedIn; + final double size; + final String clientId; + final String? redirectUri; + final void Function(Exception exception)? onError; + final VoidCallback? onCanceled; + + const _FacebookSignInButton({ + Key? key, + required this.clientId, + required this.loadingIndicator, + String? label, + bool? overrideDefaultTapAction, + this.onTap, + this.isLoading = false, + this.action = AuthAction.signIn, + this.auth, + this.onDifferentProvidersFound, + this.onSignedIn, + double? size, + this.redirectUri, + this.onError, + this.onCanceled, + }) : label = label ?? 'Sign in with Facebook', + overrideDefaultTapAction = overrideDefaultTapAction ?? false, + size = size ?? 19, + super(key: key); + + FacebookProvider get provider => FacebookProvider( + clientId: clientId, + redirectUri: redirectUri, + ); + + @override + Widget build(BuildContext context) { + return OAuthProviderButtonBase( + provider: provider, + label: label, + onTap: onTap, + loadingIndicator: loadingIndicator, + isLoading: isLoading, + action: action, + auth: auth ?? FirebaseAuth.instance, + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + overrideDefaultTapAction: overrideDefaultTapAction, + size: size, + onError: onError, + onCancelled: onCanceled, + ); + } +} diff --git a/packages/firebase_ui_oauth_facebook/lib/src/provider.dart b/packages/firebase_ui_oauth_facebook/lib/src/provider.dart new file mode 100644 index 000000000000..a51644ab5fd4 --- /dev/null +++ b/packages/firebase_ui_oauth_facebook/lib/src/provider.dart @@ -0,0 +1,78 @@ +import 'package:firebase_auth/firebase_auth.dart' hide OAuthProvider; +import 'package:flutter/foundation.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; +import 'package:flutter_facebook_auth/flutter_facebook_auth.dart'; +import 'package:firebase_ui_oauth_facebook/firebase_ui_oauth_facebook.dart'; + +class FacebookProvider extends OAuthProvider { + @override + final providerId = 'facebook.com'; + + FacebookAuth provider = FacebookAuth.instance; + final String clientId; + final String? redirectUri; + + @override + final style = const FacebookProviderButtonStyle(); + + @override + late final ProviderArgs desktopSignInArgs = FacebookSignInArgs( + clientId: clientId, + redirectUri: redirectUri ?? defaultRedirectUri, + ); + + FacebookProvider({ + required this.clientId, + this.redirectUri, + }); + + void _handleResult(LoginResult result, AuthAction action) { + switch (result.status) { + case LoginStatus.success: + final token = result.accessToken!.token; + final credential = FacebookAuthProvider.credential(token); + + onCredentialReceived(credential, action); + break; + case LoginStatus.cancelled: + authListener.onError(AuthCancelledException()); + break; + case LoginStatus.failed: + authListener.onError(Exception(result.message)); + break; + case LoginStatus.operationInProgress: + authListener.onError( + Exception('Previous login request is not complete'), + ); + } + } + + @override + OAuthCredential fromDesktopAuthResult(AuthResult result) { + return FacebookAuthProvider.credential(result.accessToken!); + } + + @override + FacebookAuthProvider get firebaseAuthProvider => FacebookAuthProvider(); + + @override + Future logOutProvider() async { + if (defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS) { + await provider.logOut(); + } + } + + @override + void mobileSignIn(AuthAction action) { + final result = provider.login(); + result + .then((result) => _handleResult(result, action)) + .catchError(authListener.onError); + } + + @override + bool supportsPlatform(TargetPlatform platform) { + return true; + } +} diff --git a/packages/firebase_ui_oauth_facebook/lib/src/theme.dart b/packages/firebase_ui_oauth_facebook/lib/src/theme.dart new file mode 100644 index 000000000000..fd841f9e2407 --- /dev/null +++ b/packages/firebase_ui_oauth_facebook/lib/src/theme.dart @@ -0,0 +1,35 @@ +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; + +const _facebookBlue = Color(0xff1878F2); +const _facebookWhite = Color(0xffffffff); + +const _backgroundColor = ThemedColor(_facebookBlue, _facebookBlue); +const _color = ThemedColor(_facebookWhite, _facebookWhite); + +const _iconSvg = ''' + + f_logo_RGB-White_1024 + + + + +'''; + +const _iconSrc = ThemedIconSrc(_iconSvg, _iconSvg); + +class FacebookProviderButtonStyle extends ThemedOAuthProviderButtonStyle { + const FacebookProviderButtonStyle(); + + @override + ThemedColor get backgroundColor => _backgroundColor; + + @override + ThemedColor get color => _color; + + @override + ThemedIconSrc get iconSrc => _iconSrc; + + @override + String get assetsPackage => 'firebase_ui_oauth_facebook'; +} diff --git a/packages/firebase_ui_oauth_facebook/pubspec.yaml b/packages/firebase_ui_oauth_facebook/pubspec.yaml new file mode 100644 index 000000000000..e94c79050c28 --- /dev/null +++ b/packages/firebase_ui_oauth_facebook/pubspec.yaml @@ -0,0 +1,62 @@ +name: firebase_ui_oauth_facebook +description: Firebase UI widgets for authentication & OAuth. +version: 1.0.0-dev.0 +homepage: https://github.com/firebase/flutterfire/tree/master/packages/firebase_ui_oauth_facebook + +environment: + sdk: '>=2.17.6 <3.0.0' + flutter: '>=1.17.0' + +dependencies: + firebase_auth: ^3.6.2 + firebase_ui_oauth: ^1.0.0-dev.0 + flutter: + sdk: flutter + flutter_facebook_auth: ^4.4.0+1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +false_secrets: + - '/example/**/google-services.json' + - '/example/**/firebase_options.dart' + - '/example/**/GoogleService-Info.plist' + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/firebase_ui_oauth_google/.gitignore b/packages/firebase_ui_oauth_google/.gitignore new file mode 100644 index 000000000000..96486fd93024 --- /dev/null +++ b/packages/firebase_ui_oauth_google/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/firebase_ui_oauth_google/.metadata b/packages/firebase_ui_oauth_google/.metadata new file mode 100644 index 000000000000..e7011f64f39d --- /dev/null +++ b/packages/firebase_ui_oauth_google/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + channel: stable + +project_type: package diff --git a/packages/firebase_ui_oauth_google/CHANGELOG.md b/packages/firebase_ui_oauth_google/CHANGELOG.md new file mode 100644 index 000000000000..a5c39d636c33 --- /dev/null +++ b/packages/firebase_ui_oauth_google/CHANGELOG.md @@ -0,0 +1,5 @@ +## 1.0.0-dev.0 + +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/firebase_ui_oauth_google/LICENSE b/packages/firebase_ui_oauth_google/LICENSE new file mode 100644 index 000000000000..5b8ff6261110 --- /dev/null +++ b/packages/firebase_ui_oauth_google/LICENSE @@ -0,0 +1,26 @@ +Copyright 2017, the Chromium project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/firebase_ui_oauth_google/README.md b/packages/firebase_ui_oauth_google/README.md new file mode 100644 index 000000000000..592ab430057a --- /dev/null +++ b/packages/firebase_ui_oauth_google/README.md @@ -0,0 +1,100 @@ +# Firebase UI OAuth Google + +[![pub package](https://img.shields.io/pub/v/firebase_ui_oauth_google.svg)](https://pub.dev/packages/firebase_ui_oauth_google) + +Google Sign In for [Firebase UI](https://pub.dev/packages/firebase_ui) + +## Installation + +Add dependencies + +```sh +flutter pub add firebase_ui +flutter pub add firebase_ui_oauth_google + +flutter pub global activate flutterfire_cli +flutterfire configure +``` + +Enable Google provider on [firebase console](https://console.firebase.google.com/). + +## Usage + +```dart +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_oauth_google/firebase_ui_oauth_google.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + + FirebaseUIAuth.configureProviders([ + GoogleProvider(clientId: 'clientId'), + ]); + + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: SignInScreen( + actions: [ + AuthStateChangeAction((context, state) { + // redirect to other screen + }) + ], + ), + ); + } +} +``` + +Alternatively you could use the `OAuthProviderButton` + +```dart +class MyScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AuthStateListener( + listener: (oldState, newState, controller) { + if (newState is SignedIn) { + // navigate to other screen. + } + }, + child: OAuthProviderButton( + provider: GoogleProvider(clientId: 'clientId'), + ), + ); + } +} +``` + +Also there is a standalone version of the `GoogleSignInButton` + +```dart +class MyScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return GoogleSignInButton( + clientId: 'clientId', + loadingIndicator: CircularProgressIndicator(), + onSignedIn: (UserCredential credential) { + // perform navigation. + } + ); + } +} +``` + +For issues, please create a new [issue on the repository](https://github.com/firebase/flutterfire/issues). + +For feature requests, & questions, please participate on the [discussion](https://github.com/firebase/flutterfire/discussions/6978) thread. + +To contribute a change to this plugin, please review our [contribution guide](https://github.com/firebase/flutterfire/blob/master/CONTRIBUTING.md) and open a [pull request](https://github.com/firebase/flutterfire/pulls). + +Please contribute to the [discussion](https://github.com/firebase/flutterfire/discussions/6978) with feedback. diff --git a/packages/firebase_ui_oauth_google/analysis_options.yaml b/packages/firebase_ui_oauth_google/analysis_options.yaml new file mode 100644 index 000000000000..a5744c1cfbe7 --- /dev/null +++ b/packages/firebase_ui_oauth_google/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/firebase_ui_oauth_google/assets/google_icon.svg b/packages/firebase_ui_oauth_google/assets/google_icon.svg new file mode 100644 index 000000000000..50d9b8963ba0 --- /dev/null +++ b/packages/firebase_ui_oauth_google/assets/google_icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/packages/firebase_ui_oauth_google/lib/firebase_ui_oauth_google.dart b/packages/firebase_ui_oauth_google/lib/firebase_ui_oauth_google.dart new file mode 100644 index 000000000000..a29bc76f44d9 --- /dev/null +++ b/packages/firebase_ui_oauth_google/lib/firebase_ui_oauth_google.dart @@ -0,0 +1,152 @@ +export 'src/provider.dart' show GoogleProvider; +export 'src/theme.dart' show GoogleProviderButtonStyle; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; + +import 'src/provider.dart'; + +class GoogleSignInButton extends _GoogleSignInButton { + const GoogleSignInButton({ + Key? key, + required Widget loadingIndicator, + required String clientId, + String? redirectUri, + List? scopes, + AuthAction? action, + FirebaseAuth? auth, + bool? isLoading, + String? label, + DifferentProvidersFoundCallback? onDifferentProvidersFound, + SignedInCallback? onSignedIn, + void Function()? onTap, + bool? overrideDefaultTapAction, + double? size, + void Function(Exception exception)? onError, + VoidCallback? onCanceled, + }) : super( + key: key, + clientId: clientId, + action: action, + auth: auth, + isLoading: isLoading ?? false, + loadingIndicator: loadingIndicator, + label: label, + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + onTap: onTap, + overrideDefaultTapAction: overrideDefaultTapAction, + size: size, + redirectUri: redirectUri, + scopes: scopes, + onError: onError, + onCanceled: onCanceled, + ); +} + +class GoogleSignInIconButton extends _GoogleSignInButton { + const GoogleSignInIconButton({ + Key? key, + required String clientId, + required Widget loadingIndicator, + List? scopes, + AuthAction? action, + FirebaseAuth? auth, + bool? isLoading, + DifferentProvidersFoundCallback? onDifferentProvidersFound, + SignedInCallback? onSignedIn, + void Function()? onTap, + bool? overrideDefaultTapAction, + double? size, + String? redirectUri, + void Function(Exception exception)? onError, + VoidCallback? onCanceled, + }) : super( + key: key, + action: action, + clientId: clientId, + auth: auth, + isLoading: isLoading ?? false, + loadingIndicator: loadingIndicator, + label: '', + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + onTap: onTap, + overrideDefaultTapAction: overrideDefaultTapAction, + size: size, + redirectUri: redirectUri, + scopes: scopes, + onError: onError, + onCanceled: onCanceled, + ); +} + +class _GoogleSignInButton extends StatelessWidget { + final String label; + final Widget loadingIndicator; + final void Function()? onTap; + final bool overrideDefaultTapAction; + final bool isLoading; + + /// {@macro ui.auth.auth_action} + final AuthAction? action; + + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + final DifferentProvidersFoundCallback? onDifferentProvidersFound; + final SignedInCallback? onSignedIn; + final double size; + final String clientId; + final String? redirectUri; + final List? scopes; + final void Function(Exception exception)? onError; + final VoidCallback? onCanceled; + + const _GoogleSignInButton({ + Key? key, + required this.clientId, + required this.loadingIndicator, + this.scopes, + String? label, + bool? overrideDefaultTapAction, + this.onTap, + this.isLoading = false, + this.action = AuthAction.signIn, + this.auth, + this.onDifferentProvidersFound, + this.onSignedIn, + double? size, + this.redirectUri, + this.onError, + this.onCanceled, + }) : label = label ?? 'Sign in with Google', + overrideDefaultTapAction = overrideDefaultTapAction ?? false, + size = size ?? 19, + super(key: key); + + GoogleProvider get provider => GoogleProvider( + clientId: clientId, + redirectUri: redirectUri, + scopes: scopes ?? [], + ); + + @override + Widget build(BuildContext context) { + return OAuthProviderButtonBase( + provider: provider, + label: label, + onTap: onTap, + loadingIndicator: loadingIndicator, + isLoading: isLoading, + action: action, + auth: auth ?? FirebaseAuth.instance, + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + overrideDefaultTapAction: overrideDefaultTapAction, + size: size, + onError: onError, + onCancelled: onCanceled, + ); + } +} diff --git a/packages/firebase_ui_oauth_google/lib/src/provider.dart b/packages/firebase_ui_oauth_google/lib/src/provider.dart new file mode 100644 index 000000000000..b261936ffdc5 --- /dev/null +++ b/packages/firebase_ui_oauth_google/lib/src/provider.dart @@ -0,0 +1,78 @@ +import 'package:firebase_auth/firebase_auth.dart' hide OAuthProvider; +import 'package:flutter/foundation.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; +import 'package:firebase_ui_oauth_google/firebase_ui_oauth_google.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +class GoogleProvider extends OAuthProvider { + @override + final providerId = 'google.com'; + final String clientId; + final String? redirectUri; + final List? scopes; + + late GoogleSignIn provider = GoogleSignIn(scopes: scopes ?? []); + + @override + final GoogleAuthProvider firebaseAuthProvider = GoogleAuthProvider(); + + @override + late final desktopSignInArgs = GoogleSignInArgs( + clientId: clientId, + redirectUri: redirectUri ?? defaultRedirectUri, + scope: scopes != null + ? scopes!.join(' ') + : 'https://www.googleapis.com/auth/plus.login', + ); + + GoogleProvider({ + required this.clientId, + this.redirectUri, + this.scopes, + }) { + firebaseAuthProvider.setCustomParameters(const { + 'prompt': 'select_account', + }); + } + + @override + void mobileSignIn(AuthAction action) async { + provider.signIn().then((user) { + if (user == null) throw AuthCancelledException(); + return user.authentication; + }).then((auth) { + final credential = GoogleAuthProvider.credential( + accessToken: auth.accessToken, + idToken: auth.idToken, + ); + + onCredentialReceived(credential, action); + }).catchError((err) { + authListener.onError(err); + }); + } + + @override + OAuthCredential fromDesktopAuthResult(AuthResult result) { + return GoogleAuthProvider.credential( + idToken: result.idToken, + accessToken: result.accessToken, + ); + } + + @override + Future logOutProvider() async { + if (defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS) { + await provider.signOut(); + } + } + + @override + final style = const GoogleProviderButtonStyle(); + + @override + bool supportsPlatform(TargetPlatform platform) { + return true; + } +} diff --git a/packages/firebase_ui_oauth_google/lib/src/theme.dart b/packages/firebase_ui_oauth_google/lib/src/theme.dart new file mode 100644 index 000000000000..7b1b67b18816 --- /dev/null +++ b/packages/firebase_ui_oauth_google/lib/src/theme.dart @@ -0,0 +1,51 @@ +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; + +const _googleBlue = Color(0xff4285f4); +const _googleWhite = Color(0xffffffff); +const _googleDark = Color(0xff757575); + +const _backgroundColor = ThemedColor(_googleBlue, _googleWhite); +const _color = ThemedColor(_googleWhite, _googleDark); +const _iconBackgroundColor = ThemedColor(_googleWhite, _googleWhite); + +const _iconSvg = ''' + + + + + + + + + + + + + + +'''; + +const _iconSrc = ThemedIconSrc(_iconSvg, _iconSvg); + +class GoogleProviderButtonStyle extends ThemedOAuthProviderButtonStyle { + const GoogleProviderButtonStyle(); + + @override + ThemedColor get backgroundColor => _backgroundColor; + + @override + ThemedColor get color => _color; + + @override + ThemedIconSrc get iconSrc => _iconSrc; + + @override + ThemedColor get iconBackgroundColor => _iconBackgroundColor; + + @override + double get iconPadding => 1; + + @override + String get assetsPackage => 'firebase_ui_oauth_google'; +} diff --git a/packages/firebase_ui_oauth_google/pubspec.yaml b/packages/firebase_ui_oauth_google/pubspec.yaml new file mode 100644 index 000000000000..875d3163c338 --- /dev/null +++ b/packages/firebase_ui_oauth_google/pubspec.yaml @@ -0,0 +1,62 @@ +name: firebase_ui_oauth_google +description: Firebase UI widgets for authentication & OAuth. +version: 1.0.0-dev.0 +homepage: https://github.com/firebase/flutterfire/tree/master/packages/firebase_ui_oauth_google + +environment: + sdk: '>=2.17.6 <3.0.0' + flutter: '>=1.17.0' + +dependencies: + firebase_auth: ^3.6.2 + firebase_ui_oauth: ^1.0.0-dev.0 + flutter: + sdk: flutter + google_sign_in: ^5.4.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +false_secrets: + - '/example/**/google-services.json' + - '/example/**/firebase_options.dart' + - '/example/**/GoogleService-Info.plist' + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/firebase_ui_oauth_twitter/.gitignore b/packages/firebase_ui_oauth_twitter/.gitignore new file mode 100644 index 000000000000..96486fd93024 --- /dev/null +++ b/packages/firebase_ui_oauth_twitter/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/firebase_ui_oauth_twitter/.metadata b/packages/firebase_ui_oauth_twitter/.metadata new file mode 100644 index 000000000000..e7011f64f39d --- /dev/null +++ b/packages/firebase_ui_oauth_twitter/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + channel: stable + +project_type: package diff --git a/packages/firebase_ui_oauth_twitter/CHANGELOG.md b/packages/firebase_ui_oauth_twitter/CHANGELOG.md new file mode 100644 index 000000000000..a5c39d636c33 --- /dev/null +++ b/packages/firebase_ui_oauth_twitter/CHANGELOG.md @@ -0,0 +1,5 @@ +## 1.0.0-dev.0 + +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/firebase_ui_oauth_twitter/LICENSE b/packages/firebase_ui_oauth_twitter/LICENSE new file mode 100644 index 000000000000..5b8ff6261110 --- /dev/null +++ b/packages/firebase_ui_oauth_twitter/LICENSE @@ -0,0 +1,26 @@ +Copyright 2017, the Chromium project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/firebase_ui_oauth_twitter/README.md b/packages/firebase_ui_oauth_twitter/README.md new file mode 100644 index 000000000000..5dfe1899e65e --- /dev/null +++ b/packages/firebase_ui_oauth_twitter/README.md @@ -0,0 +1,105 @@ +# Firebase UI OAuth Twitter + +[![pub package](https://img.shields.io/pub/v/firebase_ui_oauth_twitter.svg)](https://pub.dev/packages/firebase_ui_oauth_twitter) + +Twitter Sign In for [Firebase UI](https://pub.dev/packages/firebase_ui) + +## Installation + +Add dependencies + +```sh +flutter pub add firebase_ui +flutter pub add firebase_ui_oauth_twitter + +flutter pub global activate flutterfire_cli +flutterfire configure +``` + +Enable Twitter provider on [firebase console](https://console.firebase.twitter.com/). + +## Usage + +```dart +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_oauth_twitter/firebase_ui_oauth_twitter.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + + FirebaseUIAuth.configureProviders([ + TwitterProvider(apiKey: 'apiKey', apiSecretKey: 'apiSecretKey'), + ]); + + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: SignInScreen( + actions: [ + AuthStateChangeAction((context, state) { + // redirect to other screen + }) + ], + ), + ); + } +} +``` + +Alternatively you could use the `OAuthProviderButton` + +```dart +class MyScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AuthStateListener( + listener: (oldState, newState, controller) { + if (newState is SignedIn) { + // navigate to other screen. + } + }, + child: OAuthProviderButton( + provider: TwitterProvider(apiKey: 'apiKey'), + ), + ); + } +} +``` + +Also there is a standalone version of the `TwitterSignInButton` + +```dart +class MyScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return TwitterSignInButton( + apiKey: 'apiKey', + apiSecretKey: 'apiSecretKey', + loadingIndicator: CircularProgressIndicator(), + onSignedIn: (UserCredential credential) { + // perform navigation. + } + ); + } +} +``` + +## API Secret Key notes + +Don't hardcode your API secret key into the source code, instead use `--dart-define TWITTER_API_SECRET_KEY=secret` and `apiSecretKey: const String.fromEnvironment('TWITTER_API_SECRET_KEY)`. + +For issues, please create a new [issue on the repository](https://github.com/firebase/flutterfire/issues). + +For feature requests, & questions, please participate on the [discussion](https://github.com/firebase/flutterfire/discussions/6978) thread. + +To contribute a change to this plugin, please review our [contribution guide](https://github.com/firebase/flutterfire/blob/master/CONTRIBUTING.md) and open a [pull request](https://github.com/firebase/flutterfire/pulls). + +Please contribute to the [discussion](https://github.com/firebase/flutterfire/discussions/6978) with feedback. diff --git a/packages/firebase_ui_oauth_twitter/analysis_options.yaml b/packages/firebase_ui_oauth_twitter/analysis_options.yaml new file mode 100644 index 000000000000..a5744c1cfbe7 --- /dev/null +++ b/packages/firebase_ui_oauth_twitter/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/firebase_ui_oauth_twitter/lib/firebase_ui_oauth_twitter.dart b/packages/firebase_ui_oauth_twitter/lib/firebase_ui_oauth_twitter.dart new file mode 100644 index 000000000000..94ba1b1de253 --- /dev/null +++ b/packages/firebase_ui_oauth_twitter/lib/firebase_ui_oauth_twitter.dart @@ -0,0 +1,152 @@ +export 'src/provider.dart' show TwitterProvider; +export 'src/theme.dart' show TwitterProviderButtonStyle; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; + +import 'src/provider.dart'; + +class TwitterSignInButton extends _TwitterSignInButton { + const TwitterSignInButton({ + Key? key, + required Widget loadingIndicator, + required String apiKey, + required String apiSecretKey, + String? redirectUri, + AuthAction? action, + FirebaseAuth? auth, + bool? isLoading, + String? label, + DifferentProvidersFoundCallback? onDifferentProvidersFound, + SignedInCallback? onSignedIn, + void Function()? onTap, + bool? overrideDefaultTapAction, + double? size, + void Function(Exception exception)? onError, + VoidCallback? onCanceled, + }) : super( + key: key, + apiKey: apiKey, + apiSecretKey: apiSecretKey, + action: action, + auth: auth, + isLoading: isLoading ?? false, + loadingIndicator: loadingIndicator, + label: label, + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + onTap: onTap, + overrideDefaultTapAction: overrideDefaultTapAction, + size: size, + redirectUri: redirectUri, + onError: onError, + onCanceled: onCanceled, + ); +} + +class TwitterSignInIconButton extends _TwitterSignInButton { + const TwitterSignInIconButton({ + Key? key, + required String apiKey, + required String apiSecretKey, + required Widget loadingIndicator, + AuthAction? action, + FirebaseAuth? auth, + bool? isLoading, + DifferentProvidersFoundCallback? onDifferentProvidersFound, + SignedInCallback? onSignedIn, + void Function()? onTap, + bool? overrideDefaultTapAction, + double? size, + String? redirectUri, + void Function(Exception exception)? onError, + VoidCallback? onCanceled, + }) : super( + key: key, + action: action, + apiKey: apiKey, + apiSecretKey: apiSecretKey, + auth: auth, + isLoading: isLoading ?? false, + loadingIndicator: loadingIndicator, + label: '', + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + onTap: onTap, + overrideDefaultTapAction: overrideDefaultTapAction, + size: size, + redirectUri: redirectUri, + onError: onError, + onCanceled: onCanceled, + ); +} + +class _TwitterSignInButton extends StatelessWidget { + final String label; + final Widget loadingIndicator; + final void Function()? onTap; + final bool overrideDefaultTapAction; + final bool isLoading; + + /// {@macro ui.auth.auth_action} + final AuthAction? action; + + /// {@macro ui.auth.auth_controller.auth} + final FirebaseAuth? auth; + final DifferentProvidersFoundCallback? onDifferentProvidersFound; + final SignedInCallback? onSignedIn; + final double size; + final String apiKey; + final String apiSecretKey; + final String? redirectUri; + final void Function(Exception exception)? onError; + final VoidCallback? onCanceled; + + const _TwitterSignInButton({ + Key? key, + required this.apiKey, + required this.apiSecretKey, + required this.loadingIndicator, + String? label, + bool? overrideDefaultTapAction, + this.onTap, + this.isLoading = false, + this.action = AuthAction.signIn, + this.auth, + this.onDifferentProvidersFound, + this.onSignedIn, + double? size, + this.redirectUri, + this.onError, + this.onCanceled, + }) : label = label ?? 'Sign in with Twitter', + overrideDefaultTapAction = overrideDefaultTapAction ?? false, + size = size ?? 19, + super(key: key); + + TwitterProvider get provider => TwitterProvider( + apiKey: apiKey, + apiSecretKey: apiSecretKey, + redirectUri: redirectUri, + ); + + @override + Widget build(BuildContext context) { + return OAuthProviderButtonBase( + provider: provider, + label: label, + onTap: onTap, + loadingIndicator: loadingIndicator, + isLoading: isLoading, + action: action, + auth: auth ?? FirebaseAuth.instance, + onDifferentProvidersFound: onDifferentProvidersFound, + onSignedIn: onSignedIn, + overrideDefaultTapAction: overrideDefaultTapAction, + size: size, + onError: onError, + onCancelled: onCanceled, + ); + } +} diff --git a/packages/firebase_ui_oauth_twitter/lib/src/provider.dart b/packages/firebase_ui_oauth_twitter/lib/src/provider.dart new file mode 100644 index 000000000000..0f0c1ece135f --- /dev/null +++ b/packages/firebase_ui_oauth_twitter/lib/src/provider.dart @@ -0,0 +1,83 @@ +import 'package:firebase_auth/firebase_auth.dart' hide OAuthProvider; +import 'package:flutter/foundation.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; +import 'package:twitter_login/twitter_login.dart'; + +import 'theme.dart'; + +class TwitterProvider extends OAuthProvider { + @override + final providerId = 'twitter.com'; + final String apiKey; + final String apiSecretKey; + final String? redirectUri; + + @override + final style = const TwitterProviderButtonStyle(); + + @override + late final desktopSignInArgs = TwitterSignInArgs( + apiKey: apiKey, + apiSecretKey: apiSecretKey, + redirectUri: redirectUri ?? defaultRedirectUri, + ); + + late TwitterLogin provider = TwitterLogin( + apiKey: apiKey, + apiSecretKey: apiSecretKey, + redirectURI: redirectUri ?? defaultRedirectUri, + ); + + TwitterProvider({ + required this.apiKey, + required this.apiSecretKey, + this.redirectUri, + }); + + @override + void mobileSignIn(AuthAction action) { + final result = provider.login(); + + result.then((value) { + switch (value.status!) { + case TwitterLoginStatus.loggedIn: + final credential = TwitterAuthProvider.credential( + accessToken: value.authToken!, + secret: value.authTokenSecret!, + ); + + onCredentialReceived(credential, action); + break; + case TwitterLoginStatus.cancelledByUser: + authListener.onError(AuthCancelledException()); + break; + case TwitterLoginStatus.error: + authListener.onError(Exception(value.errorMessage)); + break; + } + }).catchError((err) { + authListener.onError(err); + }); + } + + @override + OAuthCredential fromDesktopAuthResult(AuthResult result) { + return TwitterAuthProvider.credential( + accessToken: result.accessToken!, + secret: result.tokenSecret!, + ); + } + + @override + TwitterAuthProvider get firebaseAuthProvider => TwitterAuthProvider(); + + @override + Future logOutProvider() { + return SynchronousFuture(null); + } + + @override + bool supportsPlatform(TargetPlatform platform) { + return true; + } +} diff --git a/packages/firebase_ui_oauth_twitter/lib/src/theme.dart b/packages/firebase_ui_oauth_twitter/lib/src/theme.dart new file mode 100644 index 000000000000..8caff30582f8 --- /dev/null +++ b/packages/firebase_ui_oauth_twitter/lib/src/theme.dart @@ -0,0 +1,36 @@ +import 'package:flutter/widgets.dart'; +import 'package:firebase_ui_oauth/firebase_ui_oauth.dart'; + +const _twitterBlue = Color(0xff009EF7); +const _twitterWhite = Color(0xffffffff); + +const _backgroundColor = ThemedColor(_twitterBlue, _twitterBlue); +const _color = ThemedColor(_twitterWhite, _twitterWhite); + +const _iconSvg = ''' + + + + + + + +'''; + +const _iconSrc = ThemedIconSrc(_iconSvg, _iconSvg); + +class TwitterProviderButtonStyle extends ThemedOAuthProviderButtonStyle { + const TwitterProviderButtonStyle(); + + @override + ThemedColor get backgroundColor => _backgroundColor; + + @override + ThemedColor get color => _color; + + @override + ThemedIconSrc get iconSrc => _iconSrc; + + @override + String get assetsPackage => 'firebase_ui_oauth_twitter'; +} diff --git a/packages/firebase_ui_oauth_twitter/pubspec.yaml b/packages/firebase_ui_oauth_twitter/pubspec.yaml new file mode 100644 index 000000000000..f64296c5012d --- /dev/null +++ b/packages/firebase_ui_oauth_twitter/pubspec.yaml @@ -0,0 +1,62 @@ +name: firebase_ui_oauth_twitter +description: Firebase UI widgets for authentication & OAuth. +version: 1.0.0-dev.0 +homepage: https://github.com/firebase/flutterfire/tree/master/packages/firebase_ui_oauth_twitter + +environment: + sdk: '>=2.17.6 <3.0.0' + flutter: '>=1.17.0' + +dependencies: + flutter: + sdk: flutter + firebase_auth: ^3.3.17 + firebase_ui_oauth: ^1.0.0-dev.0 + twitter_login: ^4.2.3 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +false_secrets: + - '/example/**/google-services.json' + - '/example/**/firebase_options.dart' + - '/example/**/GoogleService-Info.plist' + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages