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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Gradle cache
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
- name: AVD cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
id: avd-cache
Expand All @@ -75,9 +75,6 @@ jobs:
melos-version: "^7.5.0"
- name: "Bootstrap package"
run: melos bootstrap --scope tests
# needed because twitter_login plugin doesn't have a namespace defined and he hasn't released a new version yet: https://github.com/0maru/twitter_login/issues/139
- name: Patch twitter_login plugin
run: ./scripts/patch-twitter-login.sh
- name: Start Firebase Emulator
run: |
cd functions/
Expand Down
139 changes: 123 additions & 16 deletions docs/firebase-ui-auth/providers/oauth.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,53 +156,160 @@ See [Custom screens section](#custom-screens) to learn how to use a button on yo

## Twitter Login

To support Twitter as a provider, first install the [`twitter_login`](https://pub.dev/packages/twitter_login)
plugin to your project and make sure you've performed necessary configuration as described on [README](https://pub.dev/packages/twitter_login).

Next, enable the "Twitter" provider in the Firebase Console:
Enable the "Twitter" provider in the Firebase Console, and give it the API key and secret from your
app in the [X developer portal](https://developer.twitter.com/en/portal/projects-and-apps):

![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):
![Twitter app id](../images/ui-twitter-app-id.png)

Then set the "Callback URL" of that same X app to the Firebase auth handler, which the Firebase
Console shows when you enable the provider:

```
https://<your-project-id>.firebaseapp.com/__/auth/handler
```

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:
And add the provider to the configuration:

```dart
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

FirebaseUIAuth.configureProviders([
TwitterProvider(
apiKey: TWITTER_API_KEY,
apiSecretKey: TWITTER_API_SECRET_KEY,
),
TwitterProvider(),
]);
}
```

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).
### Android and iOS setup

![Twitter app id](../images/ui-twitter-app-id.png)
On Android and iOS, Firebase performs the sign in itself, so the API key and secret stay in the
Firebase Console and are never shipped in your app.

On **iOS**, add your encoded app ID as a URL scheme in `ios/Runner/Info.plist`. You will find it in
the Firebase Console under Project settings, listed as the App ID for your iOS app, with `:`
replaced by `-`:

```xml
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>app-1-1234567890-ios-0a1b2c3d4e5f6g7h8i9j</string>
</array>
</dict>
</array>
```

On **Android**, add your app's SHA-1 fingerprint in the Firebase Console under Project settings, so
Firebase can verify the sign in request.

### macOS and Windows setup

Firebase does not support this sign in flow on macOS or Windows, so on those platforms
`firebase_ui_oauth_twitter` performs the OAuth 1.0a flow itself and does need the API key and
secret:

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:
```dart
TwitterProvider(
apiKey: TWITTER_API_KEY,
apiSecretKey: TWITTER_API_SECRET_KEY,
),
```

They are ignored on Android, iOS and the web. On a platform that needs them, sign in fails with a
`FirebaseAuthException` rather than proceeding with empty credentials, so an absent
`--dart-define` surfaces as a clear error.

Because the secret is embedded in desktop builds, pass it at build time rather than committing it:

```bash
flutter run --dart-define TWITTER_SECRET=<your-twitter-api-secret-key>
```

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', ''),
apiSecretKey: String.fromEnvironment('TWITTER_SECRET'),
```

### Upgrading from 2.x

Version 3.0.0 moves Twitter sign in on Android and iOS from the `twitter_login` package to
Firebase's own provider flow. Your code keeps compiling, but sign in fails at runtime on those two
platforms until you update the configuration below. macOS, Windows and the web are unaffected.

#### What you must change

1. **Callback URL.** In the [X developer portal](https://developer.twitter.com/en/portal/projects-and-apps),
set your app's Callback URL to the Firebase auth handler:

```
https://<your-project-id>.firebaseapp.com/__/auth/handler
```

X accepts several callback URLs, so you can add this alongside the custom scheme you use today
and keep an older build of your app working while you roll out.

2. **iOS.** Add your encoded app ID as a URL scheme in `ios/Runner/Info.plist`, as described in the
setup section above. Without it, the sign in sheet completes but never returns to your app.

3. **Android.** Register your app's SHA-1 fingerprint in the Firebase Console, then **re-download
`google-services.json`**. Adding the fingerprint alone is not enough, because the certificate
hash is embedded in that file when you download it. If you skip either step, sign in fails with:

```
There was an error while trying to get your package certificate hash.
```

4. **Remove `twitter_login`** from your `pubspec.yaml` if you depended on it directly, along with
the callback intent filter it required in `AndroidManifest.xml`.

5. **`apiKey` and `apiSecretKey` are now optional.** Remove them unless you ship for macOS or
Windows, which still perform the OAuth 1.0a flow in process and still need them. They are
ignored on Android, iOS and the web.

#### Behaviour changes

Firebase signs the user in as part of returning the credential, which changes three things on
Android and iOS:

- `AuthAction.none` now fails with a `FirebaseAuthException` instead of handing you a credential
without signing in. There is no way to obtain the credential without also creating a session.
- The credential passed to `onCredentialLinked` is a plain `AuthCredential` rather than an
`OAuthCredential`. It carries no `secret` and cannot be cast to `OAuthCredential`.
- `redirectUri` is ignored. Firebase always completes through its own auth handler. It is still
honoured on macOS and Windows.

Cancelling sign in returns you to the app silently, with no error shown, which matches the previous
behaviour.

#### Known limitation on Android

Firebase usually opens the sign in link in a Chrome Custom Tab inside your app's task, and
dismissing it returns to your app. Occasionally it opens the full browser in its own task instead.
If the user abandons the flow there, the pending operation never resolves and further attempts fail
with:

```
A headful operation is already in progress. Please wait for that to finish.
```

Restarting the app clears it. This comes from the Firebase Android SDK rather than
`firebase_ui_oauth_twitter`, and there is nothing the Dart layer can do about a sign in the SDK
never completes.

See [Custom screens section](#custom-screens) to learn how to use a button on your custom screen.

## Custom screens
Expand Down
8 changes: 8 additions & 0 deletions packages/firebase_ui_auth/example/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@
<string>fb128693022464535</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>app-1-406099696497-ios-24bb8dcaefc434a73574d0</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
Expand Down
1 change: 0 additions & 1 deletion packages/firebase_ui_auth/example/lib/config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,5 @@ const GOOGLE_REDIRECT_URI =

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';
13 changes: 10 additions & 3 deletions packages/firebase_ui_auth/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ Future<void> main() async {
GoogleProvider(clientId: GOOGLE_CLIENT_ID),
AppleProvider(),
FacebookProvider(clientId: FACEBOOK_CLIENT_ID),
// apiKey and apiSecretKey are only used on macOS and Windows. Android and
// iOS sign in through Firebase, which holds the credentials itself.
TwitterProvider(
apiKey: TWITTER_API_KEY,
apiSecretKey: TWITTER_API_SECRET_KEY,
redirectUri: TWITTER_REDIRECT_URI,
),
]);

Expand Down Expand Up @@ -134,10 +135,16 @@ class FirebaseAuthUIExample extends StatelessWidget {
};

switch (user) {
case User(emailVerified: true):
Navigator.pushReplacementNamed(context, '/profile');
case User(emailVerified: false, email: final String _):
Navigator.pushNamed(context, '/verify-email');
// Providers are not obliged to return an email. Twitter only
// does when the app requests it, and such a user is never
// emailVerified, so without this they matched no case and
// the screen sat there after a successful sign in.
case User():
Navigator.pushReplacementNamed(context, '/profile');
case null:
break;
}
}),
mfaAction,
Expand Down
2 changes: 0 additions & 2 deletions packages/firebase_ui_auth/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ dependencies:
firebase_ui_oauth_facebook: ^2.1.1
firebase_ui_oauth_google: ^2.1.1
firebase_ui_oauth_twitter: ^2.1.1
# This and twitter oauth package need to depend on git main directly due to namespace build error on android.
twitter_login: ^4.4.2
dev_dependencies:
drive: ^1.0.0-1.0.nullsafety.5
firebase_ui_shared: ^1.5.0
Expand Down
7 changes: 7 additions & 0 deletions packages/firebase_ui_auth/lib/src/email_verification.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,14 @@ class EmailVerificationController extends ValueNotifier<EmailVerificationState>
}

/// Reloads firebase user and updates the [state].
///
/// Does nothing when no user is signed in. This runs on every app resume,
/// which includes resuming with no user at all: returning from a cancelled
/// OAuth sign in brings the app back to the foreground while
/// [fba.FirebaseAuth.currentUser] is still null.
Future<void> reload() async {
if (auth.currentUser == null) return;

await user.reload();

if (user.email == null) {
Expand Down
29 changes: 29 additions & 0 deletions packages/firebase_ui_auth/test/email_verification_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2022, 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_ui_auth/firebase_ui_auth.dart';
import 'package:flutter_test/flutter_test.dart';

import 'test_utils.dart';

void main() {
// The controller registers a lifecycle observer in its constructor.
TestWidgetsFlutterBinding.ensureInitialized();

group('EmailVerificationController', () {
test('reload does nothing when no user is signed in', () async {
final auth = MockAuth();
final controller = EmailVerificationController(
auth,
appLinks: MockAppLinks(),
);

// reload() runs on every app resume, including resumes that happen with
// no user at all, such as returning from a cancelled OAuth sign in.
// It used to read auth.currentUser! and throw an unhandled TypeError.
await expectLater(controller.reload(), completes);
expect(controller.state, EmailVerificationState.unresolved);
});
});
}
5 changes: 1 addition & 4 deletions packages/firebase_ui_oauth/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,7 @@ class _ContentState extends State<Content> {
GoogleProvider(clientId: '', redirectUri: '', scopes: []),
'Sign in with Google',
),
_button(
TwitterProvider(apiKey: '', apiSecretKey: '', redirectUri: ''),
'Sign in with Twitter',
),
_button(TwitterProvider(), 'Sign in with Twitter'),
],
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class TwitterSignInButton extends _TwitterSignInButton {
const TwitterSignInButton({
super.key,
required super.loadingIndicator,
required super.apiKey,
required super.apiSecretKey,
super.apiKey,
super.apiSecretKey,
super.redirectUri,
super.action = null,
super.auth,
Expand All @@ -35,8 +35,8 @@ class TwitterSignInButton extends _TwitterSignInButton {
class TwitterSignInIconButton extends _TwitterSignInButton {
const TwitterSignInIconButton({
super.key,
required super.apiKey,
required super.apiSecretKey,
super.apiKey,
super.apiSecretKey,
required super.loadingIndicator,
super.action = null,
super.auth,
Expand Down Expand Up @@ -74,16 +74,16 @@ class _TwitterSignInButton extends StatelessWidget {
final DifferentProvidersFoundCallback? onDifferentProvidersFound;
final SignedInCallback? onSignedIn;
final double size;
final String apiKey;
final String apiSecretKey;
final String? apiKey;
final String? apiSecretKey;
final String? redirectUri;
final void Function(Exception exception)? onError;
final VoidCallback? onCanceled;

const _TwitterSignInButton({
super.key,
required this.apiKey,
required this.apiSecretKey,
this.apiKey,
this.apiSecretKey,
required this.loadingIndicator,
String? label,
bool? overrideDefaultTapAction,
Expand Down
Loading
Loading