# 一键登录（NEOneLogin）

## 简介

`NEOneLogin` 为支持 H5 一键登录的 JS SDK，整合三大运营商（移动、联调、电信），一步校验手机号与当前 SIM 卡号是否一致，优化注册、登录、支付等场景验证流程。

## 快速上手

按照以下步骤快速调用示例。

### 资源引入

目前支持两种接入 SDK 方式。强烈推荐使用第一种 **模块引入** 的方式接入，以获取良好的类型提示。

**1. 模块引入**

下载 `npm` 包：

```bash
# npm
npm install @yidun/quickpass-sdk-one-login-h5
# yarn
yarn add @yidun/quickpass-sdk-one-login-h5
# pnpm
pnpm add @yidun/quickpass-sdk-one-login-h5
```

在代码中引用：

```js
import NEOneLogin from '@yidun/quickpass-sdk-one-login-h5';
```

**2. script 引入**

`body` 标签下引入 `SDK`：

```html
<script src="https://yidunfe.nosdn.127.net/sdk/NEOneLogin.v3.1.2.umd.js"></script>
```

引入该脚本之后，不需要像**模块引入**方式一样通过 `import` 引入 `NEOneLogin` 变量，初始化时直接按下列方式即可：

```js
const neOneLogin = new window.NEOneLogin({
  // ...
});
```

**3. 添加 head referrer**

生产上有 `https` 访问的，`https` 请求跨域，会导致上报的 `referer` 为空，运营商会去校验请求 `referer` 是否进行备案，请在 html 文件下的 `head` 标签添加如下代码解决此问题：

```html
<meta content="always" name="referrer" />
```

### 调用 SDK

**1. 初始化 SDK**

```js
import NEOneLogin from '@yidun/quickpass-sdk-one-login-h5';

const neOneLogin = new NEOneLogin({
  businessId: '从易盾申请的 businessId',
  logo: 'https://xxxxxx.com/logo.png',
  phoneInputStyle: 'square',
});
```

**2. 调起授权页**

```js
neOneLogin.getToken();
```

调用该方法后，会调起授权页，需用户输入中间四位手机号。

**3. 开始验证**

```js
neOneLogin.on('success', (data) => {
  console.log('success', data);
  ajax({
    url: 'https://xxxxx.com/login',
    data: {
      token: data.token,
      accessToken: data.accessToken,
    },
  });
});
```

用户在授权页输入完手机号码，点击登录后，会向运营商发送校验请求，获得 `accessToken`，通过 `success` 事件，SDK 会将用于验证的 `token` 和 `accessToken` 都回调给业务方。

## 配置说明

在初始化 SDK 时，可以传入以下配置：

