CSS 或 JavaScript 如何对 Azure B2C 电子邮件验证过程的不同步骤做出反应?

How can CSS or JavaScript react to the different steps of the Azure B2C Email Verification process?

我们在 Azure B2C 中使用自定义策略,我们在注册期间使用电子邮件验证来减少垃圾邮件帐户。

很多人都知道,B2C 的默认电子邮件验证过程有点尴尬,因为用户需要在同一个注册表单上完成多个步骤:

  1. 输入电子邮件。
  2. 发送验证码。
  3. 在文本框中输入验证码;重新发送代码;或更改电子邮件地址。
  4. 点击按钮验证代码。
  5. 填写其余的注册字段。
  6. 点击按钮完成注册

...全部在一个表格中。基本上,默认表单布局的认知负荷对于普通消费者来说太多了。

为了解决这个问题,我们使用了 CIAM 示例旅程项目提供的 split email verification and sign-up 用户旅程。通过这个过程,事情变得更好了,但仍然不完美——每个屏幕上的指导都不够,按钮和字段等用户控件在请求期间消失并重新出现,用户发现“继续”和“取消”按钮令人困惑。我们仍然想进一步简化它并使 UI 不那么“抽搐”。

至少,我们需要一种方法来根据用户所处的流程阶段应用不同的 CSS 样式。例如,如果用户处于关于他们输入电子邮件地址或提供验证码的步骤;我们想覆盖 B2C 在 AJAX/XHR 请求期间隐藏控件的默认行为。不幸的是,看起来 B2C 的 JavaScript 是一个封闭的系统,它只是在页面上的所有 UI 控件中添加和删除内联样式来完成它想要的。更糟糕的是,页面中似乎没有 CSS class 或数据属性来告诉我们小部件处于流程的哪个“步骤”。

有什么方法可以连接到 B2C JavaScript 以便我们可以对其逻辑做出反应?理想情况下,我们可以在我们的页面上设置一个“模式”数据属性,然后我们可以在样式表中引用它。

为了实现这一点,我们最终使用 jQuery 来侦听 B2C 发出的实际 XHR 请求,然后绑定到他们的响应以确定我们所处的模式。微软存在风险可以改变 API 的工作方式并完全破坏此实现,但它比我能想到的任何其他方法都更可靠。

在该方法之前,我们尝试寻找 B2C 添加到具有无效值的字段的 CSS 类,以及基于 UI 控件的模式或不可见,但这些方法极其脆弱。我们的代码在 Azure 之前被调用(因此页面中的 类 还不是最新的),或者我们的代码没有处理我们需要响应的所有错误情况,因为当电子邮件地址无效,用户尝试次数过多等

我们的方法采用 JavaScript 和 CSS 嵌入流程的自定义 HTML 模板的形式。我们还确保 enable JavaScript in the User Journey.

这是我们使用的代码:

const STATUS_OK        = 0,
      STATUS_BAD_EMAIL = 2;

const REQUEST_TYPE_CODE_REQUEST    = 'VERIFICATION_REQUEST',
      REQUEST_TYPE_CODE_VALIDATION = 'VALIDATION_REQUEST';

let $body              = $('body'),
    $emailField        = $('#email');
    $changeEmailButton = $('#email_ver_but_edit');

/**
 * Switches the mode of the form into requesting a confirmation code.
 */
function switchToRequestConfirmationCodeMode() {
  $body.attr('data-mode', 'send-code');
}

/**
 * Switches the mode of the form into validating a confirmation code.
 */
function switchToVerifyCodeMode() {
  $body.attr('data-mode', 'verify-code');
}

/**
 * Switches the mode of the form into completing sign-up.
 */
function switchToEmailVerifiedMode() {
  $body.attr('data-mode', 'email-verified');
}

/**
 * Callback invoked when a verification code is being requested via XHR.
 *
 * @param {jqXHR} jqXhr
 *   The XHR being sent out to get a verification code sent out.
 */
