M3(批3): 微信分享 + 登录(@tencent/wechat_open_sdk)
依赖:ohpm i @tencent/wechat_open_sdk@1.0.19(feature_capabilities)
T-M3-13 ShareProvider:friendsSharetypeUrlToptitleDescript → SendMessageToWXReq(scene 会话/朋友圈)
+ WXWebpageObject → 回传 sharesuccess{success(2成功/3取消),type(1好友/2朋友圈)}。
type 2截图/3图片/4视频暂回退网页分享(TODO);微信未安装回 cancel 不卡死
T-M3-14 LoginProvider:accreditlogin("1"=QQ空实现/其他=微信) → SendAuthReq(scope snsapi_userinfo) → 取 code。
🔴 AppSecret 不入端:code 换 profile 必须服务端,无端点时记录 code+TODO,不推不完整 sharelogin(红线)
WeChatApi 单例:createWXAPI(AppID wxd2bd650e06bdfe58 公开标识) + sendReq + handleWant 路由 onResp
EntryAbility.onCreate/onNewWant → WeChatApi.handleWant(接收微信回调)
module.json5:querySchemes ["weixin"](isWXAppInstalled)
运行期成功前提(已注释说明):微信开放平台注册本 HarmonyOS 应用(bundleName+指纹绑 AppID)、登录 profile 需服务端 code 换取端点。
devecocli build 通过(CompileArkTS 编译微信代码无误)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
"lockfileVersion": 3,
|
||||
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
||||
"specifiers": {
|
||||
"@tencent/wechat_open_sdk@^1.0.19": "@tencent/wechat_open_sdk@1.0.19",
|
||||
"common@../common": "common@../common",
|
||||
"contracts@../contracts": "contracts@../contracts",
|
||||
"domain_resource@../domain_resource": "domain_resource@../domain_resource",
|
||||
@@ -14,6 +15,13 @@
|
||||
"platform@../platform": "platform@../platform"
|
||||
},
|
||||
"packages": {
|
||||
"@tencent/wechat_open_sdk@1.0.19": {
|
||||
"name": "@tencent/wechat_open_sdk",
|
||||
"version": "1.0.19",
|
||||
"integrity": "sha512-elkkyMZVolWyhULd6zA+7xdHPRAhVja5ugyHvw/UpmeWrGfthHzNS6QtMZDMntzo7dFTNOxi8GAVvZmRA1uPBQ==",
|
||||
"resolved": "https://ohpm.openharmony.cn/ohpm/@tencent/wechat_open_sdk/-/wechat_open_sdk-1.0.19.har",
|
||||
"registryType": "ohpm"
|
||||
},
|
||||
"common@../common": {
|
||||
"name": "common",
|
||||
"version": "1.0.0",
|
||||
@@ -60,7 +68,8 @@
|
||||
"common": "file:../common",
|
||||
"platform": "file:../platform",
|
||||
"domain_resource": "file:../domain_resource",
|
||||
"feature_bridge": "file:../feature_bridge"
|
||||
"feature_bridge": "file:../feature_bridge",
|
||||
"@tencent/wechat_open_sdk": "^1.0.19"
|
||||
}
|
||||
},
|
||||
"platform@../platform": {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
import { CapabilityProvider, EchoSampleProvider, RoomStubProvider, PayStubProvider,
|
||||
VibrateProvider, ClipboardProvider, AppSystemProvider, DeviceProvider,
|
||||
NetworkProvider } from 'feature_capabilities';
|
||||
NetworkProvider, ShareProvider, LoginProvider } from 'feature_capabilities';
|
||||
|
||||
export function buildCapabilities(): CapabilityProvider[] {
|
||||
return [
|
||||
@@ -18,6 +18,8 @@ export function buildCapabilities(): CapabilityProvider[] {
|
||||
new ClipboardProvider(), // gameCopytext/gamepastetext
|
||||
new DeviceProvider(), // getTime/getphonestate/getbattery/getwifiLevel/getphoneInfo/getmarketname/getothername/getOther/getcompareCode
|
||||
new NetworkProvider(), // getnetwork + 出站广播
|
||||
new ShareProvider(), // friendsSharetypeUrlToptitleDescript → sharesuccess(微信)
|
||||
new LoginProvider(), // accreditlogin → sharelogin(微信 code 模式,profile 待服务端)
|
||||
// —— 暂缓能力占位桩(§6.5)——
|
||||
new RoomStubProvider(),
|
||||
new PayStubProvider(),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.Ab
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { window } from '@kit.ArkUI';
|
||||
import { EventBus } from 'common';
|
||||
import { WeChatApi } from 'feature_capabilities';
|
||||
import { AppEvents } from '../routes/AppRoutes';
|
||||
|
||||
const DOMAIN = 0x0000;
|
||||
@@ -14,6 +15,14 @@ export default class EntryAbility extends UIAbility {
|
||||
hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
|
||||
}
|
||||
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
|
||||
// 微信回调(冷启动经 want 拉回):交 SDK 解析 → 路由到 Share/Login 回调
|
||||
WeChatApi.getInstance().handleWant(want);
|
||||
}
|
||||
|
||||
onNewWant(want: Want, _launchParam: AbilityConstant.LaunchParam): void {
|
||||
// 微信回调(应用已在后台时拉回)
|
||||
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onNewWant');
|
||||
WeChatApi.getInstance().handleWant(want);
|
||||
}
|
||||
|
||||
onDestroy(): void {
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"deliveryWithInstall": true,
|
||||
"installationFree": false,
|
||||
"pages": "$profile:main_pages",
|
||||
"querySchemes": [
|
||||
"weixin"
|
||||
],
|
||||
"abilities": [
|
||||
{
|
||||
"name": "EntryAbility",
|
||||
|
||||
@@ -14,3 +14,8 @@ export { ClipboardProvider } from './src/main/ets/providers/ClipboardProvider';
|
||||
export { AppSystemProvider } from './src/main/ets/providers/AppSystemProvider';
|
||||
export { DeviceProvider } from './src/main/ets/providers/DeviceProvider';
|
||||
export { NetworkProvider } from './src/main/ets/providers/NetworkProvider';
|
||||
export { ShareProvider } from './src/main/ets/providers/ShareProvider';
|
||||
export { LoginProvider } from './src/main/ets/providers/LoginProvider';
|
||||
|
||||
// 微信 SDK 封装(EntryAbility 处理回调 want 需用)
|
||||
export { WeChatApi, WX_APP_ID } from './src/main/ets/wx/WeChatApi';
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"lockfileVersion": 3,
|
||||
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
||||
"specifiers": {
|
||||
"@tencent/wechat_open_sdk@^1.0.19": "@tencent/wechat_open_sdk@1.0.19",
|
||||
"common@../common": "common@../common",
|
||||
"contracts@../contracts": "contracts@../contracts",
|
||||
"domain_resource@../domain_resource": "domain_resource@../domain_resource",
|
||||
@@ -13,6 +14,13 @@
|
||||
"platform@../platform": "platform@../platform"
|
||||
},
|
||||
"packages": {
|
||||
"@tencent/wechat_open_sdk@1.0.19": {
|
||||
"name": "@tencent/wechat_open_sdk",
|
||||
"version": "1.0.19",
|
||||
"integrity": "sha512-elkkyMZVolWyhULd6zA+7xdHPRAhVja5ugyHvw/UpmeWrGfthHzNS6QtMZDMntzo7dFTNOxi8GAVvZmRA1uPBQ==",
|
||||
"resolved": "https://ohpm.openharmony.cn/ohpm/@tencent/wechat_open_sdk/-/wechat_open_sdk-1.0.19.har",
|
||||
"registryType": "ohpm"
|
||||
},
|
||||
"common@../common": {
|
||||
"name": "common",
|
||||
"version": "1.0.0",
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"common": "file:../common",
|
||||
"platform": "file:../platform",
|
||||
"domain_resource": "file:../domain_resource",
|
||||
"feature_bridge": "file:../feature_bridge"
|
||||
}
|
||||
}
|
||||
"feature_bridge": "file:../feature_bridge",
|
||||
"@tencent/wechat_open_sdk": "^1.0.19"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"dynamicDependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 登录能力(契约 §8.1/§10.2,T-M3-14)。微信开放 SDK,**code 模式**。
|
||||
* - accreditlogin:data "1"=QQ(契约即空实现)/ 其他=微信。
|
||||
* - 回传 sharelogin:用户资料 {openid/headimgurl/nickname/sex/city/province/unionid}。
|
||||
*
|
||||
* 🔴 安全红线(§13/§14.2,CLAUDE.md 附录 B):AppSecret 绝不入客户端。
|
||||
* 客户端只取微信授权 code;用 code 换 access_token→userinfo→资料 **必须由服务端完成**,
|
||||
* 服务端再把资料回给端、由本 Provider callHandler('sharelogin', 资料)。
|
||||
* 当前无服务端换取端点:记录 code 并留 TODO,不在端侧换 token,也不推不完整的 sharelogin。
|
||||
*/
|
||||
import { SendAuthReq, ErrCode } from '@tencent/wechat_open_sdk';
|
||||
import { BridgeController } from 'feature_bridge';
|
||||
import { InboundHandlers } from 'contracts';
|
||||
import { Logger } from 'common';
|
||||
import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider';
|
||||
import { WeChatApi } from '../wx/WeChatApi';
|
||||
|
||||
export class LoginProvider implements CapabilityProvider {
|
||||
readonly name: string = 'login';
|
||||
private readonly log: Logger = Logger.tag('LoginProvider');
|
||||
private bridge: BridgeController | undefined = undefined;
|
||||
private ctx: CapabilityContext | undefined = undefined;
|
||||
|
||||
register(bridge: BridgeController, ctx: CapabilityContext): void {
|
||||
this.bridge = bridge;
|
||||
this.ctx = ctx;
|
||||
bridge.registerHandler(InboundHandlers.AccreditLogin, (data: string, _cb: (resp: string) => void) => {
|
||||
this.login(data);
|
||||
});
|
||||
}
|
||||
|
||||
private login(data: string): void {
|
||||
if (data === '1') {
|
||||
this.log.i('QQ login not implemented (contract: empty impl)');
|
||||
return;
|
||||
}
|
||||
const ctx = this.ctx;
|
||||
if (ctx === undefined) {
|
||||
return;
|
||||
}
|
||||
const wx: WeChatApi = WeChatApi.getInstance();
|
||||
if (!wx.isWXInstalled()) {
|
||||
this.log.i('WeChat not installed; login aborted');
|
||||
return;
|
||||
}
|
||||
const req: SendAuthReq = new SendAuthReq();
|
||||
req.scope = 'snsapi_userinfo';
|
||||
req.state = 'tsgame';
|
||||
wx.sendAuth(ctx.uiAbilityContext, req, (resp) => {
|
||||
if (resp.errCode === ErrCode.ERR_OK && resp.code !== undefined && resp.code !== '') {
|
||||
this.exchangeCodeForProfile(resp.code);
|
||||
} else {
|
||||
this.log.i(`wx auth not granted: errCode=${resp.errCode}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用 code 换取用户资料(必须经服务端)。当前无服务端端点:记录 code、留 TODO。
|
||||
* 接入后:POST code → 服务端返回 ShareLoginResp → bridge.callHandler('sharelogin', JSON)。
|
||||
*/
|
||||
private exchangeCodeForProfile(code: string): void {
|
||||
this.log.i(`got wx auth code (len=${code.length}); server exchange required, not pushing sharelogin yet`);
|
||||
// TODO(M3 服务端就绪后):POST code 到服务端换取 ShareLoginResp(contracts),
|
||||
// 再 this.bridge?.callHandler(OutboundHandlers.ShareLogin, JSON.stringify(profile))。
|
||||
// AppSecret 仅在服务端,端侧只传 code。
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 分享能力(契约 §8.1/§10.1,T-M3-13)。微信开放 SDK。
|
||||
* - friendsSharetypeUrlToptitleDescript:data=sharetypeBean JSON。
|
||||
* sharefriend "2"→朋友圈 / 其他→会话;type "1"网页/"2"Canvas截图/"3"图片/"4"视频。
|
||||
* - 回传 sharesuccess {success(2成功/3取消), type(1好友/2朋友圈)}。
|
||||
*
|
||||
* 本版完整支持 type=1(网页分享);type 2/3/4 暂回退为网页分享(webpageUrl),见 TODO。
|
||||
* ⚠ 运行期成功需在微信开放平台注册本 HarmonyOS 应用(bundleName+指纹 绑定 AppID)。
|
||||
*/
|
||||
import { SendMessageToWXReq, WXMediaMessage, WXWebpageObject, ErrCode } from '@tencent/wechat_open_sdk';
|
||||
import { BridgeController } from 'feature_bridge';
|
||||
import { InboundHandlers, OutboundHandlers, SharetypeBean, ShareSuccessResp } from 'contracts';
|
||||
import { Logger } from 'common';
|
||||
import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider';
|
||||
import { WeChatApi } from '../wx/WeChatApi';
|
||||
|
||||
export class ShareProvider implements CapabilityProvider {
|
||||
readonly name: string = 'share';
|
||||
private readonly log: Logger = Logger.tag('ShareProvider');
|
||||
private bridge: BridgeController | undefined = undefined;
|
||||
private ctx: CapabilityContext | undefined = undefined;
|
||||
|
||||
register(bridge: BridgeController, ctx: CapabilityContext): void {
|
||||
this.bridge = bridge;
|
||||
this.ctx = ctx;
|
||||
bridge.registerHandler(InboundHandlers.FriendsShare, (data: string, _cb: (resp: string) => void) => {
|
||||
this.share(data);
|
||||
});
|
||||
}
|
||||
|
||||
private share(data: string): void {
|
||||
const ctx = this.ctx;
|
||||
if (ctx === undefined) {
|
||||
return;
|
||||
}
|
||||
let bean: SharetypeBean;
|
||||
try {
|
||||
bean = JSON.parse(data) as SharetypeBean;
|
||||
} catch (e) {
|
||||
this.log.w(`share bad json: ${(e as Error).message}`);
|
||||
return;
|
||||
}
|
||||
const typeNum: number = bean.sharefriend === '2' ? 2 : 1;
|
||||
|
||||
const wx: WeChatApi = WeChatApi.getInstance();
|
||||
if (!wx.isWXInstalled()) {
|
||||
this.log.i('WeChat not installed; report cancel');
|
||||
this.reportResult(3, typeNum);
|
||||
return;
|
||||
}
|
||||
|
||||
const req: SendMessageToWXReq = new SendMessageToWXReq();
|
||||
req.scene = bean.sharefriend === '2' ? SendMessageToWXReq.WXSceneTimeline : SendMessageToWXReq.WXSceneSession;
|
||||
|
||||
const msg: WXMediaMessage = new WXMediaMessage();
|
||||
msg.title = bean.title;
|
||||
msg.description = bean.description;
|
||||
// TODO(M3):type "2" Canvas 截图(需容器 runJavaScript canvas.toDataURL + LocalUploadServer)、
|
||||
// "3" 图片链接(下载后 WXImageObject)、"4" 视频。当前统一按网页分享回退,保证可分享不报错。
|
||||
const web: WXWebpageObject = new WXWebpageObject();
|
||||
web.webpageUrl = bean.webpageUrl;
|
||||
msg.mediaObject = web;
|
||||
req.message = msg;
|
||||
|
||||
wx.sendShare(ctx.uiAbilityContext, req, (resp) => {
|
||||
const success: number = resp.errCode === ErrCode.ERR_OK ? 2 : 3;
|
||||
this.reportResult(success, typeNum);
|
||||
});
|
||||
}
|
||||
|
||||
private reportResult(success: number, type: number): void {
|
||||
const out: ShareSuccessResp = { success, type };
|
||||
this.bridge?.callHandler(OutboundHandlers.ShareSuccess, JSON.stringify(out));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 微信开放平台 SDK 封装(@tencent/wechat_open_sdk,契约 §10.1/§10.2/§13)。单例。
|
||||
*
|
||||
* 职责:持有 WXApi(用 AppID 创建)、发起授权/分享请求、接收微信回调并路由到对应回调。
|
||||
* AppID 为公开标识,可入客户端;**AppSecret 绝不入端**(登录换 profile 须服务端,§13 红线)。
|
||||
*
|
||||
* 回调路径:微信处理完拉回宿主 App(默认 EntryAbility),EntryAbility.onNewWant/onCreate
|
||||
* 把 want 交给 handleWant() → SDK 解析 → onResp 路由到 authCb/shareCb。
|
||||
*/
|
||||
import { common, Want } from '@kit.AbilityKit';
|
||||
import {
|
||||
WXAPIFactory, WXApi, WXApiEventHandler, BaseReq, BaseResp,
|
||||
SendAuthResp, SendMessageToWXResp,
|
||||
} from '@tencent/wechat_open_sdk';
|
||||
import { Logger } from 'common';
|
||||
|
||||
/** 微信 AppID(公开标识,契约 §13)。 */
|
||||
export const WX_APP_ID: string = 'wxd2bd650e06bdfe58';
|
||||
|
||||
export type AuthRespCallback = (resp: SendAuthResp) => void;
|
||||
export type ShareRespCallback = (resp: SendMessageToWXResp) => void;
|
||||
|
||||
export class WeChatApi {
|
||||
private static readonly log: Logger = Logger.tag('WeChatApi');
|
||||
private static inst: WeChatApi | undefined = undefined;
|
||||
private readonly api: WXApi;
|
||||
private readonly handler: WXApiEventHandler;
|
||||
private authCb: AuthRespCallback | undefined = undefined;
|
||||
private shareCb: ShareRespCallback | undefined = undefined;
|
||||
|
||||
private constructor() {
|
||||
this.api = WXAPIFactory.createWXAPI(WX_APP_ID);
|
||||
this.handler = {
|
||||
onReq: (_req: BaseReq) => { },
|
||||
onResp: (resp: BaseResp) => this.routeResp(resp),
|
||||
};
|
||||
}
|
||||
|
||||
static getInstance(): WeChatApi {
|
||||
if (WeChatApi.inst === undefined) {
|
||||
WeChatApi.inst = new WeChatApi();
|
||||
}
|
||||
return WeChatApi.inst;
|
||||
}
|
||||
|
||||
isWXInstalled(): boolean {
|
||||
try {
|
||||
return this.api.isWXAppInstalled();
|
||||
} catch (e) {
|
||||
WeChatApi.log.w(`isWXAppInstalled failed: ${(e as Error).message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 发起授权登录;resp 经 cb 异步回调。 */
|
||||
sendAuth(context: common.UIAbilityContext, req: BaseReq, cb: AuthRespCallback): void {
|
||||
this.authCb = cb;
|
||||
this.api.sendReq(context, req);
|
||||
}
|
||||
|
||||
/** 发起分享;resp 经 cb 异步回调。 */
|
||||
sendShare(context: common.UIAbilityContext, req: BaseReq, cb: ShareRespCallback): void {
|
||||
this.shareCb = cb;
|
||||
this.api.sendReq(context, req);
|
||||
}
|
||||
|
||||
/** 由 EntryAbility 在 onNewWant/onCreate 调用,处理微信回调 want。 */
|
||||
handleWant(want: Want): void {
|
||||
try {
|
||||
this.api.handleWant(want, this.handler);
|
||||
} catch (e) {
|
||||
WeChatApi.log.w(`handleWant failed: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private routeResp(resp: BaseResp): void {
|
||||
if (resp instanceof SendAuthResp) {
|
||||
const cb = this.authCb;
|
||||
this.authCb = undefined;
|
||||
if (cb !== undefined) {
|
||||
cb(resp);
|
||||
}
|
||||
} else if (resp instanceof SendMessageToWXResp) {
|
||||
const cb = this.shareCb;
|
||||
this.shareCb = undefined;
|
||||
if (cb !== undefined) {
|
||||
cb(resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user