Get Information from Client

Requests information from the client, such as user input collected using a form.

About client-facing steps

A journey is a sequence of steps that are executed when it's invoked by a client application (known as the "client"). Some steps require involvement from the client, such as to collect user input. Other steps, like validating a token, are executed in the backend by the Mosaic journey engine alone.

When invoked, the journey begins executing the steps while the client waits for further instructions. When a journey reaches a client-facing step, the journey asks the client for the required input and then waits for the client to respond. The client presents their own UI if user interaction is required, and returns input to the journey. The journey then proceeds in a similar manner until it’s completed.

Description

This client-facing step is used to request information from the client application, which is typically input from the user. For example, it can be used to collect user details upon registration, or collect a user identifier like email or username.

Once the step is triggered, the journey asks the client for input and provides a step ID that uniquely identifies this particular step. The client knows what information is expected based on a JSON schema that was exchanged between the journey author and app developer during the integration process. The client is responsible for retrieving the information, including any supporting UI (e.g., HTML form).

Once the information is returned to the journey, it's stored in the configured output variable so it can be accessed by subsequent journey steps. If successful, the journey proceeds to the next step (or a custom branch if configured). If the client cancels, the journey proceeds to a cancel branch if configured and aborted otherwise.

As implied, this step requires coordination between the journey author and app developer, including:

  • How the client should process the request when the journey returns a particular step ID
  • Whether the client needs any context data like user-specific values to prefill a form. If so, this is configured in the step as application data as a free-form JSON and the schema is shared offline.
  • What information the journey expects from the client. The schema is configured in the step.
  • If the client cancels the step, whether it should fail the journey or proceed to another branch
  • Whether the step supports additional branches that can be selected by the client. For example, each branch may represent a different option that the user can choose.
Note

For authentication scenarios, the Login Form step is recommended since it provides a more comfortable setup experience. Branches are automatically created based on the authentication methods you enable for the step, and a default schema is provided for each method.

Configuration

Field Description
Step ID Identifier of the step, used by the client to process the request for information. It can only contain letters, numbers, and underscores (no spaces), and it cannot begin with a number. Some examples of valid IDs are registrationForm1 or registration_form_1
Application Data Data to send to the client, formatted as a free-form JSON and may include expressions. For example, it can contain user-specific values to prefill fields of an update form.
Schema JSON schema describing the expected data returned by the client. It may include expressions. Only used by the journey code generator and journey editor (for autocompleting expressions). This isn't expected to be used by the client dynamically. Access and customize schema via the gear icon.
Error Data When this step is executed within a While Loop step, this represents errors to return to the client. For example, if the loop validates the user input, this can provide validation errors to display to the user. May be configured as a variable or JSON object, and contain expressions.
Custom Branches Additional journey branches supported for this step. For each branch, you define a branch ID and display name to label it in the editor, and a schema for the input expected back from the client.

Note: The schema is used by the code generator and for autocompleting journey expressions, and isn't expected to be used by the client dynamically.

The client can select a branch by returning the branch ID when calling submitClientResponse(). See Example 4: Registration form with cancel and login options for implementation details.
Branch Output Variable Name of the variable used to store the data returned by the client, which can be used in subsequent journey steps.
Cancel Behavior Determines the behavior of client cancellation, which either aborts the journey (default) or proceeds to a cancel branch of the control flow.

Examples

Below are some examples of implementing this step:

Example 1: Registration form

Consider a form that collects the user's name and email. In our example, the step ID is regForm and the information will be stored in a variable named registrationData. The schema contains the fields the client is expected to return, which is used by the journey code generator to render a form.

When executed, this step returns to the client the IDO response object. The client then processes the request based on the step ID passed in journeyStepId. For example:

jsswiftkotlin
Copy
Copied
`<form id="regForm">
  <input type="text" id="full_name" name="full_name" placeholder="full_name" />
  <input type="email" id="email" name="email" placeholder="email" />
  <button>Submit</button>
</form>`

// Collect the form data
const form = document.querySelector("#regForm");
const formData = new FormData(form);
// The data will look like this:
// {
//   "full_name": "John Doe",
//   "email": "john@doe.com"
// }
const data = Object.fromEntries(formData.entries());

