FinClip为企业提供小程序生态圈技术产品,开发者可在FinClip小程序开发帮助中心找到相关FinClip小程序指引

# 代理方法

小程序中部分业务是抽象定义的,这些抽象的业务通过接口的形式暴露给了外部,外部可以自行实现具体的业务逻辑。

函数签名如果是需要返回 boolean 类型表示是否需要阻止默认行为,如果需要阻止就返回 true,否则则返回 false

函数里的参数 appIdapiServer 均为触发的小程序的信息,可以根据这两个数据来对不同的小程序来执行不同的逻辑

# 1. CapsuleHandler

胶囊按钮的代理类

// namespace IFinProxyHandlerItem
class CapsuleHandler {
    /**
     * 点击关闭按钮时触发
     * @param appId 小程序 id
     * @param apiServer 小程序 apiServer
     * @returns 返回 true 表示宿主自行处理,返回 false 表示宿主处理完,仍执行后续的默认操作
     */
    public onCloseButtonClick(appId: string, apiServer: string): boolean 

    /**
     * 点击更多按钮时触发
     * @param appId 小程序 id
     * @param apiServer 小程序 apiServer
     * @returns 返回 true 表示宿主自行处理,返回 false 表示宿主处理完,仍执行后续的默认操作
     */
    public onMoreButtonClick(appId: string, apiServer: string): boolean
  }

示例代码:

import { FinAppProxyHandlerManager, IFinProxyHandlerItem } from '@finclip/sdk'
import { promptAction } from '@kit.ArkUI'

class CapsuleHandler extends IFinProxyHandlerItem.CapsuleHandler {
  onCloseButtonClick(appId: string, apiServer: string): boolean {
    this.promptMsg(`${appId},关闭按钮点击了`)
    return false
  }

  onMoreButtonClick(appId: string, apiServer: string): boolean {
    this.promptMsg(`${appId},更多按钮点击了`)
    return false
  }

  private promptMsg(message: string) {
    promptAction.showToast({
      message
    })
  }
}

FinAppProxyHandlerManager.capsuleHandler = new CapsuleHandler()

# 2. MoreMenuHandler

更多菜单的代理类

// namespace IFinProxyHandlerItem
class MoreMenuHandler {
    /**
     * 当收到小程序返回的数据时触发
     * @param contentInfo 小程序返回的信息
     * @returns
     */
    public onCustomMenuItemClickWithInfo(appId: string, apiServer: string,
      contentInfo: IFinAppProxy.IMoreMenuItemContentInfo)

    /**
     * 当更多按钮菜单点击时出发
     * @param type 菜单按钮的唯一标识
     * @param currentPath 小程序当前的页面路径
     * @returns
     */
    public onMoreMenuItemClick(appId: string, apiServer: string, type: string, currentPath: string): boolean | void

    /**
     * 获取更多菜单的按钮列表
     * @returns 更多菜单的按钮列表
     */
    public getMoreMenuItems(): IFinAppProxy.IMoreMenuItem[]

    /**
     * 点击添加到桌面时触发的代理方法
     * @param appId 小程序 id
     * @param apiServer 小程序 apiServer
     * @param appletInfo 小程序信息
     * @param context UIAbilityContext
     * @returns
     */
    public async addToDesktop(appId: string, apiServer: string, appletInfo: IFinApplet.IAppletInfo,
      context: common.UIAbilityContext)
  }

示例代码:

import { FinAppProxyHandlerManager, IFinApplet, IFinAppProxy, IFinProxyHandlerItem } from '@finclip/sdk'
import { common } from '@kit.AbilityKit'
import { request } from '@kit.BasicServicesKit'
import { productViewManager } from '@kit.AppGalleryKit'
import { fileIo } from '@kit.CoreFileKit'
import { promptAction } from '@kit.ArkUI'

class MoreMenuHandler extends IFinProxyHandlerItem.MoreMenuHandler {
  onCustomMenuItemClickWithInfo(appId: string, apiServer: string,
    contentInfo: IFinAppProxy.IMoreMenuItemContentInfo) {
    this.promptMsg(`小程序 ${apiServer} ${appId}${contentInfo.path} 页面的菜单 ${contentInfo.menuId} 被点击了,返回的数据为${JSON.stringify(contentInfo.params)}`)
  }

  onMoreMenuItemClick(appId: string, apiServer: string, menuItemId: string, currentPath: string): boolean | void {
    this.promptMsg(`小程序 ${apiServer} ${appId}${currentPath} 页面的菜单 ${menuItemId} 被点击了`)
  }

  getMoreMenuItems(): IFinAppProxy.IMoreMenuItem[] {
    return [
      {
        label: '自定义',
        icon: $r('app.media.app_icon'),
        menuItemId: 'custom',
        menuType: 'onMiniProgram'
      }
    ]
  }

  public async addToDesktop(appId: string, apiServer: string, appletInfo: IFinApplet.IAppletInfo,
    uiContext: common.UIAbilityContext) {
    try {
      const shortcutId = appletInfo.name
      const label = appletInfo.name
      const foregroundIcon = uiContext.tempDir + '/icon.png'
      if (fileIo.accessSync(foregroundIcon)) {
        await fileIo.unlink(foregroundIcon)
      }
      if (appletInfo.logo) {
        await this.downloadFile(uiContext, appletInfo.logo, foregroundIcon)
      }

      const backgroundIcon = ''
      // 以自定义资源方式校验快捷方式是否允许添加到桌面,并返回快捷方式校验结果。
      const result = await productViewManager.checkPinShortcutPermitted(uiContext, shortcutId, {
        bundleName: uiContext.abilityInfo.bundleName,
        abilityName: uiContext.abilityInfo.name,
        moduleName: uiContext.abilityInfo.moduleName,
        parameters: {
          shortCutKey: 'startApplet',
          appId,
          apiServer
        }
      }, label, foregroundIcon, backgroundIcon)
      await productViewManager.requestNewPinShortcut(uiContext, result.tid)
      this.promptMsg('添加成功')
    } catch (err) {
      this.promptMsg(`添加失败:${err.message}`)
    }
  }

  private async downloadFile(context: common.UIAbilityContext, url: string, tempSaveAbsPath: string): Promise<void> {
    return new Promise(async (resolve) => {
      const downloadTask = await request.downloadFile(context, {
        url,
        filePath: tempSaveAbsPath
      })

      downloadTask.on('complete', () => {
        resolve()
      })
    })
  }

  private promptMsg(message: string) {
    promptAction.showToast({
      message
    })
  }
}


FinAppProxyHandlerManager.moreMenuHandler = new MoreMenuHandler()

// 小程序端
Page({
    onCustomButtonHandler(){
        return {
            appInfo:'this is appInfo'
        }
      }
})

IMoreMenuItem

属性 类型 是否必填 描述
label String 小程序 id
icon ResourceStr 菜单按钮的图片资源
disableIcon ResourceStr 菜单按钮禁用的图片资源
darkIcon ResourceStr 暗黑模式菜单按钮的图片资源
disableDarkIcon ResourceStr 暗黑模式菜单按钮禁用的图片资源
menuItemId String 菜单按钮的唯一标识,即 onMoreMenuItemClick 参数的 type
hover Boolean 是否开启 hover 效果
disable Boolean 是否禁用

IMoreMenuItemContentInfo

属性 类型 描述
title String 小程序名称
logo String 小程序 logo
description String 小程序描述
path String 当前页面路径
menuId String 菜单按钮的唯一标识
params IMoreMenuItemParams 由小程序返回的原始数据

IMoreMenuItemParams

属性 类型
title String
desc String
path String
appInfo object

# 3. PrivacyHandler

隐私协议的代理类

// namespace IFinProxyHandlerItem
export class PrivacyHandler {
  /**
   * @param scope 当前请求的权限,如果为空则是在关于页面显示的隐私协议
   * @returns IFinApplet.IPrivacy
   */
  public getPrivacyInfo(appId: string, apiServer: string, scope?: string): IFinApplet.IPrivacy | void
}

示例代码

import { FinAppProxyHandlerManager, IFinApplet, IFinProxyHandlerItem } from '@finclip/sdk'

class PrivacyHandler extends IFinProxyHandlerItem.PrivacyHandler {
  getPrivacyInfo(appId: string, apiServer: string, scope?: string): IFinApplet.IPrivacy | void {
    return {
      title: '自定义授权弹窗标题',
      docName: '自定义隐私协议文档名称',
      docUrl: 'https://www.finclip.com',
      copyWriting: '自定义授权弹窗文案,隐私协议文档名称为《自定义隐私协议文档名称》'
    }
  }
}


FinAppProxyHandlerManager.privacyHandler = new PrivacyHandler()

IFinApplet.IPrivacy

属性 类型 是否必填 描述
title String 自定义隐私授权弹窗标题
copyWriting String 自定义隐私授权弹窗文案,内容包含 自定义隐私协议文档名称
docName String 自定义隐私协议文档名称
docUrl String 自定义隐私协议文档链接

# 4. ScopeHandler

自定义权限的代理类

// namespace IFinProxyHandlerItem
export class ScopeHandler {
    /**
     * 是否自定义小程序权限设置页
     */
    public isAppletCustomizeSettingPage: boolean = false

    /**
     * 自定义小程序权限设置页
     * @param applet
     * @param scopes
     * @param updateScopes 更新 scopes 的方法
     * @returns boolean 是否实现了代理,如果返回 false 表示未实现代理,会走默认的逻辑
     */
    public openSettingPage(applet: IFinApplet.IAppletInfo, scopes: IFinApplet.IScope[],
      updateScopes: (result: IFinAppProxy.ISettingListParams) => void): boolean

    /**
     * 注册自定义权限
     * @param appId
     * @param apiServer
     * @returns 自定义权限列表
     */
    public getCustomScopes(appId: string, apiServer: string): IFinApplet.IScope[]

    /**
     * 自定义弹窗,实现方式可以参考示例代码。建议与 getCustomPartialView 二选一实现
     * @returns 弹窗 builder
     */
    public getCustomView(): (() => void) | void

    /**
     * 权限弹窗自定义部分内容。
     * 可根据 appId、apiServer 和 scope 返回不同的 builder。
     * 建议与 getCustomView 二选一实现。
     *
     * @param appId 小程序 id
     * @param apiServer 小程序 apiServer
     * @param scope 当前权限标识
     * @returns 弹窗 builder
     */
    public getCustomPartialView(appId: string, apiServer: string,
      scope: string): IFinAppProxy.ICustomPartialViewBuilder | void

    /**
     * 是否隐藏 Scope 权限弹窗内自带的权限标题和描述,默认为 false,即不隐藏。
     *
     * @param appId 小程序 id
     * @param apiServer 小程序 apiServer
     * @param scope 当前权限标识
     * @returns 是否隐藏
     */
    public isHideTitleAndDescription(appId: string, apiServer: string, scope: string): boolean

    /**
     * 是否隐藏 Scope 权限弹窗内自带的后台定位权限额外选项,默认为 false,即不隐藏。
     *
     * 对于后台定位权限,弹窗内会比其它权限多两个选项,即【使用小程序时】和【使用小程序时和离开后】。
     *
     * @param appId 小程序 id
     * @param apiServer 小程序 apiServer
     * @param scope 当前权限标识
     * @returns 是否隐藏
     */
    public isHideLocationScopeOption(appId: string, apiServer: string, scope: string): boolean

    /**
     * 是否隐藏 Scope 权限弹窗内自带的【拒绝】、【允许】按钮,默认为 false,即不隐藏。
     * 若隐藏了这两个按钮之后,务必在 `getCustomPartialView` 中自定义的布局内实现对应的按钮。
     *
     * @param appId 小程序 id
     * @param apiServer 小程序 apiServer
     * @param scope 当前权限标识
     * @returns 是否隐藏
     */
    public isHideButton(appId: string, apiServer: string, scope: string): boolean

    /**
     * @param api 当初小程序触发的 API 名称
     * @param context 当前 Ability 的 context
     * @param scopes 权限列表,包括内置权限和自定义权限
     * @param callback API 的回调函数,如果用户通过权限调用 callback 传 true,后续则会走自定义 API 的逻辑,否则则直接走 fail 逻辑
     * @param params 自定义 API 接收的参数
     * @returns boolean 是否实现了代理,如果返回 false 表示未实现代理,会走默认的权限逻辑
     */
    public onCustomApiInvoke(appId: string, apiServer: string, api: string, context: common.UIAbilityContext,
      scopes: IFinApplet.IScope[], callback: (res: boolean) => void, params?: object): boolean
  }

示例代码

import { FinAppProxyHandlerManager, IFinApplet, IFinAppProxy, IFinProxyHandlerItem } from '@finclip/sdk'
import { promptAction } from '@kit.ArkUI'
import { ISettingRouterParams } from '../../pages/Setting'
import { getFinAppClient } from '../FinAppClient'
import { customView,customPartialView } from './customView'
import { common } from '@kit.AbilityKit'

