Skip to content

Commit c3a841c

Browse files
authored
Support for Anonymous Users (#750)
* initial commit * unit tests * cleanup * more tests * Improve documentation * improve coverage * clean up
1 parent 8625ae2 commit c3a841c

File tree

7 files changed

+401
-3
lines changed

7 files changed

+401
-3
lines changed

integration/test/ParseUserTest.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,4 +439,74 @@ describe('Parse User', () => {
439439
done();
440440
});
441441
});
442+
443+
it('can save anonymous user', async () => {
444+
Parse.User.enableUnsafeCurrentUser();
445+
446+
const user = await Parse.AnonymousUtils.logIn();
447+
user.set('field', 'hello');
448+
await user.save();
449+
450+
const query = new Parse.Query(Parse.User);
451+
const result = await query.get(user.id);
452+
expect(result.get('field')).toBe('hello');
453+
});
454+
455+
it('can not recover anonymous user if logged out', async () => {
456+
Parse.User.enableUnsafeCurrentUser();
457+
458+
const user = await Parse.AnonymousUtils.logIn();
459+
user.set('field', 'hello');
460+
await user.save();
461+
462+
await Parse.User.logOut();
463+
464+
const query = new Parse.Query(Parse.User);
465+
try {
466+
await query.get(user.id);
467+
} catch (error) {
468+
expect(error.message).toBe('Object not found.');
469+
}
470+
});
471+
472+
it('can signUp anonymous user and retain data', async () => {
473+
Parse.User.enableUnsafeCurrentUser();
474+
475+
const user = await Parse.AnonymousUtils.logIn();
476+
user.set('field', 'hello world');
477+
await user.save();
478+
479+
expect(user.get('authData').anonymous).toBeDefined();
480+
481+
user.setUsername('foo');
482+
user.setPassword('baz');
483+
484+
await user.signUp();
485+
486+
const query = new Parse.Query(Parse.User);
487+
const result = await query.get(user.id);
488+
expect(result.get('username')).toBe('foo');
489+
expect(result.get('authData')).toBeUndefined();
490+
expect(result.get('field')).toBe('hello world');
491+
expect(user.get('authData').anonymous).toBeUndefined();
492+
});
493+
494+
it('can logIn user without converting anonymous user', async () => {
495+
Parse.User.enableUnsafeCurrentUser();
496+
497+
await Parse.User.signUp('foobaz', '1234');
498+
499+
const user = await Parse.AnonymousUtils.logIn();
500+
user.set('field', 'hello world');
501+
await user.save();
502+
503+
await Parse.User.logIn('foobaz', '1234');
504+
505+
const query = new Parse.Query(Parse.User);
506+
try {
507+
await query.get(user.id);
508+
} catch (error) {
509+
expect(error.message).toBe('Object not found.');
510+
}
511+
});
442512
});