// Submit the data to the SDK
window.tsPlatform.ido.submitClientResponse(ClientResponseOptionType.ClientInput, data);
Copy
Copied
// Presenting view controller
let formViewController = FormViewController()
navigationController?.pushViewController(formViewController, animated: true)

// Inside the View Controller:
 @IBAction func onSubmitForm() {
    // Collect the form data
    let full_name = usernameInput.text
    let email = emailInput.text

    // Convert form data to JSON format
    let formData =  {
       full_name: "John Doe",
       email: "john@doe.com"
    }

    // Submit the data to the SDK
    TSIdo.submitClientResponse(clientResponseOptionId: .clientInput, data: formData)
 }
Copy
Copied
// Stores the collected full name and email
// The data will look like this:
// {
//   "full_name": "John Doe",
//   "email": "john@doe.com"
// }
data class RegistrationData(val full_name: String, val email: String)

fun submitRegistrationData() {
    // Assumes the UI shows EditText - etCollectFullName, to collect the full name
    // and another EditText - etCollectEmail, to collect the email
    val responseData = RegistrationData(binding.etCollectFullName.text.toString(), binding.etCollectEmail.text.toString())

    // Submits data to the SDK
    TSIdo.submitClientResponse(TSIdoClientResponseOptionType.ClientInput.type, responseData, callback)
}

Example 2: Update form

Consider a form for updating the user's email. In our example, the step ID is updateForm and the information will be stored in a variable named updatedData. The application data contains a currentEmail field containing the user's current email so it can be used to prefill the form. Our example assumes the email was obtained in a prior step and is stored it in a variable named email. Notice that the variable isn't surrounded by quotes in the expression.

When executed, this step returns to the client the IDO response object. The client then processes the request based on the step ID passed in journeyStepId and the user's current email provided in data.app_data.currentEmail. For example:

jsswiftkotlin
Copy
Copied
function handleGetInformationFromClient(idoResponse) {
  // Generate the form's HTML
  const formHtml = `<form id="updateForm">
    <input type="email" id="email" name="email" value="${idoResponse.data.app_data.currentEmail}" />
    <button>Submit</button>
  </form>`;

  // Render the form
  document.body.innerHTML = formHtml;

  document.querySelector('#updateForm').addEventListener('submit', function(event) {
    event.preventDefault();

    // Collect the form data
    const form = event.target;
    const formData = new FormData(form);

    // The data will look like this:
    // {
    //   "email": "john@doe.com"
    // }
    const data = Object.fromEntries(formData.entries());

    // Submit the data to the SDK
    window.tsPlatform.ido.submitClientResponse(ClientResponseOptionType.ClientInput, data);
  });
}
Copy
Copied
// Inside your main class:
func handleIdoResponse(_ response: IdentityOrchestration.TSIdoServiceResponse) {

    guard let stepId = response.journeyStepId else { debugPrint("Step id is missing in response"); return }

    guard let actionData = response.data else { debugPrint("Data is missing in response"); return }

    switch stepId {
        case .custom(let formId):
            // Present view controller and pass the 'data' fetched above
            let formViewController = FormViewController(data: actionData)
            navigationController?.pushViewController(formViewController, animated: true)
        default:
          // Handle other steps
          break
    }
 }


// Inside the View Controller:
func viewDidLod() {
    super.viewDidLod()

    // Fetch email from the data
    let appData = data["app_data"] as? [String: Any]
    let email = appData["currentEmail"] as? String

    // Update email input text
    emailInput.text = email
}

@IBAction func onSubmitForm() {
    // Collect the form data
    let email = emailInput.text

    // Convert form data to JSON format
    let formData =  {
      "email": "john@doe.com"
     }

    // Submit the data to the SDK
    TSIdo.submitClientResponse(clientResponseOptionId: .clientInput, data: formData)
 }
Copy
Copied
// Updates user email
data class EmailInput(val email: String)
fun handleEmailUpdate(idoResponse: TSIdoServiceResponse) {
    val appData = (idoResponse.data as JSONObject).opt("app_data")

     // Extracts the current email from TSIdoServiceResponse
     val currentEmail = (appData as JSONObject).optString("currentEmail")

     // Assumes the etEmail EditText is used for presenting current email and collecting the updated one
     //and the btnUpdateEmail Button triggers submitClientResponse with the updated email
     binding.etEmail.setText(currentEmail)
     binding.btnUpdateEmail.setOnClickListener {
         val emailResponseData = EmailInput(binding.etEmail.text.toString())
         // Submits the data to the SDK
         TSIdo.submitClientResponse(TSIdoClientResponseOptionType.ClientInput.type, emailResponseData, callback)
     }
}

