Skip to content
This repository was archived by the owner on Apr 4, 2023. It is now read-only.

Commit d3c5b1b

Browse files
Add Email Link Authentication #665 (also needs fetchSignInMethodsForEmail)
1 parent 5fcca8c commit d3c5b1b

File tree

7 files changed

+194
-42
lines changed

7 files changed

+194
-42
lines changed

demo/app/app.css

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
.tab-content {
66
color: #808080;
7-
padding: 20 20 50 20;
7+
padding: 10 10 50 10;
88
}
99

1010
.title {
@@ -31,8 +31,8 @@ label {
3131

3232
button {
3333
background-color: #6494AA;
34-
padding: 8 12;
35-
margin: 4 10;
34+
padding: 8;
35+
margin: 4 8;
3636
font-size: 13;
3737
border-radius: 4;
3838
}
@@ -75,4 +75,4 @@ button {
7575

7676
.button-invites {
7777
background-color: #1832d5;
78-
}
78+
}

demo/app/main-page.xml

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,18 @@
2323
class="tab-content">
2424
<Button row="0" colSpan="2" text="init firebase - do this first" tap="{{ doWebInit }}" class="button button-positive"/>
2525

26-
<Label row="3" colSpan="2" text="Authentication" class="subtitle"/>
26+
<Label row="1" colSpan="2" text="Authentication" class="subtitle"/>
27+
28+
<Button row="2" col="0" text="anonymous login" tap="{{ doWebLoginAnonymously }}" class="button"/>
29+
<Button row="2" col="1" text="passwd login" tap="{{ doWebLoginByPassword }}" class="button"/>
2730

28-
<Button row="4" col="0" text="anonymous login" tap="{{ doWebLoginAnonymously }}" class="button"/>
29-
<Button row="4" col="1" text="fetch providers" tap="{{ doWebFetchProvidersForEmail }}" class="button"/>
31+
<Button row="3" col="0" text="providers for email" tap="{{ doWebFetchProvidersForEmail }}" class="button"/>
32+
<Button row="3" col="1" text="email sign-in methods" tap="{{ doWebFetchSignInMethodsForEmail }}" class="button"/>
3033

31-
<Button row="5" col="0" text="passwd login" tap="{{ doWebLoginByPassword }}" class="button"/>
32-
<Button row="5" col="1" text="create pwd user" tap="{{ doWebCreateUser }}" class="button"/>
34+
<Button row="4" col="0" text="create pwd user" tap="{{ doWebCreateUser }}" class="button"/>
35+
<Button row="4" col="1" text="get current user" tap="{{ doWebGetCurrentUser }}" class="button"/>
3336

34-
<Button row="6" col="0" text="get current user" tap="{{ doWebGetCurrentUser }}" class="button"/>
35-
<Button row="6" col="1" text="logout" tap="{{ doWebLogout }}" class="button"/>
37+
<Button row="5" colSpan="2" text="logout" tap="{{ doWebLogout }}" class="button"/>
3638

3739
<Label row="7" col="0" text="User email/phone:" class="message"/>
3840
<Label row="7" col="1" text="{{ userEmailOrPhone }}" class="message" textWrap="true"/>
@@ -82,19 +84,19 @@
8284
<Label row="3" colSpan="2" text="Authentication" class="subtitle"/>
8385

8486
<Button row="4" col="0" text="anonymous login" tap="{{ doLoginAnonymously }}" class="button"/>
85-
<Button row="4" col="1" text="fetch providers" tap="{{ doFetchProvidersForEmail }}" class="button"/>
87+
<Button row="4" col="1" text="passwd login" tap="{{ doLoginByPassword }}" class="button"/>
8688

87-
<Button row="5" col="0" text="passwd login" tap="{{ doLoginByPassword }}" class="button"/>
88-
<Button row="5" col="1" text="phone login" tap="{{ doLoginByPhone }}" class="button"/>
89+
<Button row="5" col="0" text="create pwd user" tap="{{ doCreateUser }}" class="button"/>
90+
<Button row="5" col="1" text="reset pwd" tap="{{ doResetPassword }}" class="button"/>
8991

90-
<Button row="6" col="0" text="create pwd user" tap="{{ doCreateUser }}" class="button"/>
91-
<Button row="6" col="1" text="reset pwd" tap="{{ doResetPassword }}" class="button"/>
92+
<Button row="6" col="0" text="Google login" tap="{{ doLoginByGoogle }}" class="button"/>
93+
<Button row="6" col="1" text="Facebook login" tap="{{ doLoginByFacebook }}" class="button"/>
9294

93-
<Button row="7" col="0" text="Google login" tap="{{ doLoginByGoogle }}" class="button"/>
94-
<Button row="7" col="1" text="Facebook login" tap="{{ doLoginByFacebook }}" class="button"/>
95+
<Button row="7" col="0" text="phone login" tap="{{ doLoginByPhone }}" class="button"/>
96+
<Button row="7" col="1" text="update profile" tap="{{ doUpdateProfile }}" class="button"/>
9597

96-
<Button row="8" col="0" text="get current user" tap="{{ doGetCurrentUser }}" class="button"/>
97-
<Button row="8" col="1" text="update profile" tap="{{ doUpdateProfile }}" class="button"/>
98+
<Button row="8" col="0" text="providers for email" tap="{{ doFetchProvidersForEmail }}" class="button"/>
99+
<Button row="8" col="1" text="email sign-in methods" tap="{{ doFetchSignInMethodsForEmail }}" class="button"/>
98100

99101
<Button row="9" col="0" text="re-auth passwd" tap="{{ doReauthenticatePwdUser }}" class="button"/>
100102
<Button row="9" col="1" text="re-auth Facebook" tap="{{ doReauthenticateFacebookUser }}" class="button"/>
@@ -108,7 +110,8 @@
108110
<Button row="12" col="0" text="email login link" tap="{{ doLoginByEmailLink }}" class="button"/>
109111
<Button row="12" col="1" text="send email conf" tap="{{ doSendEmailVerification }}" class="button"/>
110112

111-
<Button row="13" colSpan="2" text="logout" tap="{{ doLogout }}" class="button"/>
113+
<Button row="13" col="0" text="get current user" tap="{{ doGetCurrentUser }}" class="button"/>
114+
<Button row="13" col="1" text="logout" tap="{{ doLogout }}" class="button"/>
112115

113116
<Label row="14" colSpan="2" text="Methods on path /users" class="subtitle"/>
114117

demo/app/main-view-model.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,35 @@ export class HelloWorldModel extends Observable {
105105
);
106106
}
107107

108+
public doWebFetchSignInMethodsForEmail(): void {
109+
const user = firebaseWebApi.auth().currentUser;
110+
if (!user || !user.email) {
111+
alert({
112+
title: "Can't fetch providers",
113+
message: "No user with an emailaddress logged in.",
114+
okButtonText: "OK, makes sense.."
115+
});
116+
return;
117+
}
118+
119+
firebaseWebApi.auth().fetchSignInMethodsForEmail(user.email).then(
120+
result => {
121+
alert({
122+
title: `Sign-in methods for ${user.email}`,
123+
message: JSON.stringify(result), // ["password"], ["emailLink"], or ["password", "emailLink']
124+
okButtonText: "Thanks!"
125+
});
126+
},
127+
errorMessage => {
128+
alert({
129+
title: "Sign-in methods for Email error",
130+
message: errorMessage,
131+
okButtonText: "OK, pity.."
132+
});
133+
}
134+
);
135+
}
136+
108137
public doWebLogout(): void {
109138
firebaseWebApi.auth().signOut()
110139
.then(() => {
@@ -661,6 +690,37 @@ export class HelloWorldModel extends Observable {
661690
});
662691
}
663692

693+
public doFetchSignInMethodsForEmail(): void {
694+
firebase.getCurrentUser().then(
695+
user => {
696+
if (!user || !user.email) {
697+
alert({
698+
title: "Can't fetch providers",
699+
message: "No user with emailaddress logged in.",
700+
okButtonText: "OK, makes sense.."
701+
});
702+
return;
703+
}
704+
705+
firebase.fetchSignInMethodsForEmail(user.email).then(
706+
result => {
707+
alert({
708+
title: `Sign-in methods for ${user.email}`,
709+
message: JSON.stringify(result), // ["password"], ["emailLink"], or ["password", "emailLink']
710+
okButtonText: "Thanks!"
711+
});
712+
},
713+
errorMessage => {
714+
alert({
715+
title: "Fetch Sign-in methods for Email error",
716+
message: errorMessage,
717+
okButtonText: "OK, pity.."
718+
});
719+
}
720+
);
721+
});
722+
}
723+
664724
public doCreateUser(): void {
665725
firebase.createUser({
666726
@@ -790,7 +850,8 @@ export class HelloWorldModel extends Observable {
790850
// note that you need to enable phone login in your firebase instance
791851
type: firebase.LoginType.EMAIL_LINK,
792852
emailLinkOptions: {
793-
email: promptResult.text
853+
email: promptResult.text,
854+
url: "https://combidesk.com?foo=bar"
794855
}
795856
}).then(
796857
result => {

src/app/auth/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,9 @@ export module auth {
7676
public fetchProvidersForEmail(email: string): Promise<any> {
7777
return firebase.fetchProvidersForEmail(email);
7878
}
79+
80+
public fetchSignInMethodsForEmail(email: string): Promise<any> {
81+
return firebase.fetchSignInMethodsForEmail(email);
82+
}
7983
}
8084
}

src/firebase.android.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,14 +383,40 @@ firebase.fetchProvidersForEmail = email => {
383383
});
384384

385385
com.google.firebase.auth.FirebaseAuth.getInstance().fetchProvidersForEmail(email).addOnCompleteListener(onCompleteListener);
386-
387386
} catch (ex) {
388387
console.log("Error in firebase.fetchProvidersForEmail: " + ex);
389388
reject(ex);
390389
}
391390
});
392391
};
393392