class ScopeHandler extends IFinProxyHandlerItem.ScopeHandler {
  isAppletCustomizeSettingPage = true

  openSettingPage(applet: IFinApplet.IAppletInfo, scopes: IFinApplet.IScope[],
    updateScopes: (result: IFinAppProxy.ISettingListParams) => void): boolean {
    const param: ISettingRouterParams = {
      applet,
      scopes
    }
    // 以 Navigation 的方式初始化,使用 Navigation 跳转授权管理页
    getFinAppClient().getEntryInfo().routerState?.pushPath({
      name: 'Setting',
      param,
      onPop: (info) => {
        const param = info.result as ISettingRouterParams
        updateScopes({
          scopes: param.scopes,
          applet: param.applet
        })
      }
    })
    return true
  }

  getCustomScopes(appId: string, apiServer: string): IFinApplet.IScope[] {
    return [
      {
        scope: 'idCard',
        scopeName: '身份证',
        title: '获取身份证信息',
        desc: "获取您的身份证信息用于XXXXXX",
        scopeStatus: 0
      }
    ]
  }

  // 与 getCustomPartialView 二选一实现
  getCustomView(): (() => void) | void {
    return customView
  }

  // 与 getCustomView 二选一实现
  getCustomPartialView(appId: string, apiServer: string, scope: string): void | IFinAppProxy.ICustomPartialViewBuilder {
    if (scope === 'scope.userLocation') {
      return customPartialView
    }
  }

  isHideTitleAndDescription(appId: string, apiServer: string, scope: string): boolean {
    return true
  }

  isHideLocationScopeOption(appId: string, apiServer: string, scope: string): boolean {
    return true
  }

  isHideButton(appId: string, apiServer: string, scope: string): boolean {
    return true
  }

  onCustomApiInvoke(appId: string, apiServer: string, api: string, context: common.UIAbilityContext,
    scopes: IFinApplet.IScope[], callback: (res: boolean) => void, params?: object): boolean {
    const scope = scopes.find(i => i.scope === 'idCard')
    if (scope?.scopeStatus === 0) {
      context.eventHub.emit('showCustomToast', api)
      context.eventHub.on('onShowCustomToast', (res: boolean) => {
        scope.scopeStatus = res ? 3 : 1
        callback(res)
        context.eventHub.off('onShowCustomToast')
      })
    } else {
      callback(true)
    }
    return true
  }

  private promptMsg(message: string) {
    promptAction.showToast({
      message
    })
  }
}


FinAppProxyHandlerManager.scopeHandler = new ScopeHandler()
// customView.ets
import { EPermissionState, IFinApplet, IFinAppProxy } from '@finclip/sdk'
import { common } from '@kit.AbilityKit'

@Builder
export function customView() {
  CustomViewComponent()
}

@Component
struct CustomViewComponent {
  @State show: boolean = false
  private context = getContext(this) as common.UIAbilityContext

  aboutToAppear() {
    this.context.eventHub.on('showCustomToast', () => {
      this.show = true
    })
  }

  handlePermission(res: boolean) {
    this.context.eventHub.emit('onShowCustomToast', res)
    this.show = false
  }

  @Builder
  toast() {
    Stack({ alignContent: Alignment.Bottom }) {
      Row() {
      }
      .height('100%')
      .width('100%')
      .backgroundColor('rgba(0,0,0,0.5)')

      Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween }) {
        Row() {
          Button('拒绝').onClick(() => {
            this.handlePermission(false)
          })
          Button('同意').onClick(() => {
            this.handlePermission(true)
          })
        }
        .width('100%')
      }
      .padding({
        top: 24,
        left: 18,
        right: 18,
        bottom: 24
      })
      .width('100%')
      .height(241)
      .backgroundColor('#F5F6F6')
      .clip(true)
      .borderRadius({ topLeft: 16, topRight: 16 })
      .zIndex(10)
    }
  }

  build() {
    if (this.show) {
      this.toast()
    }
  }
}

@Builder
export function customPartialView(appId: string, apiServer: string, scope: string,
  callback: (state: EPermissionState) => void) {
  CustomPartialViewComponent({
    appId,
    apiServer,
    scope,
    callback
  })
}

@Component
struct CustomPartialViewComponent {
  @Prop appId: string
  @Prop apiServer: string
  @Prop scope: string
  callback?: (state: EPermissionState) => void
  allowState = EPermissionState.ALLOW
  private context = getContext(this) as common.UIAbilityContext

  aboutToAppear() {
  }

  @Builder
  Selector() {
    if (this.scope === 'locationBackground') {
      Row() {
        Text('使用时允许')
        Radio({ value: 'Radio1', group: 'radioGroup' })
          .checked(false)
          .onChange((isChecked: boolean) => {
            if (isChecked) {
              this.allowState = EPermissionState.ALLOW_WHEN_USING
            }
          })
      }
      .justifyContent(FlexAlign.SpaceBetween)
      .width('100%')

      Row() {
        Text('始终允许')
        Radio({ value: 'Radio2', group: 'radioGroup' })
          .checked(true)
          .onChange((isChecked: boolean) => {
            if (isChecked) {
              this.allowState = EPermissionState.ALLOW
            }
          })
      }
      .justifyContent(FlexAlign.SpaceBetween)
      .width('100%')
    }
  }

  build() {
    Column() {
      Text(`appId:${this.appId}`)
      Text(`apiServer:${this.apiServer}`)
      Text(`scope:${this.scope}`)
      this.Selector()
      Row() {
        Button('允许')
          .onClick(() => {
            this.callback?.(this.allowState)
          })
        Button('拒绝')
          .backgroundColor(Color.Red)
          .onClick(() => {
            this.callback?.(EPermissionState.DISALLOW)
          })
      }
      .justifyContent(FlexAlign.SpaceBetween)
      .width('100%')
    }
  }
}

提示

scope 参数的具体值可以参考 scope 权限

IFinApplet.IScope

属性 类型 是否必填 描述
scope String 权限标识位
title String 权限标题
desc String 权限描述
scopeStatus EPermissionState 权限状态
scopeName String 设置页描述

EPermissionState

属性 描述
UNSET 0 未设置
DISALLOW 1 不允许
ALLOW_WHEN_USING 2 使用小程序时
ALLOW 3 允许

IFinApplet.IAppletInfo

属性 类型 是否必填 描述
appId string 小程序ID
name string 小程序名称
logo string 小程序logo地址

IFinAppProxy.ISettingListParams

属性 类型 是否必填 描述
applet IFinApplet.IAppletInfo 小程序信息
scopes IFinApplet.IScope[] 权限列表

IFinAppProxy.ICustomPartialViewBuilder

  /**
   * @param appId - 小程序的 ID。
   * @param apiServer - 与小程序关联的 API 服务器。
   * @param scope - 权限标识位,内置权限的名称与 ft.authorize 参数一致
   * @param callback 回调函数,用于通知权限状态
   */
type ICustomPartialViewBuilder = (appId: string, apiServer: string, scope: string, callback: (state: EPermissionState) =>void) => void

# 5.GrayReleaseHandler

灰度发布相关的代理类

// namespace IFinProxyHandlerItem
export class GrayReleaseHandler {
    /**
     * 当获取小程序信息时会调用该方法,可以自定义灰度发布的数据,sdk 会将自定义数据和内置数据合并发送,当属性名相同时,使用自定义的数据
     * @param appId
     * @param apiServer
     * @returns 需要拼接到灰度发布配置的数据
     */
    public getGrayExtension(appId: string, apiServer: string): Record<string, string>
  }

示例代码

import { FinAppProxyHandlerManager, IFinProxyHandlerItem } from '@finclip/sdk'

class GrayReleaseHandler extends IFinProxyHandlerItem.GrayReleaseHandler {
  getGrayExtension(appId: string, apiServer: string): Record<string, string> {
    return {
      'customKey': 'customValue',
      'xUserId': '130000000'
    }
  }
}

FinAppProxyHandlerManager.grayReleaseHandler = new GrayReleaseHandler()

# 6.AppletLifeCycleHandler

小程序生命周期相关

// namespace IFinProxyHandlerItem
export class AppletLifeCycleHandler {
    /**
     * 当小程序打开时触发。
     * @param appId 小程序的 ID。
     * @param apiServer 与小程序关联的 API 服务器。
     */
    public onOpen(appId: string, apiServer: string)

    /**
     * 当小程序初始化完成时触发。
     * @param appId 小程序的 ID。
     * @param apiServer 与小程序关联的 API 服务器。
     */
    public onInitCompletion(appId: string, apiServer: string)

    /**
     * 当小程序关闭时触发。
     * @param appId 小程序的 ID。
     * @param apiServer 与小程序关联的 API 服务器。
     */
    public onClose(appId: string, apiServer: string)

    /**
     * 当小程序发生错误时触发。
     * @param appId 小程序的 ID。
     * @param apiServer 与小程序关联的 API 服务器。
     * @param code 错误代码。
     * @param message 错误信息。
     */
    public onError(appId: string, apiServer: string, code: string, message: string)

    /**
     * 当小程序准备就绪时触发。
     * @param appId 小程序的 ID。
     * @param apiServer 与小程序关联的 API 服务器。
     */
    public onReady(appId: string, apiServer: string)

    /**
     * 当小程序显示时触发。
     * @param appId 小程序的 ID。
     * @param apiServer 与小程序关联的 API 服务器。
     */
    public onShow(appId: string, apiServer: string) 

    /**
     * 当小程序隐藏时触发。
     * @param appId 小程序的 ID。
     * @param apiServer 与小程序关联的 API 服务器。
     */
    public onHide(appId: string, apiServer: string)

    /**
     * 当小程序销毁时触发。
     * @param appId 小程序的 ID。
     * @param apiServer 与小程序关联的 API 服务器。
     */
    public onDestroy(appId: string, apiServer: string)
  }

示例代码

import { FinAppProxyHandlerManager, IFinProxyHandlerItem } from '@finclip/sdk'
import { promptAction } from '@kit.ArkUI'

class AppletLifeCycleProxy extends IFinProxyHandlerItem.AppletLifeCycleHandler {
  onOpen(appId: string, apiServer: string) {
    this.promptMsg(`${appId} onOpen`)
  }

  onInitCompletion(appId: string, apiServer: string) {
    this.promptMsg(`${appId} onInitCompletion`)
  }

  onClose(appId: string, apiServer: string) {
    this.promptMsg(`${appId} onClose`)
  }

  onError(appId: string, apiServer: string, code: string, message: string) {
    this.promptMsg(`${appId} onError,code:${code},message:${message}`)
  }

  onReady(appId: string, apiServer: string) {
    this.promptMsg(`${appId} onReady`)
  }

  onShow(appId: string, apiServer: string) {
    this.promptMsg(`${appId} onShow`)
  }

  onHide(appId: string, apiServer: string) {
    this.promptMsg(`${appId} onHide`)
  }

  onDestroy(appId: string, apiServer: string) {
    this.promptMsg(`${appId} onDestroy`)
  }

  private promptMsg(message: string) {
    promptAction.showToast({
      message
    })
  }
}

FinAppProxyHandlerManager.appletLifeCycleHandler = new AppletLifeCycleProxy()

# 7.CustomLayoutHandler

自定义布局相关的代理类

// namespace IFinProxyHandlerItem
export class CustomLayoutHandler {
    /**
     * 自定义分包下载错误页
     * @returns 自定义分包下载错误页 builder
     */
    public getDownloadSubpackageFileFailedLayout(): ((appId: string, apiServer: string, path: string,
      reload: () => void) => void) | void

    /**
     * 自定义加载错误页
     * @returns 自定义加载错误页 builder
     */
    public getLoadFailedLayout(): ((appId: string, apiServer: string, title: string, label: string) => void) | void

    /**
     * 当页面准备完成触发,该函数结束后销毁 loading 组件
     * @returns
     */
    public async onPageLoadingLayoutReady(): Promise<void>

    /**
     * 当小程序准备完成触发,该函数结束后销毁 loading 页
     * @returns
     */
    public async onLoadingLayoutReady(): Promise<void>

    /**
     * 当小程序资源初始化完成且未加载时触发,该函数结束后开始加载小程序。
     * @param appId 小程序的 ID。
     * @param apiServer 与小程序关联的 API 服务器。
     * @returns hook 执行完成后的 Promise。
     */
    public async onInitCompletion(appId: string, apiServer: string): Promise<void>

    /**
     * 自定义页面 loading 组件
     * @returns 自定义页面 loading 的 builder
     */
    public getPageCustomLoadingLayout(): ((appId: string, apiServer: string, path: string) => void) | void

    /**
     * 获取自定义 loading 页的布局方法
     * @returns 自定义 loading 页 builder
     */
    public getCustomLoadingLayout(): (() => void) | void

    /**
     * 获取自定义 Toast 方法
     * @returns 自定义 Toast builder
     */
    public getCustomToastLayout(): (() => void) | void