function handleSendVerificationCodeRequest(jqXhr) {
  let controlsToDisableDuringRequest = [
    '#email_ver_but_send',
  ];

  disableControlsDuringRequest(controlsToDisableDuringRequest, jqXhr);

  jqXhr.done((data) => {
    if ((data.status === "200") && (data.result === STATUS_OK)) {
      // Code sent successfully.
      switchToVerifyCodeMode();
    }
  });
}

/**
 * Callback invoked when a verification code is being validated via XHR.
 *
 * @param {jqXHR} jqXhr
 *   The XHR being sent out to validate a verification code entered by the
 *   user.
 */
function handleValidateCodeRequest(jqXhr) {
  let controlsToDisableDuringRequest = [
    '#email_ver_input',
    '#email_ver_but_verify',
    '#email_ver_but_resend',
  ];

  disableControlsDuringRequest(controlsToDisableDuringRequest, jqXhr);

  jqXhr.done((data) => {
    if (data.status === "200") {
      if (data.result === STATUS_OK) {
        // Code was accepted.
        switchToEmailVerifiedMode();
      } else if (data.result === STATUS_BAD_EMAIL) {
        // Too many attempts; switch back to requesting a new code.
        switchToRequestConfirmationCodeMode();
      }
    }
  });
}

/**
 * Disables the given controls during the provided XHR.
 *
 * @param {string[]} controls
 *   A list of jQuery selectors for the controls to disable during the
 *   XHR.
 * @param {jqXHR} jqXhr
 *   The XHR during which controls should be disabled.
 */
function disableControlsDuringRequest(controls, jqXhr) {
  let $controls = $(controls.join(','));

  // Disable controls during the XHR.
  $controls.prop("disabled", true);

  // Release the controls after the XHR, even on failure.
  jqXhr.always(() => {
    $controls.prop("disabled", false);
  })
}

// At present, there doesn't seem to be a way to be notified about/detect
// whether the user should be prompted for a verification code. But, for
// our styling needs, we need a way to determine what "mode" the form is
// in.
//
// To accomplish this, we listen for when B2C is sending out a
// verification code, and then toggle the "mode" of the form only if the
// request is successful. That way, we do not toggle the form if there are
// client-side or server-side validation errors with the email address
// that the user provided.
$(document).ajaxSend(function (e, jqXhr, settings) {
  if (settings.contentType.startsWith('application/x-www-form-urlencoded')) {
    let parsedData  = new URLSearchParams(settings.data),
        requestType = parsedData.get('request_type')

    if (requestType === REQUEST_TYPE_CODE_REQUEST) {
      handleSendVerificationCodeRequest(jqXhr);
    }
    else if (requestType === REQUEST_TYPE_CODE_VALIDATION) {
      handleValidateCodeRequest(jqXhr);
    }
  }
});

// Reset the mode of the form if the user changes the email address entry.
$emailField.keydown(() => switchToRequestConfirmationCodeMode());
$emailField.change(() => switchToRequestConfirmationCodeMode());

// Reset the mode of the form if the user decides to change emails after
// verification.
$changeEmailButton.click(() => switchToRequestConfirmationCodeMode());

switchToRequestConfirmationCodeMode();

然后我们的 CSS 看起来像这样:

/* Simplify UX by hiding buttons that aren't relevant. */
body[data-mode="send-code"] #continue,
body[data-mode="verify-code"] #continue {
  display: none;
}

/*
  Override B2C's inline style that sets "display: inline" on instructions
  and field labels.
 */
body[data-mode="send-code"] #email_intro,
body[data-mode="verify-code"] #email_info,
body[data-mode="verify-code"] #attributeList label,
body[data-mode="email-verified"] #email_success {
  display: block !important;
}

/*
  Override B2C's logic that hides buttons and controls during requests.
 */
body[data-mode="send-code"] #email_ver_but_send,
body[data-mode="verify-code"] #email_ver_input,
body[data-mode="verify-code"] #email_ver_but_verify,
body[data-mode="verify-code"] #email_ver_but_resend {
  display: block !important;
}

/*
  BUGBUG: This button serves no function but can randomly appear on the
  page if the user reloads it during sign-up.
 */
#email_ver_but_default {
  display: none !important;
}