开发前提
-
在进行代码开发前,请先确认已完成开发准备工作。
-
应用使用华为账号一键登录功能之前,需要完成quickLoginMobilePhone(华为账号一键登录)的scope权限申请,详情参见配置scope权限。scope权限申请审批未完成或未通过,将报错1001502014 应用未申请scopes或permissions权限。
细分场景 对应scope 权限名称 权限描述 权限是否需要申请 华为账号一键登录 quickLoginAnonymousPhone quickLoginMobilePhone 华为账号一键登录,包含获取完整手机号 是
客户端开发
IDE辅助开发(推荐)
-
打开需要提供一键登录功能的页面,在页面的build()中创建一个容器(如Column)。
-
在DevEco Studio菜单栏点击View > Tool Windows > Kit Assistant,或使用快捷键Alt + K,进入Kit Assistant页面。
-
在左侧目录中点击选中AccountKit > QuickLoginButton,并拖拽至新创建的容器中。即可在当前位置插入相应的代码片段。
若代码片段插入失败,可查询快速插入场景化代码片段的说明排查原因。
-
在自动生成的代码段的getQuickLoginAnonymousPhone函数中,执行executeRequest函数可获取响应结果。
根据获取的响应结果判断,可能存在以下场景:
-
已正确获取到用户身份标识UnionID、OpenID,应用可通过用户身份标识查询该用户是否已关联。
1)如已关联,结合风控、安全因素及自身业务场景判断,可展示已关联的账号,由用户选择是否使用华为账号登录应用,或免用户操作,静默登录应用,客户端开发结束。
2)如未关联,再判断是否存在下面的异常场景,如无,则参考下面步骤5继续开发。
-
存在如下返回ArkTS错误码的异常场景:
1)返回1001502001 用户未登录华为账号错误码,说明华为账号未登录。
2)返回1001500003 不支持该scopes或permissions错误码,说明华为账号用户注册地非中国境内(不包含中国香港、中国澳门、中国台湾)。
3)获取到的匿名手机号为空,说明华为账号没有绑定手机号、权限未申请或未生效。
上述异常场景应用需要展示其他登录方式。
-
-
根据上述代码实现应用的登录页面,并展示华为账号一键登录按钮和华为账号用户认证协议(Account Kit提供跳转链接,应用需实现协议跳转,参见约束与限制第2点),用户同意协议并点击一键登录按钮后,可获取到Authorization Code,将该值传给应用服务端用于获取用户信息(完整手机号、UnionID、OpenID)。
-
源代码 有小伙伴反馈无法拖动 可以复制下面的源代码
import { util } from '@kit.ArkTS'; import { authentication, loginComponentManager, LoginWithHuaweiIDButton } from '@kit.AccountKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { promptAction, router } from '@kit.ArkUI'; @Component @Entry export struct HuaweiLoginPage { build() { Column() { QuickLoginButtonComponent() }.width('100%') .height('100%') } } @Component struct QuickLoginButtonComponent { logTag: string = 'QuickLoginButtonComponent'; domainId: number = 0x0000; // 匿名化手机号 @State quickLoginAnonymousPhone: string = ''; // 是否勾选协议 @State isSelected: boolean = false; // 华为账号用户认证协议链接,此处仅为示例,实际开发过程中,出于可维护性、安全性等方面考虑,域名不建议硬编码在本地 private static USER_AUTHENTICATION_PROTOCOL: string = 'https://privacy.consumer.huawei.com/legal/id/authentication-terms.htm?code=CN&language=zh-CN'; private static USER_SERVICE_TAG = '用户服务协议'; private static PRIVACY_TAG = '隐私协议'; private static USER_AUTHENTICATION_TAG = '华为账号用户认证协议'; // 定义LoginWithHuaweiIDButton展示的隐私文本,展示应用的用户服务协议、隐私协议和华为账号用户认证协议 privacyText: loginComponentManager.PrivacyText[] = [{ text: '已阅读并同意', type: loginComponentManager.TextType.PLAIN_TEXT }, { text: '《用户服务协议》', tag: QuickLoginButtonComponent.USER_SERVICE_TAG, type: loginComponentManager.TextType.RICH_TEXT }, { text: '《隐私协议》', tag: QuickLoginButtonComponent.PRIVACY_TAG, type: loginComponentManager.TextType.RICH_TEXT }, { text: '和', type: loginComponentManager.TextType.PLAIN_TEXT }, { text: '《华为账号用户认证协议》', tag: QuickLoginButtonComponent.USER_AUTHENTICATION_TAG, type: loginComponentManager.TextType.RICH_TEXT }, { text: '。', type: loginComponentManager.TextType.PLAIN_TEXT }]; // 构造LoginWithHuaweiIDButton组件的控制器 controller: loginComponentManager.LoginWithHuaweiIDButtonController = new loginComponentManager.LoginWithHuaweiIDButtonController() /** * 当应用使用自定义的登录页时,如果用户未同意协议,需要设置协议状态为NOT_ACCEPTED,当用户同意协议后再设置 * 协议状态为ACCEPTED,才可以使用华为账号一键登录功能 */ .setAgreementStatus(loginComponentManager.AgreementStatus.NOT_ACCEPTED) .onClickLoginWithHuaweiIDButton((error: BusinessError | undefined, response: loginComponentManager.HuaweiIDCredential) => { this.handleLoginWithHuaweiIDButton(error, response); }) .onClickEvent((error: BusinessError, clickEvent: loginComponentManager.ClickEvent) => { if (error) { this.dealAllError(error); return; } hilog.info(this.domainId, this.logTag, `onClickEvent clickEvent: ${clickEvent}`); }); agreementDialog: CustomDialogController = new CustomDialogController({ builder: AgreementDialog({ privacyText: this.privacyText, cancel: () => { this.agreementDialog.close(); this.controller.setAgreementStatus(loginComponentManager.AgreementStatus.NOT_ACCEPTED); }, confirm: () => { this.agreementDialog.close(); this.isSelected = true; this.controller.setAgreementStatus(loginComponentManager.AgreementStatus.ACCEPTED); // 调用此方法,同意协议与登录一并完成,无需再次点击登录按钮 this.controller.continueLogin((error: BusinessError) => { if (error) { hilog.error(this.domainId, this.logTag, `Failed to login with agreementDialog. errCode is ${error.code}, errMessage is ${error.message}`); } else { hilog.info(this.domainId, this.logTag, 'Succeed in clicking agreementDialog continueLogin.'); } }); }, clickHyperlinkText: () => { this.agreementDialog.close(); this.jumpToPrivacyWebView(); } }), autoCancel: false, alignment: DialogAlignment.Center, }); aboutToAppear(): void { this.getQuickLoginAnonymousPhone(); } // Toast提示 showToast(resource: string) { try { promptAction.showToast({ message: resource, duration: 2000 }); } catch (error) { const message = (error as BusinessError).message const code = (error as BusinessError).code hilog.error(this.domainId, this.logTag, `showToast args errCode is ${code}, errMessage is ${message}`); } } // 跳转华为账号用户认证协议页,该页面需在工程main_pages.json文件配置 jumpToPrivacyWebView() { router.pushUrl({ // 在工程main_pages.json文件配置跳转页,具体可参考AccountKit开发指南使用华为账号一键登录WebPage示例代码 url: 'pages/WebPage', params: { isFromDialog: true, url: QuickLoginButtonComponent.USER_AUTHENTICATION_PROTOCOL, } }, (err) => { if (err) { hilog.error(this.domainId, this.logTag, `Failed to jumpToPrivacyWebView, errCode is ${err.code}, errMessage is ${err.message}`); } }); } handleLoginWithHuaweiIDButton(error: BusinessError | undefined, response: loginComponentManager.HuaweiIDCredential) { if (error) { hilog.error(this.domainId, this.logTag, `Failed to login with LoginWithHuaweiIDButton. errCode is ${error.code}, errMessage is ${error.message}`); if (error.code === ErrorCode.ERROR_CODE_NETWORK_ERROR) { AlertDialog.show( { message: "网络未连接,请检查网络设置。", offset: { dx: 0, dy: -12 }, alignment: DialogAlignment.Bottom, autoCancel: false, confirm: { value: "知道了", action: () => { } } } ); } else if (error.code === ErrorCode.ERROR_CODE_AGREEMENT_STATUS_NOT_ACCEPTED) { // 未同意协议,弹出协议弹框,推荐使用该回调方式 this.agreementDialog.open(); } else if (error.code === ErrorCode.ERROR_CODE_LOGIN_OUT) { // 华为账号未登录提示 this.showToast("华为账号未登录,请重试"); } else if (error.code === ErrorCode.ERROR_CODE_NOT_SUPPORTED) { // 不支持该scopes或permissions提示 this.showToast("该scopes或permissions不支持"); } else { // 其他提示系统或服务异常 this.showToast('服务或网络异常,请稍后重试'); // TODO: 其他错误码处理,请参考API中的错误码查看详细错误原因 } return; } try { if (this.isSelected) { if (response) { hilog.info(this.domainId, this.logTag, 'Succeed in clicking LoginWithHuaweiIDButton.'); // 开发者根据实际业务情况使用以下信息 const authCode = response.authorizationCode; const openID = response.openID; const unionID = response.unionID; const idToken = response.idToken; } } else { this.agreementDialog.open(); } } catch (err) { hilog.error(this.domainId, this.logTag, `Failed to login with LoginWithHuaweiIDButton, errCode: ${err.code}, errMessage: ${err.message}`); AlertDialog.show( { message: '服务或网络异常,请稍后重试', offset: { dx: 0, dy: -12 }, alignment: DialogAlignment.Bottom, autoCancel: false, confirm: { value: '知道了', action: () => { } } } ); } } // 错误处理 dealAllError(error: BusinessError): void { hilog.error(this.domainId, this.logTag, `Failed to login, errorCode is ${error.code}, errorMessage is ${error.message}`); // TODO: 错误码处理,请参考API中的错误码根据实际情况处理 } getQuickLoginAnonymousPhone() { // 创建授权请求,并设置参数 const authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest(); // 获取匿名手机号需传quickLoginAnonymousPhone这个scope,传参之前需要先申请“华为账号一键登录”权限 //(权限名称为:quickLoginMobilePhone),后续才能获取匿名手机号数据 authRequest.scopes = ['quickLoginAnonymousPhone']; // 用于防跨站点请求伪造 authRequest.state = util.generateRandomUUID(); // 一键登录场景该参数只能设置为false authRequest.forceAuthorization = false; const controller = new authentication.AuthenticationController(); try { controller.executeRequest(authRequest).then((response: authentication.AuthorizationWithHuaweiIDResponse) => { // 获取到UnionID、OpenID、匿名手机号 const unionID = response.data?.unionID; const openID = response.data?.openID; const anonymousPhone = response.data?.extraInfo?.quickLoginAnonymousPhone as string; if (anonymousPhone) { hilog.info(this.domainId, this.logTag, 'Succeeded in authentication.'); this.quickLoginAnonymousPhone = anonymousPhone; return; } hilog.info(this.domainId, this.logTag, 'Succeeded in authentication. AnonymousPhone is empty.'); // 未获取到匿名手机号需要跳转到应用自定义的登录页面 }).catch((error: BusinessError) => { this.dealAllError(error); }) } catch (error) { this.dealAllError(error); } } build() { Scroll() { Column() { Column() { Column() { Image($r('app.media.app_icon')) .width(48) .height(48) .draggable(false) .copyOption(CopyOptions.None) .onComplete(() => { hilog.info(this.domainId, this.logTag, 'appIcon loading success.'); }) .onError(() => { hilog.error(this.domainId, this.logTag, 'appIcon loading fail.'); }) Text($r('app.string.app_name')) .fontFamily($r('sys.string.ohos_id_text_font_family_medium')) .fontWeight(FontWeight.Medium) .fontWeight(FontWeight.Bold) .maxFontSize($r('sys.float.ohos_id_text_size_headline8')) .minFontSize($r('sys.float.ohos_id_text_size_body1')) .maxLines(1) .fontColor($r('sys.color.ohos_id_color_text_primary')) .constraintSize({ maxWidth: '100%' }) .margin({ top: 12, }) Text('应用描述') .fontSize($r('sys.float.ohos_id_text_size_body2')) .fontColor($r('sys.color.ohos_id_color_text_secondary')) .fontFamily($r('sys.string.ohos_id_text_font_family_regular')) .fontWeight(FontWeight.Regular) .constraintSize({ maxWidth: '100%' }) .margin({ top: 8, }) }.margin({ top: 100 }) Column() { Text(this.quickLoginAnonymousPhone) .fontSize(36) .fontColor($r('sys.color.ohos_id_color_text_primary')) .fontFamily($r('sys.string.ohos_id_text_font_family_medium')) .fontWeight(FontWeight.Bold) .lineHeight(48) .textAlign(TextAlign.Center) .maxLines(1) .constraintSize({ maxWidth: '100%', minHeight: 48 }) Text('华为账号绑定号码') .fontSize($r('sys.float.ohos_id_text_size_body2')) .fontColor($r('sys.color.ohos_id_color_text_secondary')) .fontFamily($r('sys.string.ohos_id_text_font_family_regular')) .fontWeight(FontWeight.Regular) .lineHeight(19) .textAlign(TextAlign.Center) .maxLines(1) .constraintSize({ maxWidth: '100%' }) .margin({ top: 8 }) }.margin({ top: 64 }) Column() { LoginWithHuaweiIDButton({ params: { // LoginWithHuaweiIDButton支持的样式 style: loginComponentManager.Style.BUTTON_RED, // 账号登录按钮在登录过程中展示加载态 extraStyle: { buttonStyle: new loginComponentManager.ButtonStyle().loadingStyle({ show: true }) }, // LoginWithHuaweiIDButton的边框圆角半径 borderRadius: 24, // LoginWithHuaweiIDButton支持的登录类型 loginType: loginComponentManager.LoginType.QUICK_LOGIN, // LoginWithHuaweiIDButton支持按钮的样式跟随系统深浅色模式切换 supportDarkMode: true, // verifyPhoneNumber:如果华为账号用户在过去90天内未进行短信验证,是否拉起Account Kit提供的短信验证码页面 verifyPhoneNumber: true }, controller: this.controller }) } .height(40) .margin({ top: 56 }) Column() { Button({ type: ButtonType.Capsule, stateEffect: true }) { Text('其他方式登录') .fontColor($r('sys.color.ohos_id_color_text_primary_activated')) .fontFamily($r('sys.string.ohos_id_text_font_family_medium')) .fontWeight(FontWeight.Medium) .fontSize($r('sys.float.ohos_id_text_size_button1')) .focusable(true) .focusOnTouch(true) .textOverflow({ overflow: TextOverflow.Ellipsis }) .maxLines(1) .padding({ left: 8, right: 8 }) } .fontColor($r('sys.color.ohos_id_color_text_primary_activated')) .fontFamily($r('sys.string.ohos_id_text_font_family_medium')) .fontWeight(FontWeight.Medium) .backgroundColor($r('sys.color.ohos_id_color_button_normal')) .focusable(true) .focusOnTouch(true) .constraintSize({ minHeight: 40 }) .width('100%') .onClick(() => { hilog.info(this.domainId, this.logTag, 'click optionalLoginButton.'); }) }.margin({ top: 16 }) }.width('100%') Row() { Row() { Checkbox({ name: 'privacyCheckbox', group: 'privacyCheckboxGroup' }) .width(24) .height(24) .focusable(true) .focusOnTouch(true) .margin({ top: 0 }) .select(this.isSelected) .onChange((value: boolean) => { if (value) { this.isSelected = true; this.controller.setAgreementStatus(loginComponentManager.AgreementStatus.ACCEPTED); } else { this.isSelected = false; this.controller.setAgreementStatus(loginComponentManager.AgreementStatus.NOT_ACCEPTED); } hilog.info(this.domainId, this.logTag, `agreementChecked: ${value}`); }) } Row() { Text() { ForEach(this.privacyText, (item: loginComponentManager.PrivacyText) => { if (item?.type == loginComponentManager.TextType.PLAIN_TEXT && item?.text) { Span(item?.text) .fontColor($r('sys.color.ohos_id_color_text_secondary')) .fontFamily($r('sys.string.ohos_id_text_font_family_regular')) .fontWeight(FontWeight.Regular) .fontSize($r('sys.float.ohos_id_text_size_body3')) } else if (item?.type == loginComponentManager.TextType.RICH_TEXT && item?.text) { Span(item?.text) .fontColor($r('sys.color.ohos_id_color_text_primary_activated')) .fontFamily($r('sys.string.ohos_id_text_font_family_medium')) .fontWeight(FontWeight.Medium) .fontSize($r('sys.float.ohos_id_text_size_body3')) .focusable(true) .focusOnTouch(true) .onClick(() => { // 应用需要根据item.tag实现协议页面的跳转逻辑 hilog.info(this.domainId, this.logTag, `click privacy text tag: ${item.tag}`); // 华为账号用户认证协议 if (item.tag === QuickLoginButtonComponent.USER_AUTHENTICATION_TAG) { this.jumpToPrivacyWebView(); } }) } }, (item: loginComponentManager.PrivacyText, index: number) => { return item?.tag + index.toString(); }) } .width('100%') } .margin({ left: 12 }) .layoutWeight(1) .constraintSize({ minHeight: 24 }) } .alignItems(VerticalAlign.Top) .margin({ bottom: 16, top: 16 }) } .justifyContent(FlexAlign.SpaceBetween) .constraintSize({ minHeight: '100%' }) .margin({ left: 16, right: 16 }) } .width('100%') .height('100%') } } @CustomDialog export struct AgreementDialog { logTag: string = 'AgreementDialog'; domainId: number = 0x0000; dialogController?: CustomDialogController; cancel: () => void = () => { }; confirm: () => void = () => { }; clickHyperlinkText: () => void = () => { }; privacyText: loginComponentManager.PrivacyText[] = []; private static USER_AUTHENTICATION_TAG = '华为账号用户认证协议'; build() { Column() { Row() { Text('用户协议与隐私条款') .id('loginPanel_agreement_dialog_privacy_title') .maxFontSize($r('sys.float.ohos_id_text_size_headline8')) .minFontSize($r('sys.float.ohos_id_text_size_body1')) .fontColor($r('sys.color.ohos_id_color_text_primary')) .fontFamily($r('sys.string.ohos_id_text_font_family_medium')) .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Center) .textOverflow({ overflow: TextOverflow.Ellipsis }) .maxLines(2) } .alignItems(VerticalAlign.Center) .constraintSize({ minHeight: 56, maxWidth: 400 }) .margin({ left: $r('sys.float.ohos_id_max_padding_start'), right: $r('sys.float.ohos_id_max_padding_start') }) Row() { Text() { ForEach(this.privacyText, (item: loginComponentManager.PrivacyText) => { if (item?.type == loginComponentManager.TextType.PLAIN_TEXT && item?.text) { Span(item?.text) .fontSize($r('sys.float.ohos_id_text_size_body1')) .fontColor($r('sys.color.ohos_id_color_text_primary')) .fontFamily($r('sys.string.ohos_id_text_font_family_regular')) .fontWeight(FontWeight.Regular) } else if (item?.type == loginComponentManager.TextType.RICH_TEXT && item?.text) { Span(item?.text) .fontSize($r('sys.float.ohos_id_text_size_body1')) .fontColor('#CE0E2D') .fontFamily($r('sys.string.ohos_id_text_font_family_medium')) .fontWeight(FontWeight.Medium) .focusable(true) .focusOnTouch(true) .onClick(() => { // 应用需要根据item.tag实现协议页面的跳转逻辑 hilog.info(this.domainId, this.logTag, `click privacy text tag: ${item.tag}`); // 华为账号用户认证协议 if (item.tag === AgreementDialog.USER_AUTHENTICATION_TAG) { hilog.info(this.domainId, this.logTag, 'AgreementDialog click.'); this.clickHyperlinkText(); } }) } }, (item: loginComponentManager.PrivacyText, index: number) => { return item?.tag + index.toString(); }) } .width('100%') .textOverflow({ overflow: TextOverflow.Ellipsis }) .maxLines(10) .textAlign(TextAlign.Start) .focusable(true) .focusOnTouch(true) .padding({ left: 24, right: 24 }) }.width('100%') Flex({ direction: FlexDirection.Row }) { Button('取消', { type: ButtonType.Capsule, stateEffect: true }) .id('loginPanel_agreement_cancel_btn') .fontColor($r('sys.color.ohos_id_color_text_primary')) .fontSize($r('sys.float.ohos_id_text_size_button1')) .fontFamily($r('sys.string.ohos_id_text_font_family_medium')) .backgroundColor(Color.Transparent) .fontWeight(FontWeight.Medium) .focusable(true) .focusOnTouch(true) .constraintSize({ minHeight: 40, maxWidth: 400 }) .width('50%') .onClick(() => { hilog.info(this.domainId, this.logTag, 'AgreementDialog cancel.'); this.cancel(); }) Button('同意并登录', { type: ButtonType.Capsule, stateEffect: true }) .id('loginPanel_agreement_dialog_huawei_id_login_btn') .fontColor(Color.White) .backgroundColor('#CE0E2D') .fontSize($r('sys.float.ohos_id_text_size_button1')) .fontFamily($r('sys.string.ohos_id_text_font_family_medium')) .fontWeight(FontWeight.Medium) .focusable(true) .focusOnTouch(true) .constraintSize({ minHeight: 40, maxWidth: 400 }) .width('50%') .onClick(() => { hilog.info(this.domainId, this.logTag, 'AgreementDialog confirm.'); this.confirm(); }) } .margin({ top: 8, left: $r('sys.float.ohos_id_elements_margin_horizontal_l'), right: $r('sys.float.ohos_id_elements_margin_horizontal_l'), bottom: 16 }) }.backgroundColor($r('sys.color.ohos_id_color_dialog_default_bg')) .padding({ left: 16, right: 16 }) } } export enum ErrorCode { // 账号未登录 ERROR_CODE_LOGIN_OUT = 1001502001, // 该账号不支持一键登录,如儿童账号、海外账号 ERROR_CODE_NOT_SUPPORTED = 1001500003, // 网络错误 ERROR_CODE_NETWORK_ERROR = 1001502005, // 用户未同意用户协议 ERROR_CODE_AGREEMENT_STATUS_NOT_ACCEPTED = 1005300001 }
自行开发
- 导入Account Kit的authentication模块及相关公共模块。
import { util } from '@kit.ArkTS';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
-
调用authentication模块的AuthorizationWithHuaweiIDRequest请求获取华为账号用户的UnionID、OpenID、匿名手机号。匿名手机号用于登录页面展示。
注意
该场景下forceAuthorization参数需设置为false。
根据获取的响应结果判断,可能存在以下场景:
-
已正确获取到用户身份标识UnionID、OpenID,应用可通过用户身份标识查询该用户是否已关联。
1)如已关联,结合风控、安全因素及自身业务场景判断,可展示已关联的账号,由用户选择是否使用华为账号登录应用,或免用户操作,静默登录应用,客户端开发结束。
2)如未关联,再判断是否存在下面的异常场景,如无,则参考下面步骤3继续开发。
-
存在如下返回ArkTS错误码的异常场景:
1)返回1001502001 用户未登录华为账号错误码,说明华为账号未登录。
2)返回1001500003 不支持该scopes或permissions错误码,说明华为账号用户注册地非中国境内(不包含中国香港、中国澳门、中国台湾)。
3)获取到的匿名手机号为空,说明华为账号没有绑定手机号、权限未申请或未生效或者开发者开启了代码混淆,quickLoginAnonymousPhone(匿名手机号)属性需要配置混淆白名单防止被混淆。
上述异常场景应用需要展示其他登录方式。
-
-
将获取到的匿名手机号设置给下面QuickLoginButtonComponent组件示例代码中的quickLoginAnonymousPhone变量,调用LoginWithHuaweiIDButton组件,实现应用自己的登录页面,并展示华为账号一键登录按钮和华为账号用户认证协议(Account Kit提供跳转链接,应用需实现协议跳转,参见约束与限制第2点),用户同意协议并点击一键登录按钮后,可获取到Authorization Code,将该值传给应用服务端用于获取用户信息(完整手机号、UnionID、OpenID)。 代码地址链接