Skip to content
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
7 changes: 5 additions & 2 deletions example/android/app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
plugins {
id "com.android.application"
// START: FlutterFire Configuration
id 'com.google.gms.google-services'
// END: FlutterFire Configuration
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
Expand All @@ -8,7 +11,7 @@ plugins {
android {
namespace = "com.example.phone_auth_handler_demo"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
ndkVersion = flutter.ndkVersion // ndkVersion = "29.0.14206865"

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
Expand All @@ -24,7 +27,7 @@ android {
applicationId = "com.example.phone_auth_handler_demo"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 23
minSdkVersion = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
Expand Down
8 changes: 0 additions & 8 deletions example/android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.google.gms:google-services:4.3.10'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

allprojects {
repositories {
google()
Expand Down
2 changes: 1 addition & 1 deletion example/android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
7 changes: 5 additions & 2 deletions example/android/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ pluginManagement {

plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
id "com.android.application" version "8.11.0" apply false
// START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.3.15" apply false
// END: FlutterFire Configuration
id "org.jetbrains.kotlin.android" version "2.2.20" apply false
}

include ":app"
2 changes: 1 addition & 1 deletion example/lib/utils/app_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class AppTheme {
// selectionColor: accentColor?.withOpacity(0.75),
// selectionHandleColor: accentColor?.withOpacity(0.75),
// ),
dialogTheme: DialogTheme(
dialogTheme: DialogThemeData(
elevation: _defaultElevation,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
Expand Down
8 changes: 4 additions & 4 deletions example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ dependencies:
flutter:
sdk: flutter

pinput: ^5.0.0
firebase_core: ^3.6.0
easy_container: ^1.0.5
pinput: ^6.0.2
firebase_core: ^4.7.0
easy_container: ^1.0.5+1
intl_phone_field: ^3.2.0
firebase_phone_auth_handler:
path: "../"

dev_dependencies:
flutter_lints: ^5.0.0
flutter_lints: ^6.0.0


flutter:
Expand Down
43 changes: 24 additions & 19 deletions lib/src/auth_controller.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
part of 'auth_handler.dart';

class FirebasePhoneAuthController extends ChangeNotifier {
/// Internal constructor for injection.
FirebasePhoneAuthController({FirebaseAuth? auth})
: _auth = auth ?? FirebaseAuth.instance;

final FirebaseAuth _auth;

static FirebasePhoneAuthController _of(
BuildContext context, {
bool listen = true,
}) =>
BuildContext context, {
bool listen = true,
}) =>
Provider.of<FirebasePhoneAuthController>(context, listen: listen);

/// {@macro autoRetrievalTimeOutDuration}
static const kAutoRetrievalTimeOutDuration = Duration(minutes: 1);

/// Firebase auth instance using the default [FirebaseApp].
static final FirebaseAuth _auth = FirebaseAuth.instance;

/// Web confirmation result for OTP.
ConfirmationResult? _webConfirmationResult;

Expand Down Expand Up @@ -93,7 +96,7 @@ class FirebasePhoneAuthController extends ChangeNotifier {
/// button, to let user request a new OTP.
Duration get otpExpirationTimeLeft {
final otpTickDuration = Duration(
seconds: (_otpExpirationTimer?.tick ?? 0),
seconds: _otpExpirationTimer?.tick ?? 0,
);
return _otpExpirationDuration - otpTickDuration;
}
Expand All @@ -106,7 +109,7 @@ class FirebasePhoneAuthController extends ChangeNotifier {
/// the OTP, and requires user to manually enter it.
Duration get autoRetrievalTimeLeft {
final otpTickDuration = Duration(
seconds: (_otpAutoRetrievalTimer?.tick ?? 0),
seconds: _otpAutoRetrievalTimer?.tick ?? 0,
);
return _autoRetrievalTimeOutDuration - otpTickDuration;
}
Expand Down Expand Up @@ -143,7 +146,7 @@ class FirebasePhoneAuthController extends ChangeNotifier {
try {
if (kIsWeb) {
final userCredential = await _webConfirmationResult!.confirm(otp);
return await _loginUser(
return _loginUser(
userCredential: userCredential,
autoVerified: false,
);
Expand All @@ -152,7 +155,7 @@ class FirebasePhoneAuthController extends ChangeNotifier {
verificationId: _verificationId!,
smsCode: otp,
);
return await _loginUser(
return _loginUser(
authCredential: credential,
autoVerified: false,
);
Expand Down Expand Up @@ -181,16 +184,18 @@ class FirebasePhoneAuthController extends ChangeNotifier {
/// code send callback to be fired, and [sendOTP] will complete only after
/// that callback is fired. Not applicable on web.
Future<bool> sendOTP({bool shouldAwaitCodeSend = true}) async {
if (_phoneNumber == null) return false;

Completer? codeSendCompleter;

codeSent = false;
await Future.delayed(Duration.zero, notifyListeners);

verificationCompletedCallback(AuthCredential authCredential) async {
void verificationCompletedCallback(AuthCredential authCredential) async {
await _loginUser(authCredential: authCredential, autoVerified: true);
}

verificationFailedCallback(FirebaseAuthException authException) {
void verificationFailedCallback(FirebaseAuthException authException) {
final stackTrace = authException.stackTrace ?? StackTrace.current;

if (codeSendCompleter != null && !codeSendCompleter.isCompleted) {
Expand All @@ -199,10 +204,10 @@ class FirebasePhoneAuthController extends ChangeNotifier {
_onLoginFailed?.call(authException, stackTrace);
}

codeSentCallback(
String verificationId, [
int? forceResendingToken,
]) async {
Future<void> codeSentCallback(
String verificationId, [
int? forceResendingToken,
]) async {
_verificationId = verificationId;
_forceResendingToken = forceResendingToken;
codeSent = true;
Expand All @@ -213,7 +218,7 @@ class FirebasePhoneAuthController extends ChangeNotifier {
_setTimer();
}

codeAutoRetrievalTimeoutCallback(String verificationId) {
void codeAutoRetrievalTimeoutCallback(String verificationId) {
_verificationId = verificationId;
}

Expand Down Expand Up @@ -309,7 +314,7 @@ class FirebasePhoneAuthController extends ChangeNotifier {
void _setTimer() {
_otpExpirationTimer = Timer.periodic(
const Duration(seconds: 1),
(timer) {
(timer) {
if (timer.tick == _otpExpirationDuration.inSeconds) {
_otpExpirationTimer?.cancel();
}
Expand All @@ -320,7 +325,7 @@ class FirebasePhoneAuthController extends ChangeNotifier {
);
_otpAutoRetrievalTimer = Timer.periodic(
const Duration(seconds: 1),
(timer) {
(timer) {
if (timer.tick == _autoRetrievalTimeOutDuration.inSeconds) {
_otpAutoRetrievalTimer?.cancel();
}
Expand Down
8 changes: 4 additions & 4 deletions lib/src/auth_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,12 @@ class FirebasePhoneAuthHandler extends StatefulWidget {
class _FirebasePhoneAuthHandlerState extends State<FirebasePhoneAuthHandler> {
@override
void initState() {
(() async {
unawaited(() async {
final con = FirebasePhoneAuthController._of(context, listen: false);

RecaptchaVerifier? captcha;
if (widget.recaptchaVerifierForWebProvider != null) {
captcha = widget.recaptchaVerifierForWebProvider!(kIsWeb);
if (kIsWeb && widget.recaptchaVerifierForWebProvider != null) {
captcha = widget.recaptchaVerifierForWebProvider!(true);
}

con._setData(
Expand All @@ -204,7 +204,7 @@ class _FirebasePhoneAuthHandlerState extends State<FirebasePhoneAuthHandler> {
);

if (widget.sendOtpOnInitialize) await con.sendOTP();
})();
}());
super.initState();
}

Expand Down
7 changes: 6 additions & 1 deletion lib/src/auth_provider.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_phone_auth_handler/src/auth_handler.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
Expand All @@ -8,15 +9,19 @@ class FirebasePhoneAuthProvider extends StatelessWidget {
const FirebasePhoneAuthProvider({
super.key,
required this.child,
this.auth,
});

/// The child of the widget.
final Widget child;

/// The [FirebaseAuth] instance to use.
final FirebaseAuth? auth;

@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<FirebasePhoneAuthController>(
create: (_) => FirebasePhoneAuthController(),
create: (_) => FirebasePhoneAuthController(auth: auth),
child: child,
);
}
Expand Down
10 changes: 7 additions & 3 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,15 @@ dependencies:
flutter:
sdk: flutter

provider: ^6.1.2
firebase_auth: ^5.3.1
provider: ^6.1.5+1
firebase_auth: ^6.4.0

dev_dependencies:
flutter_lints: ^5.0.0
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
mocktail: ^1.0.5
firebase_auth_mocks: ^0.15.1

false_secrets:
- /example/android/app/google-services.json
Expand Down
55 changes: 55 additions & 0 deletions test/firebase_phone_auth_controller_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@

import 'package:firebase_phone_auth_handler/firebase_phone_auth_handler.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

import 'mocks.dart';

void main() {
late MockFirebaseAuth auth;
late FirebasePhoneAuthController controller;

setUpAll(() {
registerFallbackValue(MockAuthCredential());
registerFallbackValue(MockFirebaseAuthException());
registerFallbackValue(const Duration(seconds: 30));
});

setUp(() {
auth = MockFirebaseAuth();
when(() => auth.signOut()).thenAnswer((_) async => {});
controller = FirebasePhoneAuthController(auth: auth);
});

group('FirebasePhoneAuthController - Verification Logic', () {
test('sendOTP fails if phoneNumber is not set', () async {
// Expect error or false because _phoneNumber is null initially
final result = await controller.sendOTP();
expect(result, isFalse);
});

test('verifyOtp returns false if verificationId is missing', () async {
final result = await controller.verifyOtp('123456');
expect(result, isFalse);
});
});

group('FirebasePhoneAuthController - Basic State', () {
test('initial state is correct', () {
expect(controller.codeSent, isFalse);
expect(controller.isSendingCode, isTrue);
expect(controller.isOtpExpired, isTrue);
});

test('clear() resets state', () {
controller.clear();
expect(controller.codeSent, isFalse);
expect(controller.isOtpExpired, isTrue);
});

test('signOut() calls firebase auth signOut', () async {
await controller.signOut();
verify(() => auth.signOut()).called(1);
});
});
}
Loading