Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(flutter_firebase_login): implement sign in with apple #2480

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions examples/flutter_firebase_login/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Example Flutter app built with `flutter_bloc` to implement login using Firebase.
## Features

- Sign in with Google
- Sign in with Apple
- Sign up with email and password
- Sign in with email and password

Expand Down
11 changes: 6 additions & 5 deletions examples/flutter_firebase_login/lib/app/bloc_observer.dart
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
// ignore_for_file: avoid_print
import 'dart:developer';

import 'package:bloc/bloc.dart';

class AppBlocObserver extends BlocObserver {
@override
void onEvent(Bloc bloc, Object? event) {
super.onEvent(bloc, event);
print(event);
log('event: $event');
}

@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
print(error);
log('error: $error');
super.onError(bloc, error, stackTrace);
}

@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
print(change);
log('change: $change');
}

@override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print(transition);
log('transition: $transition');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ List<Page> onGenerateAppViewPages(AppStatus state, List<Page<dynamic>> pages) {
case AppStatus.authenticated:
return [HomePage.page()];
case AppStatus.unauthenticated:
default:
return [LoginPage.page()];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,22 @@ class LoginCubit extends Cubit<LoginState> {

void emailChanged(String value) {
final email = Email.dirty(value);
emit(state.copyWith(
email: email,
status: Formz.validate([email, state.password]),
));
emit(
state.copyWith(
email: email,
status: Formz.validate([email, state.password]),
),
);
}

void passwordChanged(String value) {
final password = Password.dirty(value);
emit(state.copyWith(
password: password,
status: Formz.validate([state.email, password]),
));
emit(
state.copyWith(
password: password,
status: Formz.validate([state.email, password]),
),
);
}

Future<void> logInWithCredentials() async {
Expand Down Expand Up @@ -60,4 +64,17 @@ class LoginCubit extends Cubit<LoginState> {
emit(state.copyWith(status: FormzStatus.submissionFailure));
}
}

Future<void> logInWithApple() async {
emit(state.copyWith(status: FormzStatus.submissionInProgress));
try {
await _authenticationRepository.logInWithApple();
emit(state.copyWith(status: FormzStatus.submissionSuccess));
} on Exception {
emit(state.copyWith(status: FormzStatus.submissionFailure));
// ignore: avoid_catching_errors
} on NoSuchMethodError {
emit(state.copyWith(status: FormzStatus.pure));
}
}
}
23 changes: 23 additions & 0 deletions examples/flutter_firebase_login/lib/login/view/login_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ class LoginForm extends StatelessWidget {
_LoginButton(),
const SizedBox(height: 8),
_GoogleLoginButton(),
const SizedBox(height: 8),
_AppleLoginButton(),
const SizedBox(height: 4),
_SignUpButton(),
],
Expand Down Expand Up @@ -141,6 +143,27 @@ class _GoogleLoginButton extends StatelessWidget {
}
}

class _AppleLoginButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ElevatedButton.icon(
key: const Key('loginForm_appleLogin_raisedButton'),
label: const Text(
'SIGN IN WITH Apple',
style: TextStyle(color: Colors.white),
),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
primary: Colors.black,
),
icon: const Icon(FontAwesomeIcons.apple, color: Colors.white),
onPressed: () => context.read<LoginCubit>().logInWithApple(),
);
}
}

class _SignUpButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ class SignUpCubit extends Cubit<SignUpState> {

void emailChanged(String value) {
final email = Email.dirty(value);
emit(state.copyWith(
email: email,
status: Formz.validate([
email,
state.password,
state.confirmedPassword,
]),
));
emit(
state.copyWith(
email: email,
status: Formz.validate([
email,
state.password,
state.confirmedPassword,
]),
),
);
}

void passwordChanged(String value) {
Expand All @@ -29,30 +31,35 @@ class SignUpCubit extends Cubit<SignUpState> {
password: password.value,
value: state.confirmedPassword.value,
);
emit(state.copyWith(
password: password,
confirmedPassword: confirmedPassword,
status: Formz.validate([
state.email,
password,
confirmedPassword,
]),
));

emit(
state.copyWith(
password: password,
confirmedPassword: confirmedPassword,
status: Formz.validate([
state.email,
password,
state.confirmedPassword,
]),
),
);
}

void confirmedPasswordChanged(String value) {
final confirmedPassword = ConfirmedPassword.dirty(
password: state.password.value,
value: value,
);
emit(state.copyWith(
confirmedPassword: confirmedPassword,
status: Formz.validate([
state.email,
state.password,
confirmedPassword,
]),
));
emit(
state.copyWith(
confirmedPassword: confirmedPassword,
status: Formz.validate([
state.email,
state.password,
confirmedPassword,
]),
),
);
}

Future<void> signUpFormSubmitted() async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import 'dart:async';

import 'package:authentication_repository/authentication_repository.dart';
import 'package:cache/cache.dart';
import 'package:crypto_api/crypto_api.dart';
import 'package:firebase_auth/firebase_auth.dart' as firebase_auth;
import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:google_sign_in/google_sign_in.dart';
import 'package:meta/meta.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';

/// {@template sign_up_with_email_and_password_failure}
/// Thrown if during the sign up process if a failure occurs.
Expand Down Expand Up @@ -145,6 +147,9 @@ class LogInWithGoogleFailure implements Exception {
final String message;
}

/// Thrown during the sign in with apple process if a failure occurs.
class LogInWithAppleFailure implements Exception {}

/// Thrown during the logout process if a failure occurs.
class LogOutFailure implements Exception {}

Expand All @@ -157,13 +162,16 @@ class AuthenticationRepository {
CacheClient? cache,
firebase_auth.FirebaseAuth? firebaseAuth,
GoogleSignIn? googleSignIn,
CryptoApi? cryptoApi,
}) : _cache = cache ?? CacheClient(),
_firebaseAuth = firebaseAuth ?? firebase_auth.FirebaseAuth.instance,
_googleSignIn = googleSignIn ?? GoogleSignIn.standard();
_googleSignIn = googleSignIn ?? GoogleSignIn.standard(),
_crypto = cryptoApi ?? CryptoApi();

