Ping SDKs

Step 6. Implement the UI in Flutter

Review how the application renders the home view.

In Android Studio, navigate to the Flutter project, flutter_todo_app > java/main.dart.

Open up the second file in the above sequence, the java/main.dart file, and notice the following:

  1. The use of import 'package:flutter/material.dart'; from the Flutter library.

  2. The TodoApp class extending StatefulWidget.

  3. The _TodoAppState class extending State<TodoApp>.

  4. Building the UI for the navigation bar.

    import 'package:flutter/material.dart';
    import 'package:flutter_todo_app/home.dart';
    import 'package:flutter_todo_app/login.dart';
    import 'package:flutter_todo_app/todolist.dart';
    
    void main() => runApp(
      new TodoApp(),
    );
    
    class TodoApp extends StatefulWidget {
      @override
      _TodoAppState createState() => new _TodoAppState();
    }
    
    class _TodoAppState extends State<TodoApp> {
      int _selectedIndex = 0;
    
      final _pageOptions = [
        HomePage(),
        LoginPage(),
        TodoList(),
      ];
    
      void _onItemTapped(int index) {
        setState(() {
          _selectedIndex = index;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return new MaterialApp(
          home: Scaffold(
            body: _pageOptions[_selectedIndex],
            bottomNavigationBar: BottomNavigationBar(
              items: const <BottomNavigationBarItem>[
                BottomNavigationBarItem(
                  icon: Icon(Icons.home),
                  label: 'Home',
                ),
                BottomNavigationBarItem(
                  icon: Icon(Icons.vpn_key),
                  label: 'Sign In',
                ),
              ],
              currentIndex: _selectedIndex,
              selectedItemColor: Colors.blueAccent[800],
              onTap: _onItemTapped,
              backgroundColor: Colors.grey[200],
            )
          ),
        );
      }
    }

Flutter uses something called MethodChannel to communicate between Flutter and the Native layer. In this application we will define a MethodChannel with the following identifier: 'forgerock.com/SampleBridge'.

The same identifier will be used in the iOS FRSampleBridge so that the two layers communicate and pass information. To initialize the Ping SDK when the log in view first loads, we call the frStart() method on the bridge code.

It’s important to initialize the SDK as early as possible. Call this initialization step, so it resolves before any other native SDK methods can be used.

Building the login view

Navigate to the app’s login view within the Simulator. You should see an empty screen with a button, since the app doesn’t have the data needed to render the form. To render the correct form, retrieve the initial data from the server. This is our first task.

Since most of the action is taking place in flutter_todo_app/Java/login.dart, open it and add the following:

  1. Import FRNode.dart from the Dart helper classes provided for improved ergonomics for handling callbacks:

    import 'package:flutter_todo_app/FRNode.dart';
  2. If not already there, import async, convert, scheduler, services from the flutter package. Add the following:

    import 'dart:async';
    import 'dart:convert';
    import 'package:flutter/scheduler.dart';
    import 'package:flutter/services.dart';
  3. Create a static reference for the method channel

    MethodChannel('forgerock.com/SampleBridge')
  4. Override the initState Flutter lifecycle method and initialize the SDK.

    class _LoginPageState extends State<LoginPage> {
    +  static const platform = MethodChannel('forgerock.com/SampleBridge'); //Method channel as defined in the native Bridge code
    
    @@ collapsed @@
    
     //Lifecycle Methods
    +  @override
    +  void initState() {
    +    super.initState();
    +    SchedulerBinding.instance?.addPostFrameCallback((_) => {
    +      //After creating the first controller that uses the SDK, call the 'frAuthStart' method to initialize the native SDKs.
    +      _startSDK()
    +    });
    +  }
    
     // SDK Calls -  Note the promise type responses. Handle errors on the UI layer as required
       Future<void> _startSDK() async {
    +    String response;
    +    try {
    +
    +      //Start the SDK. Call the frAuthStart channel method to initialise the native SDKs
    +      final String result = await platform.invokeMethod('frAuthStart');
    +      response = 'SDK Started';
    +      _login();
    +    } on PlatformException catch (e) {
    +      response = "SDK Start Failed: '${e.message}'.";
    +    }
      }
    
    @@ collapsed @@

To develop the login functionality, we first need to use the login() method from the bridge code to get the first set of callbacks, and then render the form appropriately. This login() method is an asynchronous method. Let’s get started!

Compose the data gathering process using the following:

  1. After the SDK initialization is complete, call the _login() method.

  2. Use the platform reference to call the Bridge login method platform.invokeMethod('login').

  3. Parse the response and call _handleNode() method.

  4. Handle any errors that might be returned from the Bridge.

    @@ collapsed @@
    
      Future<void> _login() async {
    +    try {
    +      //Call the default login tree.
    +      final String result = await platform.invokeMethod('login');
    +      Map<String, dynamic> frNodeMap = jsonDecode(result);
    +      var frNode = FRNode.fromJson(frNodeMap);
    +      currentNode = frNode;
    +
    +      //Upon completion, a node with callbacks will be returned, handle that node and present the callbacks to UI as needed.
    +      _handleNode(frNode);
    +    } on PlatformException catch (e) {
    +      debugPrint('SDK Error: $e');
    +      Navigator.pop(context);
    +    }
      }

The above code is expected to return either a Node with a set of Callback objects, or a success/error message. We need to handle any exceptions thrown from the bridge on the catch block. Typically, when we begin the authentication journey/tree, this returns a Node. Using the FRNode helper object, we parse the result in a native Flutter FRNode object.

In the next step we are going to "handle" this node, and produce our UI.

@@ collapsed @@
  // Handling methods
  void _handleNode(FRNode frNode) {
+    // Go through the node callbacks and present the UI fields as needed. To determine the required UI element, check the callback type.
+    frNode.callbacks.forEach((frCallback) {
+      final controller = TextEditingController();
+      final field = TextField(
+        controller: controller,
+        obscureText: frCallback.type == "PasswordCallback", // If the callback type is 'PasswordCallback', make this a 'secure' textField.
+        enableSuggestions: false,
+        autocorrect: false,
+        decoration: InputDecoration(
+          border: OutlineInputBorder(),
+          labelText: frCallback.output[0].value,
+        ),
+      );
+      setState(() {
+        _controllers.add(controller);
+        _fields.add(field);
+      });
+    });
  }

The _handleNode() method focuses on the callbacks property. This property contains instructions about what to render to collect user input.

The previous code processes the Node callbacks and generates two TextField objects:

  • A TextField for the username.

  • A TextField for the password.

Use the frCallback.type to differentiate between the two TextField objects and obscure the text of each TextField. Next, add the TextField objects to the List and create the accompanying TextEditingControllers.

Run the app again, and you should see a dynamic form that reacts to the callbacks returned from our initial call to ForgeRock.

flutter login form updated
Figure 1. Login screen form

Submitting the login form

Since a form that can’t submit anything isn’t very useful, we’ll now handle the submission of the user input values to ForgeRock. Continuing in login.dart, edit the current _okButton element, adding an onPressed handler calling the _next() function. This function should do the following:

  1. Go through the _controllers array to capture the values of the form elements.

  2. Update the Node callbacks with those values.

  3. Submit the results to ForgeRock.

  4. Check the response for a LoginSuccess message, or if a new node is returned, handle this in a similar way and resubmit the user inputs as needed.

  5. Handle errors with a generic failure message.

    @@ collapsed @@
    
    Widget _okButton() {
      return Container(
        color: Colors.transparent,
        width: MediaQuery.of(context).size.width,
        margin: EdgeInsets.all(15.0),
        height: 60,
        child: TextButton(
          style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.blue)),
          onPressed: () async {
            showAlertDialog(context);
    +       _next();
          },
          child:
          Text(
            "Sign in",
            style: TextStyle(color: Colors.white),
          ),
        ),
      );
    }
    
    @@ collapsed @@
    
     Future<void> _next() async {
       // Capture the User Inputs from the UI, populate the currentNode callbacks and submit back to {am_name}
    +  currentNode.callbacks.asMap().forEach((index, frCallback) {
    +    _controllers.asMap().forEach((controllerIndex, controller) {
    +      if (controllerIndex == index) {
    +        frCallback.input[0].value = controller.text;
    +      }
    +    });
    +  });
    +  String jsonResponse = jsonEncode(currentNode);
    +  try {
    +    // Call the SDK next method, to submit the User Inputs to {am_name}. This will return the next Node or a Success/Failure
    +    String result = await platform.invokeMethod('next', jsonResponse);
    +    Navigator.pop(context);
    +    Map<String, dynamic> response = jsonDecode(result);
    +    if (response["type"] == "LoginSuccess") {
    +      _navigateToNextScreen(context);
    +    } else  {
    +      //If a new node is returned, handle this in a similar way and resubmit the user inputs as needed.
    +      Map<String, dynamic> frNodeMap = jsonDecode(result);
    +      var frNode = FRNode.fromJson(frNodeMap);
    +      currentNode = frNode;
    +      _handleNode(frNode);
    +    }
    +  } catch (e) {
    +    Navigator.pop(context);
    +    debugPrint('SDK Error: $e');
    +  }
     }
    
    @@ collapsed @@

After the app refreshes, use the test user to login. If successful, you should see a success message. Congratulations, you are now able to authenticate users!

flutter login success empty
Figure 2. Login screen with successful authentication

What’s more, you can verify the authentication details by going to the Xcode or Android Studio log, and observing the result of the last call to the server. It should have a type of LoginSuccess with token information.

LoginSuccess
Figure 3. Successful login response from Xcode

Handling the user provided values

You may ask, "How did the user’s input values get added to the Node object?" Let’s take a look at the component for handling the user input submission. Notice how we loop through the Node Callbacks and the _controllers array. Each input is set on the frCallback.input[0].value, and then we call FRSampleBridge.next() method.

@@ collapsed @@

// Capture the User Inputs from the UI, populate the currentNode callbacks and submit back to {am_name}
  currentNode.callbacks.asMap().forEach((index, frCallback) {
    _controllers.asMap().forEach((controllerIndex, controller) {
      if (controllerIndex == index) {
        frCallback.input[0].value = controller.text;
      }
    });
  });

  String jsonResponse = jsonEncode(currentNode);

@@ collapsed @@

  try {
    // Call the SDK next method, to submit the User Inputs to {am_name}. This will return the next Node or a Success/Failure
    String result = await platform.invokeMethod('next', jsonResponse);

@@ collapsed @@

  } catch (e) {
    Navigator.pop(context);
    debugPrint('SDK Error: $e');
  }

There are two important items to focus on regarding the FRCallback object.

callback.type

Retrieves the call back type so that can identify how to present the callback in the UI.

callback.input

The input array that contains the inputs that you need to set the values for.

Since the NameCallback and PasswordCallback only have one input, you can set the value of them by calling frCallback.input[0].value = controller.text;. Some other callbacks might contain multiple inputs, so some extra code will be required to set the values of those.

Each callback type has its own collection of inputs and outputs. Those are exposed as arrays that the developer can loop through and act upon. Many callbacks have common base objects in iOS and Android, like the SingleValueCallback, but appear as different types NameCallback or PasswordCallback to allow for easier differentiation in the UI layer. You can find a full list of the supported callbacks of the SDKs here.

Redirecting to the TodoList screen and requesting user info

Now that the user can log in, let’s go one step further and redirect to the TodoList screen. After we get the LoginSccess message we can call the _navigateToNextScreen() method. This will navigate to the TodoList class. When the TodoList initializes, we want to request information about the authenticated user to display their name and other information. We will now utilize the existing FRAuthSampleBridge.getUserInfo() method already included in the bridge code.

Let’s do a little setup before we make the request to the server:

  1. Override the initState() method in the _TodoListState class in todolist.dart.

  2. Create a SchedulerBinding.instance?.addPostFrameCallback to execute some code when the state is loaded.

  3. Call _getUserInfo().

    @@ collapsed @@
    //Lifecycle methods
    
    + @override
    + void initState() {
    +   super.initState();
    +   SchedulerBinding.instance?.addPostFrameCallback((_) => {
    +     //Calling the userinfo endpoint is going to give use some user profile information to enrich our UI. Additionally, verifies that we have a valid access token.
    +     _getUserInfo()
    +   });
    + }
    
    @@ collapsed @@

With the setup complete, implement the request to your server for the user’s information. Within this empty _getUserInfo(), add an async function to make that call FRAuthSampleBridge.getUserInfo() and parse the response.

@@ collapsed @@

Future<void> _getUserInfo() async {
  showAlertDialog(context);
  String response;
+  try {
+    final String result = await platform.invokeMethod('getUserInfo');
+    Map<String, dynamic> userInfoMap = jsonDecode(result);
+    response = result;
+    header = userInfoMap["name"];
+    subtitle = userInfoMap["email"];
+    Navigator.pop(context);
+    setState(() {
+      _getTodos();
+    });
+  } on PlatformException catch (e) {
+    response = "SDK Start Failed: '${e.message}'.";
+    Navigator.pop(context);
+  }
+  debugPrint('SDK: $response');
}

@@ collapsed @@

In the code above, we collected the user information and set the name and email of the user in some variables. In addition to updating the user info, we will call the _getTodos() method in order to retrieve ToDos from the server. Notice that we use the setState() function. This ensures that our UI is updated based on the newly received information.

When you test this in the Simulator, completing a successful authentication results in the home screen being rendered with a success message. The user’s name and email are included for visual validation. You can also view the console in Xcode and see more complete logs.

LoginSuccess
Figure 4. Home screen after successful authentication

Adding logout functionality

Clicking the Sign Out button results in creating and rendering an alert view asking you if you are sure you want to log out with two options (yes/no). Clicking yes does nothing at the moment. We will now implement that missing logic.

To add the logic into the view to call this new Swift method:

  1. Open the todolist.dart file, and add the following:

    @@ collapsed @@
    
      TextButton(
        child: const Text('Yes'),
    +        onPressed: () {
    +            Navigator.of(context).pop();
    +            _logout();
    +        },
      ),
    
    @@ collapsed @@
    
      Future<void> _logout() async {
    +    final String result = await platform.invokeMethod('logout');
    +    _navigateToNextScreen(context);
      }
  2. Revisit the app within the Simulator, and tap the Sign Out button.

    This time around when clicking Yes will dispose of the alert and log you out, returning you back to the log in screen.

    If you tap No, you will return to the main list screen.

Testing the app

You should now be able to successfully authenticate a user, display the user’s information, and log a user out.

Congratulations, you just built a protected iOS app with Flutter!