Example 3: Update form with cancel option

Building on the previous example, consider an update form that includes a cancel option in case the user decides not to proceed with the update. In this example, a cancel branch is added to the step by updating the Cancel Behavior field to Go To Cancel Branch. This branch will be executed in case the client selects the cancel option.

Update form
Click to open the image in a dedicated tab.

When executed, this step returns to the client the IDO response object, which now includes the cancel option in the client response options. For example:

jsswiftkotlin
Copy
Copied
function handleGetInformationFromClient(idoResponse) {
  // Generate the form's HTML
  const formHtml = `<div>
    <form id="updateForm">
      <input type="email" id="email" name="email" value="${idoResponse.data.app_data.currentEmail}" />
      <button type="submit">Submit</button>
    </form>
    <button id="cancel">Cancel</button>
  </div>`;

  // Render the form
  document.body.innerHTML = formHtml;

  document.querySelector('#updateForm').addEventListener('submit', function(event) {
    event.preventDefault();

    // Collect the form data
    const form = event.target;
    const formData = new FormData(form);

    // The data will look like this:
    // {
    //   "email": "john@doe.com"
    // }
    const data = Object.fromEntries(formData.entries());

    // Submit the data to the SDK
    window.tsPlatform.ido.submitClientResponse(ClientResponseOptionType.ClientInput, data);
  });

  // Handle the cancel button
  document.querySelector('#cancel').addEventListener('click', function() {
    window.tsPlatform.ido.submitClientResponse(ClientResponseOptionType.Cancel);
  });
}
Copy
Copied
// Inside your main class:
func handleIdoResponse(_ response: IdentityOrchestration.TSIdoServiceResponse) {

    guard let stepId = response.journeyStepId else { debugPrint("Step id is missing in response"); return }

    guard let actionData = response.data else { debugPrint("Data is missing in response"); return }

    switch stepId {
        case .custom(let formId):
            // Present view controller and pass the 'data' fetched above
            let formViewController = FormViewController(data: actionData)
            navigationController?.pushViewController(formViewController, animated: true)
        default:
          // Handle other steps
          break
    }
 }

// Inside the View Controller:
func viewDidLod() {
    super.viewDidLod()

    // Fetch email from the data
    let appData = data["app_data"] as? [String: Any]
    let email = appData["currentEmail"] as? String

    // Update email input text
    emailInput.text = email
}

@IBAction func onSubmitForm() {
    // Collect the form data
    let email = emailInput.text

    // Convert form data to JSON format
    let formData =  {
      "email": "john@doe.com"
     }

    // Submit the data to the SDK
    TSIdo.submitClientResponse(clientResponseOptionId: .clientInput, data: formData)
 }

@IBAction func onCancel() {
	// Handle the cancel button
    TSIdo.submitClientResponse(clientResponseOptionId: .cancel, data: data)
 }
Copy
Copied
// Updates user email
data class EmailInput(val email: String)
fun handleEmailUpdate(idoResponse: TSIdoServiceResponse) {
    val appData = (idoResponse.data as JSONObject).opt("app_data")
    // Extracts the current email from TSIdoServiceResponse
    val currentEmail = (appData as JSONObject).optString("currentEmail")

    // Assumes the etEmail EditText is used for presenting current email and collecting the updated one
    // the btnUpdateEmail Button triggers submitClientResponse with the updated email
    // the btnCancel Button submits user's cancellation to the SDK
    binding.etEmail.setText(currentEmail)
    binding.btnUpdateEmail.setOnClickListener {
        val emailResponseData = EmailInput(binding.etEmail.text.toString())
        // Submits the data to the SDK
        TSIdo.submitClientResponse(TSIdoClientResponseOptionType.ClientInput.type, emailResponseData, callback)
    }

    // Handles the cancel button
    binding.btnCancel.setOnClickListener {
        // Submits the cancellation to the SDK
        TSIdo.submitClientResponse(TSIdoClientResponseOptionType.Cancel.type, null, callback)
    }
}