src/AnonymousUtils.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Copyright (c) 2015-present, Parse, LLC.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @flow-weak
10+
*/
11+
import ParseUser from './ParseUser';
12+
const uuidv4 = require('uuid/v4');
13+
14+
let registered = false;
15+
16+
/**
17+
* Provides utility functions for working with Anonymously logged-in users. <br />
18+
* Anonymous users have some unique characteristics:
19+
* <ul>
20+
* <li>Anonymous users don't need a user name or password.</li>
21+
* <ul>
22+
* <li>Once logged out, an anonymous user cannot be recovered.</li>
23+
* </ul>
24+
* <li>signUp converts an anonymous user to a standard user with the given username and password.</li>
25+
* <ul>
26+
* <li>Data associated with the anonymous user is retained.</li>
27+
* </ul>
28+
* <li>logIn switches users without converting the anonymous user.</li>
29+
* <ul>
30+
* <li>Data associated with the anonymous user will be lost.</li>
31+
* </ul>
32+
* <li>Service logIn (e.g. Facebook, Twitter) will attempt to convert
33+
* the anonymous user into a standard user by linking it to the service.</li>
34+
* <ul>
35+
* <li>If a user already exists that is linked to the service, it will instead switch to the existing user.</li>
36+
* </ul>
37+
* <li>Service linking (e.g. Facebook, Twitter) will convert the anonymous user
38+
* into a standard user by linking it to the service.</li>
39+
* </ul>
40+
* @class Parse.AnonymousUtils
41+
* @static
42+
*/
43+
const AnonymousUtils = {
44+
/**
45+
* Gets whether the user has their account linked to anonymous user.
46+
*
47+
* @method isLinked
48+
* @name Parse.AnonymousUtils.isLinked
49+
* @param {Parse.User} user User to check for.
50+
* The user must be logged in on this device.
51+
* @return {Boolean} <code>true</code> if the user has their account
52+
* linked to an anonymous user.
53+
* @static
54+
*/
55+
isLinked(user: ParseUser) {
56+
const provider = this._getAuthProvider();
57+
return user._isLinked(provider.getAuthType());
58+
},
59+
60+
/**
61+
* Logs in a user Anonymously.
62+
*
63+
* @method logIn
64+
* @name Parse.AnonymousUtils.logIn
65+
* @returns {Promise}
66+
* @static
67+
*/
68+
logIn() {
69+
const provider = this._getAuthProvider();
70+
return ParseUser._logInWith(provider.getAuthType(), provider.getAuthData());
71+
},
72+
73+
/**
74+
* Links Anonymous User to an existing PFUser.
75+
*
76+
* @method link
77+
* @name Parse.AnonymousUtils.link
78+
* @param {Parse.User} user User to link. This must be the current user.
79+
* @returns {Promise}
80+
* @static
81+
*/
82+
link(user: ParseUser) {
83+
const provider = this._getAuthProvider();
84+
return user._linkWith(provider.getAuthType(), provider.getAuthData());
85+
},
86+
87+
_getAuthProvider() {
88+
const provider = {
89+
restoreAuthentication() {
90+
return true;
91+
},
92+
93+
getAuthType() {
94+
return 'anonymous';
95+
},
96+
97+
getAuthData() {
98+
return {
99+
authData: {
100+
id: uuidv4(),
101+
},
102+
};
103+
},
104+
};
105+
if (!registered) {
106+
ParseUser._registerAuthenticationProvider(provider);
107+
registered = true;
108+
}
109+
return provider;
110+
}
111+
};
112+
113+
export default AnonymousUtils;

