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

Prevent dialog from appearing on top of another dialog #40

Closed
jlubeck opened this issue Apr 15, 2020 · 13 comments
Closed

Prevent dialog from appearing on top of another dialog #40

jlubeck opened this issue Apr 15, 2020 · 13 comments

Comments

@jlubeck
Copy link

jlubeck commented Apr 15, 2020

I'm currently displaying a dialog when my API throws an error.

The problem I'm running into is that I'm making mulitple calls at the same time, and sometimes I get multiple errors, so I end up with getting multiple dialogs open at the same time.

Is there a way to prevent this? Or is there a way to figure out if there is currently a dialog open? So I can check for that before displaying the dialog.

@jonataslaw
Copy link
Owner

jonataslaw commented Apr 15, 2020

I'm currently displaying a dialog when my API throws an error.

The problem I'm running into is that I'm making mulitple calls at the same time, and sometimes I get multiple errors, so I end up with getting multiple dialogs open at the same time.

Is there a way to prevent this? Or is there a way to figure out if there is currently a dialog open? So I can check for that before displaying the dialog.

This code snippet should resolve most of your issues.
You are exposing the routing of your Middleware in a Global way through a singleton.
There are endless ways to do this, but if you are not using any state manager, it is the most practical way to do this.

class MiddleWare {
  static MiddleWare _instance;

  factory MiddleWare() {
    if (_instance == null) _instance = MiddleWare._();
    return _instance;
  }

  MiddleWare._();

  static Routing _routing;

  static Routing get routing => _routing;

  static observer(Routing rt) {
    _routing = rt;
  }
}

class Foo {
  openDialog() {
    if(!MiddleWare.routing.isDialog){
   // or if(MiddleWare.routing.isDialog) Get.back(); to close dialog and open other
      Get.dialog(YourDialogClass())
    }
  }
}

Middleware.rounting will give you everything you need. Current route, if you have an open dialog, if you have an open snackbar, if you have an open bottomsheet, if the open route is "x" or "y", and anyway, you can use this approach.
Later on you can create your own approach using your favorite state manager, or reactive programming.

@jlubeck
Copy link
Author

jlubeck commented Apr 15, 2020

Looks like whatever sets the routing.isDialog to true is not fast enough, because I'm still getting dual dialogs when 2 api connections fail at the same time.

Any other ideas?

@jonataslaw
Copy link
Owner

if(!MiddleWare.routing.isDialog){ is on 2 apis ?

@jonataslaw
Copy link
Owner

I'm trying to reproduce the problem, but I'm not getting it.
Are you making two requests to the api at the same time, and are you turning off the internet or something? or is the error an error response from your own API?
This should not happen for the simple reason that there are no parallel routes. As soon as the dialog is opened, it instantly notifies Middleware. Unless the second dialog is called before the first is opened.
It is a case to think about.

@jonataslaw
Copy link
Owner

For testing purposes only, does adding await before Get.dialog change anything?
await Get.dialog(YourDialogClass());

@jlubeck
Copy link
Author

jlubeck commented Apr 15, 2020

It's multiple requests to an api that expires an auth token.

This is my request method:

  static Future<dynamic> post(String endpoint, {Object params}) async {
    Map<String, String> headers = {"Content-Type": "application/json"};

    LoginResultsDTO loginResults = CurrentUser().loginResults;
    if (loginResults?.oAuth?.accessToken != null &&
        loginResults?.oAuth?.tokenType != null) {
      headers['Authorization'] =
          loginResults.oAuth.tokenType + ' ' + loginResults.oAuth.accessToken;
    }

    final body = params == null ? null : jsonEncode(params);
    print('\n${URLS.BASE_URL}$endpoint\n${params ?? ''}');
    final response = await http.post('${URLS.BASE_URL}$endpoint',
        headers: headers, body: body);
    if (response.statusCode == 200) {
      if (response.body.length == 0) return true;
      return json.decode(response.body);
    } else {
      if (response.body != null) {
        ErrorDTO error = ErrorDTO.fromJson(json.decode(response.body));
        if (error.raw != null) {
          switch (error.code) {
            case ServerError.authorizationExpired:
              print('Try to refresh token');
              if (loginResults != null) {
                OAuthDTO refreshOAuth = await AuthenticationService.refresh(
                    loginResults.oAuth?.refreshToken,
                    loginResults.user?.userID);

                var refreshLoginResults =
                    LoginResultsDTO.withOAuth(refreshOAuth, loginResults);
                CurrentUser().setLoginResults(refreshLoginResults);
                return await Api.post(endpoint, params: params);
              }
              throw ServerException('No login results', error);
              break;
            case ServerError.authorizationFailed:
            case ServerError.invalidAuthorizationToken:
              CurrentUser().setLoginResults(null);
              if (!MiddleWare.routing.isDialog) {
                print('error dialog');
                await Get.defaultDialog(
                  title: 'Error',
                  content: Text(error.code.description),
                  confirm: FlatButton(
                    child: Text('OK'),
                    onPressed: () {
                      Get.until(Routes.Login, (Route route) {
                        return route.isFirst;
                      });
                    },
                  ),
                );
              }
              //throw ServerException(error.code.description, error);
              break;
            case ServerError.Unknown:
              print(error.raw);
              throw ServerException(error.raw, error);
              break;
          }
        }
      } else {
        print(response.toString());
        throw Exception('Failed');
      }
    }
  }

@jonataslaw
Copy link
Owner

jonataslaw commented Apr 16, 2020

Well, I did a lot of tests, including sending the context by parameter to test if this behavior is a Get problem, and I found out that this is the default behavior of the framework, and that you must implement your own logic to prevent this from happening.

I didn't close this question because I intend to help you with that.

Are you sure your error code is not making a double call? Try this

 static Future<dynamic> post(String endpoint, {Object params}) async {
    Map<String, String> headers = {"Content-Type": "application/json"};

    bool isDialogOpen = false; // ADD THIS

    LoginResultsDTO loginResults = CurrentUser().loginResults;
    if (loginResults?.oAuth?.accessToken != null &&
        loginResults?.oAuth?.tokenType != null) {
      headers['Authorization'] =
          loginResults.oAuth.tokenType + ' ' + loginResults.oAuth.accessToken;
    }

    final body = params == null ? null : jsonEncode(params);
    print('\n${URLS.BASE_URL}$endpoint\n${params ?? ''}');
    final response = await http.post('${URLS.BASE_URL}$endpoint',
        headers: headers, body: body);
    if (response.statusCode == 200) {
      if (response.body.length == 0) return true;
      return json.decode(response.body);
    } else {
      if (response.body != null) {
        ErrorDTO error = ErrorDTO.fromJson(json.decode(response.body));
        if (error.raw != null) {
          switch (error.code) {
            case ServerError.authorizationExpired:
              print('Try to refresh token');
              if (loginResults != null) {
                OAuthDTO refreshOAuth = await AuthenticationService.refresh(
                    loginResults.oAuth?.refreshToken,
                    loginResults.user?.userID);

                var refreshLoginResults =
                    LoginResultsDTO.withOAuth(refreshOAuth, loginResults);
                CurrentUser().setLoginResults(refreshLoginResults);
                return await Api.post(endpoint, params: params);
              }
              throw ServerException('No login results', error);
              break;
            case ServerError.authorizationFailed:
            case ServerError.invalidAuthorizationToken:
              CurrentUser().setLoginResults(null);
              if (!isDialogOpen) { // CHANGE THIS
               isDialogOpen = true;
                print('error dialog');
                 Get.defaultDialog(
                  title: 'Error',
                  content: Text(error.code.description),
                  confirm: FlatButton(
                    child: Text('OK'),
                    onPressed: () {
                      Get.until(Routes.Login, (Route route) {
                        return route.isFirst;
                      });
                    },
                  ),
                );
              }
              //throw ServerException(error.code.description, error);
              break;
            case ServerError.Unknown:
              print(error.raw);
              throw ServerException(error.raw, error);
              break;
          }
        }
      } else {
        print(response.toString());
        throw Exception('Failed');
      }
    }
  }

@jlubeck
Copy link
Author

jlubeck commented Apr 16, 2020

Ok, here is a fully reproducible code:

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:rpm/middleware.dart';

class TestScreen extends StatelessWidget {
  void login() {
    getTestError();
    getTestError();
  }

  Future<dynamic> getTestError() async {
    final response =
        await http.get('http://www.mocky.io/v2/5e97d4673000007900b6e08c');
    if (response.statusCode == 200) {
      return response.body;
    } else {
      if (MiddleWare.routing.isDialog == null ||
          MiddleWare.routing.isDialog == false) {
        print('error dialog');
        await Get.defaultDialog(
          title: 'Error',
          content: Text('There was an error'),
          confirm: FlatButton(
            child: Text('OK'),
            onPressed: () {
              Get.back();
            },
          ),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          child: FlatButton(
            child: Text('Test'),
            onPressed: login,
          ),
        ),
      ),
    );
  }
}

Your addition with the isDialogOpen doesn't help either

@jonataslaw
Copy link
Owner

Thanks for opening this issue, after the reproduction code I was able to trace the root of the problem, and GetObserver is improved now. Can you reproduce the problem using version 1.16.1-dev?
Unfortunately my Macbook is not turning on (I burn the midnight oil trying to get it to turn on), and I made the correction to the stable branch that was installed on my personal notebook. I made the correction to the dev branch by the text editor, so ... I'm not sure it's working, but I hope it works for you.

@jlubeck
Copy link
Author

jlubeck commented Apr 16, 2020

Nice!! It seems to be working on the test snippet. I'll implement it in the app now and let you know how it goes

@jlubeck
Copy link
Author

jlubeck commented Apr 16, 2020

Flawless! Thanks!!

@jlubeck jlubeck closed this as completed Apr 16, 2020
@jonataslaw
Copy link
Owner

jonataslaw commented Apr 19, 2020

New API added

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:rpm/middleware.dart';

class TestScreen extends StatelessWidget {
  void login() {
    getTestError();
    getTestError();
  }

  Future<dynamic> getTestError() async {
    final response =
        await http.get('http://www.mocky.io/v2/5e97d4673000007900b6e08c');
    if (response.statusCode == 200) {
      return response.body;
    } else {
      if (!Get.isDialogOpen) { // Now you can made this
        print('error dialog');
        await Get.defaultDialog(
          title: 'Error',
          content: Text('There was an error'),
          confirm: FlatButton(
            child: Text('OK'),
            onPressed: () {
              Get.back();
            },
          ),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          child: FlatButton(
            child: Text('Test'),
            onPressed: login,
          ),
        ),
      ),
    );
  }
}

@jlubeck
Copy link
Author

jlubeck commented Apr 21, 2020

Great! I verified it. Works like a charm

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

No branches or pull requests

2 participants