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
44 changes: 40 additions & 4 deletions packages/firebase_ui_oauth_apple/lib/src/provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,29 @@ class AppleProvider extends OAuthProvider {
void mobileSignIn(AuthAction action) {
authListener.onBeforeSignIn();

if (action == AuthAction.link) {
auth.currentUser
?.linkWithProvider(firebaseAuthProvider)
// Linking is also used to upgrade an anonymous user, so that the
// anonymous uid survives the sign in.
if (action == AuthAction.link || shouldUpgradeAnonymous) {
Comment thread
Mastersam07 marked this conversation as resolved.
final currentUser = auth.currentUser;

// Only AuthAction.link can reach this with no user, since
// shouldUpgradeAnonymous is false when currentUser is null. Reporting it
// matters because a null-shorting call would leave the flow stuck in its
// loading state with no error and no completion.
if (currentUser == null) {
authListener.onError(
fba.FirebaseAuthException(
code: 'no-current-user',
message:
'AuthAction.link requires a signed in user to link the '
'Apple credential to, but FirebaseAuth.currentUser is null.',
),
);
return;
}

currentUser
.linkWithProvider(firebaseAuthProvider)
Comment thread
demolaf marked this conversation as resolved.
.then(_onLinked)
.catchError(authListener.onError);
return;
Expand Down Expand Up @@ -97,6 +117,22 @@ class AppleProvider extends OAuthProvider {
}

void _onLinked(fba.UserCredential userCredential) {
authListener.onCredentialLinked(userCredential.credential!);
final credential = userCredential.credential;

// Nullable on every platform, and a force unwrap here would throw inside
// .then, reaching onError as an Error and hanging the flow.
if (credential == null) {
authListener.onError(
fba.FirebaseAuthException(
code: 'missing-credential',
message:
'The Apple account was linked, but Firebase returned no '
'credential for it.',
),
);
return;
}

authListener.onCredentialLinked(credential);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import '../utils.dart';

void main() async {
final provider = AppleProvider();
late fba.FirebaseAuth auth;
late MockAuth auth;
late MockProvider fbProvider;

const labels = DefaultLocalizations();
Expand Down Expand Up @@ -95,6 +95,61 @@ void main() async {
expect(user.displayName, 'Test User');
expect(user.email, 'test@test.com');
});

testWidgets('links the credential when the user is anonymous', (
tester,
) async {
final anonymousUser = MockAnonymousUser();
auth.currentUserOverride = anonymousUser;

// AuthAction.signIn is explicit: the flow would otherwise resolve to
// AuthAction.link on its own whenever currentUser is non-null, and the
// test would pass without shouldUpgradeAnonymous being consulted.
await render(
tester,
OAuthProviderButton(
provider: provider,
auth: auth,
action: AuthAction.signIn,
),
);

final button = find.byType(OAuthProviderButtonBase);
await tester.tap(button);
await tester.pumpAndSettle();

verify(anonymousUser.linkWithProvider(fbProvider)).called(1);
verifyNever(auth.signInWithProvider(fbProvider));
});

testWidgets('reports an error when linking with no signed in user', (
tester,
) async {
final listener = MockListener();

await render(
tester,
AuthStateListener<OAuthController>(
listener: (oldState, state, controller) {
listener(state);
return null;
},
child: OAuthProviderButton(
provider: provider,
auth: auth,
action: AuthAction.link,
),
),
);

final button = find.byType(OAuthProviderButtonBase);
await tester.tap(button);
await tester.pumpAndSettle();

final result = verify(listener.call(captureAny));
expect(result.captured.last, isA<AuthFailed>());
verifyNever(auth.signInWithProvider(fbProvider));
});
},
skip: !provider.supportsPlatform(defaultTargetPlatform),
);
Expand Down Expand Up @@ -124,18 +179,45 @@ class MockUser extends Mock implements fba.User {

@override
String? get email => 'test@test.com';

@override
bool get isAnonymous => false;
}

class MockAnonymousUser extends Mock implements fba.User {
@override
bool get isAnonymous => true;

@override
Future<fba.UserCredential> linkWithProvider(Object provider) async {
return super.noSuchMethod(
Invocation.method(#linkWithProvider, [provider]),
returnValue: Future.value(MockCredential()),
returnValueForMissingStub: Future.value(MockCredential()),
);
}
}

class MockAuthCredential extends Mock implements fba.AuthCredential {}

class MockCredential extends Mock implements fba.UserCredential {
@override
fba.User? get user => MockUser();

@override
fba.AuthCredential? get credential => MockAuthCredential();
}

class MockProvider extends Mock implements fba.AppleAuthProvider {}

class MockApp extends Mock implements FirebaseApp {}

class MockAuth extends Mock implements fba.FirebaseAuth {
fba.User? currentUserOverride;

@override
fba.User? get currentUser => currentUserOverride;

@override
Future<fba.UserCredential> signInWithProvider(Object provider) async {
return super.noSuchMethod(
Expand Down
Loading