final CacheClient _cache;
final firebase_auth.FirebaseAuth _firebaseAuth;
final GoogleSignIn _googleSignIn;
final CryptoApi _crypto;

/// Whether or not the current environment is web
/// Should only be overriden for testing purposes. Otherwise,
Expand Down Expand Up @@ -239,6 +247,38 @@ class AuthenticationRepository {
}
}

/// Starts the Sign In with Apple Flow.
///
/// Throws a [logInWithApple] if an exception occurs.
Future<void> logInWithApple() async {
// To prevent replay attacks with the credential returned from Apple, we
// include a nonce in the credential request. When signing in with
// Firebase, the nonce in the id token returned by Apple, is expected to
// match the sha256 hash of `rawNonce`.
final rawNonce = _crypto.generateNonce();
final nonce = _crypto.sha256ofString(rawNonce);

try {
final appleCredential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
nonce: nonce,
);

// Create an `OAuthCredential` from the credential returned by Apple.
final credential = firebase_auth.OAuthProvider('apple.com').credential(
idToken: appleCredential.identityToken,
rawNonce: rawNonce,
);

await _firebaseAuth.signInWithCredential(credential);
} on Exception {
throw LogInWithAppleFailure();
}
}

/// Signs in with the provided [email] and [password].
///
/// Throws a [LogInWithEmailAndPasswordFailure] if an exception occurs.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ environment:
dependencies:
cache:
path: ../cache
crypto_api:
path: ../crypto_api
equatable: ^2.0.3
firebase_auth: ^3.0.2
firebase_core: ^1.5.0
flutter:
sdk: flutter
google_sign_in: ^5.0.7
meta: ^1.3.0
sign_in_with_apple: ^3.0.0
very_good_analysis: ^2.3.0

dev_dependencies:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ void main() {
test('uses value equality', () {
expect(
User(email: email, id: id),
equals(User(email: email, id: id)),
User(email: email, id: id),
);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: cache
description: A simple in memory cache made for dart
description: A simple in memory cache made for Dart.
version: 1.0.0
publish_to: none

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:very_good_analysis/analysis_options.2.3.0.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
library crypto_api;

import 'dart:convert';
import 'dart:math';

import 'package:crypto/crypto.dart';

/// {@template crypto_api}
/// A crypto API with common encryption methods.
/// {@endtemplate}
class CryptoApi {
/// {@macro crypto_api}
CryptoApi({Random? random}) : _random = random ?? Random.secure();

final Random _random;

/// Set of characters used to generate encrypted values.
static const _charset =
'''0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._''';

/// Generates a cryptographically secure random nonce,
/// to be included in a credential request.
String generateNonce([int length = 32]) {
return List.generate(
length,
(_) => _charset[_random.nextInt(_charset.length)],
).join();
}

/// Returns the sha256 hash of [input] in hex notation.
String sha256ofString(String input) {
final bytes = utf8.encode(input);
final digest = sha256.convert(bytes);
return digest.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: crypto_api
description: A crypto API with common encryption methods.
version: 1.0.0
publish_to: none

environment:
sdk: ">=2.13.0 <3.0.0"

dependencies:
crypto: ^3.0.1

dev_dependencies:
test: ^1.17.11
very_good_analysis: ^2.3.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'package:crypto_api/crypto_api.dart';
import 'package:test/test.dart';

void main() {
group('CryptoApi', () {
test('instanciates', () {
test('can be instantiated', () {
expect(CryptoApi(), isNotNull);
});
});
});
}
Loading