src/Parse.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ Object.defineProperty(Parse, 'liveQueryServerURL', {
173173

174174
Parse.ACL = require('./ParseACL').default;
175175
Parse.Analytics = require('./Analytics');
176+
Parse.AnonymousUtils = require('./AnonymousUtils').default;
176177
Parse.Cloud = require('./Cloud');
177178
Parse.CoreManager = require('./CoreManager');
178179
Parse.Config = require('./ParseConfig').default;

src/ParseObject.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1157,7 +1157,10 @@ class ParseObject {
11571157
if (options.hasOwnProperty('sessionToken') && typeof options.sessionToken === 'string') {
11581158
saveOptions.sessionToken = options.sessionToken;
11591159
}
1160-
1160+
// Pass sessionToken if saving currentUser
1161+
if (typeof this.getSessionToken === 'function' && this.getSessionToken()) {
1162+
saveOptions.sessionToken = this.getSessionToken();
1163+
}
11611164
const controller = CoreManager.getObjectController();
11621165
const unsaved = unsavedChildren(this);
11631166
return controller.save(unsaved, saveOptions).then(() => {

src/ParseUser.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* @flow
1010
*/
1111

12+
import AnonymousUtils from './AnonymousUtils';
1213
import CoreManager from './CoreManager';
1314
import isRevocableSession from './isRevocableSession';
1415
import ParseError from './ParseError';
@@ -119,7 +120,6 @@ class ParseUser extends ParseObject {
119120
/**
120121
* Synchronizes auth data for a provider (e.g. puts the access token in the
121122
* right place to be used by the Facebook SDK).
122-
123123
*/
124124
_synchronizeAuthData(provider: string) {
125125
if (!this.isCurrent() || !provider) {
@@ -814,10 +814,15 @@ const DefaultController = {
814814
},
815815

816816
setCurrentUser(user) {
817+
const currentUser = this.currentUser();
818+
let promise = Promise.resolve();
819+
if (currentUser && !user.equals(currentUser) && AnonymousUtils.isLinked(currentUser)) {
820+
promise = currentUser.destroy({ sessionToken: currentUser.getSessionToken() })
821+
}
817822
currentUserCache = user;
818823
user._cleanupAuthData();
819824
user._synchronizeAllAuthData();
820-
return DefaultController.updateUserOnDisk(user);
825+
return promise.then(() => DefaultController.updateUserOnDisk(user));
821826
},
822827

823828
currentUser(): ?ParseUser {
@@ -986,9 +991,14 @@ const DefaultController = {
986991
let promise = Storage.removeItemAsync(path);
987992
const RESTController = CoreManager.getRESTController();
988993
if (currentUser !== null) {
994+
const isAnonymous = AnonymousUtils.isLinked(currentUser);
989995
const currentSession = currentUser.getSessionToken();
990996
if (currentSession && isRevocableSession(currentSession)) {
991997
promise = promise.then(() => {
998+
if (isAnonymous) {
999+
return currentUser.destroy({ sessionToken: currentSession });
1000+
}
1001+
}).then(() => {
9921002
return RESTController.request(
9931003
'POST', 'logout', {}, { sessionToken: currentSession }
9941004
);

src/__tests__/AnonymousUtils-test.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Copyright (c) 2015-present, Parse, LLC.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
jest.dontMock('../AnonymousUtils');
11+
12+
class MockUser {
13+
constructor () {
14+
this.className = '_User';
15+
this.attributes = {};
16+
}
17+
_isLinked() {}
18+
_linkWith() {}
19+
static _registerAuthenticationProvider() {}
20+
static _logInWith() {}
21+
}
22+
23+
jest.setMock('../ParseUser', MockUser);
24+
25+
const mockProvider = {
26+
restoreAuthentication() {
27+
return true;
28+
},
29+
30+
getAuthType() {
31+
return 'anonymous';
32+
},
33+
34+
getAuthData() {
35+
return {
36+
authData: {
37+
id: '1234',
38+
},
39+
};
40+
},
41+
};
42+
43+
const AnonymousUtils = require('../AnonymousUtils').default;
44+
45+
describe('AnonymousUtils', () => {
46+
beforeEach(() => {
47+
jest.clearAllMocks();
48+
jest.spyOn(
49+
AnonymousUtils,
50+
'_getAuthProvider'
51+
)
52+
.mockImplementation(() => mockProvider);
53+
});
54+
55+
it('can register provider', () => {
56+
AnonymousUtils._getAuthProvider.mockRestore();
57+
jest.spyOn(MockUser, '_registerAuthenticationProvider');
58+
AnonymousUtils._getAuthProvider();
59+
AnonymousUtils._getAuthProvider();
60+
expect(MockUser._registerAuthenticationProvider).toHaveBeenCalledTimes(1);
61+
});
62+
63+
it('can check user isLinked', () => {
64+
const user = new MockUser();
65+
jest.spyOn(user, '_isLinked');
66+
AnonymousUtils.isLinked(user);
67+
expect(user._isLinked).toHaveBeenCalledTimes(1);
68+
expect(user._isLinked).toHaveBeenCalledWith('anonymous');
69+
expect(AnonymousUtils._getAuthProvider).toHaveBeenCalledTimes(1);
70+
});
71+
72+
it('can link user', () => {
73+
const user = new MockUser();
74+
jest.spyOn(user, '_linkWith');
75+
AnonymousUtils.link(user);
76+
expect(user._linkWith).toHaveBeenCalledTimes(1);
77+
expect(user._linkWith).toHaveBeenCalledWith('anonymous', mockProvider.getAuthData());
78+
expect(AnonymousUtils._getAuthProvider).toHaveBeenCalledTimes(1);
79+
});
80+
81+
it('can login user', () => {
82+
jest.spyOn(MockUser, '_logInWith');
83+
AnonymousUtils.logIn();
84+
expect(MockUser._logInWith).toHaveBeenCalledTimes(1);
85+
expect(MockUser._logInWith).toHaveBeenCalledWith('anonymous', mockProvider.getAuthData());
86+
expect(AnonymousUtils._getAuthProvider).toHaveBeenCalledTimes(1);
87+
});
88+
});

0 commit comments

Comments
 (0)