Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to add and manage session tokens in flutter project #76

Open
jeneena-jose opened this issue Jun 18, 2020 · 6 comments
Open

How to add and manage session tokens in flutter project #76

jeneena-jose opened this issue Jun 18, 2020 · 6 comments

Comments

@jeneena-jose
Copy link

Hello,
Thanks for sharing such a useful package for integrating various Google APIs under one roof.

I am glad using it. But I am facing issues with pricing because in Google Autocomplete requests, there are a lot of queries generated without maintaining session tokens.

The APIs I use :

  1. Google Autocomplete API (multiple times)
  2. Google Place Details (single time as per user selected location)
  3. Google Geocoding (single time as per user inputs any location))

Please guide how to handle session tokens in flutter app to reduce pricing from Google Maps !

@minoesteban
Copy link

Hi, I'll stick around, I'm struggling with the same topic

@flikkr
Copy link

flikkr commented Jul 12, 2020

If you're still struggling with this, you can manage session tokens by creating a TokenGenerator class that issues a new token whenever a autocomplete query is completed or expired. Using my implementation, you call TokenGenerator.token whenever you want a token and it will return that same token until it expires. Once you are done with the search (i.e. the user selects a place), you can call TokenGenerator.done() for the generator to clear the current token. The token is generated using UUID package.

import 'package:uuid/uuid.dart';

class TokenGenerator {
  // Session token validity duration, according to Google
  static const Duration _validity = Duration(seconds: 180);
  static String _token;
  static DateTime _lastFetched;

  static String get _generate {
    _lastFetched = DateTime.now();
    _token = Uuid().v4();
    return _token;
  }

  static String get token {
    if (_lastFetched == null ||
        _token == null ||
        _lastFetched.add(_validity).isBefore(DateTime.now())) return _generate;
    return _token;
  }

  static void done() {
    _lastFetched = null;
    _token = null;
  }
}

@jeneena-jose
Copy link
Author

Thanks @flikkr ! I will integrate UUID package and check.

@minoesteban
Copy link

Thanks @flikkr ! But actually, if i'm not mistaken the places autocomplete API returns a session token in the first response, and you can use that session token as many times as you want for autocomplete calls until the first call you make to the places details api (once a place was selected). What would be great is a way to handle these session tokens internally (capture it within the PlacesResponse object and reuse it with further autocomplete calls en place details calls)

@lejard-h
Copy link
Owner

lejard-h commented May 1, 2021

I am not very familiar with this topic. But I don't see any session token return by the API

And Google seems to recommend to generate your own session token - https://developers.google.com/maps/documentation/places/web-service/session-tokens

@tspoke
Copy link

tspoke commented May 5, 2022

I created this handy class to wrap the autocomplete() of this library :

Features :

  • Debounce user input (I use 300ms and found that it's a good value) to reduce api calls
  • Check for empty or identical input to reduce calls (disposing previous timer). Handy when the user clear the inputText keeping the back button pressed...
  • Auto-recreate token when place details is called to recreate a session and prevent further calls made with an invalid token (and be billed more).
import 'dart:async';
import 'package:google_maps_webservice/places.dart';
import 'package:uuid/uuid.dart';

class AutoCompleteSession {
  static const Uuid uuid = Uuid();
  static final _logger = AppLogger.getLogger(); // TODO provide your own logger or just use print()

  final StreamController<List<Prediction>> _controller = StreamController();
  final GoogleMapsPlaces _places;
  final int _debounceDuration;
  Timer? _debounce;
  late String _sessionToken;

  var _previousSearch = "";

  AutoCompleteSession(this._places, this._debounceDuration) {
    _resetToken();
  }

  /// Search for address
  ///
  /// @remarks:
  ///  The session begins when the user starts typing a query,
  ///  and concludes when they select a place and a call to Place Details is made.
  ///
  ///  Each session can have multiple queries, followed by one place selection.
  ///  The API key(s) used for each request within a session must belong to the same Google Cloud Console project.
  ///  Once a session has concluded, the token is no longer valid; your app must generate a fresh token for each session.
  ///  If the sessiontoken parameter is omitted, or if you reuse a session token, the session is charged as if no session
  ///  token was provided (each request is billed separately).
  ///  @return bool   TRUE if the autocomplete will search for value
  bool search(final String input) {
    if (_previousSearch == input.trim()) {
      _logger.d("Skip autocomplete searching - same input detected");
      return false;
    }
    _previousSearch = input.trim();

    if (_debounce?.isActive ?? false) {
      _debounce?.cancel();
    }

    if (_previousSearch == "") { // skip empty requests
      _logger.d("Skip autocomplete searching - empty input");
      return false;
    }

    _debounce = Timer(Duration(milliseconds: _debounceDuration), () async {
      _places.autocomplete(input, sessionToken: _sessionToken, components: [Component(Component.country, "fr")], types: ["address"]).then((result) {
        if (result.isOkay) {
          _controller.add(result.predictions);
        } else if(result.hasNoResults) {
          _controller.add([]);
        } else {
        _logger.d(result.errorMessage);
        _controller.addError(Exception(result.errorMessage));
        }
      }).catchError((err) {
        _logger.d(err.errorMessage);
        _controller.addError(Exception(err.errorMessage));
      });
    });

    return true;
  }

  /// Get place details
  ///
  /// @fields: Basic fields (cheap): address_component, adr_address, business_status, formatted_address, geometry, icon, icon_mask_base_uri, icon_background_color, name,
  /// permanently_closed, photo, type, url, utc_offset, or vicinity
  Future<PlacesDetailsResponse?> getDetails(final Prediction prediction) async {
    final String? placeId = prediction.placeId;
    if (placeId != null) {
      if (_debounce?.isActive ?? false) {
        _debounce?.cancel();
      }

      final result = await _places.getDetailsByPlaceId(placeId, sessionToken: _sessionToken, fields: ["place_id", "geometry", "formatted_address", "name", "type"]);
      _resetToken();
      return result;
    }
    return null;
  }

  /// Get stream
  Stream<List<Prediction>> stream() {
    return _controller.stream;
  }

  /// Close the session
  void close() {
    if (_debounce?.isActive ?? false) {
      _debounce?.cancel();
    }

    if (!_controller.isClosed) {
      _controller.close();
    }
  }

  void _resetToken() {
    _sessionToken = uuid.v4().toString();
    _previousSearch = "";
    _logger.d("Autocomplete session - resetting token !");
  }
}

You can use it like this in your code :

var places = GoogleMapsPlaces(apiKey: "GOOGLE  PLACES API KEY");

var autocompleteSession = AutoCompleteSession(_places, 330);

// observe results in a stream an update your UI with it
autocompleteSession.stream().listen(
      (results) => /* update your ui or store here */,
      onError: (err) => print("err in mapAddController"),
      onDone: () => print("DONE"),
    );
    
// to search for string input
autoCompleteSession.search(input);

// then when the user click on a predictions 
autoCompleteSession.getPlaceDetails(prediction); // will reset token and cleanup :)

// don't forget to close the session when UI is disposed
autoCompleteSession.close();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants