Trouble getting query parameter from deep link on resume

Custom Code

Firstly, let me start off by saying that I am a Flutter novice, and even more of a novice when it comes to FlutterFlow. I have been attempting to solve this issue for a few days now with very little progress, and I'm sure it's something simple I'm missing.

We are building an app for internal employee usage, to facilitate mobile processes against our ERP (IFS). Our authentication for users will be handled through Microsoft Azure SSO. We have used this auth method across a number of other platforms, including our internal custom built applications, and we are very familiar with it. Getting this authentication set up in FlutterFlow was a bit tricky, but we were able to handle it through the following method.

The initial page has a button that the user must tap to sign in. This tap then initiates Launch URL, directing the user to the MS authentication page, passing a redirect URI of a deep link to come back to our app. This part works just fine. The page in the app that the user is redirected to has an onload event that kicks off an action chain to handle the rest of the OAuth process. The first step is a custom action that is using the following code (there are many print statements for debugging purposes):

// Automatic FlutterFlow imports
import '/backend/schema/structs/index.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/actions/index.dart'; // Imports other custom actions
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom action code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

import 'package:uni_links/uni_links.dart';
import 'dart:async';

Future<String?> authCodeHandler() async {
  StreamSubscription<Uri?>? _linkSubscription;

  if (!FFAppState().hasConsumedInitialUri) {
    try {
      // 1. Check for a deep link via a cold start
      final initialUri = await getInitialUri();
      if (initialUri != null && initialUri.host == 'jensenmobile.com') {
        String? authCode = initialUri.queryParameters['code'];
        print('Cold start: Received auth code: $authCode');
        // If this deep link is new, update global state and return it
        if (FFAppState().lastDeepLink != initialUri.toString()) {
          FFAppState().lastDeepLink = initialUri.toString();
          FFAppState().hasConsumedInitialUri = true;
          return authCode;
        } else {
          print('Cold start: Duplicate deep link detected, ignoring.');
        }
      }
    } catch (e) {
      print('Error checking initial URI: $e');
      // Continue to listen on the stream
    }
  } else {
    print('[' +
        DateTime.now().toIso8601String() +
        '] Initial URI already consumed. Skipping getInitialUri');
  }

  // 2. If no code from cold start, listen to the stream for new deep links.
  final completer = Completer<String?>();

  // Cancel any previous subscription if it exists
  _linkSubscription?.cancel();

  _linkSubscription = uriLinkStream.listen(
    (Uri? uri) {
      if (uri != null && uri.host == 'jensenmobile.com') {
        final authCode = uri.queryParameters['code'];
        print('Stream: Received auth code: $authCode');

        // Cancel the subscription and complete the Future
        _linkSubscription?.cancel();
        if (!completer.isCompleted) {
          print('Is completed: ' + completer.isCompleted.toString());
          completer.complete(authCode);
          print('Is completed: ' + completer.isCompleted.toString());
          print('Stream: Is Completed: $authCode');
        }
      }
    },
    onError: (err) {
      print('Error in deep link stream: $err');
      _linkSubscription?.cancel();
      if (!completer.isCompleted) {
        completer.complete(null);
      }
    },
  );

  // 3. Return the future, with a timeout so it doesn't wait indefinitely.
  return completer.future.timeout(
    const Duration(seconds: 5),
    onTimeout: () {
      print('Timeout completed check - Is Completed: ' +
          completer.isCompleted.toString());
      print("Timeout waiting for new deep link event");
      // _linkSubscription?.cancel();
      return null;
    },
  );
}

I figured out that getInitialUri() caches the first URI that opens the app, so it works perfectly when someone goes through this process to log in. However, we also have a sign out button on our settings page that clears out any access tokens or auth codes that have been saved to state variables (or authenticated user data), and then redirects back to the Sign In page. Signing in, then signing out, then trying to sign in again will cause Microsoft to give an error stating that the auth code has already been used even though the code sent back in the redirect is new. This is due to getInitialUri() always returning the first URI. Through googling and ChatGPT, I learned about uriLinkStream, and added that with a completer to grab the new code when the user is redirected back to the OAuth processing page. With the print statements, I can watch the debugger as I go through this process and see the new codes come across (blocked out the actual codes):

With this, I am seeing that isCompleted on the completer is set to false before I set it to complete, and true after. But when I try to return the future from the completer, it is timing out. I have tried this without the timeout as well and it just hangs indefinitely. I'm assuming that something is interfering with the Future somehow. Either through my lack of understanding when it comes to the asynchronous nature of flutter here, or some type of race condition. I have also added alert dialogs in the action chain to verify the onload is firing (it is), and to see what the custom action is returning. Can anyone offer any insight into what I might be missing with this? Any help would be greatly appreciated.

What have you tried so far?

I have tried using a completer, as well as await uriLinkStream.firstWhere. Both are timing out.

Did you check FlutterFlow's Documentation for this topic?
Yes
1