Step 4a. Implement iOS bridge code
Review the files that allow for the "bridging" between the Flutter project and the native Ping SDK.
In Xcode, open sdk-sample-apps/flutter/flutter_todo/ios/Runner.xcworkspace
, and navigate to the Runner/Runner directory, and you will see a few important files:
FRAuthSampleBridge
-
The main Swift bridging code that provides the callable methods for the Flutter layer.
FRAuthSampleHelpers
-
Provides the extensions to often used objects within the bridge code.
FRAuthSampleStructs
-
Provides the structs for the Swift bridging code.
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 the iOS project
In Xcode, open FRAuthSampleBridge
, and update the Configuration
struct with the details of your server, and the authentication journey and public OAuth 2.0 client you created earlier.
A hypothetical example (your values may vary):
FRAuthSampleBridge.swift
struct Configuration {
/// The main authentication journey name.
static let mainAuthenticationJourney = "sdkUsernamePasswordJourney"
/// The URL of the authentication server.
static let amURL = "https://openam-forgerock-sdks.forgeblocks.com/am"
/// The name of the cookie used for authentication.
static let cookieName = "ch15fefc5407912"
/// The realm used for authentication.
static let realm = "alpha"
/// The OAuth client ID.
static let oauthClientId = "sdkPublicClient"
/// The OAuth redirect URI.
static let oauthRedirectURI = "https://com.example.flutter.todo/callback"
/// The OAuth scopes.
static let oauthScopes = "openid profile email address"
/// The discovery endpoint for OAuth configuration.
static let discoveryEndpoint = "https://openam-forgerock-sdks.forgeblocks.com/am/oauth2/realms/root/realms/alpha/.well-known/openid-configuration"
}
Write the start()
method
For the SDK to initialize, write the start
function as follows:
FRAuthSampleBridge.swift
@objc func frAuthStart(result: @escaping FlutterResult) {
// Set log level according to your needs
FRLog.setLogLevel([.all])
do {
let options = FROptions(
url: Configuration.amURL,
realm: Configuration.realm,
cookieName: Configuration.cookieName,
authServiceName: Configuration.mainAuthenticationJourney,
oauthClientId: Configuration.oauthClientId,
oauthRedirectUri: Configuration.oauthRedirectURI,
oauthScope: Configuration.oauthScopes
)
try FRAuth.start(options: options)
result("SDK Initialised")
FRUser.currentUser?.logout()
} 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.
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.
FRAuthSampleBridge.swift
@objc func login(result: @escaping FlutterResult) {
FRUser.login { (user, node, error) in
self.handleNode(user, node, error, completion: result)
}
}
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 authentication journey 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.
FRAuthSampleBridge.swift
@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 {
print(String(describing: error))
completion(FlutterError(code: "Error",
message: error.localizedDescription,
details: nil))
return
}
let callbacksArray = responseObject?.callbacks ?? []
// If the array is empty there are no user inputs. This can happen in callbacks like the DeviceProfileCallback, that do not require user interaction.
// Other callbacks like SingleValueCallback, will return the user inputs in an array of dictionaries [[String:String]] with the keys: identifier and text
if callbacksArray.count == 0 {
for nodeCallback in node.callbacks {
if let thisCallback = nodeCallback as? DeviceProfileCallback {
let semaphore = DispatchSemaphore(value: 1)
semaphore.wait()
thisCallback.execute { _ in
semaphore.signal()
}
}
}
} else {
for (outerIndex, nodeCallback) in node.callbacks.enumerated() {
if let thisCallback = nodeCallback as? KbaCreateCallback {
for (innerIndex, rawCallback) in callbacksArray.enumerated() {
if let inputsArray = rawCallback.input, outerIndex == innerIndex {
for input in inputsArray {
if let value = input.value?.value as? String {
if input.name.contains("question") {
thisCallback.setQuestion(value)
} else {
thisCallback.setAnswer(value)
}
}
}
}
}
}
if let thisCallback = nodeCallback as? SingleValueCallback {
for (innerIndex, rawCallback) in callbacksArray.enumerated() {
if let inputsArray = rawCallback.input, outerIndex == innerIndex, let value = inputsArray.first?.value {
switch value.originalType {
case .string:
thisCallback.setValue(value.value as? String)
case .int:
thisCallback.setValue(value.value as? Int)
case .double:
thisCallback.setValue(value.value as? Double)
case .bool:
thisCallback.setValue(value.value as? Bool)
default:
break
}
}
}
}
}
}
} else {
completion(FlutterError(code: "Error",
message: "UnkownError",
details: nil))
}
}
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.
Add the following to the next
function:
//Call node.next
node.next(completion: { (user: FRUser?, node, error) in
if let node = node {
//Handle node and return
self.handleNode(user, node, error, completion: completion)
} else {
if let error = error {
//Send the error back in the rejecter - nextStep.type === 'LoginFailure'
completion(FlutterError(code: "LoginFailure",
message: error.localizedDescription,
details: nil))
return
}
//Transform the response for the nextStep.type === 'LoginSuccess'
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) {
FRLog.i("LoginSuccess - sessionToken: \(jsonAccessToken)")
completion(try ["type": "LoginSuccess", "sessionToken": jsonAccessToken].toJson())
} else {
FRLog.i("LoginSuccess")
completion(try ["type": "LoginSuccess", "sessionToken": ""].toJson())
}
}
catch {
completion(FlutterError(code: "Serializing Response failed",
message: error.localizedDescription,
details: nil))
}
}
})
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.