     /**
     * 获取自定义覆盖物 builder,会渲染在小程序页面上层
     * @returns 自定义覆盖物 builder
     */
    public getCustomOverlayLayout(): ((appId: string, apiServer: string) => void) | void

     /**
     * 获取自定义分包加载 loading toast builder
     * @returns 自定义分包加载 loading toast builder
     */
    public getCustomSubpackageLoadingLayout(): ((appId: string, apiServer: string) => void) | void
  }

# 7.1 自定义分包下载错误页

import { FinAppProxyHandlerManager, IFinProxyHandlerItem } from '@finclip/sdk'

@Builder
export function DownloadSubpackageFileFailedLayout(appId: string, apiServer: string, path: string,
  reload: () => void) {
  CustomViewComponent({
    appId,
    apiServer,
    path,
    reload
  })
}

@Component
struct CustomViewComponent {
  @Prop appId: string
  @Prop apiServer: string
  @Prop path: string
  reload?: () => void

  build() {
    Flex({
      alignItems: ItemAlign.Center,
      justifyContent: FlexAlign.Center,
      direction: FlexDirection.Column
    }) {
      Column() {
        Text('appId: ' + this.appId)
        Text('apiServer: ' + this.apiServer)
        Text('path: ' + this.path)
        Button('重新加载').onClick(() => {
          this.reload?.()
        })
      }
    }
    .backgroundColor(Color.White)
    .width('100%')
    .height('100%')
  }
}

class CustomLayoutHandler extends IFinProxyHandlerItem.CustomLayoutHandler {
  /**
   * 自定义分包下载错误页
   * @returns 自定义分包下载错误页 builder
   */
  getDownloadSubpackageFileFailedLayout(): ((appId: string, apiServer: string, path: string,
    reload: () => void) => void) | void {
    return DownloadSubpackageFileFailedLayout
  }
}

FinAppProxyHandlerManager.customLayoutHandler = new CustomLayoutHandler()

# 7.2 自定义加载错误页

import { FinAppProxyHandlerManager, IFinProxyHandlerItem } from '@finclip/sdk'

@Builder
export function LoadFailedLayout(appId: string, apiServer: string, title: string, label: string, code: string) {
  CustomViewComponent({
    appId,
    apiServer,
    title,
    label,
    code
  })
}

@Component
struct CustomViewComponent {
  @Prop appId: string
  @Prop apiServer: string
  @Prop title: string
  @Prop label: string
  @Prop code: string

  build() {
    Flex({
      alignItems: ItemAlign.Center,
      justifyContent: FlexAlign.Center,
      direction: FlexDirection.Column
    }) {
      Column() {
        Text('appId: ' + this.appId)
        Text('apiServer: ' + this.apiServer)
        Text('title: ' + this.title)
        Text('label: ' + this.label)
        Text('code: ' + this.code)
      }
    }
    .backgroundColor(Color.White)
    .width('100%')
    .height('100%')
  }
}

class CustomLayoutHandler extends IFinProxyHandlerItem.CustomLayoutHandler {
  getLoadFailedLayout(){
    return LoadFailedLayout
  }
}

FinAppProxyHandlerManager.customLayoutHandler = new CustomLayoutHandler()

# 7.3 自定义 loading 页

import { FinAppProxyHandlerManager, IFinProxyHandlerItem, IFinAppProxy } from '@finclip/sdk'

@Builder
export function CustomLoadingLayout(appId?: string, apiServer?: string) {
  CustomViewComponent()
}

@Component
struct CustomViewComponent {
  @Consume layoutParams: IFinAppProxy.ICustomLoadingLayoutParams = {
    appId: '',
    apiServer: '',
    name: '',
    logo: ''
  }

  build() {
    Flex({
      alignItems: ItemAlign.Center,
      justifyContent: FlexAlign.Center,
      direction: FlexDirection.Column
    }) {
      Column() {
        Text('自定义 loading')
        Text('appId: ' + this.layoutParams.appId)
        Text('apiServer: ' + this.layoutParams.apiServer)
        Text('path: ' + this.layoutParams.name)
        Image(this.layoutParams.logo)
          .width('100%')
          .height('100%')
          .padding(5)
          .border({
            width: 0.5,
            radius: 30,
            color: 'rgba(222,222,222,1)'
          })
          .draggable(false)
      }
    }
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
    .backgroundColor(Color.White)
    .width('100%')
    .height('100%')
  }
}

class CustomLayoutHandler extends IFinProxyHandlerItem.CustomLayoutHandler {
  onLoadingLayoutReady(): Promise<void> {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve()
      }, 200)
    })
  }

  getCustomLoadingLayout(): (() => void) | void {
    return CustomLoadingLayout
  }
}

FinAppProxyHandlerManager.customLayoutHandler = new CustomLayoutHandler()

# 7.4 自定义页面 loading 页

import { FinAppProxyHandlerManager, IFinProxyHandlerItem, IFinAppProxy } from '@finclip/sdk'

@Builder
export function PageCustomLoadingLayout(appId: string, apiServer: string, path: string) {
  CustomViewComponent({
    appId,
    apiServer,
    path,
  })
}

@Component
struct CustomViewComponent {
  @Prop appId: string
  @Prop apiServer: string
  @Prop path: string

  build() {
    Flex({
      alignItems: ItemAlign.Center,
      justifyContent: FlexAlign.Center,
      direction: FlexDirection.Column
    }) {
      Column() {
        Text('页面自定义 loading')
        Text('appId: ' + this.appId)
        Text('apiServer: ' + this.apiServer)
        Text('path: ' + this.path)
      }
    }
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
    .backgroundColor(Color.White)
    .width('100%')
    .height('100%')
  }
}

class CustomLayoutHandler extends IFinProxyHandlerItem.CustomLayoutHandler {
   onPageLoadingLayoutReady(): Promise<void> {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve()
      }, 200)
    })
  }

  getPageCustomLoadingLayout(): ((appId: string, apiServer: string, path: string) => void) | void {
    return PageCustomLoadingLayout
  }
}

FinAppProxyHandlerManager.customLayoutHandler = new CustomLayoutHandler()

# 7.5 通过 onInitCompletion 控制加载并监听隐私确认

下面示例把 CustomLayoutHandler.onInitCompletion 和触发 privacyPolicy 的自定义 loading 组件放在一起,展示如何在用户同意隐私政策后再继续加载小程序。

import { FinAppClient, FinAppProxyHandlerManager, IFinProxyHandlerItem } from '@finclip/sdk'
import { common } from '@kit.AbilityKit'

class CustomLayoutHandler extends IFinProxyHandlerItem.CustomLayoutHandler {
  context: common.UIAbilityContext
  onLoadingLayoutReadyResolve?: () => void
  agreed: boolean = false

  constructor(context: common.UIAbilityContext) {
    super()
    this.context = context
    // 监听隐私政策结果,同意后放行小程序加载
    this.context.eventHub.on('privacyPolicy', (result: boolean, appId: string, apiServer: string) => {
      if (result) {
        if (this.onLoadingLayoutReadyResolve) {
          this.onLoadingLayoutReadyResolve()
          this.onLoadingLayoutReadyResolve = undefined
        }
        this.agreed = true
      } else {
        FinAppClient.getInstance()?.clearApplet(appId, apiServer)
      }
    })
  }

  /**
   * 当小程序资源初始化完成且未加载时触发,该函数结束后开始加载小程序。
   * @param appId 小程序的 ID。
   * @param apiServer 与小程序关联的 API 服务器。
   * @returns hook 执行完成后的 Promise。
   */
  onInitCompletion(appId: string, apiServer: string): Promise<void> {
    return new Promise((resolve) => {
      if (this.agreed) {
        resolve()
      } else {
        this.onLoadingLayoutReadyResolve = resolve
      }
    })
  }
}

export function initCustomLayoutHandler(context: common.UIAbilityContext) {
  FinAppProxyHandlerManager.customLayoutHandler = new CustomLayoutHandler(context)
}
@Builder
export function CustomLoadingLayout(appId?: string, apiServer?: string) {
  CustomViewComponent({
    appId,
    apiServer
  })
}

@Component
struct CustomViewComponent {
  @Prop appId: string
  @Prop apiServer: string
  context = this.getUIContext().getHostContext()

  // 同意后继续加载
  onAgree() {
    this.context?.eventHub.emit('privacyPolicy', true, this.appId, this.apiServer)
  }

  // 取消后中断加载
  onCancel() {
    this.context?.eventHub.emit('privacyPolicy', false, this.appId, this.apiServer)
  }

  build() {
    Flex({
      alignItems: ItemAlign.Center,
      justifyContent: FlexAlign.Center,
      direction: FlexDirection.Column
    }) {
      Column() {
        Text('自定义 loading 页')
        Text('appId: ' + this.appId)
        Text('apiServer: ' + this.apiServer)
        Button('同意').onClick(() => {
          this.onAgree()
        })
        Button('取消').onClick(() => {
          this.onCancel()
        })
      }
    }
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
    .backgroundColor(Color.Green)
    .width('100%')
    .height('100%')
  }
}

# 7.6 自定义 Toast

import { FinAppProxyHandlerManager, IFinProxyHandlerItem, IFinAppProxy } from '@finclip/sdk'

@Builder
export function CustomToast() {
  CustomViewComponent()
}

@Component
struct CustomViewComponent {
  @Consume toastInfo: IFinAppProxy.ICustomToastLayoutParams

  build() {
    this.toastWithMask()
  }

  @Builder
  toastImage() {
    if (this.toastInfo.image) {
      Image(this.toastInfo.image)
        .width(40)
        .height(40)
    } else if (this.toastInfo.icon) {
      if (this.toastInfo.icon === 'loading') {
        Text('loading 图案')
      }
      if (this.toastInfo.icon === 'success') {
        Text('success 图案')
      }
      if (this.toastInfo.icon === 'error') {
        Text('error 图案')
      }
    }
  }

  @Builder
  toast() {
    if (this.toastInfo.icon === 'none') {
      Column() {
        Text(this.toastInfo.title)
          .fontSize(14)
          .fontColor('rgba(255, 255, 255, 0.9)')
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .maxLines(2)
          .textAlign(TextAlign.Center)
      }
      .constraintSize({
        maxWidth: 200,
        minHeight: 48
      })
      .padding({
        left: 12,
        right: 12,
        top: 14,
        bottom: 14
      })
      // 加一个占位空间,让垂直居中稍微靠上
      .margin({
        bottom: 120
      })
      .backgroundColor('rgba(0, 0, 0, 0.7)')
      .align(Alignment.Center)
      .justifyContent(FlexAlign.Center)
      .opacity(1)
      .borderRadius(10)
      .clip(true)
    } else {
      Column() {
        this.toastImage()
        Text(this.toastInfo.title)
          .fontSize(14)
          .fontColor('rgba(255, 255, 255, 0.9)')
          .margin({ top: 12 })
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .maxLines(2)
          .textAlign(TextAlign.Center)
      }
      .width(130)
      .height(130)
      .padding({
        left: 14,
        right: 14
      })
      // 加一个占位空间,让垂直居中稍微靠上
      .margin({
        bottom: 120
      })
      .backgroundColor('rgba(0, 0, 0, 0.7)')
      .align(Alignment.Center)
      .justifyContent(FlexAlign.Center)
      .opacity(1)
      .borderRadius(10)
      .clip(true)
    }
  }

  @Builder
  toastWithMask() {
    Flex({
      direction: FlexDirection.Column,
      justifyContent: FlexAlign.Center,
      alignItems: ItemAlign.Center
    }) {
      this.toast()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('transparent')
    // 稍微小于 modal 和 actionsheet
    .zIndex(110)
    .position({
      top: 0,
      left: 0
    })
    .hitTestBehavior(this.toastInfo.mask ? HitTestMode.Default : HitTestMode.Transparent)
  }
}

class CustomLayoutHandler extends IFinProxyHandlerItem.CustomLayoutHandler {
   getCustomToastLayout(): (() => void) | void {
    return CustomToast
  }
}

FinAppProxyHandlerManager.customLayoutHandler = new CustomLayoutHandler()

# 7.7 自定义覆盖物

import { FinAppProxyHandlerManager, IFinProxyHandlerItem, IFinAppProxy } from '@finclip/sdk'

@Builder
export function CustomOverlayLayout(appId: string, apiServer: string) {
  Column() {
    Text('CustomLayout')
    Text(appId)
    Text(apiServer)
  }
}

class CustomLayoutHandler extends IFinProxyHandlerItem.CustomLayoutHandler {
  getCustomOverlayLayout() {
    return CustomOverlayLayout
  }
}

FinAppProxyHandlerManager.customLayoutHandler = new CustomLayoutHandler()

# 7.8 自定义分包加载 loading

import { FinAppProxyHandlerManager, IFinProxyHandlerItem, IFinAppProxy } from '@finclip/sdk'

@Builder
export function CustomOverlayLayout(appId: string, apiServer: string) {
  Column() {
    Text('CustomLayout')
    Text(appId)
    Text(apiServer)
  }
}

class CustomLayoutHandler extends IFinProxyHandlerItem.CustomLayoutHandler {
  getCustomSubpackageLoadingLayout() {
    return CustomOverlayLayout
  }
}

FinAppProxyHandlerManager.customLayoutHandler = new CustomLayoutHandler()

IFinAppProxy.ICustomLoadingLayoutParams

属性 类型 描述
name String 小程序名称,当未获取到小程序详情时,默认为空
logo ResourceStr 小程序图标,当未获取到小程序详情或者小程序详情中的小程序图标字段为空会返回一个默认的图标,开发者可以自行选择是否使用
appId String 小程序 appId,当未获取到小程序详情时,默认为空
apiServer String 小程序 apiServer,当未获取到小程序详情时,默认为空

# 8.ButtonOpenTypeHandler

小程序中的 button 有一系列的open-type事件,有部分行为需要App来实现,所以也会通过代理方法来触发对应的事件

// namespace IFinProxyHandlerItem
export class ButtonOpenTypeHandler {
    /**
     * 调用获取用户信息的Api(getUserProfile) 时,会触发该代理方法。
     * @param appId 小程序 id
     * @param apiServer 小程序 apiServer
     * @returns Promise<IFinApplet.IUserProfile | void>
     *
     */
    public async getUserProfile(appId: string, apiServer: string): Promise<IFinApplet.IUserProfile | void>