| 参数 | 描述 | 类型 | 默认值 | 是否必填 |
| --- | --- | --- | --- | --- |
| businessId | 从易盾申请的 businessId | string | - | 是 |
| timeout | `getToken` 超时时间，单位毫秒，版本 3.1.3 开始支持 | number | - | 否 |
| mode | 授权页面弹出模式，`float` 模式下占满全屏，`popup` 模式下需要通过 `popupContainer` 传入一个容器。弹框 `popup` 模式如何使用及其效果，可见[弹框模式](#弹框模式) | `float` \| `popup` | `float` | 否 |
| popupContainer | 只在 `popup` 模式下生效，用于承接授权页的容器，若不设置此项，默认为 `float` 模式 | string \| `HTMLElement` | - | 否 |
| elements | 需要渲染在授权页的模块，可选项有 `back` ， `logo` ， `slogan` ， `phone` ， `switchButton` ， `policy` ， `loginButton`，根据数组中模块的位置进行顺序渲染，详细使用方法见[自定义模块渲染方式](#自定义模块渲染方式) | string[] | `['back', 'logo', 'slogan', 'phone', 'policy', 'loginButton']` | 否 |
| backText | 授权页返回按钮文本 | string | `返回` | 否 |
| logo | logo 的 url 地址，可传 base64 | string | - | 否 |
| logoStyles | logo 的样式 | `LogoStyles` | - | 否 |
| sloganAppName | 标语要显示的 app 名称 | string | `网易易盾` | 否 |
| sloganStyles | 标语样式 | `SloganStyles` | - | 否 |
| phoneStyles | 手机号模块样式 | `PhoneStyles` | - | 否 |
| phoneInputStyle | 手机号输入框类型 | `sub` \| `square` | `sub` | 否 |
| switchBtnText | 切换按钮文本 | string | `其它登录方式` | 否 |
| switchBtnStyles | 切换按钮样式 | `SwitchButtonStyles` | - | 否 |
| policy | 隐私条款 | `PolicyDefine[]` | - | 否 |
| policyTarget | 指定在何处显示隐私条款链接的 URL，当 `mode` 为 `popup` 模式时，推荐设置为非 `self` 的任意值。当设置为 `drawer` 时(v3.3.0 新增)，会以抽屉形式展示条款内容，使用 iframe 加载 URL 内容。 | `self` \| `blank` \| `parent` \| `top` \| `drawer` | - | 否 |
| policyStyles | 隐私条款模块样式 | `PolicyStyles` | - | 否 |
| loginBtnText | 登录按钮文本 | string | `一键登录` | 否 |
| loginBtnStyles | 登录按钮样式 | `LoginButtonStyles` | - | 否 |
| iframeStyles | 授权页 iframe 的样式，你可以传入任何 css 样式，key 支持短横线连接或小驼峰形式 | `CSSStyleDeclaration` | 不同的 `mode` 有不同的默认样式，皆可以覆盖 | 否 |
| containerStyles | 授权页容器的样式配置 | `ContainerStyles` | `{ overflow: 'scroll' }` | 否 |
| toastDelay | 提示弹框的存在时间，单位为 `ms`（毫秒） | number | `3000` | 否 |
| toastShow | 是否显示提示框，如果设置为 `false`，开发可监听 `tip` 事件使用自己的提示组件 | boolean | `true` | 否 |
| toastStyles | 提示弹框模块样式 | ToastStyles | － | 否 |

### LogoStyles

| 参数 | 描述 | 类型 | 默认值 | 是否必填 |
| --- | --- | --- | --- | --- |
| width | 宽度大小，比如 `220`、`50%` 和 `220px` | number \| string | `220` | 否 |
| height | 高度大小，比如 `110` 和 `110px`，如果你的图片高度是确定的，建议把这个配置加上，设置为你希望的图片高度大小，这样可以避免由于图片加载过程中出现的布局跳动问题 | number \| string | - | 否 |
| top | 距离顶部位置，比如 `220` 和 `220px` | number \| string | `150` | 否 |
| align | 垂直排列方式 | `left` \| `center` \| `right` | `center` | 否 |

### SloganStyles

| 参数  | 描述                                   | 类型                          | 默认值   | 是否必填 |
| ----- | -------------------------------------- | ----------------------------- | -------- | -------- |
| width | 宽度大小，比如 `220`、`50%` 和 `220px` | number \| string              | `100%`   | 否       |
| top   | 距离顶部位置，比如 `50` 和 `50px`      | number \| string              | `50`     | 否       |
| align | 垂直排列方式                           | `left` \| `center` \| `right` | `center` | 否       |
| color | 字体颜色                               | string                        | `#333`   | 否       |
| size  | 字体大小                               | number \| string              | `14px`   | 否       |

### PhoneStyles

| 参数 | 描述 | 类型 | 默认值 | 是否必填 |
| --- | --- | --- | --- | --- |
| top | 距离顶部位置，比如 `220` 和 `220px` | number \| string | `150` | 否 |
| align | 垂直排列方式 | `left` \| `center` \| `right` | `center` | 否 |
| color | 字体颜色 | string | `#333` | 否 |
| size | 字体大小 | number \| string | `26px` | 否 |
| cursorColor | 闪烁光标颜色 | number \| string | `#2a62ff` | 否 |
| subBottomColor | `phoneInputStyle` 为 `sub` 模式时，底线的背景颜色 | string | `#2a62ff` | 否 |
| subWidth | `phoneInputStyle` 为 `sub` 模式时，输入框的宽度 | number \| string | `28px` | 否 |
| subHeight | `phoneInputStyle` 为 `sub` 模式时，输入框的高度 | number \| string | `32px` | 否 |
| squareBackground | `phoneInputStyle` 为 `square` 模式时，输入框的背景色 | string | `rgba(192, 208, 238, 0.4)` | 否 |
| squareWidth | `phoneInputStyle` 为 `square` 模式时，输入框的宽度 | number \| string | `32px` | 否 |
| squareHeight | `phoneInputStyle` 为 `square` 模式时，输入框的高度 | number \| string | `32px` | 否 |
| squareRadius | `phoneInputStyle` 为 `square` 模式时，输入框的圆角 | number \| string | `8px` | 否 |

### SwitchButtonStyles

| 参数  | 描述                                   | 类型                          | 默认值    | 是否必填 |
| ----- | -------------------------------------- | ----------------------------- | --------- | -------- |
| width | 宽度大小，比如 `220`、`50%` 和 `220px` | number \| string              | `100%`    | 否       |
| top   | 距离顶部位置，比如 `30` 和 `30px`      | number \| string              | `30`      | 否       |
| align | 垂直排列方式                           | `left` \| `center` \| `right` | `center`  | 否       |
| color | 字体颜色                               | string                        | `#2a62ff` | 否       |
| size  | 字体大小                               | number \| string              | `14px`    | 否       |

### PolicyDefine

| 参数    | 描述               | 类型   | 默认值 | 是否必填 |
| ------- | ------------------ | ------ | ------ | -------- |
| url     | 条款页链接         | string | -      | 是       |
| content | 条款展示的文本名称 | string | -      | 是       |
| target  | 指定该条款链接的 target，优先级高于 policyTarget, v3.3.0 新增 | 'self' | 'blank' | 'parent' | 'top' | 'drawer' | - | 否 |

> 每个 PolicyDefine 可单独设置 target，优先级高于 policyTarget。未设置时使用 policyTarget。

**示例：**

```js
policy: [
  { url: 'https://dun.163.com/clause/privacy', content: '网易易盾隐私政策', target: 'blank' },
  { url: '#', content: '自定义条款2' } // 不设置 target，使用全局 policyTarget
]
```

### PolicyStyles

| 参数      | 描述                                   | 类型                          | 默认值    | 是否必填 |
| --------- | -------------------------------------- | ----------------------------- | --------- | -------- |
| width     | 宽度大小，比如 `220`、`50%` 和 `220px` | number \| string              | `100%`    | 否       |
| top       | 距离顶部位置，比如 `50` 和 `50px`      | number \| string              | `50`      | 否       |
| align     | 垂直排列方式                           | `left` \| `center` \| `right` | `center`  | 否       |
| color     | 字体颜色                               | string                        | `#333`    | 否       |
| size      | 字体大小                               | number \| string              | `14px`    | 否       |
| linkColor | 链接字体颜色                           | string                        | `#2a62ff` | 否       |

### LoginButtonStyles

| 参数 | 描述 | 类型 | 默认值 | 是否必填 |
| --- | --- | --- | --- | --- |
| width | 宽度大小，比如 `220`、`100%` 和 `220px` | number \| string | `100%` | 否 |
| top | 距离顶部位置，比如 `50` 和 `50px` | number \| string | `50` | 否 |
| align | 垂直排列方式 | `left` \| `center` \| `right` | `center` | 否 |
| color | 字体颜色 | string | `#fff` | 否 |
| size | 字体大小 | number \| string | `18px` | 否 |
| height | 按钮高度大小，比如 `48`、`10%` 和 `48px` | number \| string | `48` | 否 |
| background | 按钮背景颜色 | string | `linear-gradient(90deg,#5f83fe,#324dff)` | 否 |
| radius | 按钮四周圆角大小 | number \| string | `8px` | 否 |

### ToastStyles

| 参数  | 描述                                                                | 类型                          | 默认值   | 是否必填 |
| ----- | ------------------------------------------------------------------- | ----------------------------- | -------- | -------- |
| width | 宽度大小，比如 `auto`、`80%` 和 `220px`                             | number \| string              | `auto`   | 否       |
| top   | 提示弹框距离顶部第一个非静态定位父元素的距离，比如 `284` 和 `284px` | number \| string              | `284`    | 否       |
| align | 垂直排列方式                                                        | `left` \| `center` \| `right` | `center` | 否       |

### ContainerStyles

| 参数    | 描述                                   | 类型                          | 默认值   | 是否必填 |
| ------- | -------------------------------------- | ----------------------------- | -------- | -------- |
| overflow | 授权页容器的 overflow 样式，用于控制滚动条的显示方式 | `scroll` \| `auto` \| `hidden` \| `visible` | `scroll` | 否       |

## 自定义模块渲染方式

在一键登录授权页，你看到的所有 UI 都是由一个个模块组成的，每个模块由一个字符串表示，比如 `back` 代表的是顶部的返回按钮，`logo` 代表的是你看到的 logo。

`elements` 配置项可以让你自定义这些模块的渲染顺序，以及决定它们渲不渲染，下图将为你直观演示该配置项的作用：

<div align="center">
  <img src="https://yidunfe.nosdn.127.net/images/one-login-h5-elements1679284992207.png" />
</div>

手动配置代码演示：

```js
const neOneLogin = new NEOneLogin({
  businessId: '易盾申请的 businessId',
  elements: ['back', 'logo', 'slogan', 'phone', 'loginButton', 'switchButton', 'policy'],
});
```

## 弹框模式

关于配置项 `mode` 默认是 `float` 模式，即授权页是全屏占满的，如果你希望是在当前页以弹框的形式展示授权页面，需要同时设置 `mode` 和 `popupContainer` 配置项。

弹框模式示例代码：

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, viewport-fit=cover" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <meta content="always" name="referrer" />
    <title>一键登录H5 - demo</title>
    <style>
      #get-token-btn {
        position: fixed;
        left: 10px;
        top: 10px;
      }
      #popup-container {
        position: fixed;
        left: 50%;
        top: 50%;
        width: 90%;
        height: 400px;
        transform: translate(-50%, -50%);
        border-radius: 20px;
        box-shadow: rgb(0 0 0 / 10%) 0px 4px 12px;
      }
      .title {
        text-align: center;
        margin-top: 30px;
      }
      .sub-title {
        display: inline-block;
        text-align: center;
        width: 100%;
      }
    </style>
    <script src="libs/jquery@3.6.0.js"></script>
  </head>
  <body>
    <div id="root">
      <button id="get-token-btn">调起授权页</button>
      <div id="popup-container">
        <h2 class="title">免费领试听课</h2>
        <span class="sub-title">提交后会有老师联系您</span>
      </div>
    </div>

    <script>
      (function () {
        const popupContainer = document.querySelector('#popup-container');
        const neOneLogin = new NEOneLogin({
          businessId: '从易盾申请的 businessId',
          mode: 'popup',
          popupContainer,
          phoneInputStyle: 'square',
          // 只渲染 4 个模块，其它的自定义
          elements: ['phone', 'switchButton', 'loginButton', 'policy'],
          logoStyles: {
            top: 10,
          },
          sloganAppName: '网易易盾',
          sloganStyles: {
            top: 10,
          },
          phoneStyles: {
            top: 20,
          },
          switchBtnText: '切换手机号',
          loginBtnText: '授权登录',
          loginBtnStyles: {
            top: 20,
          },
          policy: [
            {
              content: '自定义条款1',
              url: 'https://www.baidu.com',
            },
            {
              content: '自定义条款',
              url: 'https://www.baidu.com',
            },
          ],
          policyStyles: {
            top: 20,
          },
          iframeStyles: {
            position: 'absolute',
            left: '0',
            bottom: '0',
            height: '274px',
          },
        });

        document.querySelector('#get-token-btn').onclick = () => {
          neOneLogin.getToken();
        };
      })();
    </script>
  </body>
