Async-ify.

R=rnystrom@google.com

Review URL: https://codereview.chromium.org//1308163004 .
diff --git a/README.md b/README.md
index 52b724c..23be5ff 100644
--- a/README.md
+++ b/README.md
@@ -48,53 +48,60 @@
 // query parameters.
 final redirectUrl = Uri.parse("http://my-site.com/oauth2-redirect");
 
-var credentialsFile = new File("~/.myapp/credentials.json");
-return credentialsFile.exists().then((exists) {
-  // If the OAuth2 credentials have already been saved from a previous
-  // run, we just want to reload them.
+/// A file in which the users credentials are stored persistently. If the server
+/// issues a refresh token allowing the client to refresh outdated credentials,
+/// these may be valid indefinitely, meaning the user never has to
+/// re-authenticate.
+final credentialsFile = new File("~/.myapp/credentials.json");
+
+/// Either load an OAuth2 client from saved credentials or authenticate a new
+/// one.
+Future<oauth2.Client> getClient() async {
+  var exists = await credentialsFile.exists();
+
+  // If the OAuth2 credentials have already been saved from a previous run, we
+  // just want to reload them.
   if (exists) {
-    return credentialsFile.readAsString().then((json) {
-      var credentials = new oauth2.Credentials.fromJson(json);
-      return new oauth2.Client(identifier, secret, credentials);
-    });
+    var credentials = new oauth2.Credentials.fromJson(
+        await credentialsFile.readAsString());
+    return new oauth2.Client(identifier, secret, credentials);
   }
 
-  // If we don't have OAuth2 credentials yet, we need to get the
-  // resource owner to authorize us. We're assuming here that we're a
-  // command-line application.
+  // If we don't have OAuth2 credentials yet, we need to get the resource owner
+  // to authorize us. We're assuming here that we're a command-line application.
   var grant = new oauth2.AuthorizationCodeGrant(
       identifier, secret, authorizationEndpoint, tokenEndpoint);
 
-  // Redirect the resource owner to the authorization URL. This will be
-  // a URL on the authorization server (authorizationEndpoint with some
-  // additional query parameters). Once the resource owner has
-  // authorized, they'll be redirected to `redirectUrl` with an
-  // authorization code.
+  // Redirect the resource owner to the authorization URL. This will be a URL on
+  // the authorization server (authorizationEndpoint with some additional query
+  // parameters). Once the resource owner has authorized, they'll be redirected
+  // to `redirectUrl` with an authorization code.
   //
   // `redirect` is an imaginary function that redirects the resource
   // owner's browser.
-  return redirect(grant.getAuthorizationUrl(redirectUrl)).then((_) {
-    // Another imaginary function that listens for a request to
-    // `redirectUrl`.
-    return listen(redirectUrl);
-  }).then((request) {
-    // Once the user is redirected to `redirectUrl`, pass the query
-    // parameters to the AuthorizationCodeGrant. It will validate them
-    // and extract the authorization code to create a new Client.
-    return grant.handleAuthorizationResponse(request.uri.queryParameters);
-  })
-}).then((client) {
-  // Once you have a Client, you can use it just like any other HTTP
-  // client.
-  return client.read("http://example.com/protected-resources.txt")
-      .then((result) {
-    // Once we're done with the client, save the credentials file. This
-    // ensures that if the credentials were automatically refreshed
-    // while using the client, the new credentials are available for the
-    // next run of the program.
-    return credentialsFile.open(FileMode.WRITE).then((file) {
-      return file.writeString(client.credentials.toJson());
-    }).then((file) => file.close()).then((_) => result);
-  });
-}).then(print);
+  await redirect(grant.getAuthorizationUrl(redirectUrl));
+  
+  // Another imaginary function that listens for a request to `redirectUrl`.
+  var request = await listen(redirectUrl);
+
+  // Once the user is redirected to `redirectUrl`, pass the query parameters to
+  // the AuthorizationCodeGrant. It will validate them and extract the
+  // authorization code to create a new Client.
+  return await grant.handleAuthorizationResponse(request.uri.queryParameters);
+}
+
+main() async {
+  var client = await loadClient();
+
+  // Once you have a Client, you can use it just like any other HTTP client.
+  var result = client.read("http://example.com/protected-resources.txt");
+
+  // Once we're done with the client, save the credentials file. This ensures
+  // that if the credentials were automatically refreshed while using the
+  // client, the new credentials are available for the next run of the
+  // program.
+  await credentialsFile.writeAsString(client.credentials.toJson());
+
+  print(result);
+}
 ```
diff --git a/lib/src/authorization_code_grant.dart b/lib/src/authorization_code_grant.dart
index c879e8c..9ec4a27 100644
--- a/lib/src/authorization_code_grant.dart
+++ b/lib/src/authorization_code_grant.dart
@@ -96,12 +96,12 @@
   /// [httpClient] is used for all HTTP requests made by this grant, as well as
   /// those of the [Client] is constructs.
   AuthorizationCodeGrant(
-      this.identifier,
-      this.secret,
-      this.authorizationEndpoint,
-      this.tokenEndpoint,
-      {http.Client httpClient})
-    : _httpClient = httpClient == null ? new http.Client() : httpClient;
+          this.identifier,
+          this.secret,
+          this.authorizationEndpoint,
+          this.tokenEndpoint,
+          {http.Client httpClient})
+      : _httpClient = httpClient == null ? new http.Client() : httpClient;
 
   /// Returns the URL to which the resource owner should be redirected to
   /// authorize this client. The resource owner will then be redirected to
@@ -157,42 +157,41 @@
   /// [FormatError] if the `state` parameter doesn't match the original value.
   ///
   /// Throws [AuthorizationException] if the authorization fails.
-  Future<Client> handleAuthorizationResponse(Map<String, String> parameters) {
-    return async.then((_) {
-      if (_state == _INITIAL_STATE) {
-        throw new StateError(
-            'The authorization URL has not yet been generated.');
-      } else if (_state == _FINISHED_STATE) {
-        throw new StateError(
-            'The authorization code has already been received.');
-      }
-      _state = _FINISHED_STATE;
+  Future<Client> handleAuthorizationResponse(Map<String, String> parameters)
+      async {
+    if (_state == _INITIAL_STATE) {
+      throw new StateError(
+          'The authorization URL has not yet been generated.');
+    } else if (_state == _FINISHED_STATE) {
+      throw new StateError(
+          'The authorization code has already been received.');
+    }
+    _state = _FINISHED_STATE;
 
-      if (_stateString != null) {
-        if (!parameters.containsKey('state')) {
-          throw new FormatException('Invalid OAuth response for '
-              '"$authorizationEndpoint": parameter "state" expected to be '
-              '"$_stateString", was missing.');
-        } else if (parameters['state'] != _stateString) {
-          throw new FormatException('Invalid OAuth response for '
-              '"$authorizationEndpoint": parameter "state" expected to be '
-              '"$_stateString", was "${parameters['state']}".');
-        }
-      }
-
-      if (parameters.containsKey('error')) {
-        var description = parameters['error_description'];
-        var uriString = parameters['error_uri'];
-        var uri = uriString == null ? null : Uri.parse(uriString);
-        throw new AuthorizationException(parameters['error'], description, uri);
-      } else if (!parameters.containsKey('code')) {
+    if (_stateString != null) {
+      if (!parameters.containsKey('state')) {
         throw new FormatException('Invalid OAuth response for '
-            '"$authorizationEndpoint": did not contain required parameter '
-            '"code".');
+            '"$authorizationEndpoint": parameter "state" expected to be '
+            '"$_stateString", was missing.');
+      } else if (parameters['state'] != _stateString) {
+        throw new FormatException('Invalid OAuth response for '
+            '"$authorizationEndpoint": parameter "state" expected to be '
+            '"$_stateString", was "${parameters['state']}".');
       }
+    }
 
-      return _handleAuthorizationCode(parameters['code']);
-    });
+    if (parameters.containsKey('error')) {
+      var description = parameters['error_description'];
+      var uriString = parameters['error_uri'];
+      var uri = uriString == null ? null : Uri.parse(uriString);
+      throw new AuthorizationException(parameters['error'], description, uri);
+    } else if (!parameters.containsKey('code')) {
+      throw new FormatException('Invalid OAuth response for '
+          '"$authorizationEndpoint": did not contain required parameter '
+          '"code".');
+    }
+
+    return await _handleAuthorizationCode(parameters['code']);
   }
 
   /// Processes an authorization code directly. Usually
@@ -209,26 +208,24 @@
   /// responses while retrieving credentials.
   ///
   /// Throws [AuthorizationException] if the authorization fails.
-  Future<Client> handleAuthorizationCode(String authorizationCode) {
-    return async.then((_) {
-      if (_state == _INITIAL_STATE) {
-        throw new StateError(
-            'The authorization URL has not yet been generated.');
-      } else if (_state == _FINISHED_STATE) {
-        throw new StateError(
-            'The authorization code has already been received.');
-      }
-      _state = _FINISHED_STATE;
+  Future<Client> handleAuthorizationCode(String authorizationCode) async {
+    if (_state == _INITIAL_STATE) {
+      throw new StateError(
+          'The authorization URL has not yet been generated.');
+    } else if (_state == _FINISHED_STATE) {
+      throw new StateError(
+          'The authorization code has already been received.');
+    }
+    _state = _FINISHED_STATE;
 
-      return _handleAuthorizationCode(authorizationCode);
-    });
+    return await _handleAuthorizationCode(authorizationCode);
   }
 
   /// This works just like [handleAuthorizationCode], except it doesn't validate
   /// the state beforehand.
-  Future<Client> _handleAuthorizationCode(String authorizationCode) {
+  Future<Client> _handleAuthorizationCode(String authorizationCode) async {
     var startTime = new DateTime.now();
-    return _httpClient.post(this.tokenEndpoint, body: {
+    var response = await _httpClient.post(this.tokenEndpoint, body: {
       "grant_type": "authorization_code",
       "code": authorizationCode,
       "redirect_uri": this._redirectEndpoint.toString(),
@@ -237,12 +234,12 @@
       // it be configurable?
       "client_id": this.identifier,
       "client_secret": this.secret
-    }).then((response) {
-      var credentials = handleAccessTokenResponse(
-          response, tokenEndpoint, startTime, _scopes);
-      return new Client(
-          this.identifier, this.secret, credentials, httpClient: _httpClient);
     });
+
+    var credentials = handleAccessTokenResponse(
+        response, tokenEndpoint, startTime, _scopes);
+    return new Client(
+        this.identifier, this.secret, credentials, httpClient: _httpClient);
   }
 
   /// Closes the grant and frees its resources.
diff --git a/lib/src/client.dart b/lib/src/client.dart
index d473be1..60495a0 100644
--- a/lib/src/client.dart
+++ b/lib/src/client.dart
@@ -80,37 +80,34 @@
   /// Sends an HTTP request with OAuth2 authorization credentials attached. This
   /// will also automatically refresh this client's [Credentials] before sending
   /// the request if necessary.
-  Future<http.StreamedResponse> send(http.BaseRequest request) {
-    return async.then((_) {
-      if (!credentials.isExpired) return new Future.value();
+  Future<http.StreamedResponse> send(http.BaseRequest request) async {
+    if (credentials.isExpired) {
       if (!credentials.canRefresh) throw new ExpirationException(credentials);
-      return refreshCredentials();
-    }).then((_) {
-      request.headers['authorization'] = "Bearer ${credentials.accessToken}";
-      return _httpClient.send(request);
-    }).then((response) {
-      if (response.statusCode != 401 ||
-          !response.headers.containsKey('www-authenticate')) {
-        return response;
-      }
+      await refreshCredentials();
+    }
 
-      var authenticate;
-      try {
-        authenticate = new AuthenticateHeader.parse(
-            response.headers['www-authenticate']);
-      } on FormatException catch (e) {
-        return response;
-      }
+    request.headers['authorization'] = "Bearer ${credentials.accessToken}";
+    var response = await _httpClient.send(request);
 
-      if (authenticate.scheme != 'bearer') return response;
+    if (response.statusCode != 401) return response;
+    if (!response.headers.containsKey('www-authenticate')) return response;
 
-      var params = authenticate.parameters;
-      if (!params.containsKey('error')) return response;
+    var authenticate;
+    try {
+      authenticate = new AuthenticateHeader.parse(
+          response.headers['www-authenticate']);
+    } on FormatException catch (e) {
+      return response;
+    }
 
-      throw new AuthorizationException(
-          params['error'], params['error_description'],
-          params['error_uri'] == null ? null : Uri.parse(params['error_uri']));
-    });
+    if (authenticate.scheme != 'bearer') return response;
+
+    var params = authenticate.parameters;
+    if (!params.containsKey('error')) return response;
+
+    throw new AuthorizationException(
+        params['error'], params['error_description'],
+        params['error_uri'] == null ? null : Uri.parse(params['error_uri']));
   }
 
   /// Explicitly refreshes this client's credentials. Returns this client.
@@ -122,20 +119,18 @@
   /// You may request different scopes than the default by passing in
   /// [newScopes]. These must be a subset of the scopes in the
   /// [Credentials.scopes] field of [Client.credentials].
-  Future<Client> refreshCredentials([List<String> newScopes]) {
-    return async.then((_) {
-      if (!credentials.canRefresh) {
-        var prefix = "OAuth credentials";
-        if (credentials.isExpired) prefix = "$prefix have expired and";
-        throw new StateError("$prefix can't be refreshed.");
-      }
+  Future<Client> refreshCredentials([List<String> newScopes]) async {
+    if (!credentials.canRefresh) {
+      var prefix = "OAuth credentials";
+      if (credentials.isExpired) prefix = "$prefix have expired and";
+      throw new StateError("$prefix can't be refreshed.");
+    }
 
-      return credentials.refresh(identifier, secret,
-          newScopes: newScopes, httpClient: _httpClient);
-    }).then((credentials) {
-      _credentials = credentials;
-      return this;
-    });
+    _credentials = await credentials.refresh(
+        identifier, secret,
+        newScopes: newScopes, httpClient: _httpClient);
+
+    return this;
   }
 
   /// Closes this client and its underlying HTTP client.
diff --git a/lib/src/credentials.dart b/lib/src/credentials.dart
index ce4e064..88b1f5d 100644
--- a/lib/src/credentials.dart
+++ b/lib/src/credentials.dart
@@ -146,47 +146,44 @@
       String identifier,
       String secret,
       {List<String> newScopes,
-       http.Client httpClient}) {
+       http.Client httpClient}) async {
     var scopes = this.scopes;
     if (newScopes != null) scopes = newScopes;
     if (scopes == null) scopes = <String>[];
     if (httpClient == null) httpClient = new http.Client();
 
     var startTime = new DateTime.now();
-    return async.then((_) {
-      if (refreshToken == null) {
-        throw new StateError("Can't refresh credentials without a refresh "
-            "token.");
-      } else if (tokenEndpoint == null) {
-        throw new StateError("Can't refresh credentials without a token "
-            "endpoint.");
-      }
+    if (refreshToken == null) {
+      throw new StateError("Can't refresh credentials without a refresh "
+          "token.");
+    } else if (tokenEndpoint == null) {
+      throw new StateError("Can't refresh credentials without a token "
+          "endpoint.");
+    }
 
-      var fields = {
-        "grant_type": "refresh_token",
-        "refresh_token": refreshToken,
-        // TODO(nweiz): the spec recommends that HTTP basic auth be used in
-        // preference to form parameters, but Google doesn't support that.
-        // Should it be configurable?
-        "client_id": identifier,
-        "client_secret": secret
-      };
-      if (!scopes.isEmpty) fields["scope"] = scopes.join(' ');
+    var fields = {
+      "grant_type": "refresh_token",
+      "refresh_token": refreshToken,
+      // TODO(nweiz): the spec recommends that HTTP basic auth be used in
+      // preference to form parameters, but Google doesn't support that.
+      // Should it be configurable?
+      "client_id": identifier,
+      "client_secret": secret
+    };
+    if (!scopes.isEmpty) fields["scope"] = scopes.join(' ');
 
-      return httpClient.post(tokenEndpoint, body: fields);
-    }).then((response) {
-      return handleAccessTokenResponse(
+    var response = await httpClient.post(tokenEndpoint, body: fields);
+    var credentials = await handleAccessTokenResponse(
           response, tokenEndpoint, startTime, scopes);
-    }).then((credentials) {
-      // The authorization server may issue a new refresh token. If it doesn't,
-      // we should re-use the one we already have.
-      if (credentials.refreshToken != null) return credentials;
-      return new Credentials(
-          credentials.accessToken,
-          this.refreshToken,
-          credentials.tokenEndpoint,
-          credentials.scopes,
-          credentials.expiration);
-    });
+
+    // The authorization server may issue a new refresh token. If it doesn't,
+    // we should re-use the one we already have.
+    if (credentials.refreshToken != null) return credentials;
+    return new Credentials(
+        credentials.accessToken,
+        this.refreshToken,
+        credentials.tokenEndpoint,
+        credentials.scopes,
+        credentials.expiration);
   }
 }
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 12a429e..b292625 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -105,7 +105,3 @@
     return new AuthenticateHeader(scheme, parameters);
   }
 }
-
-/// Returns a [Future] that asynchronously completes to `null`.
-Future get async => new Future.delayed(const Duration(milliseconds: 0),
-                                       () => null);
diff --git a/pubspec.yaml b/pubspec.yaml
index 31c0b50..e40265a 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -7,7 +7,7 @@
   behalf of a user, and making authorized HTTP requests with the user's
   OAuth2 credentials.
 environment:
-  sdk: '>=1.0.0 <2.0.0'
+  sdk: '>=1.9.0 <2.0.0'
 dependencies:
   http: '>=0.11.0 <0.12.0'
   http_parser: '>=0.0.0 <0.1.0'
diff --git a/test/client_test.dart b/test/client_test.dart
index 11a3bf8..7115095 100644
--- a/test/client_test.dart
+++ b/test/client_test.dart
@@ -39,7 +39,7 @@
     });
 
     test("that can be refreshed refreshes the credentials and sends the "
-        "request", () {
+        "request", () async {
       var expiration = new DateTime.now().subtract(new Duration(hours: 1));
       var credentials = new oauth2.Credentials(
           'access token', 'refresh token', tokenEndpoint, [], expiration);
@@ -64,9 +64,8 @@
         return new Future.value(new http.Response('good job', 200));
       });
 
-      expect(client.read(requestUri).then((_) {
-        expect(client.credentials.accessToken, equals('new access token'));
-      }), completes);
+      await client.read(requestUri);
+      expect(client.credentials.accessToken, equals('new access token'));
     });
   });
 
@@ -89,7 +88,7 @@
       expect(client.read(requestUri), completion(equals('good job')));
     });
 
-    test("can manually refresh the credentials", () {
+    test("can manually refresh the credentials", () async {
       var credentials = new oauth2.Credentials(
           'access token', 'refresh token', tokenEndpoint);
       var client = new oauth2.Client('identifier', 'secret', credentials,
@@ -104,9 +103,8 @@
         }), 200, headers: {'content-type': 'application/json'}));
       });
 
-      expect(client.refreshCredentials().then((_) {
-        expect(client.credentials.accessToken, equals('new access token'));
-      }), completes);
+      await client.refreshCredentials();
+      expect(client.credentials.accessToken, equals('new access token'));
     });
 
     test("without a refresh token can't manually refresh the credentials", () {
@@ -141,7 +139,7 @@
           throwsA(new isInstanceOf<oauth2.AuthorizationException>()));
     });
 
-    test('passes through a 401 response without www-authenticate', () {
+    test('passes through a 401 response without www-authenticate', () async {
       var credentials = new oauth2.Credentials('access token');
       var client = new oauth2.Client('identifier', 'secret', credentials,
           httpClient: httpClient);
@@ -155,12 +153,11 @@
         return new Future.value(new http.Response('bad job', 401));
       });
 
-      expect(
-          client.get(requestUri).then((response) => response.statusCode),
-          completion(equals(401)));
+      expect((await client.get(requestUri)).statusCode, equals(401));
     });
 
-    test('passes through a 401 response with invalid www-authenticate', () {
+    test('passes through a 401 response with invalid www-authenticate',
+        () async {
       var credentials = new oauth2.Credentials('access token');
       var client = new oauth2.Client('identifier', 'secret', credentials,
           httpClient: httpClient);
@@ -177,12 +174,11 @@
                 headers: {'www-authenticate': authenticate}));
       });
 
-      expect(
-          client.get(requestUri).then((response) => response.statusCode),
-          completion(equals(401)));
+      expect((await client.get(requestUri)).statusCode, equals(401));
     });
 
-    test('passes through a 401 response with non-bearer www-authenticate', () {
+    test('passes through a 401 response with non-bearer www-authenticate',
+        () async {
       var credentials = new oauth2.Credentials('access token');
       var client = new oauth2.Client('identifier', 'secret', credentials,
           httpClient: httpClient);
@@ -197,12 +193,11 @@
                 headers: {'www-authenticate': 'Digest'}));
       });
 
-      expect(
-          client.get(requestUri).then((response) => response.statusCode),
-          completion(equals(401)));
+      expect((await client.get(requestUri)).statusCode, equals(401));
     });
 
-    test('passes through a 401 response with non-OAuth2 www-authenticate', () {
+    test('passes through a 401 response with non-OAuth2 www-authenticate',
+        () async {
       var credentials = new oauth2.Credentials('access token');
       var client = new oauth2.Client('identifier', 'secret', credentials,
           httpClient: httpClient);
@@ -217,9 +212,7 @@
                 headers: {'www-authenticate': 'Bearer'}));
       });
 
-      expect(
-          client.get(requestUri).then((response) => response.statusCode),
-          completion(equals(401)));
+      expect((await client.get(requestUri)).statusCode, equals(401));
     });
   });
 }
diff --git a/test/credentials_test.dart b/test/credentials_test.dart
index 94b5445..3bd44d3 100644
--- a/test/credentials_test.dart
+++ b/test/credentials_test.dart
@@ -56,7 +56,7 @@
         throwsStateError);
   });
 
-  test("can refresh with a refresh token and a token endpoint", () {
+  test("can refresh with a refresh token and a token endpoint", () async {
     var credentials = new oauth2.Credentials(
         'access token', 'refresh token', tokenEndpoint, ['scope1', 'scope2']);
     expect(credentials.canRefresh, true);
@@ -80,14 +80,13 @@
     });
 
 
-    expect(credentials.refresh('identifier', 'secret', httpClient: httpClient)
-        .then((credentials) {
-      expect(credentials.accessToken, equals('new access token'));
-      expect(credentials.refreshToken, equals('new refresh token'));
-    }), completes);
+    credentials = await credentials.refresh('identifier', 'secret',
+        httpClient: httpClient);
+    expect(credentials.accessToken, equals('new access token'));
+    expect(credentials.refreshToken, equals('new refresh token'));
   });
 
-  test("uses the old refresh token if a new one isn't provided", () {
+  test("uses the old refresh token if a new one isn't provided", () async {
     var credentials = new oauth2.Credentials(
         'access token', 'refresh token', tokenEndpoint);
     expect(credentials.canRefresh, true);
@@ -109,11 +108,10 @@
     });
 
 
-    expect(credentials.refresh('identifier', 'secret', httpClient: httpClient)
-        .then((credentials) {
-      expect(credentials.accessToken, equals('new access token'));
-      expect(credentials.refreshToken, equals('refresh token'));
-    }), completes);
+    credentials = await credentials.refresh('identifier', 'secret',
+        httpClient: httpClient);
+    expect(credentials.accessToken, equals('new access token'));
+    expect(credentials.refreshToken, equals('refresh token'));
   });
 
   group("fromJson", () {