    /**
     * 调用获取用户信息API(getUserInfo) 或者 点击open-type 属性为 getUserInfo 的 Button 时触发该代理事件
     * @param appId 小程序 id
     * @param apiServer 小程序 apiServer
     * @returns Promise<IFinApplet.IUserInfo | void>
     *
     */
    public async getUserInfo(appId: string, apiServer: string): Promise<IFinApplet.IUserInfo | void>

    /**
     * 用户点击转发按钮或者 点击open-type 属性为 share 的 Button 时触发该代理事件
     * @param appId 小程序 id
     * @param apiServer 小程序 apiServer
     * @param forwardInfo 转发携带的参数
     * @returns Promise<void>
     *
     */
    public async forwardAppletWithInfo(appId: string, apiServer: string, forwardInfo: IFinAppProxy.IForwardInfo): Promise<void>

    /**
     * 点击open-type 属性为 getPhoneNumber 的 Button 时触发该代理事件
     * @param appId 小程序 id
     * @param apiServer 小程序 apiServer
     * @param callback 同自定义 API 的 callback,由宿主自行处理返回结果
     * @returns Promise<void>
     *
     */
    public async getPhoneNumber(appId: string, apiServer: string, callback: IApiManager.IInvokeCallback): Promise<void>

    /**
     * 点击open-type 属性为 launchApp 的 Button 时触发该代理事件
     * @param appId 小程序 id
     * @param apiServer 小程序 apiServer
     * @param callback 同自定义 API 的 callback,由宿主自行处理返回结果
     * @param appParameter 打开 APP 时,向 APP 传递的参数
     * @returns Promise<void>
     *
     */
    public async launchApp(appId: string, apiServer: string, callback: IApiManager.IInvokeCallback, appParameter: string): Promise<void>

    /**
     * 点击open-type 属性为 feedback 的 Button 时触发该代理事件
     * @param appId 小程序 id
     * @param apiServer 小程序 apiServer
     * @returns Promise<boolean> 默认返回 false,返回 true 表示由代理实现逻辑,返回 false 表示打开更多菜单里的投诉反馈页面。
     *
     */
    public async feedback(appId: string, apiServer: string): Promise<boolean>

    /**
     * 点击open-type 属性为 chooseAvatar 的 Button 时触发该代理事件
     * @param appId 小程序 id
     * @param apiServer 小程序 apiServer
     * @param callback 同自定义 API 的 callback,由宿主自行处理返回结果
     * @returns Promise<void>
     *
     */
    public async chooseAvatar(appId: string, apiServer: string, callback: IApiManager.IInvokeCallback): Promise<void>

    /**
     * 点击open-type 属性为 contact 的 Button 时触发该代理事件
     * @param appId 小程序 id
     * @param apiServer 小程序 apiServer
     * @param callback 同自定义 API 的 callback,由宿主自行处理返回结果
     * @param sessionFrom 会话来源
     * @param sendMessageTitle 会话内消息卡片标题
     * @param sendMessagePath 会话内消息卡片点击跳转小程序路径
     * @param sendMessageImg 会话内消息卡片图片
     * @param showMessageCard 小程序信息
     * @returns Promise<void>
     */
    public async contact(appId: string, apiServer: string, callback: IApiManager.IInvokeCallback, sessionFrom: string, sendMessageTitle: string, sendMessagePath: string, sendMessageImg: string, showMessageCard: boolean): Promise<void>
  }

示例代码

import { FinAppProxyHandlerManager, IFinApplet, IFinProxyHandlerItem } from '@finclip/sdk'

class ButtonOpenTypeHandler extends IFinProxyHandlerItem.ButtonOpenTypeHandler {
  async getUserProfile(appId: string, apiServer: string): Promise<IFinApplet.IUserProfile | void> {
    return {
      nickName: "昵称",
      avatarUrl: "头像地址",
      gender: "性别",
      province: "省份",
      city: "城市",
      country: "国家",
    }
  }

  async getUserInfo(appId: string, apiServer: string): Promise<IFinApplet.IUserInfo | void> {
    return {
      nickName: "昵称",
      avatarUrl: "头像地址",
      gender: "性别",
      province: "省份",
      city: "城市",
      country: "国家",
    }
  }

  async forwardAppletWithInfo(appId: string, apiServer: string, forwardInfo: IFinAppProxy.IForwardInfo): Promise<void> {
    this.promptMsg(JSON.stringify(forwardInfo))
  }

  async feedback(appId: string, apiServer: string): Promise<boolean> {
    this.promptMsg(`触发了 feedback`)
    return false
  }

  async chooseAvatar(appId: string, apiServer: string, callback: IApiManager.IInvokeCallback) {
    this.promptMsg(`触发了 chooseAvatar`)
    callback.onSuccess({})
  }

  async launchApp(appId: string, apiServer: string, callback: IApiManager.IInvokeCallback,
    appParameter: string): Promise<void> {
    this.promptMsg(`触发了 launchApp,appParameter${appParameter}`)
    callback.onSuccess({})
  }

  async getPhoneNumber(appId: string, apiServer: string, callback: IApiManager.IInvokeCallback): Promise<void> {
    this.promptMsg(`触发了 getPhoneNumber`)
    const res: IButtonOpenTypeHandler.IGetPhoneNumberRes = {
      phone: 123456789
    }
    callback.onSuccess(res)
  }

  async contact(appId: string, apiServer: string, callback: IApiManager.IInvokeCallback, sessionFrom: string,
    sendMessageTitle: string, sendMessagePath: string, sendMessageImage: string, showMessageCard: boolean) {
    this.promptMsg(`触发了 contact\n sessionFrom:${sessionFrom}\n sendMessageTitle:${sendMessageTitle}\n sendMessagePath:${sendMessagePath}\n sendMessageImage:${sendMessageImage}\n showMessageCard:${showMessageCard}`)
    callback.onSuccess({})
  }

  private promptMsg(message: string) {
    promptAction.showToast({
      message
    })
  }
}

FinAppProxyHandlerManager.buttonOpenTypeHandler = new ButtonOpenTypeHandler()

IFinApplet.IUserInfo

属性 类型
nickName String
avatarUrl String
gender String
province String
city String
country String

IFinApplet.IUserProfile

type IUserInfo =  string | number | boolean | null | Serializable[] | { [key: string]: Serializable }

# 9.onAppBackPressHandler

侧滑代理

// namespace IFinProxyHandlerItem
export class onAppBackPressHandler {
  /**
   * 当用户侧滑时触发
   * @param appId 小程序 id
   * @param apiServer 小程序 apiServer
   * @param path 触发的小程序页面路径
   * @returns 返回 true 表示宿主自行处理,返回 false 表示宿主处理完,仍执行后续的默认操作
   */
  public onBackPress(appId: string, apiServer: string, path?:string): boolean | void
}

示例代码

import { FinAppProxyHandlerManager, IFinProxyHandlerItem } from '@finclip/sdk'
import { promptAction } from '@kit.ArkUI'

class onAppBackPressHandler extends IFinProxyHandlerItem.onAppBackPressHandler {
  onBackPress(appId: string, apiServer: string) {
    // 仅对小程序生效
    // 返回 true 表示宿主自行处理
    // 返回 false 表示宿主处理完,仍执行后续的默认操作
    this.promptMsg(`触发了侧滑代理`)
    return false
  }

  private promptMsg(message: string) {
    promptAction.showToast({
      message
    })
  }
}


FinAppProxyHandlerManager.onAppBackPressHandler = new onAppBackPressHandler()

# 10. LogHandler

日志的代理类

// namespace IFinProxyHandlerItem
export class LogHandler {
  /**
   * 是否开启控制台输出,默认 false
   */
  public isConsoleLog: boolean = false;
  /**
   * 是否写入文件,默认 true
   */
  public writeFile: boolean = true;
  /**
   * 设置日志文件保留时间,单位为秒(默认10天,至少为1天)
   */
  public logFileAliveDuration: number = 60 * 60 * 24 * 10
  /**
   * 日志输出的完整的沙箱路径
   * 默认路径 /data/app/el2/100/base/包名/haps/entry/files/Applet/logs
   */
  public logDir?: string

  /**
   * 触发 log 时会执行该方法,如果想自行处理日志文件,可以将 writeFile 设置为 false,然后在该方法处理日志内容
   */
  public onLog(level: IFinAppConfig.ILogLevel, log: string)
}

示例代码

import { FinAppProxyHandlerManager, IFinAppConfig, IFinProxyHandlerItem } from '@finclip/sdk';

class LogHandler extends IFinProxyHandlerItem.LogHandler {
  isConsoleLog: boolean = true;
  writeFile: boolean = true;

  onLog(level: IFinAppConfig.ILogLevel, log: string): void {
    console.log(`level:${level},log:${log}`)
  }
}

FinAppProxyHandlerManager.logHandler = new LogHandler()

# 11. WatermarkHandler

水印代理类

// namespace IFinProxyHandlerItem
class WatermarkHandler {
  /**
   * 获取水印 builder
   * @returns 水印 builder
   */
  public getWatermarkLayout(): ((appId: string, apiServer: string) => void) | void {
  }
}

示例代码

import { FinAppProxyHandlerManager, IFinProxyHandlerItem } from '@finclip/sdk';

@Component
struct Watermark {
  @Prop appId: string
  @Prop apiServer: string
  watermarkWidth: number = 120;
  watermarkHeight: number = 120;
  watermarkText: string = this.getWatermarkText();
  rotationAngle: number = -30;
  fillColor: string | number | CanvasGradient | CanvasPattern = '#10000000';
  font: string = '16vp';
  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

  draw() {
    this.context.fillStyle = this.fillColor;
    this.context.font = this.font;
    const colCount = Math.ceil(this.context.width / this.watermarkWidth);
    const rowCount = Math.ceil(this.context.height / this.watermarkHeight);
    for (let col = 0; col <= colCount; col++) {
      let row = 0;
      for (; row <= rowCount; row++) {
        const angle = this.rotationAngle * Math.PI / 180;
        this.context.rotate(angle);
        const positionX = this.rotationAngle > 0 ? this.watermarkHeight * Math.tan(angle) : 0;
        const positionY = this.rotationAngle > 0 ? 0 : this.watermarkWidth * Math.tan(-angle);
        this.context.fillText(this.watermarkText, positionX, positionY);
        this.context.rotate(-angle);
        this.context.translate(0, this.watermarkHeight);
      }
      this.context.translate(0, -this.watermarkHeight * row);
      this.context.translate(this.watermarkWidth, 0);
    }
  }

  getWatermarkText() {
    return `finclip--${this.appId}`
  }

  build() {
    Canvas(this.context)
      .width('100%')
      .height('100%')
      .hitTestBehavior(HitTestMode.Transparent)
      .onReady(() => this.draw())
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM, SafeAreaEdge.TOP])
  }
}

@Builder
function watermarkBuilder(appId: string, apiServer: string) {
  Watermark({
    appId,
    apiServer
  })
}

class WatermarkHandler extends IFinProxyHandlerItem.WatermarkHandler {
  public getWatermarkLayout(): ((appId: string, apiServer: string) => void) | void {
    return watermarkBuilder
  }
}

FinAppProxyHandlerManager.watermarkHandler = new WatermarkHandler()

# 12. AppletTraceHandler

小程序可回溯的代理类

export class AppletTraceHandler {
  /**
   * 请求用户授权
   * @returns 返回授权结果
   */
  public async requestUserAuthorization(): Promise<boolean> {
    return true
  }
}