</html>
```

从上面可以看到，我们调整了各个模块的 `top` 以覆盖 SDK 的默认距离，并将授权页放到了我们自定义的容器中。下图是实际效果：

<div align="center">
  <img src="https://yidunfe.nosdn.127.net/images/one-login-h5-51679292030709.png" />
</div>

## 实例方法

以下是初始化实例上可调用的方法：

- **调起授权页**

核心方法，调用该方法后即调起授权页，用户进行手机号补全，示例：

```js
neOneLogin.getToken();
```

- **关闭授权页**

调用该方法后可关闭授权页，会删除授权页 `iframe`，若重新调用 `getToken` 方法，会重新生成授权页 `iframe`，示例：

```js
neOneLogin.disposeAuthFrame();
```

- **销毁流程**

销毁流程，建议在组件卸载时使用，示例：

```js
neOneLogin.dispose();
```

- **启用登录按钮**

将登录按钮状态设置为可用，示例：

```js
neOneLogin.enableLoginButton();
```

用户点击登录后，登录按钮就会一直保持禁用状态，可手动调用上面方法将其启用。不过大多数情况下，接入方无需使用该方法，建议失败时走降级或者销毁实例重新验证，因为运营商会对同一个 token 做次数校验。

- **禁用登录按钮**

将登录按钮状态设置为禁用，示例：

```js
neOneLogin.disableLoginButton();
```

## 事件回调

在验证过程中会抛出⼀些回调事件，业务上可以监听这些事件，⾃⾏做⼀些处理。

- **success 事件**

```js
neOneLogin.on('success', (data) => {
  console.log('success', data);
  // 成功拿到 token 和 accessToken 后像服务端发起校验
  ajax({
    url: 'https://xxxxx.com/login',
    data: {
      token: data.token,
      accessToken: data.accessToken,
    },
  });
});
```

- **error 事件**

```js
neOneLogin.on('error', (err) => {
  // err.data 中可以拿到错误信息
  console.log('error', err.data);
  neOneLogin.dispose();
  neOneLogin = null;
  // 失败后可以走业务方自己的降级流程
  // handleOneLoginFailProcess()
});
```

- **ready 事件**

```js
neOneLogin.on('ready', () => {
  // 当预取号->授权页面成功挂载时触发
});
```

- **close 事件**

```js
neOneLogin.on('close', () => {
  // 当授权页通过返回按钮关闭时触发
});
```

- **switch 事件**

```js
neOneLogin.on('switch', () => {
  // 当授权页点击切换按钮时触发
  // 与 close 事件不同的是，close 事件触发时意味着授权页已经被关闭
  // 但 switch 事件不会关闭授权页，如果需要关闭，需要开发调用 disposeAuthFrame 方法
});
```

- **tip 事件**

```js
neOneLogin.on('tip', ({ code, text }) => {
  // 根据　code　或者　text　在⻚面中给出提示文案
});
```

当前包含以下 code 和 text:

| code           | text                               | 说明                         |
| -------------- | ---------------------------------- | ---------------------------- |
| complete_phone | 请补全手机号后登录                 | 授权页手机号未补全           |
| agree_terms    | 请勾选同意运营商认证服务条款后登录 | 未勾选同意运营商认证服务条款 |

## 错误对照表

| code  | msg                                | 说明                                          |
| ----- | ---------------------------------- | --------------------------------------------- |
| -8001 | 网络请求错误                       | 网络错误，请检查网络                          |
| -8002 | 预取号超时                       | 预取号超时                         |
| 10000 | 预取号失败                       | 预取号失败                                    |
| 20001 | 移动取号失败                       | 移动取号失败                                  |
| 20002 | 移动获取 accessToken 失败          | 移动获取 accessToken 失败                     |
| 20010 | 移动报备 referer 校验失败          | 访问域名的 referer 与移动报备的地址不一致     |
| 30001 | 联通获取省份 URL 失败              | 联通获取省份 URL 失败                         |
| 30002 | 联通获取省份失败                   | 联通获取省份 URL 后，在获取省份时接口请求失败 |
| 30003 | 联通获取 accessToken 失败          | 联通获取 accessToken 失败                     |
| 30010 | 联通报备 referer 校验失败          | 访问域名的 referer 与联通报备的地址不一致     |
| 40001 | 电信预授权失败                     | 电信预授权失败                                |
| 40002 | 电信获取 accessCode 失败           | 电信获取 accessCode 失败                      |
| 40010 | 电信报备 referer 校验失败          | 访问域名的 referer 与电信报备的地址不一致     |
| 40000 | 未知运营商，仅支持电信、联通、移动 | 仅支持主流运营商                              |

## 常见问题

**1. 发生错误时该如何排查问题？**

首先看返回的错误码，可从错误信息 `err.data` 中获取，然后与上述的【错误对照表】对比查看，需要强调的是，如果错误码为 `40000`，请检查设备网络是否关闭了 wifi。

**2. 错误码在上述【错误对照表】中没有怎么办？**

因为 SDK 内部在调用各个运营商服务时，他们有自己的错误提示信息，我们的策略是优先显示他们的错误信息，所以当出现这种情况时，开发者可根据错误信息中的 `err.data.msg` 大致排查下，如果还是不知道如何解决，请联系我们的技术支持。

**3. 我用电脑如何进行开发？**

首先要保证开发时本地服务的访问地址不能是 `localhost` 或者 ip，需要配置一个**已报备过**的完整的地址，例如 `http://ye-dev.dun.163.com`，这个时候推荐使用 SwitchHost 软件配置：`127.0.0.1 ye-dev.dun.163.com`，即可正常访问。其次保证使用电脑连接插卡的手机热点，进行开发，走手机流量。如果你通过宽带流量进行开发，一定只会收到错误码。

**4. 小屏幕会自适应吗？**

SDK 内部做了根据屏幕高度自适应，在屏幕高度 `0 - 500px` 以下和 `500px - 600px` 区间，各个 UI 模块的间距会减小，以保证在用户屏幕内能够显示所有模块。如果你自定义了模块的 `top` 属性，将会覆盖默认的间距，这个时候需要业务方自己做自适应工作。如果业务方使用的 `mode` 为 `popup` 模式，因为我们无法得知容器的大小，所以无法做自适应，需要业务方自己设置各 UI 模块的 `top` 来缩减距离。
