Repository: parse-community/Parse-SDK-JS (JavaScript)

Issue:
linkWith() on React Native using currentUser()
### New Issue Checklist
- [x] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md).
- [x] I am not just asking a [question](https://github.com/parse-community/.github/blob/master/SUPPORT.md).
- [x] I have searched through [existing issues](https://github.com/parse-community/Parse-SDK-JS/issues?q=is%3Aissue).
- [x] I can reproduce the issue with the latest versions of [Parse Server](https://github.com/parse-community/parse-server/releases) and the [Parse JS SDK](https://github.com/parse-community/Parse-SDK-JS/releases). <!-- We don't investigate issues for outdated releases. -->

### Issue Description
When using `linkWith()` on React Native using Google as a provider, it calls `currentUser()` on the background, which causes the following error:
```
Error: Cannot call currentUser() when using a platform with an async storage system. Call currentUserAsync() instead.
```

### Steps to reproduce
Call `linkWith()` on a new `Parse.User` object and pass Google as the provider along with the correct `authData` from Google Signin. I've set this project [here](https://github.com/eberperetto/ParseSDKJSlinkWithIssue) with the complete setup.

### Actual Outcome
Error message:
```
Error: Cannot call currentUser() when using a platform with an async storage system. Call currentUserAsync() instead.
```

### Expected Outcome
The linked `Parse.User` object.

###  Environment
React Native v0.63.4

Server
- Parse Server version: `4.4.0`
- Operating system: `Mac OS Catalina v10.15.7`
- Local or remote host (AWS, Azure, Google Cloud, Heroku, Digital Ocean, etc): `local`

Database
- System (MongoDB or Postgres): `MongoDB`
- Database version: `4.4.1`
- Local or remote host (MongoDB Atlas, mLab, AWS, Azure, Google Cloud, etc): `local`

Client
- Parse JS SDK version: `2.19,0`

### Logs
```
Error: Cannot call currentUser() when using a platform with an async storage system. Call currentUserAsync() instead.
currentUser@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:111793:24
value@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:111502:38
value@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:111280:40
http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:111393:31
tryCallOne@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:3475:16
http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:3576:27
_callTimer@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:30664:17
_callImmediatesPass@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:30703:17
callImmediates@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:30920:33
__callImmediates@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:2769:35
http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:2555:34
__guard@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:2752:15
flushedQueue@http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false:2554:21
flushedQueue@[native code]
callFunctionReturnFlushedQueue@[native code]
```



Discussion:
I see this has been an issue for a while now. I think we can use `currentUserAsync()` in as many places as we can. Also I think a new method `isCurrentAsync()` would be useful for react-native users.
Thanks for this PR, it's now working as expected! Any idea on when will this be officially released? I'm using this library in a public user guide and need to only use official releases. Sorry for commenting here, not sure where to do this.
Has this been released?



Patch:
diff --git a/src/ParseUser.js b/src/ParseUser.js
index 9ea7f1480..69d702fb5 100644
--- a/src/ParseUser.js
+++ b/src/ParseUser.js
@@ -308,6 +308,16 @@ class ParseUser extends ParseObject {
     return !!current && current.id === this.id;
   }
 
+  /**
+   * Returns true if <code>current</code> would return this user.
+   *
+   * @returns {Promise<boolean>} true if user is cached on disk
+   */
+  async isCurrentAsync(): Promise<boolean> {
+    const current = await ParseUser.currentAsync();
+    return !!current && current.id === this.id;
+  }
+
   /**
    * Returns get("username").
    *
@@ -458,13 +468,13 @@ class ParseUser extends ParseObject {
    * @param {...any} args
    * @returns {Promise}
    */
-  save(...args: Array<any>): Promise<ParseUser> {
-    return super.save.apply(this, args).then(() => {
-      if (this.isCurrent()) {
-        return CoreManager.getUserController().updateUserOnDisk(this);
-      }
-      return this;
-    });
+  async save(...args: Array<any>): Promise<ParseUser> {
+    await super.save.apply(this, args);
+    const current = await this.isCurrentAsync();
+    if (current) {
+      return CoreManager.getUserController().updateUserOnDisk(this);
+    }
+    return this;
   }
 
   /**
@@ -474,13 +484,13 @@ class ParseUser extends ParseObject {
    * @param {...any} args
    * @returns {Parse.User}
    */
-  destroy(...args: Array<any>): Promise<ParseUser> {
-    return super.destroy.apply(this, args).then(() => {
-      if (this.isCurrent()) {
-        return CoreManager.getUserController().removeUserFromDisk();
-      }
-      return this;
-    });
+  async destroy(...args: Array<any>): Promise<ParseUser> {
+    await super.destroy.apply(this, args);
+    const current = await this.isCurrentAsync();
+    if (current) {
+      return CoreManager.getUserController().removeUserFromDisk();
+    }
+    return this;
   }
 
   /**
@@ -490,13 +500,13 @@ class ParseUser extends ParseObject {
    * @param {...any} args
    * @returns {Parse.User}
    */
-  fetch(...args: Array<any>): Promise<ParseUser> {
-    return super.fetch.apply(this, args).then(() => {
-      if (this.isCurrent()) {
-        return CoreManager.getUserController().updateUserOnDisk(this);
-      }
-      return this;
-    });
+  async fetch(...args: Array<any>): Promise<ParseUser> {
+    await super.fetch.apply(this, args);
+    const current = await this.isCurrentAsync();
+    if (current) {
+      return CoreManager.getUserController().updateUserOnDisk(this);
+    }
+    return this;
   }
 
   /**
@@ -506,13 +516,13 @@ class ParseUser extends ParseObject {
    * @param {...any} args
    * @returns {Parse.User}
    */
-  fetchWithInclude(...args: Array<any>): Promise<ParseUser> {
-    return super.fetchWithInclude.apply(this, args).then(() => {
-      if (this.isCurrent()) {
-        return CoreManager.getUserController().updateUserOnDisk(this);
-      }
-      return this;
-    });
+  async fetchWithInclude(...args: Array<any>): Promise<ParseUser> {
+    await super.fetchWithInclude.apply(this, args);
+    const current = await this.isCurrentAsync();
+    if (current) {
+      return CoreManager.getUserController().updateUserOnDisk(this);
+    }
+    return this;
   }
 
   /**
@@ -1161,7 +1171,7 @@ const DefaultController = {
     return RESTController.request('POST', 'requestPasswordReset', { email: email }, options);
   },
 
-  upgradeToRevocableSession(user: ParseUser, options: RequestOptions) {
+  async upgradeToRevocableSession(user: ParseUser, options: RequestOptions) {
     const token = user.getSessionToken();
     if (!token) {
       return Promise.reject(
@@ -1172,15 +1182,15 @@ const DefaultController = {
     options.sessionToken = token;
 
     const RESTController = CoreManager.getRESTController();
-    return RESTController.request('POST', 'upgradeToRevocableSession', {}, options).then(result => {
-      const session = new ParseSession();
-      session._finishFetch(result);
-      user._finishFetch({ sessionToken: session.getSessionToken() });
-      if (user.isCurrent()) {
-        return DefaultController.setCurrentUser(user);
-      }
-      return Promise.resolve(user);
-    });
+    const result = await RESTController.request('POST', 'upgradeToRevocableSession', {}, options);
+    const session = new ParseSession();
+    session._finishFetch(result);
+    user._finishFetch({ sessionToken: session.getSessionToken() });
+    const current = await user.isCurrentAsync();
+    if (current) {
+      return DefaultController.setCurrentUser(user);
+    }
+    return Promise.resolve(user);
   },
 
   linkWith(user: ParseUser, authData: AuthData, options: FullOptions) {


Test patch:
diff --git a/src/__tests__/ParseUser-test.js b/src/__tests__/ParseUser-test.js
index 1e85375cb..828b14b0e 100644
--- a/src/__tests__/ParseUser-test.js
+++ b/src/__tests__/ParseUser-test.js
@@ -1280,6 +1280,54 @@ describe('ParseUser', () => {
     });
   });
 
+  it('can linkWith async', async () => {
+    const currentStorage = CoreManager.getStorageController();
+    CoreManager.setStorageController(mockAsyncStorage);
+    ParseUser.enableUnsafeCurrentUser();
+    ParseUser._clearCache();
+    CoreManager.setRESTController({
+      request() {
+        return Promise.resolve(
+          {
+            objectId: 'uid5Async',
+            sessionToken: 'r:123abc',
+            authData: {
+              test: {
+                id: 'id',
+                access_token: 'access_token',
+              },
+            },
+          },
+          200
+        );
+      },
+      ajax() {},
+    });
+    const provider = {
+      authenticate(options) {
+        if (options.success) {
+          options.success(this, {
+            id: 'id',
+            access_token: 'access_token',
+          });
+        }
+      },
+      restoreAuthentication() {},
+      getAuthType() {
+        return 'test';
+      },
+      deauthenticate() {},
+    };
+
+    const user = new ParseUser();
+    await user.linkWith(provider, null, { useMasterKey: true });
+
+    expect(user.get('authData')).toEqual({
+      test: { id: 'id', access_token: 'access_token' },
+    });
+    CoreManager.setStorageController(currentStorage);
+  });
+
   it('handle linkWith authentication failure', async () => {
     const provider = {
       authenticate(options) {