示例代码

import { FinAppProxyHandlerManager, IFinAppConfig, IFinProxyHandlerItem } from '@finclip/sdk';
import { promptAction } from '@kit.ArkUI';

class AppletTraceHandler extends IFinProxyHandlerItem.AppletTraceHandler {
  requestUserAuthorization(): Promise<boolean> {
    return new Promise((resolve, reject)=>{
      promptAction.showDialog({
        title: '是否开启录制',
        buttons: [
          {
            text: '确定',
            color: '#409EFF'
          },
          {
            text: '取消',
            color: '#000000'
          }
        ]
      }).then(data => {
        resolve(data.index !== 1)
      })
    })
  }
}

FinAppProxyHandlerManager.appletTraceHandler = new AppletTraceHandler()

# 13. AppletWebViewLoadHandler

web-view 组件加载 H5 链接代理

class AppletWebViewLoadHandler {
  /**
   * 宿主应用控制webview是否可加载H5链接
   * @param appId 小程序 id
   * @param apiServer 小程序 apiServer
   * @returns 返回 true 表示允许加载,返回 false 表示拒绝加载,仍执行后续的默认操作
   */
  public webviewCanLoadUrl(appId: string, apiServer: string, url: string): Promise<boolean>

   /**
   * 宿主应用处理 webview onPermissionRequest
   * @param appId 小程序 id
   * @param apiServer 小程序 apiServer
   * @param event 系统 onPermissionRequest 返回的参数
   * @returns
   */
  public onPermissionRequest(appId: string, apiServer: string, event: OnPermissionRequestEvent)
}

示例代码

import { FinAppProxyHandlerManager, IFinProxyHandlerItem } from '@finclip/sdk'
import { promptAction } from '@kit.ArkUI'

class AppletWebViewLoadHandler extends IFinProxyHandlerItem.AppletWebViewLoadHandler {
  public async webviewCanLoadUrl(appId: string, apiServer: string, url: string): Promise<boolean> {
    try {
      const res = await promptAction.showActionMenu({
        title: `load url:${url}`,
        buttons: [
          {
            text: '允许',
            color: '#409EFF'
          },
          {
            text: '拒绝',
            color: '#ff0000'
          },
        ]
      })

      if (res.index === 0) {
        return true
      } else {
        return false
      }
    } catch (e) {
    }
    return true
  }

  public onPermissionRequest(appId: string, apiServer: string, event: OnPermissionRequestEvent) {
    // 根据业务需求处理 webview 的权限事件
  }
}

FinAppProxyHandlerManager.appletWebViewLoadHandler = new AppletWebViewLoadHandler()

# 14. XComponentHandler

自定义组件代理

class XComponentHandler {
  /**
   * 获取 XComponent builder
   * @returns XComponent builder
   */
  public getXComponentLayout(): ((appId: string, apiServer: string, controller: IFinAppProxy.XComponentController) => void) | void
}

示例代码

import { FinAppProxyHandlerManager, IApiManager, IFinAppProxy, IFinProxyHandlerItem } from '@finclip/sdk'

@Component
struct XComponentProxy {
  @Prop appId: string
  @Prop apiServer: string
  controller: IFinAppProxy.XComponentController = new IFinAppProxy.XComponentController()
  @State tag: string = ''
  @State componentId: string = ''
  @State data: string = ''
  @State domId: string = ''
  @State xComponentWidth: number = 0
  @State xComponentHeight: number = 0
  @State count: number = 0
  scrollerForScroll: Scroller = new Scroller()

  aboutToAppear(): void {
    // 监听数据更新事件
    this.controller.on('valueUpdated', () => {
      this.onValueUpdated()
    })
    // 监听小程序通过 context 发送的事件,可以返回自定义数据给小程序
    this.controller.handleXComponentEvent = async (eventName: string, params: object) => {
      let res: IApiManager.IInvokeBaseResponse = { errMsg: '' }
      this.count++
      switch (eventName) {
        case 'appletEvent':
          res.data = `fromArkTs,data:${JSON.stringify(params)}`
          break
      }
      return res
    }
    this.onValueUpdated()
  }

  onValueUpdated() {
    this.tag = this.controller.getTag() // 小程序组件的 tag 属性
    this.componentId = this.controller.getId() // 原生组件的唯一 id
    this.data = JSON.stringify(this.controller.getData()) // 小程序组件的 data 属性
    this.xComponentWidth = this.controller.getWidth() // 原生组件容器的宽
    this.xComponentHeight = this.controller.getHeight() // 原生组件容器的高
    this.domId = this.controller.getDomId() // 小程序组件的 id 属性
  }

  build() {
    Scroll(this.scrollerForScroll) {
      Column() {
        Text('XComponent')
        Text(`appletEvent count:${this.count}`)
        Text(`id:${this.componentId}`)
        Text(`domId:${this.domId}`)
        Text(`tag:${this.tag}`)
        Text(`data:${this.data}`)
        Text(`xComponentWidth:${this.xComponentWidth}`)
        Text(`xComponentHeight:${this.xComponentHeight}`)
        Button('send onTestEvent').onClick(() => {
          // 可以通过 emitToApplet 发送事件给小程序,小程序端通过 context.on 来监听
          this.controller.emitToApplet('onTestEvent', { from: 'arkts' } as ESObject)
        })
      }
      .backgroundColor(Color.Red)
      .width(this.xComponentWidth)
      .height(this.xComponentHeight)
    }
    // 原生组件默认是内部消费手势事件,即默认在原生组件内部滑动是无法使页面滚动的,如果想在原生组件内滑动可以滚动页面,可以使用 Scroll 搭配 onScrollFrameBegin 方法实现
    .onScrollFrameBegin((offset: number, state: ScrollState) => {
      this.controller.onScrollFrameBegin(offset, state, this.scrollerForScroll)
      return { offsetRemain: 0 }
    })
    .width(this.xComponentWidth) // 按照同层渲染的规格,根组件的宽必须与小程序容器组件的宽一致,否则会出现渲染异常的问题
    .height(this.xComponentHeight) // 按照同层渲染的规格,根组件的高必须与小程序容器组件的高一致,否则会出现渲染异常的问题
  }
}

@Builder
function XComponentProxyBuilder(appId: string, apiServer: string, controller: IFinAppProxy.XComponentController) {
  XComponentProxy({
    appId,
    apiServer,
    controller
  })
}

class XComponentHandler extends IFinProxyHandlerItem.XComponentHandler {
  public getXComponentLayout(): void | ((appId: string, apiServer: string,
    controller: IFinAppProxy.XComponentController) => void) {
    return XComponentProxyBuilder
  }
}


FinAppProxyHandlerManager.xComponentHandler = new XComponentHandler()

# 15. AppletCustomJSInjectionHandler

加载外部 js 文件代理

class AppletCustomJSInjectionHandler {
  /**
   * 获取需要加载的 js 字符串
   * @param context
   * @param appId
   * @param apiServer
   * @returns js 字符串数组
   */
  public async getJSContentList(context: common.UIAbilityContext, appId: string,
    apiServer: string): Promise<string[]>
}

示例代码

class AppletCustomJSInjectionProxy extends IFinProxyHandlerItem.AppletCustomJSInjectionHandler {
  public async getJSContentList(context: common.UIAbilityContext, appId: string, apiServer: string): Promise<string[]> {
    return ['console.log("inject success")']
  }
}


FinAppProxyHandlerManager.appletCustomJSInjectionHandler = new AppletCustomJSInjectionProxy()

# 16. ACLPermissionHandler

涉及 ACL 权限的代理,如果证书没有 ACL 权限,可以使用该代理类覆盖执行对应的方法

class ACLPermissionHandler {
   /**
     * 保存图片或视频到相册的方法,从 1.3.3 开始,默认会报错,需要 app 自行实现或者集成 @finclip/album-sdk
     * @param type 需要保存的媒体类型
     * @param realPath 文件的完整路径
     * @param context
     * @returns 成功时不需要返回值,如果失败请将对应的错误信息 reject 返回,比如 reject('auth deny')
     */
    public saveMediaToPhotosAlbum(type: 'image' | 'video', realPath: string,context: common.UIAbilityContext): Promise<void> 
}

示例代码

import { FinAppProxyHandlerManager, IFinProxyHandlerItem } from '@finclip/sdk';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';

class ACLPermissionHandler extends IFinProxyHandlerItem.ACLPermissionHandler {
  // 这里使用基于弹窗授权的方式来完成保存逻辑,具体可根据业务逻辑实现
  public saveMediaToPhotosAlbum(type: 'image' | 'video', realPath: string,
    context: common.UIAbilityContext): Promise<void> {
    return new Promise((resolve, reject) => {
      const phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);

      // 指定待保存到媒体库的位于应用沙箱的图片uri。
      let srcFileUri = realPath
      let srcFileUris: Array<string> = [
        realPath
      ];
      // 指定待保存照片的创建选项,包括文件后缀和照片类型,标题和照片子类型可选。
      const photoCreationConfigs: Array<photoAccessHelper.PhotoCreationConfig> = [];
      if (type === 'image') {
        photoCreationConfigs.push({
          fileNameExtension: this.getFileExtension(srcFileUri),
          photoType: photoAccessHelper.PhotoType.IMAGE,
        })
      } else if (type === 'video') {
        photoCreationConfigs.push({
          fileNameExtension: this.getFileExtension(srcFileUri),
          photoType: photoAccessHelper.PhotoType.VIDEO,
        })
      }
      // 基于弹窗授权的方式获取媒体库的目标uri。
      phAccessHelper.showAssetsCreationDialog(srcFileUris, photoCreationConfigs).then((desFileUris) => {
        if (!desFileUris.length) {
          reject('用户取消')
        }
        try {
          // 将来源于应用沙箱的照片内容写入媒体库的目标uri。
          let desFile: fileIo.File = fileIo.openSync(desFileUris[0], fileIo.OpenMode.WRITE_ONLY);
          let srcFile: fileIo.File = fileIo.openSync(srcFileUri, fileIo.OpenMode.READ_ONLY);
          fileIo.copyFileSync(srcFile.fd, desFile.fd);
          fileIo.closeSync(srcFile);
          fileIo.closeSync(desFile);
          resolve()
        } catch (e) {
          reject(e.message)
        }
      }).catch((e: BusinessError) => {
        reject(e.message)
      })
    })
  }

  private getFileExtension(filePath: string): string {
    if (!filePath) {
      return ''
    }
    // 从文件路径中提取文件名
    const fileName = filePath.split('/').pop();
    // 从文件名中提取后缀
    const fileExtension = fileName!.split('.').pop();
    return fileExtension || '';
  }
}

FinAppProxyHandlerManager.ACLPermissionHandler = new ACLPermissionHandler()

# 17. ShareAppletHandler

1.3.6 版本开始小程序更多菜单中会新增“分享”按钮(需要将 uiConfig 里的 hideShareAppletMenu 设置为 false,在更多菜单里才会显示分享按钮) ShareSDK 并进行相应的简单配置,实现分享小程序的功能,也可以自行实现 ShareAppletHandler 接口,实现对应功能。

/**
 * 分享代理
 */
export class ShareAppletHandler {
  /**
   * 点击更多面板分享按钮触发
   * @param context UIAbilityContext
   * @param appInfo 小程序信息
   * @param appletPagePath 小程序当前完整页面路径 pages/index/index?a=b
   * @returns
   */
  public onShareApplet(context: common.UIAbilityContext, appInfo: IFinApplet.IAppletInfo, appletPagePath: string)

  /**
   * 获取分享界面 builder,如果没有可以不实现
   * @returns
   */
  public getShareLayout(): ((appId: string, apiServer: string) => void) | void
}

示例代码

import { FinAppProxyHandlerManager, IFinApplet, IFinProxyHandlerItem } from '@finclip/sdk';
import { common } from '@kit.AbilityKit';


class ShareController {
  static instanceMap: Map<string, ShareController> = new Map()
  appId: string
  apiServer: string
  uiContext?: UIContext
  appInfo?: IFinApplet.IAppletInfo
  private context: common.UIAbilityContext

  constructor(appId: string, apiServer: string, context: common.UIAbilityContext) {
    this.appId = appId
    this.apiServer = apiServer
    this.context = context
  }

  static getInstance(appId: string, apiServer: string, context: common.UIAbilityContext) {
    const key = `${appId}__${apiServer}`
    let instance = ShareController.instanceMap.get(key)
    if (!instance) {
      instance = new ShareController(appId, apiServer, context)
      ShareController.instanceMap.set(key, instance)
    }
    return instance
  }

  onShareApplet: (appInfo: IFinApplet.IAppletInfo, appletPagePath: string) => void = () => {
  }
}

@Component
struct ShareToast {
  @Prop appId: string
  @Prop apiServer: string
  @State showShareMenu: boolean = false
  @State appletPagePath: string = ''
  uiContext = this.getUIContext()
  context = this.uiContext.getHostContext() as common.UIAbilityContext
  shareController: ShareController = ShareController.getInstance(this.appId, this.apiServer, this.context)