393+
firebase.fetchSignInMethodsForEmail = email => {
394+
return new Promise((resolve, reject) => {
395+
try {
396+
if (typeof(email) !== "string") {
397+
reject("A parameter representing an email address is required.");
398+
return;
399+
}
400+
401+
const onCompleteListener = new com.google.android.gms.tasks.OnCompleteListener({
402+
onComplete: task /* <SignInMethodQueryResult> */ => {
403+
if (!task.isSuccessful()) {
404+
reject((task.getException() && task.getException().getReason ? task.getException().getReason() : task.getException()));
405+
} else {
406+
const signInMethods = task.getResult().getSignInMethods();
407+
resolve(firebase.toJsObject(signInMethods));
408+
}
409+
}
410+
});
411+
412+
com.google.firebase.auth.FirebaseAuth.getInstance().fetchSignInMethodsForEmail(email).addOnCompleteListener(onCompleteListener);
413+
} catch (ex) {
414+
console.log("Error in firebase.fetchSignInMethodsForEmail: " + ex);
415+
reject(ex);
416+
}
417+
});
418+
};
419+
394420
firebase.getCurrentPushToken = () => {
395421
return new Promise((resolve, reject) => {
396422
try {

src/firebase.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ export interface FirebasePasswordLoginOptions {
184184

185185
export interface FirebaseEmailLinkLoginOptions {
186186
email: string;
187+
url: string;
188+
iosBundleId?: string;
189+
androidPackageId?: string;
187190
}
188191

189192
export interface FirebasePhoneLoginOptions {
@@ -887,6 +890,8 @@ export function logout(): Promise<any>;
887890

888891
export function fetchProvidersForEmail(email: string): Promise<Array<string>>;
889892

893+
export function fetchSignInMethodsForEmail(email: string): Promise<Array<string>>;
894+
890895
export function sendEmailVerification(): Promise<any>;
891896

892897
export function createUser(options: CreateUserOptions): Promise<CreateUserResult>;

src/firebase.ios.ts

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -182,23 +182,49 @@ firebase.addAppDelegateMethods = appDelegate => {
182182

183183
if (userActivity.webpageURL) {
184184
// check for an email-link-login flow
185-
if (FIRAuth.auth().isSignInWithEmailLink(userActivity.webpageURL.absoluteString)) {
185+
const fAuth = FIRAuth.auth();
186+
if (fAuth.isSignInWithEmailLink(userActivity.webpageURL.absoluteString)) {
186187
const rememberedEmail = firebase.getRememberedEmailForEmailLinkLogin();
187188
if (rememberedEmail !== undefined) {
188-
FIRAuth.auth().signInWithEmailLinkCompletion(
189-
rememberedEmail,
190-
userActivity.webpageURL.absoluteString,
191-
(authData: FIRAuthDataResult, error: NSError) => {
192-
if (error) {
193-
console.log(error.localizedDescription);
194-
} else {
195-
// TODO if already logged in with another prover, consider merging them (https://firebase.google.com/docs/auth/ios/email-link-auth)
196-
firebase.notifyAuthStateListeners({
197-
loggedIn: true,
198-
user: authData.user
199-
});
200-
}
201-
});
189+
190+
if (fAuth.currentUser) {
191+
const onCompletionLink = (result: FIRAuthDataResult, error: NSError) => {
192+
if (error) {
193+
console.log("linkAndRetrieveDataWithCredentialCompletion error: " + error.localizedDescription);
194+
// ignore, and complete the email link sign in flow
195+
fAuth.signInWithEmailLinkCompletion(rememberedEmail, userActivity.webpageURL.absoluteString, (authData: FIRAuthDataResult, error: NSError) => {
196+
if (error) {
197+
console.log("signInWithEmailLinkCompletion error: " + error.localizedDescription);
198+
} else {
199+
firebase.notifyAuthStateListeners({
200+
loggedIn: true,
201+
user: result.user
202+
});
203+
}
204+
});
205+
} else {
206+
// linking successful, so the user can now log in with either their email address, or however he logged in previously
207+
firebase.notifyAuthStateListeners({
208+
loggedIn: true,
209+
user: result.user
210+
});
211+
}
212+
};
213+
const fIRAuthCredential = FIREmailAuthProvider.credentialWithEmailLink(rememberedEmail, userActivity.webpageURL.absoluteString);
214+
fAuth.currentUser.linkAndRetrieveDataWithCredentialCompletion(fIRAuthCredential, onCompletionLink);
215+
216+
} else {
217+
fAuth.signInWithEmailLinkCompletion(rememberedEmail, userActivity.webpageURL.absoluteString, (authData: FIRAuthDataResult, error: NSError) => {
218+
if (error) {
219+
console.log(error.localizedDescription);
220+
} else {
221+
firebase.notifyAuthStateListeners({
222+
loggedIn: true,
223+
user: authData.user
224+
});
225+
}
226+
});
227+
}
202228
}
203229
result = true;
204230

@@ -237,7 +263,7 @@ firebase.fetchProvidersForEmail = email => {
237263
return;
238264
}
239265

240-
FIRAuth.auth().fetchProvidersForEmailCompletion(email, (providerNSArray, error) /* FIRProviderQueryCallback */ => {
266+
FIRAuth.auth().fetchProvidersForEmailCompletion(email, (providerNSArray, error) => {
241267
if (error) {
242268
reject(error.localizedDescription);
243269
} else {
@@ -251,6 +277,28 @@ firebase.fetchProvidersForEmail = email => {
251277
});
252278
};
253279

280+
firebase.fetchSignInMethodsForEmail = email => {
281+
return new Promise((resolve, reject) => {
282+
try {
283+
if (typeof(email) !== "string") {
284+
reject("A parameter representing an email address is required.");
285+
return;
286+
}
287+
288+
FIRAuth.auth().fetchSignInMethodsForEmailCompletion(email, (methodsNSArray, error) => {
289+
if (error) {
290+
reject(error.localizedDescription);
291+
} else {
292+
resolve(firebase.toJsObject(methodsNSArray));
293+
}
294+
});
295+
} catch (ex) {
296+
console.log("Error in firebase.fetchSignInMethodsForEmail: " + ex);
297+
reject(ex);
298+
}
299+
});
300+
};
301+
254302
firebase.getCurrentPushToken = () => {
255303
return new Promise((resolve, reject) => {
256304
try {
@@ -1265,14 +1313,19 @@ firebase.login = arg => {
12651313
return;
12661314
}
12671315

1316+
if (!arg.emailLinkOptions.url) {
1317+
reject("Auth type EMAIL_LINK requires an 'emailLinkOptions.url' argument");
1318+
return;
1319+
}
1320+
12681321
const firActionCodeSettings = FIRActionCodeSettings.new();
12691322
// This 'continue URL' is what's emailed to the receiver, and the domain must be whitelisted in the Firebase console
1270-
firActionCodeSettings.URL = NSURL.URLWithString("https://combidesk.com"); // TODO have this passed in
1323+
firActionCodeSettings.URL = NSURL.URLWithString(arg.emailLinkOptions.url);
12711324
// The sign-in operation has to always be completed in the app.
12721325
firActionCodeSettings.handleCodeInApp = true;
1273-
firActionCodeSettings.setIOSBundleID(utils.ios.getter(NSBundle, NSBundle.mainBundle).bundleIdentifier);
1326+
firActionCodeSettings.setIOSBundleID(arg.emailLinkOptions.iosBundleId || NSBundle.mainBundle.bundleIdentifier);
12741327
firActionCodeSettings.setAndroidPackageNameInstallIfNotAvailableMinimumVersion(
1275-
"org.nativescript.firebasedemo", // TODO add to options (same for iOS, used in the Android implementation)
1328+
arg.emailLinkOptions.androidPackageId || NSBundle.mainBundle.bundleIdentifier,
12761329
false, // TODO not sure
12771330
"12"); // TODO not sure
12781331
fAuth.sendSignInLinkToEmailActionCodeSettingsCompletion(

0 commit comments

Comments
 (0)