Step 5. Implement the iOS bridge code
Review the files that allow for the "bridging" between the Flutter project and the native Ping SDK.
In Xcode, navigate to the Runner/Runner directory, and you will see a few important files:
FRAuthSampleBridge.swift
-
The main Swift bridging code that provides the callable methods for the Flutter layer.
FRAuthSampleStructs.swift
-
Provides the structs for the Swift bridging code.
FRAuthSampleHelpers.swift
-
Provides the extensions to often used objects within the bridge code.
FRAuthConfig
-
A
.plist
file that configures the Ping SDK for iOS to the appropriate authorization server.
The remainder of the files within the workspace are automatically generated when you create a Flutter project with the CLI command, so you can ignore them. |
Configure your .plist
file
In the Xcode directory/file list section, also known as the Project Navigator, complete the following:
-
Find
FRAuthConfig.plist
file within theios/Runner
directory. -
Add the name of your PingOne Advanced Identity Cloud or PingAM cookie.
-
Add the OAuth client you created from above.
-
Add your authorization server URLs.
-
Add the login tree you created above.
A hypothetical example (your values may vary):
<dict>
<key>forgerock_cookie_name</key>
- <string></string>
+ <string>e1babb394ea5130</string>
<key>forgerock_enable_cookie</key>
<true/>
<key>forgerock_oauth_client_id</key>
<string>flutterOAuthClient</string>
<key>forgerock_oauth_redirect_uri</key>
<string>https://com.example.flutter.todo/callback</string>
<key>forgerock_oauth_scope</key>
<string>openid profile email address</string>
<key>forgerock_oauth_url</key>
- <string></string>
+ <string>https://auth.forgerock.com/am</string>
<key>forgerock_oauth_threshold</key>
<string>60</string>
<key>forgerock_url</key>
- <string></string>
+ <string>https://auth.forgerock.com/am</string>
<key>forgerock_realm</key>
- <string></string>
+ <string>alpha</string>
<key>forgerock_timeout</key>
<string>60</string>
<key>forgerock_keychain_access_group</key>
<string>com.forgerock.flutterTodoApp</string>
<key>forgerock_auth_service_name</key>
- <string></string>
+ <string>UsernamePassword</string>
<key>forgerock_registration_service_name</key>
- <string></string>
+ <string>Registration</string>
</dict>
Descriptions of relevant values:
forgerock_cookie_name
-
If you have an PingOne Advanced Identity Cloud tenant, you can find this random string value under the Tenant Settings in the top-right dropdown in the admin UI. If you have your own installation of PingAM, this is often
iPlanetDirectoryPro
. forgerock_url
andforgerock_oauth_url
-
The URL of PingAM within your server installation.
forgerock_realm
-
The realm of your server (likely
root
,alpha
, orbeta
). forgerock_auth_service_name
-
This is the journey/tree that you use for login.
forgerock_registration_service_name
-
This is the journey/tree that you use for registration, but it will not be used until a future part of this tutorial series.
Write the start()
method
Staying within the Runner
directory, find the FRAuthSampleBridge
file and open it.
We have parts of the file already stubbed out and the dependencies are already installed.
All you need to do is write the functionality.
For the SDK to initialize with the FRAuth.plist
configuration from Step 2, write the start
function as follows:
import Foundation
import FRAuth
import FRCore
import Flutter
public class FRAuthSampleBridge {
var currentNode: Node?
private let session = URLSession(configuration: .default)
@objc func frAuthStart(result: @escaping FlutterResult) {
+ /**
+ * Set log level to all
+ */
+ FRLog.setLogLevel([.all])
+
+ do {
+ try FRAuth.start()
+ let initMessage = "SDK is initialized"
+ FRLog.i(initMessage)
+ result(initMessage)
+ } catch {
+ FRLog.e(error.localizedDescription)
+ result(FlutterError(code: "SDK Init Failed",
+ message: error.localizedDescription,
+ details: nil))
+ }
}
The start()
function above calls the Ping SDK for iOS’s start()
method on the FRAuth
class.
There’s a bit more that may be required within this function for a production app.
We’ll get more into this in a separate part of this series, but for now, let’s keep this simple.
Write the login()
method
Once the start()
method is called, and it has initialized, the SDK is ready to handle user requests.
Let’s start with login()
.
Just underneath the start()
method we wrote above, add the login()
method.
@@ collapsed @@
@objc func frAuthStart(result: @escaping FlutterResult) {
// Set log level according to your needs
FRLog.setLogLevel([.all])
do {
try FRAuth.start()
result("SDK Initialised")
FRUser.currentUser?.logout()
}
catch {
FRLog.e(error.localizedDescription)
result(FlutterError(code: "SDK Init Failed",
message: error.localizedDescription,
details: nil))
}
}
@objc func login(result: @escaping FlutterResult) {
+ FRUser.login { (user, node, error) in
+ self.handleNode(user, node, error, completion: result)
+ }
}
@@ collapsed @@
This login()
function initializes the journey/tree specified for authentication.
You call this method without arguments as it does not log the user in.
This initial call to the server will return the first set of callbacks
that represents the first node in your journey/tree to collect user data.
Also, notice that we have a special "handler" function within the callback of FRUser.login()
.
This handleNode()
method serializes the node
object that the Ping SDK for iOS returns in a JSON string.
Data passed between the "native" layer and the Flutter layer is limited to serialized objects.
This method can be written in many ways and should be written in whatever way is best for your application.
Write the next()
method
To finalize the functionality needed to complete user authentication,
we need a way to iteratively call next()
until the tree completes successfully or fails.
In the bridge file, add a private method called handleNode()
.
First, we will write the decoding of the JSON string and prepare the node for submission.
@@ collapsed @@
@objc func login(result: @escaping FlutterResult) {
FRUser.login { (user, node, error) in
self.handleNode(user, node, error, completion: result)
}
}
@objc func next(_ response: String, completion: @escaping FlutterResult) {
+ let decoder = JSONDecoder()
+ let jsonData = Data(response.utf8)
+ if let node = self.currentNode {
+ var responseObject: Response?
+ do {
+ responseObject = try decoder.decode(Response.self, from: jsonData)
+ } catch {
+ FRLog.e(String(describing: error))
+ completion(FlutterError(code: "Error",
+ message: error.localizedDescription,
+ details: nil))
+ }
+
+ let callbacksArray = responseObject!.callbacks ?? []
+
+ for (outerIndex, nodeCallback) in node.callbacks.enumerated() {
+ if let thisCallback = nodeCallback as? SingleValueCallback {
+ for (innerIndex, rawCallback) in callbacksArray.enumerated() {
+ if let inputsArray = rawCallback.input, outerIndex == innerIndex,
+ let value = inputsArray.first?.value {
+
+ thisCallback.setValue(value.value as! String)
+ }
+ }
+ }
+ }
+
+ //node.next logic goes here
+
+
+ } else {
+ completion(FlutterError(code: "Error",
+ message: "UnkownError",
+ details: nil))
+ }
}
@@ collapsed @@
Now that you’ve prepared the data for submission, introduce the node.next()
call from the Ping SDK for iOS.
Then, handle the subsequent node
returned from the next()
call,
or process the success or failure representing the completion of the journey/tree.
@@ collapsed @@
for (outerIndex, nodeCallback) in node.callbacks.enumerated() {
if let thisCallback = nodeCallback as? SingleValueCallback {
for (innerIndex, rawCallback) in callbacksArray.enumerated() {
if let inputsArray = rawCallback.input, outerIndex == innerIndex,
let value = inputsArray.first?.value {
thisCallback.setValue(value)
}
}
}
}
//node.next logic goes here
+ node.next(completion: { (user: FRUser?, node, error) in
+ if let node = node {
+ self.handleNode(user, node, error, completion: completion)
+ } else {
+ if let error = error {
+ completion(FlutterError(code: "LoginFailure",
+ message: error.localizedDescription,
+ details: nil))
+ return
+ }
+
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = .prettyPrinted
+ do {
+ if let user = user, let token = user.token, let data = try? encoder.encode(token), let jsonAccessToken = String(data: data, encoding: .utf8) {
+ completion(try ["type": "LoginSuccess", "sessionToken": jsonAccessToken].toJson())
+ } else {
+ completion(try ["type": "LoginSuccess", "sessionToken": ""].toJson())
+ }
+ }
+ catch {
+ completion(FlutterError(code: "Serializing Response failed",
+ message: error.localizedDescription,
+ details: nil))
+ }
+ }
+ })
} else {
completion(FlutterError(code: "Error",
message: "UnkownError",
details: nil))
}
}
@@ collapsed @@
The above code handles a limited number of callback types.
Handling full authentication and registration journeys/trees requires additional callback handling.
To keep this tutorial simple, we’ll focus just on SingleValueCallback
type.
Write the logout()
bridge method
Finally, add the following lines of code to enable logout for the user:
@@ collapsed @@
} else {
completion(FlutterError(code: "Error",
message: "UnkownError",
details: nil))
}
@objc func frLogout(result: @escaping FlutterResult) {
+ FRUser.currentUser?.logout()
+ result("User logged out")
}
@@ collapsed @@