  aboutToAppear(): void {
    this.shareController.onShareApplet = (appInfo, path) => {
      this.onToggleShareMenus(appInfo, path)
    }
  }

  onToggleShareMenus(appInfo: IFinApplet.IAppletInfo, appletPagePath: string) {
    this.shareController.appInfo = appInfo
    this.appletPagePath = appletPagePath
    this.toggleShareMenus()
  }

  toggleShareMenus() {
    this.showShareMenu = !this.showShareMenu
  }

  @Builder
  previewBuilder() {
    Column() {
      Text(`appId:${this.appId}`)
      Text(`apiServer:${this.apiServer}`)
      Text(`name:${this.shareController.appInfo?.name}`)
      Text(`appletPagePath:${this.appletPagePath}`)
    }
    .backgroundColor(Color.White)
  }

  @Builder
  share() {
    Stack() {
      Row() {
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#6a000000')
      .onClick(() => {
        this.toggleShareMenus()
      })
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])

      Column({}) {
        this.previewBuilder()
      }
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
      .transition(
        TransitionEffect.move(TransitionEdge.BOTTOM).animation({
          duration: 150
        })
      )
      .zIndex(999)
      .width('100%')
      .border({
        radius: {
          topLeft: 12,
          topRight: 12
        }
      })
      .clip(true)
    }
    .width('100%')
  }

  build() {
    if (this.showShareMenu) {
      this.share()
    }
  }
}

@Builder
function shareToastBuilder(appId: string, apiServer: string) {
  ShareToast({
    appId,
    apiServer
  })
}

class ShareAppletHandler extends IFinProxyHandlerItem.ShareAppletHandler {
  public onShareApplet(context: common.UIAbilityContext, appInfo: IFinApplet.IAppletInfo,
    appletPagePath: string): void {
    const instance = ShareController.getInstance(appInfo.appId, appInfo.apiServer, context)
    instance.onShareApplet(appInfo, appletPagePath)
  }

  public getShareLayout(): void | ((appId: string, apiServer: string) => void) {
    return shareToastBuilder
  }
}

FinAppProxyHandlerManager.shareAppletHandler = new ShareAppletHandler()

# 18.AppletInteractionHandler

/**
 * 长按代理
 */
export class AppletInteractionHandler {
  /**
   * previewMedia、previewImage、image 组件等媒体长按时会触发
   * @param appId 小程序 id
   * @param apiServer 小程序 apiServer
   * @param uiContext context
   * @param mediaInfo 触发的媒体数据,包含媒体类型,媒体地址
   * @returns boolean 如果实现了代理 则返回 true,表示由外部处理长按事件,如果返回 false 表示需要 sdk 内部继续处理长按事件
   */
  public async mediaLongPressed(appId: string, apiServer: string, uiContext: UIContext, mediaInfo: IFinAppProxy.FinMediaInfo): Promise<boolean>

  /**
   * text 组件长按时会触发
   * @param appId 小程序 id
   * @param apiServer 小程序 apiServer
   * @param uiContext context
   * @param text 选中的文字
   * @returns boolean 如果实现了代理 则返回 true,表示由外部处理长按事件,如果返回 false 表示需要 sdk 内部继续处理长按事件
   */
  public async textLongPressed(appId: string, apiServer: string, uiContext: UIContext, text: string): Promise<boolean>
}

示例代码

import { FinAppProxyHandlerManager, IFinAppProxy, IFinProxyHandlerItem } from '@finclip/sdk';

class AppletInteractionHandler extends IFinProxyHandlerItem.AppletInteractionHandler {
  public async mediaLongPressed(appId: string, apiServer: string,
    uiContext: UIContext, mediaInfo: IFinAppProxy.FinMediaInfo): Promise<boolean> {
    uiContext.getPromptAction().showToast({
      message: `mediaLongPressed:${JSON.stringify(mediaInfo)}`
    })
    return true
  }

  public async textLongPressed(appId: string, apiServer: string, uiContext: UIContext,
    text: string): Promise<boolean> {
    uiContext.getPromptAction().showToast({
      message: `textLongPressed:${text}`
    })
    return true
  }
}


FinAppProxyHandlerManager.appletInteractionHandler = new AppletInteractionHandler()

# 19. AppletNetWorkRequestHandler

网络代理的实现类,用于接管小程序的 requestuploadFiledownloadFile 三类网络能力。

# 19.1 代理管理方式

网络代理通过 FinAppProxyHandlerManager.appletNetWorkRequestHandler 进行统一管理。

import { FinAppProxyHandlerManager, IFinAppProxy, IFinProxyHandlerItem } from '@finclip/sdk'
import { http } from '@kit.NetworkKit'
import { common } from '@kit.AbilityKit'

class AppletNetWorkRequestProxy extends IFinProxyHandlerItem.AppletNetWorkRequestHandler {
  /**
   * 接管普通请求。
   * @param appId 小程序的 appId。
   * @param apiServer 小程序对应的服务端地址。
   * @param context UIAbility 的上下文对象。
   * @param params 普通请求的归一化参数。
   * @param controller 请求任务的控制器,用于回传 headers、数据和最终结果。
   * @returns Promise<boolean> 返回 true 表示由宿主接管后续请求生命周期。
   */
  public async request(
    appId: string,
    apiServer: string,
    context: common.UIAbilityContext,
    params: IFinAppProxy.IAppletRequestRequest,
    controller: IFinAppProxy.IAppletRequestTaskController
  ): Promise<boolean>

  /**
   * 接管文件上传。
   * @param appId 小程序的 appId。
   * @param apiServer 小程序对应的服务端地址。
   * @param context UIAbility 的上下文对象。
   * @param params 文件上传的归一化参数。
   * @param controller 上传任务的控制器,用于回传进度、headers 和最终结果。
   * @returns Promise<boolean> 返回 true 表示由宿主接管后续上传生命周期。
   */
  public async uploadFile(
    appId: string,
    apiServer: string,
    context: common.UIAbilityContext,
    params: IFinAppProxy.IAppletUploadFileRequest,
    controller: IFinAppProxy.IAppletUploadTaskController
  ): Promise<boolean>

  /**
   * 接管文件下载。
   * @param appId 小程序的 appId。
   * @param apiServer 小程序对应的服务端地址。
   * @param context UIAbility 的上下文对象。
   * @param params 文件下载的归一化参数。
   * @param controller 下载任务的控制器,用于回传 headers 和最终结果。
   * @returns Promise<boolean> 返回 true 表示由宿主接管后续下载生命周期。
   */
  public async downloadFile(
    appId: string,
    apiServer: string,
    context: common.UIAbilityContext,
    params: IFinAppProxy.IAppletDownloadFileRequest,
    controller: IFinAppProxy.IAppletDownloadTaskController
  ): Promise<boolean>
}

# 19.2 Controller 约定

网络代理方法会收到一个 controller,宿主通过它把结果回传给 SDK。controller 的方法含义如下:

方法 作用
registerAbort(handler) 注册中止回调,当 SDK 触发 taskAborttimeout 或销毁时会执行
emitHeaders(statusCode?, header, cookies) 回传响应头,供 SDK 分发 request.onTaskHeadersReceived 事件。chunked 场景下可以先传 undefined,等状态码就绪后再通过 success 收口
emitProgress(processedBytes, totalBytes) 回传上传或下载进度,供 SDK 分发 request.onTaskProgressUpdate 事件
emitDataReceived(data) 回传 request 的流式数据分片,供 SDK 分发 request.onTaskDataReceived 事件
success(payload) 回传成功结果,SDK 会继续完成小程序侧最终回调
fail(errMsg) 回传失败结果,SDK 会按失败态结束当前网络任务

emitHeaders 的调用时机与参考实现一致:

  • chunked 场景下,先拿到完整响应,再调用 emitHeaders(responseCode, header, cookies),然后调用 success(...)
  • chunked 场景下,先在 headersReceive 回调里调用 emitHeaders(undefined, header, cookies),然后在请求结束后通过 success(...) 完成最终收口

request 对应的 payload 字段说明如下:

字段 类型 描述
statusCode Number 响应状态码
header Record<string, string> 响应头
cookies String[] 从响应头解析出的 cookie 列表
data String | Object | ArrayBuffer 响应体内容

uploadFile 对应的 payload 字段说明如下:

字段 类型 描述
statusCode Number 响应状态码
header Record<string, string> 响应头
cookies String[] 从响应头解析出的 cookie 列表
data String | ArrayBuffer 响应体内容
contentType String 响应体内容类型

downloadFile 对应的 payload 字段说明如下:

字段 类型 描述
statusCode Number 响应状态码
header Record<string, string> 响应头
cookies String[] 从响应头解析出的 cookie 列表

# 19.3 完整版

requestuploadFiledownloadFile 的完整版示例统一放在同一段中,直接参考宿主里的完整实现。这个版本包含 task 生命周期、multipart boundary 拼装、下载落盘、header / cookies 归一化,以及异常收口。

class AppletNetWorkRequestProxy extends IFinProxyHandlerItem.AppletNetWorkRequestHandler {
  /**
   * request 代理,返回 true 表示由宿主接管后续任务生命周期
   */
  public async request(appId: string, apiServer: string, context: common.UIAbilityContext,
    params: IFinAppProxy.IAppletRequestRequest,
    controller: IFinAppProxy.IAppletRequestTaskController): Promise<boolean> {
    // 先基于 SDK 归一化后的参数准备底层 httpRequest 配置。
    const httpRequest = http.createHttp()
    const timeout = params.timeout > 0 ? params.timeout : 60000
    const method = (params.method || 'GET').toUpperCase() as http.RequestMethod
    const requestHeader = this.cloneRequestHeader(params.header)
    const expectDataType =
      params.responseType === 'arraybuffer' ? http.HttpDataType.ARRAY_BUFFER : http.HttpDataType.STRING
    let requestData = params.data
    if (method === http.RequestMethod.GET) {
      requestData = undefined
    }
    let isFinished: boolean = false
    let isAborted: boolean = false
    let isHeaderEmitted: boolean = false
    let responseCode: number = 200
    const responseHeader: Record<string, string> = {}
    const responseCookies: string[] = []

    const destroyHttpRequest = () => {
      try {
        httpRequest.destroy()
      } catch (e) {
      }
    }

    const emitHeadersIfNeeded = () => {
      if (isHeaderEmitted) {
        return
      }
      // 非 chunked 场景会在成功态前补发 headers,保证宿主不显式派发时协议仍完整。
      controller.emitHeaders(responseCode, responseHeader, responseCookies)
      isHeaderEmitted = true
    }

    const emitHeadersImmediately = () => {
      if (isHeaderEmitted) {
        return
      }
      // chunked 场景需要在收到响应头时尽快通知 JS 侧,保持和 SDK 默认实现一致。
      controller.emitHeaders(undefined, responseHeader, responseCookies)
      isHeaderEmitted = true
    }

    controller.registerAbort(() => {
      if (isFinished || isAborted) {
        return
      }
      // request task 被 SDK 中止时,只负责终止底层请求,终态由 SDK 统一收口。
      isAborted = true
      destroyHttpRequest()
    })

    try {
      if (params.enableChunked) {
        // chunked 模式下,headers 和 data 都按流式事件持续回推给 SDK controller。
        httpRequest.on("headersReceive", (rawHeaders: Object) => {
          this.appendResponseHeaders(rawHeaders as Record<string, string | string[]>, responseHeader, responseCookies)
          emitHeadersImmediately()
        });
        httpRequest.on('dataReceive', (data: ArrayBuffer) => {
          if (!isFinished && !isAborted) {
            controller.emitDataReceived(data)
          }
        })

        responseCode = await httpRequest.requestInStream(params.url, {
          method,
          header: requestHeader,
          extraData: requestData,
          expectDataType: http.HttpDataType.ARRAY_BUFFER,
          readTimeout: timeout,
          connectTimeout: timeout,
        })
        if (!isAborted) {
          // chunked 结束后只补终态,不重复拼接 body,body 分片已经在 dataReceive 中逐步上报。
          isFinished = true
          emitHeadersIfNeeded()
          controller.success({
            statusCode: responseCode,
            header: responseHeader,
            cookies: responseCookies,
            data: '',
          })
        }
      } else {
        const response = await httpRequest.request(params.url, {
          method,
          header: requestHeader,
          extraData: requestData,
          expectDataType,
          readTimeout: timeout,
          connectTimeout: timeout,
        })

        if (!isAborted) {
          // 非 chunked 模式拿到完整响应后,一次性回推 headers 和 success。
          isFinished = true
          this.appendResponseHeaders(response.header as Record<string, string | string[]>, responseHeader,
            responseCookies)
          this.appendResponseCookie(response.cookies, responseCookies)
          emitHeadersIfNeeded()
          controller.success({
            statusCode: response.responseCode as number,
            header: responseHeader,
            cookies: responseCookies,
            data: response.result,
          })
        }
      }
    } catch (e) {
      if (!isAborted) {
        isFinished = true
        controller.fail(e.message)
      }
    } finally {
      destroyHttpRequest()
    }
    return true
  }

