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

Commit 10e7ae9

Browse files
Add Email Link Authentication #665
1 parent f8fb887 commit 10e7ae9

File tree

9 files changed

+200
-45
lines changed

9 files changed

+200
-45
lines changed

demo/app/App_Resources/Android/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
<category android:name="android.intent.category.BROWSABLE"/>
4848
<data android:host="www.coolapp.com" android:scheme="http"/>
4949
<data android:host="www.coolapp.com" android:scheme="https"/>
50+
<data android:host="combidesk.com" android:scheme="https"/>
5051
</intent-filter>
5152

5253
</activity>

demo/app/main-view-model.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export class HelloWorldModel extends Observable {
6464

6565
public doWebLoginByPassword(): void {
6666
this.ensureWebOnAuthChangedHandler();
67-
firebaseWebApi.auth().signInWithEmailAndPassword('[email protected]', 'firebase')
67+
firebaseWebApi.auth().signInWithEmailAndPassword('[email protected]', 'firebase')
6868
.then(() => console.log("User logged in"))
6969
.catch(err => {
7070
alert({
@@ -154,7 +154,7 @@ export class HelloWorldModel extends Observable {
154154
}
155155

156156
public doWebCreateUser(): void {
157-
firebaseWebApi.auth().createUserWithEmailAndPassword('[email protected]', 'firebase')
157+
firebaseWebApi.auth().createUserWithEmailAndPassword('[email protected]', 'firebase')
158158
.then(result => {
159159
alert({
160160
title: "User created",
@@ -723,7 +723,7 @@ export class HelloWorldModel extends Observable {
723723

724724
public doCreateUser(): void {
725725
firebase.createUser({
726-
726+
727727
password: 'firebase'
728728
}).then(
729729
result => {
@@ -767,7 +767,7 @@ export class HelloWorldModel extends Observable {
767767
type: firebase.LoginType.PASSWORD,
768768
passwordOptions: {
769769
// note that these credentials have been pre-configured in our demo firebase instance
770-
770+
771771
password: 'firebase'
772772
}
773773
}).then(
@@ -918,7 +918,7 @@ export class HelloWorldModel extends Observable {
918918

919919
public doResetPassword(): void {
920920
firebase.resetPassword({
921-
921+
922922
}).then(
923923
result => {
924924
alert({
@@ -1359,7 +1359,7 @@ export class HelloWorldModel extends Observable {
13591359
firebase.reauthenticate({
13601360
type: firebase.LoginType.PASSWORD,
13611361
passwordOptions: {
1362-
1362+
13631363
password: 'firebase'
13641364
}
13651365
}).then(

docs/AUTHENTICATION.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ You can sign in a user either
55

66
* [anonymously](#anonymous-login),
77
* by [email and password](#email-password-login),
8+
* by [email link](#email-link),
89
* by [phone verification](#phone-verification),
910
* using a [custom token](#custom-login),
1011
* using [Facebook](#facebook-login),
@@ -239,6 +240,69 @@ Don't forget to enable email-password login in your firebase instance.
239240
</details>
240241

241242

243+
### Email-Link login
244+
Enable email-password login in your firebase instance, and flip the "E-mail link" switch.
245+
246+
This login type allows your users to login without providing a password. They can simply click a link
247+
and get redirected to the app. The app may even run on a different device.
248+
249+
Enable dynamic links, as described in the [Dynamic Links readme]("./INVITES_DYNAMICLINKS.md"), because the user
250+
that receives the link will need to be redirected to your app.
251+
252+
#### iOS configuration
253+
- Specify the bundle id of your app in the Firebase console.
254+
255+
#### Android configuration
256+
- Specify the package name of your app in the Firebase console.
257+
- Upload the SHA-1 and SHA-256 of the (debug) signing certificates to the Firebase console, as described in the [Dynamic Links readme]("./INVITES_DYNAMICLINKS.md").
258+
- Also add an `android:host` for the `emailLinkOptions.url` to your `app/App_Resources/Android/AndroidManifest.xml` file as described in that readme.
259+
260+
<details>
261+
<summary>Native API</summary>
262+
263+
```typescript
264+
firebase.login(
265+
{
266+
type: firebase.LoginType.EMAIL_LINK,
267+
emailLinkOptions: {
268+
269+
url: "https://domain.com?foo=bar",
270+
// the stuff below is optional, if not set the plugin will infer this for you (bundle/package is taken from currently used platform)
271+
iOS: {
272+
bundleId: "my.bundle.id"
273+
},
274+
android: {
275+
packageName: "my.package.name"
276+
}
277+
}
278+
})
279+
.then(result => JSON.stringify(result))
280+
.catch(error => console.log(error));
281+
```
282+
</details>
283+
284+
<details>
285+
<summary>Web API</summary>
286+
287+
```typescript
288+
firebaseWebApi.auth().sendSignInLinkToEmail(
289+
290+
{
291+
url: "https://domain.com?foo=bar",
292+
// the stuff below is optional, if not set the plugin will infer this for you (bundle/package is taken from currently used platform)
293+
iOS: {
294+
bundleId: "my.bundle.id"
295+
},
296+
android: {
297+
packageName: "my.package.name"
298+
}
299+
})
300+
.then(() => console.log("Email link sent"))
301+
.catch(err => console.log("Login error: " + JSON.stringify(err)));
302+
```
303+
</details>
304+
305+
242306
#### Managing email-password accounts
243307

244308
##### Creating a Password account

docs/INVITES_DYNAMICLINKS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ _Invites_ lets you invite other users to your app from right within your own app
88
Keep in mind that invites are based of dynamic links, and so calling for an invite may return a plain dynamic link, in which case invitationId is null.
99

1010
### Android
11-
* [Make sure you've uploaded your SHA1 fingerprint(s)](https://developers.google.com/android/guides/client-auth) to the Firebase console, then download the latest `google-services.json` file and add it to `app/App_Resources/Android`.
11+
* [Make sure you've uploaded your SHA1 and SHA256 fingerprints](https://developers.google.com/android/guides/client-auth) to the Firebase console.
1212

1313
### iOS
1414
* On iOS the user must be signed in with their Google Account to send invitations.

src/app/auth/index.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as firebase from "../../firebase";
2-
import { LoginType, User } from "../../firebase";
2+
import { FirebaseEmailLinkActionCodeSettings, LoginType, User } from "../../firebase";
33

44
export module auth {
55
export class Auth {
@@ -49,6 +49,27 @@ export module auth {
4949
});
5050
}
5151

52+
public sendSignInLinkToEmail(email: string, actionCodeSettings: FirebaseEmailLinkActionCodeSettings): Promise<any> {
53+
return new Promise((resolve, reject) => {
54+
firebase.login({
55+
type: LoginType.EMAIL_LINK,
56+
emailLinkOptions: {
57+
email: email,
58+
url: actionCodeSettings.url,
59+
}
60+
}).then((user: User) => {
61+
this.currentUser = user;
62+
this.authStateChangedHandler && this.authStateChangedHandler(user);
63+
resolve();
64+
}, (err => {
65+
reject({
66+
// code: "",
67+
message: err
68+
});
69+
}));
70+
});
71+
}
72+
5273
public createUserWithEmailAndPassword(email: string, password: string): Promise<any> {
5374
return firebase.createUser({
5475
email: email,

src/firebase.android.ts

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const messagingEnabled = lazy(() => typeof(com.google.firebase.messaging) !== "u
2525
const dynamicLinksEnabled = lazy(() => typeof(com.google.android.gms.appinvite) !== "undefined");
2626

2727
(() => {
28-
// note that this means we need to require the plugin before the app is loaded
28+
// note that this means we need to 'require()' the plugin before the app is loaded
2929
appModule.on(appModule.launchEvent, args => {
3030
if (messagingEnabled()) {
3131
org.nativescript.plugins.firebase.FirebasePluginLifecycleCallbacks.registerCallbacks(appModule.android.nativeApp);
@@ -63,30 +63,58 @@ const dynamicLinksEnabled = lazy(() => typeof(com.google.android.gms.appinvite)
6363
}
6464

6565
} else if (isLaunchIntent && dynamicLinksEnabled()) {
66-
const getDynamicLinksCallback = new com.google.android.gms.tasks.OnCompleteListener({
67-
onComplete: task => {
68-
if (task.isSuccessful() && task.getResult() !== null) {
69-
const result = task.getResult();
70-
if (firebase._dynamicLinkCallback === null) {
71-
firebase._cachedDynamicLink = {
72-
url: result.getLink().toString(),
73-
matchConfidence: 1,
74-
minimumAppVersion: result.getMinimumAppVersion()
75-
};
76-
} else {
77-
setTimeout(() => {
78-
firebase._dynamicLinkCallback({
66+
// let's see if this is part of an email-link authentication flow
67+
const firebaseAuth = com.google.firebase.auth.FirebaseAuth.getInstance();
68+
const emailLink = "" + intent.getData();
69+
if (firebaseAuth.isSignInWithEmailLink(emailLink)) {
70+
const rememberedEmail = firebase.getRememberedEmailForEmailLinkLogin();
71+
if (rememberedEmail !== undefined) {
72+
const emailLinkOnCompleteListener = new com.google.android.gms.tasks.OnCompleteListener({
73+
onComplete: task => {
74+
if (task.isSuccessful()) {
75+
const authResult = task.getResult();
76+
firebase.notifyAuthStateListeners({
77+
loggedIn: true,
78+
user: authResult.getUser()
79+
});
80+
}
81+
}
82+
});
83+
const user = com.google.firebase.auth.FirebaseAuth.getInstance().getCurrentUser();
84+
if (user) {
85+
const authCredential = com.google.firebase.auth.EmailAuthProvider.getCredentialWithLink(rememberedEmail, emailLink);
86+
user.linkWithCredential(authCredential).addOnCompleteListener(emailLinkOnCompleteListener);
87+
} else {
88+
firebaseAuth.signInWithEmailLink(rememberedEmail, emailLink).addOnCompleteListener(emailLinkOnCompleteListener);
89+
}
90+
}
91+
92+
} else {
93+
const getDynamicLinksCallback = new com.google.android.gms.tasks.OnCompleteListener({
94+
onComplete: task => {
95+
if (task.isSuccessful() && task.getResult() !== null) {
96+
const result = task.getResult();
97+
if (firebase._dynamicLinkCallback === null) {
98+
firebase._cachedDynamicLink = {
7999
url: result.getLink().toString(),
80100
matchConfidence: 1,
81101
minimumAppVersion: result.getMinimumAppVersion()
102+
};
103+
} else {
104+
setTimeout(() => {
105+
firebase._dynamicLinkCallback({
106+
url: result.getLink().toString(),
107+
matchConfidence: 1,
108+
minimumAppVersion: result.getMinimumAppVersion()
109+
});
82110
});
83-
});
111+
}
84112
}
85113
}
86-
}
87-
});
88-
const firebaseDynamicLinks = com.google.firebase.dynamiclinks.FirebaseDynamicLinks.getInstance();
89-
firebaseDynamicLinks.getDynamicLink(intent).addOnCompleteListener(getDynamicLinksCallback);
114+
});
115+
const firebaseDynamicLinks = com.google.firebase.dynamiclinks.FirebaseDynamicLinks.getInstance();
116+
firebaseDynamicLinks.getDynamicLink(intent).addOnCompleteListener(getDynamicLinksCallback);
117+
}
90118
}
91119
});
92120
})();
@@ -1104,6 +1132,43 @@ firebase.login = arg => {
11041132
firebaseAuth.signInWithEmailAndPassword(arg.passwordOptions.email, arg.passwordOptions.password).addOnCompleteListener(onCompleteListener);
11051133
}
11061134

1135+
} else if (arg.type === firebase.LoginType.EMAIL_LINK) {
1136+
if (!arg.emailLinkOptions || !arg.emailLinkOptions.email) {
1137+
reject("Auth type EMAIL_LINK requires an 'emailLinkOptions.email' argument");
1138+
return;
1139+
}
1140+
1141+
if (!arg.emailLinkOptions.url) {
1142+
reject("Auth type EMAIL_LINK requires an 'emailLinkOptions.url' argument");
1143+
return;
1144+
}
1145+
1146+
const actionCodeSettings = com.google.firebase.auth.ActionCodeSettings.newBuilder()
1147+
// URL you want to redirect back to. The domain must be whitelisted in the Firebase Console.
1148+
.setUrl(arg.emailLinkOptions.url)
1149+
.setHandleCodeInApp(true)
1150+
.setIOSBundleId(arg.emailLinkOptions.iOS ? arg.emailLinkOptions.iOS.bundleId : appModule.android.context.getPackageName())
1151+
.setAndroidPackageName(
1152+
arg.emailLinkOptions.android ? arg.emailLinkOptions.android.packageName : appModule.android.context.getPackageName(),
1153+
arg.emailLinkOptions.android ? arg.emailLinkOptions.android.installApp || false : false,
1154+
arg.emailLinkOptions.android ? arg.emailLinkOptions.android.minimumVersion || "1" : "1")
1155+
.build();
1156+
1157+
const onEmailLinkCompleteListener = new com.google.android.gms.tasks.OnCompleteListener({
1158+
onComplete: task => {
1159+
if (!task.isSuccessful()) {
1160+
reject((task.getException() && task.getException().getReason ? task.getException().getReason() : task.getException()));
1161+
} else {
1162+
// The link was successfully sent.
1163+
// Save the email locally so you don't need to ask the user for it again if they open the link on the same device.
1164+
firebase.rememberEmailForEmailLinkLogin(arg.emailLinkOptions.email);
1165+
resolve();
1166+
}
1167+
}
1168+
});
1169+
1170+
firebaseAuth.sendSignInLinkToEmail(arg.emailLinkOptions.email, actionCodeSettings).addOnCompleteListener(onEmailLinkCompleteListener);
1171+
11071172
} else if (arg.type === firebase.LoginType.PHONE) {
11081173
// https://firebase.google.com/docs/auth/android/phone-auth
11091174
if (!arg.phoneOptions || !arg.phoneOptions.phoneNumber) {

src/firebase.d.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,20 @@ export interface FirebasePasswordLoginOptions {
182182
password: string;
183183
}
184184

185-
export interface FirebaseEmailLinkLoginOptions {
186-
email: string;
185+
export interface FirebaseEmailLinkActionCodeSettings {
187186
url: string;
188-
iosBundleId?: string;
189-
androidPackageId?: string;
187+
iOS?: {
188+
bundleId: string;
189+
};
190+
android?: {
191+
packageName: string;
192+
installApp?: false;
193+
minimumVersion?: string;
194+
}
195+
}
196+
197+
export interface FirebaseEmailLinkLoginOptions extends FirebaseEmailLinkActionCodeSettings {
198+
email: string;
190199
}
191200

192201
export interface FirebasePhoneLoginOptions {

src/firebase.ios.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -190,15 +190,12 @@ firebase.addAppDelegateMethods = appDelegate => {
190190
if (fAuth.currentUser) {
191191
const onCompletionLink = (result: FIRAuthDataResult, error: NSError) => {
192192
if (error) {
193-
console.log("linkAndRetrieveDataWithCredentialCompletion error: " + error.localizedDescription);
194193
// ignore, and complete the email link sign in flow
195194
fAuth.signInWithEmailLinkCompletion(rememberedEmail, userActivity.webpageURL.absoluteString, (authData: FIRAuthDataResult, error: NSError) => {
196-
if (error) {
197-
console.log("signInWithEmailLinkCompletion error: " + error.localizedDescription);
198-
} else {
195+
if (!error) {
199196
firebase.notifyAuthStateListeners({
200197
loggedIn: true,
201-
user: result.user
198+
user: authData.user
202199
});
203200
}
204201
});
@@ -1323,11 +1320,11 @@ firebase.login = arg => {
13231320
firActionCodeSettings.URL = NSURL.URLWithString(arg.emailLinkOptions.url);
13241321
// The sign-in operation has to always be completed in the app.
13251322
firActionCodeSettings.handleCodeInApp = true;
1326-
firActionCodeSettings.setIOSBundleID(arg.emailLinkOptions.iosBundleId || NSBundle.mainBundle.bundleIdentifier);
1323+
firActionCodeSettings.setIOSBundleID(arg.emailLinkOptions.iOS ? arg.emailLinkOptions.iOS.bundleId : NSBundle.mainBundle.bundleIdentifier);
13271324
firActionCodeSettings.setAndroidPackageNameInstallIfNotAvailableMinimumVersion(
1328-
arg.emailLinkOptions.androidPackageId || NSBundle.mainBundle.bundleIdentifier,
1329-
false, // TODO not sure
1330-
"12"); // TODO not sure
1325+
arg.emailLinkOptions.android ? arg.emailLinkOptions.android.packageName : NSBundle.mainBundle.bundleIdentifier,
1326+
arg.emailLinkOptions.android ? arg.emailLinkOptions.android.installApp || false : false,
1327+
arg.emailLinkOptions.android ? arg.emailLinkOptions.android.minimumVersion || "1" : "1");
13311328
fAuth.sendSignInLinkToEmailActionCodeSettingsCompletion(
13321329
arg.emailLinkOptions.email,
13331330
firActionCodeSettings,

src/package.json

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,10 @@
3939
"tsc": "tsc -skipLibCheck",
4040
"plugin.tscwatch": "npm run tsc -- -w",
4141
"package": "cd ../publish && rm -rf ./package && ./pack.sh",
42-
"demo.ios": "npm run preparedemo && cd ../demo && tns run ios --emulator",
43-
"demo.ios.device": "npm run preparedemo && cd ../demo && tns platform remove ios && tns run ios",
44-
"demo-ng.ios": "npm run preparedemo-ng && cd ../demo-ng && tns run ios --emulator",
45-
"demo-ng.ios.device": "npm run preparedemo-ng && cd ../demo-ng && tns platform remove ios && tns run ios",
42+
"demo.ios": "npm run preparedemo && cd ../demo && tns platform remove ios && tns run ios",
43+
"demo-ng.ios": "npm run preparedemo-ng && cd ../demo-ng && tns platform remove ios && tns run ios",
4644
"demo.android": "npm run preparedemo && cd ../demo && tns platform remove android && tns run android --justlaunch",
47-
"demo-ng.android": "npm run preparedemo-ng && cd ../demo-ng && tns run android --justlaunch",
45+
"demo-ng.android": "npm run preparedemo-ng && cd ../demo-ng && tns run android",
4846
"test": "npm run tslint && npm run tslint.demo && cd ../demo && tns build ios && tns build android",
4947
"test.ios": "cd ../demo && tns test ios --emulator",
5048
"test.ios.device": "cd ../demo && tns platform remove ios && tns test ios",

0 commit comments

Comments
 (0)