Example 4: Registration form with cancel and login options

Building on the first example, consider a registration form that on top of "Submit" and "Cancel" options includes a option to proceed to login, for example, if the user has already registered with the service before. In this example, a custom branch (login) is added to the step, which automatically creates a new branch that will be used to login in the existing user. For this branch, the schema is empty.

Update form
Click to open the image in a dedicated tab.

When executed, this step returns to the client the idoServiceResponse object, which now includes the login option in the clientResponseOptions. For example, if the user clicks a "Go to login" link on the registration form, the client can proceed to direct the journey to the relevant branch that handles login.

jsswiftkotlin
Copy
Copied
function handleGetInformationFromClient(idoResponse) {
  // Generate the form's HTML
  const formHtml = `
    <div>
      <form id="regForm">
        <input type="text" id="full_name" name="full_name" placeholder="Full name" />
        <input type="email" id="email" name="email" placeholder="Email address" />
        <button>Submit</button>
      </form>
      <button id="cancel">Cancel</button>
      <button id="login">Go to login</button>
    </div>
  `;

  // Render the form
  document.body.innerHTML = formHtml;

  document.querySelector('#regForm').addEventListener('submit', function(event) {
    event.preventDefault();

    // Collect the form data
    const form = event.target;
    const formData = new FormData(form);

    // The data will look like this:
    // {
    //   "full_name": "John Doe",
    //   "email": "john@doe.com"
    // }
    const data = Object.fromEntries(formData.entries());

    // Submit the form data to the SDK
    window.tsPlatform.ido.submitClientResponse(ClientResponseOptionType.ClientInput, data);
  });

  // Handle the cancel button
  document.querySelector('#cancel').addEventListener('click', function() {
    // Proceed to cancel branch
    window.tsPlatform.ido.submitClientResponse(ClientResponseOptionType.Cancel);
  });

  // Handle the login button
  document.querySelector('#login').addEventListener('click', function() {
    // Proceed to custom branch
    window.tsPlatform.ido.submitClientResponse("login");
  });
}
Copy
Copied
    // Presenting view controller
    let formViewController = FormViewController()
    navigationController?.pushViewController(formViewController, animated: true)

    // Inside the View Controller:
     @IBAction func onSubmitForm() {
        // Collect the form data
        let full_name = usernameInput.text
        let email = emailInput.text

        // Convert form data to JSON format
        let formData =  {
           full_name: "John Doe",
           email: "john@doe.com"
        }

        // Submit the data to the SDK
        TSIdo.submitClientResponse(clientResponseOptionId: .clientInput, data: formData)
     }

    // Proceed to custom branch
    @IBAction func onLogin() {
        TSIdo.submitClientResponse(clientResponseOptionId: .custom(id: "login"))
    }

    // Proceed to cancel branch
    @IBAction func onCancel() {
        TSIdo.submitClientResponse(clientResponseOptionId: .cancel)
    }
Copy
Copied
// Data class that stores the collected full name and email
// The data will look like this:
// {
//   "full_name": "John Doe",
//   "email": "john@doe.com"
// }
data class RegistrationData(val full_name: String, val email: String)
...
private fun setupUI() {
        //handle submit button click
        binding.btnSubmitUserInfo.setOnClickListener{
            submitRegistrationData()
        }

        //handle login button click
        binding.btnSubmitUserInfo.setOnClickListener{
            handleLoginBranch()
        }

        //handle cancel button click
        binding.btnCancel.setOnClickListener{
            handleCancellation()
        }
    }


private fun submitRegistrationData() {
    // Assumes the UI shows EditText - etCollectFullName, to collect the full name
    // and another EditText - etCollectEmail, to collect the email
    val responseData = RegistrationData(binding.etCollectFullName.text.toString(), binding.etCollectEmail.text.toString())

    // Submits data to the SDK
    TSIdo.submitClientResponse(TSIdoClientResponseOptionType.ClientInput.type, responseData, callback)
}

private fun handleLoginBranch() {
    // Proceed to custom branch
    TSIdo.submitClientResponse("login", null, callback)
}

private fun handleCancellation() {
    // Proceed to cancel branch
    TSIdo.submitClientResponse(TSIdoClientResponseOptionType.Cancel.type, null, callback)
}