  /**
   * uploadFile 代理,返回 true 表示由宿主接管后续任务生命周期
   */
  public async uploadFile(appId: string, apiServer: string, context: common.UIAbilityContext,
    params: IFinAppProxy.IAppletUploadFileRequest,
    controller: IFinAppProxy.IAppletUploadTaskController): Promise<boolean> {
    // 上传默认实现完全基于传入参数构造 multipart 请求,不再依赖旧的布尔短路逻辑。
    const httpRequest = http.createHttp()
    const timeout = params.timeout > 0 ? params.timeout : 60000
    const boundary = `----FinClipBoundary${Date.now()}`
    const fileName = params.filePath.split('/').pop() || 'file'
    const contentType = this.getUploadContentType(fileName)
    // 宿主默认上传实现仍按 multipart/form-data 手工拼 body,保证与小程序 uploadFile 语义一致。
    const bodyContent =
      this.buildUploadBody(boundary, params.name, fileName, this.readUploadFileBuffer(params.filePath),
        contentType, params.formData)
    const requestHeader = this.cloneRequestHeader(params.header)
    // Content-Type 和 Content-Length 由 SDK 统一覆盖,避免宿主传入不匹配的 header。
    this.setRequestHeader(requestHeader, 'Content-Type', `multipart/form-data; boundary=${boundary}`)
    this.setRequestHeader(requestHeader, 'Content-Length', `${bodyContent.byteLength}`)
    let isFinished: boolean = false
    let isAborted: boolean = false

    const destroyHttpRequest = () => {
      try {
        httpRequest.destroy()
      } catch (e) {
      }
    }

    controller.registerAbort(() => {
      if (isFinished || isAborted) {
        return
      }
      // 上传 task 被 SDK 中止时,这里只终止底层请求,终态仍由 SDK 统一控制。
      isAborted = true
      destroyHttpRequest()
    })

    try {
      // 上传过程中的进度由底层 httpRequest 直接回推给 SDK controller。
      httpRequest.on('dataSendProgress', (progress: http.DataSendProgressInfo) => {
        if (!isFinished && !isAborted) {
          controller.emitProgress(progress.sendSize, progress.totalSize)
        }
      })

      const response = await httpRequest.request(params.url, {
        method: http.RequestMethod.POST,
        header: requestHeader,
        expectDataType: http.HttpDataType.ARRAY_BUFFER,
        readTimeout: timeout,
        connectTimeout: timeout,
        extraData: bodyContent
      })

      if (isAborted) {
        return true
      }

      // 上传成功后统一归一化响应头、cookie 和 body,再交给 SDK 最终收口。
      isFinished = true
      const responseHeader: Record<string, string> = {}
      const responseCookies: string[] = []
      this.appendResponseHeaders(response.header as Record<string, string | string[]>, responseHeader,
        responseCookies)
      this.appendResponseCookie(response.cookies, responseCookies)

      controller.emitHeaders(response.responseCode as number, responseHeader, responseCookies)
      controller.success({
        statusCode: response.responseCode as number,
        header: responseHeader,
        cookies: responseCookies,
        data: response.result instanceof ArrayBuffer ? response.result :
          (typeof response.result === 'string' ? response.result : JSON.stringify(response.result)),
        contentType: this.getHeaderValue(responseHeader, 'content-type')
      })
    } catch (e) {
      if (!isAborted) {
        isFinished = true
        controller.fail(e.message)
      }
    } finally {
      destroyHttpRequest()
    }

    return true
  }

  /**
   * downloadFile 代理,返回 true 表示由宿主接管后续任务生命周期
   */
  public async downloadFile(appId: string, apiServer: string, context: common.UIAbilityContext,
    params: IFinAppProxy.IAppletDownloadFileRequest,
    controller: IFinAppProxy.IAppletDownloadTaskController): Promise<boolean> {
    // 下载默认实现只负责真实拉流和落盘,最终 filePath/tempFilePath 仍由 SDK 侧决定。
    const httpRequest = http.createHttp()
    const targetDir = params.targetAbsPath.split('/').slice(0, -1).join('/')
    const timeout = params.timeout > 0 ? params.timeout : 60000
    const requestHeader = this.cloneRequestHeader(params.header)
    let targetFile: fs.File | undefined = undefined
    let responseCode: number = 200
    const responseHeader: Record<string, string> = {}
    const responseCookies: string[] = []
    let isHeaderEmitted: boolean = false
    let isResponseCodeReady: boolean = false
    let isFinished: boolean = false
    let isAborted: boolean = false

    const closeTargetFile = () => {
      if (!targetFile) {
        return
      }
      try {
        fs.closeSync(targetFile.fd)
      } catch (e) {
      }
      targetFile = undefined
    }

    const destroyHttpRequest = () => {
      try {
        httpRequest.destroy()
      } catch (e) {
      }
    }

    const tryEmitHeaders = () => {
      if (isHeaderEmitted || !isResponseCodeReady) {
        return
      }
      // 只有在状态码可用时再派发 headers,避免把错误状态伪装成 200。
      controller.emitHeaders(responseCode, responseHeader, responseCookies)
      isHeaderEmitted = true
    }

    const finishFail = (errMsg: string) => {
      if (isFinished || isAborted) {
        return
      }
      // 下载失败时需要同时清理文件句柄、底层请求和半成品文件。
      isFinished = true
      closeTargetFile()
      destroyHttpRequest()
      this.clearDownloadTargetFile(params.targetAbsPath)
      controller.fail(errMsg)
    }

    const finishSuccess = () => {
      if (isFinished || isAborted || !isResponseCodeReady) {
        return
      }
      // 只有状态码就绪后才允许成功收口,避免 headers/success 拿到不完整状态。
      isFinished = true
      closeTargetFile()
      tryEmitHeaders()
      destroyHttpRequest()
      controller.success({
        statusCode: responseCode,
        header: responseHeader,
        cookies: responseCookies
      })
    }

    controller.registerAbort(() => {
      if (isFinished || isAborted) {
        return
      }
      // taskAbort / timeout / destroy 时只清理底层资源,终态由 SDK task 状态机统一收口。
      isAborted = true
      closeTargetFile()
      destroyHttpRequest()
      this.clearDownloadTargetFile(params.targetAbsPath)
    })

    try {
      this.ensureDirSync(targetDir)
      this.clearDownloadTargetFile(params.targetAbsPath)
      // 目标文件由宿主代理直接写入,SDK 只负责准备目录和清理半成品。
      targetFile = fs.openSync(params.targetAbsPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)

      // 下载流的 headers、进度、分片写盘分别独立处理,避免单个回调承担太多职责。
      httpRequest.on("headersReceive", (rawHeaders: Object) => {
        this.appendResponseHeaders(rawHeaders as Record<string, string | string[]>, responseHeader, responseCookies)
        tryEmitHeaders()
      });
      httpRequest.on('dataReceiveProgress', (progress: http.DataReceiveProgressInfo) => {
        if (!isFinished && !isAborted) {
          controller.emitProgress(progress.receiveSize, progress.totalSize)
        }
      })
      httpRequest.on('dataReceive', (data: ArrayBuffer) => {
        if (isFinished || isAborted || !targetFile) {
          return
        }
        try {
          fs.writeSync(targetFile.fd, data)
        } catch (e) {
          finishFail(e.message)
        }
      })

      responseCode = await httpRequest.requestInStream(params.url, {
        expectDataType: http.HttpDataType.ARRAY_BUFFER,
        method: http.RequestMethod.GET,
        header: requestHeader,
        readTimeout: timeout,
        connectTimeout: timeout,
      })
      isResponseCodeReady = true
      finishSuccess()
    } catch (e) {
      if (!isAborted) {
        finishFail(e.message)
      }
    }
    return true
  }

  private encodeString(value: string): ArrayBuffer {
    return buffer.from(value, 'utf-8').buffer
  }

  private mergeArrayBuffers(buffers: Array<ArrayBuffer>): ArrayBuffer {
    let totalLength = 0
    buffers.forEach((currentBuffer: ArrayBuffer) => {
      totalLength += currentBuffer.byteLength
    })
    const mergedBuffer = new ArrayBuffer(totalLength)
    const mergedView = new Uint8Array(mergedBuffer)
    let offset = 0
    buffers.forEach((currentBuffer: ArrayBuffer) => {
      const currentView = new Uint8Array(currentBuffer)
      mergedView.set(currentView, offset)
      offset += currentView.byteLength
    })
    return mergedBuffer
  }

  private getUploadContentType(fileName: string): string {
    const extension = fileName.split('.').pop()?.toLowerCase()
    switch (extension) {
      case 'txt':
        return 'text/plain'
      case 'jpg':
      case 'jpeg':
        return 'image/jpeg'
      case 'png':
        return 'image/png'
      default:
        return 'application/octet-stream'
    }
  }

  private cloneRequestHeader(header: Record<string, string>): Record<string, string> {
    const requestHeader: Record<string, string> = {}
    Object.keys(header).forEach((key: string) => {
      requestHeader[key] = header[key]
    })
    return requestHeader
  }

  private setRequestHeader(header: Record<string, string>, key: string, value: string) {
    let matchedKey: string | undefined = undefined
    Object.keys(header).forEach((headerKey: string) => {
      if (headerKey.toLowerCase() === key.toLowerCase()) {
        matchedKey = headerKey
      }
    })
    if (matchedKey) {
      header[matchedKey] = value
    } else {
      header[key] = value
    }
  }

  private getHeaderValue(header: Record<string, string>, key: string): string | undefined {
    let targetValue: string | undefined = undefined
    Object.keys(header).forEach((headerKey: string) => {
      if (headerKey.toLowerCase() === key.toLowerCase()) {
        targetValue = header[headerKey]
      }
    })
    return targetValue
  }

  private appendResponseCookie(cookie: string | undefined, responseCookies: string[]) {
    if (cookie) {
      responseCookies.push(cookie)
    }
  }

  private appendResponseHeaderValue(key: string, value: string | string[] | undefined,
    responseHeader: Record<string, string>, responseCookies: string[]) {
    if (value === undefined) {
      return
    }
    if (key.toLowerCase() === 'set-cookie') {
      if (Array.isArray(value)) {
        value.forEach((cookie: string) => {
          this.appendResponseCookie(cookie, responseCookies)
        })
        responseHeader[key] = value.join(',')
      } else {
        this.appendResponseCookie(value, responseCookies)
        responseHeader[key] = value
      }
      return
    }
    responseHeader[key] = Array.isArray(value) ? value.join(',') : value
  }

  private appendResponseHeaders(rawHeaders: Record<string, string | string[]>, responseHeader: Record<string, string>,
    responseCookies: string[]) {
    Object.keys(rawHeaders).forEach((key: string) => {
      this.appendResponseHeaderValue(key, rawHeaders[key], responseHeader, responseCookies)
    })
  }

  private readUploadFileBuffer(filePath: string): ArrayBuffer {
    const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY)
    try {
      const stat = fs.statSync(filePath)
      const fileBuffer = new ArrayBuffer(stat.size)
      fs.readSync(file.fd, fileBuffer)
      return fileBuffer
    } finally {
      try {
        fs.closeSync(file.fd)
      } catch (e) {
      }
    }
  }

  private buildUploadBody(boundary: string, fileFieldName: string, fileName: string, fileContent: ArrayBuffer,
    contentType: string, formData: Record<string, string>): ArrayBuffer {
    const buffers: Array<ArrayBuffer> = []
    // multipart body 顺序保持为:文件 part -> 额外 formData part -> 结束 boundary。
    const fileHeader =
      `--${boundary}\r\n` +
        `Content-Disposition: form-data; name="${fileFieldName}"; filename="${fileName}"\r\n` +
        `Content-Type: ${contentType}\r\n\r\n`
    buffers.push(this.encodeString(fileHeader))
    buffers.push(fileContent)
    buffers.push(this.encodeString('\r\n'))

    // 额外表单字段复用同一条 boundary 规则追加到文件 part 后面。
    Object.keys(formData).forEach((key: string) => {
      const value = formData[key]
      const formHeader =
        `--${boundary}\r\n` +
          `Content-Disposition: form-data; name="${key}"\r\n\r\n` +
          `${value}\r\n`
      buffers.push(this.encodeString(formHeader))
    })

    buffers.push(this.encodeString(`--${boundary}--\r\n`))
    return this.mergeArrayBuffers(buffers)
  }

  private clearDownloadTargetFile(targetAbsPath: string) {
    try {
      if (fs.accessSync(targetAbsPath)) {
        fs.unlinkSync(targetAbsPath)
      }
    } catch (e) {
    }
  }

  private ensureDirSync(dirPath: string) {
    const isExist = fs.accessSync(dirPath)
    if (!isExist) {
      fs.mkdirSync(dirPath, true)
    }
  }
}

export function initAppletNetWorkRequestHandler() {
  FinAppProxyHandlerManager.appletNetWorkRequestHandler = new AppletNetWorkRequestProxy()
}

# 19.4 简化版

如果只需要基础请求、上传、下载落盘,可以参考这个更轻量的实现。它保留了 headercookies 的处理,但不维护 task、进度、分片等额外逻辑。

import { FinAppProxyHandlerManager, IFinAppProxy, IFinProxyHandlerItem } from '@finclip/sdk'
import { http } from '@kit.NetworkKit'
import { common } from '@kit.AbilityKit'
import { fileIo as fs } from '@kit.CoreFileKit'
import { buffer } from '@kit.ArkTS'

type THttpHeaders = Record<string, string | string[]>

class AppletNetWorkRequestProxy extends IFinProxyHandlerItem.AppletNetWorkRequestHandler {
  public async request(appId: string, apiServer: string, context: common.UIAbilityContext,
    params: IFinAppProxy.IAppletRequestRequest,
    controller: IFinAppProxy.IAppletRequestTaskController): Promise<boolean> {
    const httpRequest = http.createHttp()
    const timeout = params.timeout > 0 ? params.timeout : 60000
    const method = (params.method || 'GET').toUpperCase() as http.RequestMethod

    try {
      const response = await httpRequest.request(params.url, {
        method,
        header: this.cloneRequestHeader(params.header),
        extraData: method === http.RequestMethod.GET ? undefined : params.data,
        expectDataType: params.responseType === 'arraybuffer'
          ? http.HttpDataType.ARRAY_BUFFER
          : http.HttpDataType.STRING,
        readTimeout: timeout,
        connectTimeout: timeout,
      })

      controller.success({
        statusCode: response.responseCode as number,
        header: this.normalizeResponseHeader(response.header as THttpHeaders),
        cookies: this.normalizeResponseCookies(response.cookies),
        data: response.result
      })
    } catch (e) {
      controller.fail(e.message)
    } finally {
      try {
        httpRequest.destroy()
      } catch (e) {
      }
    }

    return true
  }

  public async uploadFile(appId: string, apiServer: string, context: common.UIAbilityContext,
    params: IFinAppProxy.IAppletUploadFileRequest,
    controller: IFinAppProxy.IAppletUploadTaskController): Promise<boolean> {
    const httpRequest = http.createHttp()
    const timeout = params.timeout > 0 ? params.timeout : 60000
    const boundary = `----FinClipBoundary${Date.now()}`
    const fileName = params.filePath.split('/').pop() || 'file'
    const bodyContent = this.buildMultipartBody(boundary, params.name, fileName,
      this.readFileBuffer(params.filePath), params.formData)
    const requestHeader = this.cloneRequestHeader(params.header)
    requestHeader['Content-Type'] = `multipart/form-data; boundary=${boundary}`
    requestHeader['Content-Length'] = `${bodyContent.byteLength}`

    try {
      const response = await httpRequest.request(params.url, {
        method: http.RequestMethod.POST,
        header: requestHeader,
        extraData: bodyContent,
        expectDataType: http.HttpDataType.ARRAY_BUFFER,
        readTimeout: timeout,
        connectTimeout: timeout,
      })

      controller.success({
        statusCode: response.responseCode as number,
        header: this.normalizeResponseHeader(response.header as THttpHeaders),
        cookies: this.normalizeResponseCookies(response.cookies),
        data: response.result
      })
    } catch (e) {
      controller.fail(e.message)
    } finally {
      try {
        httpRequest.destroy()
      } catch (e) {
      }
    }

    return true
  }

  public async downloadFile(appId: string, apiServer: string, context: common.UIAbilityContext,
    params: IFinAppProxy.IAppletDownloadFileRequest,
    controller: IFinAppProxy.IAppletDownloadTaskController): Promise<boolean> {
    const httpRequest = http.createHttp()
    const timeout = params.timeout > 0 ? params.timeout : 60000
    const targetDir = params.targetAbsPath.split('/').slice(0, -1).join('/')

    try {
      const response = await httpRequest.request(params.url, {
        method: http.RequestMethod.GET,
        header: this.cloneRequestHeader(params.header),
        expectDataType: http.HttpDataType.ARRAY_BUFFER,
        readTimeout: timeout,
        connectTimeout: timeout,
      })

      this.ensureDirSync(targetDir)
      this.writeResponseToFile(params.targetAbsPath, response.result)
      controller.success({
        statusCode: response.responseCode as number,
        header: this.normalizeResponseHeader(response.header as THttpHeaders),
        cookies: this.normalizeResponseCookies(response.cookies)
      })
    } catch (e) {
      controller.fail(e.message)
    } finally {
      try {
        httpRequest.destroy()
      } catch (e) {
      }
    }

    return true
  }

  private cloneRequestHeader(header: Record<string, string>): Record<string, string> {
    const requestHeader: Record<string, string> = {}
    Object.keys(header).forEach((key: string) => {
      requestHeader[key] = header[key]
    })
    return requestHeader
  }

  private normalizeResponseHeader(header: THttpHeaders | undefined): Record<string, string> {
    const responseHeader: Record<string, string> = {}
    if (!header) {
      return responseHeader
    }
    Object.keys(header).forEach((key: string) => {
      const value = header[key]
      responseHeader[key] = Array.isArray(value) ? value.join(',') : value
    })
    return responseHeader
  }

  private normalizeResponseCookies(cookies?: string | string[]): string[] {
    if (!cookies) {
      return []
    }
    if (Array.isArray(cookies)) {
      return cookies
    }
    return [cookies]
  }

  private buildMultipartBody(boundary: string, fileFieldName: string, fileName: string, fileContent: ArrayBuffer,
    formData: Record<string, string>): ArrayBuffer {
    const buffers: Array<ArrayBuffer> = []
    buffers.push(this.encodeString(
      `--${boundary}\r\nContent-Disposition: form-data; name="${fileFieldName}"; filename="${fileName}"\r\n` +
      `Content-Type: application/octet-stream\r\n\r\n`
    ))
    buffers.push(fileContent)
    buffers.push(this.encodeString('\r\n'))

    Object.keys(formData).forEach((key: string) => {
      buffers.push(this.encodeString(
        `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${formData[key]}\r\n`
      ))
    })

    buffers.push(this.encodeString(`--${boundary}--\r\n`))
    return this.mergeArrayBuffers(buffers)
  }

  private writeResponseToFile(filePath: string, data: ArrayBuffer | string | Object) {
    const file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.TRUNC)
    try {
      const bufferData = data instanceof ArrayBuffer
        ? data
        : typeof data === 'string'
          ? this.encodeString(data)
          : this.encodeString(JSON.stringify(data))
      fs.writeSync(file.fd, bufferData)
      fs.fsyncSync(file.fd)
    } finally {
      try {
        fs.closeSync(file.fd)
      } catch (e) {
      }
    }
  }

  private readFileBuffer(filePath: string): ArrayBuffer {
    const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY)
    try {
      const stat = fs.statSync(filePath)
      const fileBuffer = new ArrayBuffer(stat.size)
      fs.readSync(file.fd, fileBuffer)
      return fileBuffer
    } finally {
      try {
        fs.closeSync(file.fd)
      } catch (e) {
      }
    }
  }

  private ensureDirSync(dirPath: string) {
    const isExist = fs.accessSync(dirPath)
    if (!isExist) {
      fs.mkdirSync(dirPath, true)
    }
  }

  private encodeString(value: string): ArrayBuffer {
    return buffer.from(value, 'utf-8').buffer
  }

  private mergeArrayBuffers(buffers: Array<ArrayBuffer>): ArrayBuffer {
    let totalLength = 0
    buffers.forEach((currentBuffer: ArrayBuffer) => {
      totalLength += currentBuffer.byteLength
    })
    const mergedBuffer = new ArrayBuffer(totalLength)
    const mergedView = new Uint8Array(mergedBuffer)
    let offset = 0
    buffers.forEach((currentBuffer: ArrayBuffer) => {
      mergedView.set(new Uint8Array(currentBuffer), offset)
      offset += currentBuffer.byteLength
    })
    return mergedBuffer
  }
}

FinAppProxyHandlerManager.appletNetWorkRequestHandler = new AppletNetWorkRequestProxy()

# 20. AuthRequestHandler

小程序权限请求代理类,用于在 SDK 展示权限弹窗前由宿主应用前置处理本次权限申请,并在 SDK 权限流程结束后接收最终授权结果。

# 20.1 代理管理方式

权限请求代理通过 FinAppProxyHandlerManager.authRequestHandler 进行统一管理。

import { FinAppProxyHandlerManager, IFinAppProxy, IFinProxyHandlerItem } from '@finclip/sdk'
import { common } from '@kit.AbilityKit'

class AuthRequestHandler extends IFinProxyHandlerItem.AuthRequestHandler {
  /**
   * 权限请求前置拦截。
   *
   * @param appId 当前小程序的 appId。
   * @param apiServer 当前小程序绑定的 apiServer。
   * @param context 当前宿主的 UIAbilityContext。
   * @param auth 当前请求的权限类型。
   * @returns Promise<boolean> 返回 true 表示继续 SDK 默认权限弹窗流程;返回 false 表示宿主拒绝本次请求,SDK 直接按失败收口。
   */
  public async onAuthRequest(
    appId: string,
    apiServer: string,
    context: common.UIAbilityContext,
    auth: IFinAppProxy.AuthEnum
  ): Promise<boolean>

  /**
   * 权限结果回调。
   *
   * @param appId 当前小程序的 appId。
   * @param apiServer 当前小程序绑定的 apiServer。
   * @param context 当前宿主的 UIAbilityContext。
   * @param auth 本次请求的权限类型。
   * @param result 最终授权结果,true 表示本次权限请求成功,false 表示本次权限请求被拒绝。
   */
  public onAuthResult(
    appId: string,
    apiServer: string,
    context: common.UIAbilityContext,
    auth: IFinAppProxy.AuthEnum,
    result: boolean
  ): void
}

FinAppProxyHandlerManager.authRequestHandler = new AuthRequestHandler()

# 20.2 调用时机

  • onAuthRequest 会在 SDK 展示权限弹窗前触发,宿主可以在这里展示自定义确认弹窗、做业务校验或直接拒绝本次权限请求
  • onAuthRequest 返回 true 时,SDK 继续默认权限弹窗流程
  • onAuthRequest 返回 false 时,SDK 不再展示默认权限弹窗,并按权限申请失败处理
  • onAuthResult 会在 SDK 权限流程完成后触发,无论最终结果是允许还是拒绝,都会把 result 回传给宿主

# 20.3 AuthEnum 类型

auth 参数使用 IFinAppProxy.AuthEnum 表示本次请求的权限类型。

枚举值 描述
AUTH_USERINFO 用户信息权限
AUTH_USER_LOCATION 位置信息权限
AUTH_RECORD 录音权限
AUTH_CALENDAR 日历权限
AUTH_PHOTO_ALBUM 相册权限
AUTH_CAMERA 相机权限
AUTH_BLUETOOTH 蓝牙权限
AUTH_CONTACT 通讯录权限
AUTH_PHONE_NUMBER 手机号权限
AUTH_OTHER 其他权限

# 20.4 示例代码

import { FinAppProxyHandlerManager, IFinAppProxy, IFinProxyHandlerItem } from '@finclip/sdk'
import { common } from '@kit.AbilityKit'
import { promptAction } from '@kit.ArkUI'

class AuthRequestHandler extends IFinProxyHandlerItem.AuthRequestHandler {
  public onAuthResult(
    appId: string,
    apiServer: string,
    context: common.UIAbilityContext,
    auth: IFinAppProxy.AuthEnum,
    result: boolean
  ) {
    promptAction.showDialog({
      title: '权限申请结果回调',
      message: `权限:${auth}, 申请结果:${result}`,
      buttons: [
        {
          text: '确认',
          color: '#0000ff'
        }
      ],
    })
  }

  public async onAuthRequest(
    appId: string,
    apiServer: string,
    context: common.UIAbilityContext,
    auth: IFinAppProxy.AuthEnum
  ): Promise<boolean> {
    let res = true
    try {
      const response = await promptAction.showDialog({
        title: '宿主app前置处理权限申请',
        message: `权限:${auth}`,
        buttons: [
          {
            text: '允许',
            color: '#0000ff'
          },
          {
            text: '拒绝',
            color: '#ff0000'
          }
        ],
      })
      res = response.index === 0
    } catch (e) {
      // 弹窗异常时按默认允许处理,避免宿主 UI 异常阻断 SDK 默认权限流程。
    }
    return res
  }
}

export function initAuthRequestHandler() {
  FinAppProxyHandlerManager.authRequestHandler = new AuthRequestHandler()
}
© FinClip with ❤ , Since 2017
AI助手