diff --git a/README.md b/README.md index 2cf24b3..0bec521 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,36 @@ following fields: | lang | String | The user language | | country | String | The user country | +#### authByScan([scope, nonceStr, onQRGet]) 微信扫码授权登录 + +- `appId` {String} the appId you get from WeChat dashboard +- `appSecret` {String} the appSecret you get from WeChat dashboard +- `onQRGet` (String) => void + +调用 authByScan 后,需要监听二维码的获取,展示完二维码,用户扫码登录完成后才会回调 callback,字段如下 + +| field | type | description | +| ------- | ------ | ----------------------------------- | +| errCode | Number | Error Code | +| errStr | String | Error message if any error occurred | +| nickname | String | 微信昵称 | +| headimgurl | String | 微信头像链接 | +| openid | String | openid | +| unionid | String | unionid | + + +示例如下 + +```js +const ret = await WeChat.authByScan(WeiXinId, WeiXinSecret, (qrcode) => { + console.log(qrcode) + // 拿到 qrcode 用 Image 去渲染 +}); +console.log('登录信息', ret); +``` + +如有不懂,可以查看[微信官方文档](https://developers.weixin.qq.com/doc/oplatform/Mobile_App/WeChat_Login/Login_via_Scan.html) + #### ShareText(ShareTextMetadata) 分享文本 ShareTextMetadata diff --git a/android/src/main/java/com/wechatlib/WeChatLibModule.java b/android/src/main/java/com/wechatlib/WeChatLibModule.java index 3663186..9c1bd99 100644 --- a/android/src/main/java/com/wechatlib/WeChatLibModule.java +++ b/android/src/main/java/com/wechatlib/WeChatLibModule.java @@ -6,6 +6,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Environment; +import android.util.Base64; import androidx.annotation.Nullable; import androidx.core.content.FileProvider; @@ -25,11 +26,17 @@ import com.facebook.imagepipeline.request.ImageRequestBuilder; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.modules.core.RCTNativeAppEventEmitter; +import com.tencent.mm.opensdk.diffdev.DiffDevOAuthFactory; +import com.tencent.mm.opensdk.diffdev.IDiffDevOAuth; +import com.tencent.mm.opensdk.diffdev.OAuthErrCode; +import com.tencent.mm.opensdk.diffdev.OAuthListener; import com.tencent.mm.opensdk.modelbase.BaseReq; import com.tencent.mm.opensdk.modelbase.BaseResp; import com.tencent.mm.opensdk.modelbiz.ChooseCardFromWXCardPackage; @@ -149,6 +156,42 @@ public class WeChatLibModule extends ReactContextBaseJavaModule implements IWXAP } } + private void sendEvent(ReactContext reactContext, String eventName, WritableMap params) { + reactContext.getJSModule(RCTNativeAppEventEmitter.class).emit(eventName, params); + } + + @ReactMethod + public void authByScan(String appid, String nonceStr, String timeStamp, String scope, String signature, String schemeData, final Callback callback) { + if (api == null) { + callback.invoke(NOT_REGISTERED); + return; + } + + IDiffDevOAuth oauth = DiffDevOAuthFactory.getDiffDevOAuth(); + oauth.stopAuth(); + oauth.auth(appid, scope, nonceStr, timeStamp, signature, new OAuthListener() { + @Override + public void onAuthGotQrcode(String var1, byte[] var2){ + WritableMap map = Arguments.createMap(); + String base64String = Base64.encodeToString(var2, Base64.DEFAULT); + map.putString("qrcode", base64String); + sendEvent(getReactApplicationContext(), "onAuthGotQrcode", map); + } + + @Override + public void onQrcodeScanned() { + + } + @Override + public void onAuthFinish(OAuthErrCode var1, String var2){ + WritableMap map = Arguments.createMap(); + map.putString("authCode", var2); + map.putInt("errCode", var1.getCode()); + callback.invoke(null, map); + } + }); + } + @ReactMethod public void registerApp(String appid, String universalLink, Callback callback) { this.appId = appid; diff --git a/ios/WechatLib.mm b/ios/WechatLib.mm index 8ece4c1..ce0f9f4 100644 --- a/ios/WechatLib.mm +++ b/ios/WechatLib.mm @@ -5,6 +5,13 @@ #import #import #import +#import "WechatAuthSDK.h" + + +@interface WechatLib () +@property (nonatomic, strong) WechatAuthSDK *authSDK; +@property (nonatomic, strong) RCTResponseSenderBlock scanCallback; +@end @implementation WechatLib @@ -20,6 +27,8 @@ RCT_EXPORT_MODULE() self = [super init]; if (self) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleOpenURL:) name:@"RCTOpenURLNotification" object:nil]; + self.authSDK = [[WechatAuthSDK alloc] init]; + self.authSDK.delegate = self; } return self; } @@ -644,4 +653,56 @@ RCT_EXPORT_METHOD(openCustomerServiceChat return type; } +#pragma mark - WechatAuthAPIDelegate + +RCT_EXPORT_METHOD(addListener:(NSString *)eventName) { + +} + +RCT_EXPORT_METHOD(removeListeners:(double)count) { + +} + +RCT_EXPORT_METHOD(authByScan:(NSString *)appid + nonceStr:(NSString *)nonceStr + timeStamp:(NSString *)timeStamp + scope:(NSString *)scope + signature:(NSString *)signature + schemeData:(nullable NSString *)schemeData + callback:(RCTResponseSenderBlock)callback) { + self.scanCallback = callback; + [self.authSDK StopAuth]; + [self.authSDK Auth:appid nonceStr:nonceStr timeStamp:timeStamp scope:scope signature:signature schemeData:schemeData]; +} + +//得到二维码 +- (void)onAuthGotQrcode:(UIImage *)image { + NSLog(@"onAuthGotQrcode"); + NSData *imageData = UIImagePNGRepresentation(image); + if (!imageData) { + imageData = UIImageJPEGRepresentation(image, 1); + } + NSString *base64String = [imageData base64EncodedStringWithOptions:0]; + [self.bridge.eventDispatcher sendDeviceEventWithName:@"onAuthGotQrcode" body:@{@"qrcode": base64String}]; +} + +//二维码被扫描 +- (void)onQrcodeScanned { + NSLog(@"onQrcodeScanned"); +} + +//成功登录 +- (void)onAuthFinish:(int)errCode AuthCode:(nullable NSString *)authCode { + NSLog(@"onAuthFinish"); + if (self.scanCallback) { + self.scanCallback(@[[NSNull null], @{@"authCode": authCode?:@"", @"errCode": @(errCode)}]); + self.scanCallback = nil; + } +} + +- (NSArray *)supportedEvents +{ + return @[@"onAuthGotQrcode", @"onQrcodeScanned", @"onAuthFinish"]; +} + @end diff --git a/package.json b/package.json index a58bdd6..786450c 100644 --- a/package.json +++ b/package.json @@ -71,14 +71,16 @@ "react-native": "0.70.6", "react-native-builder-bob": "^0.20.0", "release-it": "^15.0.0", - "typescript": "^4.5.2" + "typescript": "^4.5.2", + "js-sha1": "^0.7.0" }, "resolutions": { "@types/react": "17.0.21" }, "peerDependencies": { "react": "*", - "react-native": "*" + "react-native": "*", + "js-sha1": "*" }, "engines": { "node": ">= 16.0.0" @@ -160,4 +162,4 @@ "dependencies": { "events": "^3.3.0" } -} +} \ No newline at end of file diff --git a/src/index.d.ts b/src/index.d.ts index 43c09c9..49756ce 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -46,10 +46,20 @@ declare module 'react-native-wechat-lib' { state?: string; returnKey?: string; } + export interface ScanLoginResp { + nickname?: string; + headimgurl?: string; + openid?: string; + unionid?: string; + errCode?: number; + errStr?: string; + } export function sendAuthRequest( scope: string | string[], state?: string ): Promise; + export function authByScan(appId: string, appSecret: string, onQRGet: (qrcode: string)=>void): Promise; + export interface ShareMetadata { type: | 'news' diff --git a/src/index.js b/src/index.js index c1e98fe..638ca8f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,8 @@ 'use strict'; -import { DeviceEventEmitter, NativeModules, Platform } from 'react-native'; import { EventEmitter } from 'events'; +import { sha1 } from 'js-sha1'; +import { DeviceEventEmitter, NativeEventEmitter, NativeModules, Platform } from 'react-native'; let isAppRegistered = false; let { WeChat, WechatLib } = NativeModules; @@ -170,6 +171,152 @@ const nativeSubscribeMessage = wrapApi(WeChat.subscribeMessage); const nativeChooseInvoice = wrapApi(WeChat.chooseInvoice); const nativeShareFile = wrapApi(WeChat.shareFile); +const nativeScan = wrapApi(WeChat.authByScan); + +// https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html +const getAccessToken = async (WeiXinId, WeiXinSecret) => { + let url = + 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' + + WeiXinId + + '&secret=' + + WeiXinSecret; + const response = await fetch(url); + const res = await response.json(); + return res.access_token; +}; + +const getSDKTicket = async (accessToken) => { + let url = + 'https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=2&access_token=' + + accessToken; + const response = await fetch(url); + const res = await response.json(); + return res.ticket; +}; + +const createSignature = ( + WeiXinId, + nonceStr, + sdkTicket, + timestamp +) => { + const origin = + 'appid=' + + WeiXinId + + '&noncestr=' + + nonceStr + + '&sdk_ticket=' + + sdkTicket + + '×tamp=' + + timestamp; + const ret = sha1(origin); + // console.log('wx scan signature', origin, ret); + return ret; +}; + +const getUserInfo = ( + WeiXinId, + WeiXinSecret, + code, + callback +) => { + let accessTokenUrl = + 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=' + + WeiXinId + + '&secret=' + + WeiXinSecret + + '&code=' + + code + + '&grant_type=authorization_code'; + fetch(accessTokenUrl) + .then((res) => { + return res.json(); + }) + .then((res) => { + // console.log('wechat get access code success: ', res.access_token); + let userInfoUrl = + 'https://api.weixin.qq.com/sns/userinfo?access_token=' + + res.access_token + + '&openid=' + + res.openid; + fetch(userInfoUrl) + .then((res2) => { + return res2.json(); + }) + .then((json) => { + // console.log('wechat get user info success: ', json); + callback({ + nickname: json.nickname, + headimgurl: json.headimgurl, + openid: json.openid, + unionid: json.unionid, + }); + }) + .catch((e) => { + console.warn('wechat get user info fail ', e); + callback({ error: e }); + }); + }) + .catch((e) => { + console.warn('wechat get access code fail ', e); + callback({ error: e }); + }); +}; + +const generateObjectId = () => { + var timestamp = ((new Date().getTime() / 1000) | 0).toString(16); // eslint-disable-line no-bitwise + return ( + timestamp + + 'xxxxxxxxxxxxxxxx'.replace(/[x]/g, function () { + return ((Math.random() * 16) | 0).toString(16).toLowerCase(); // eslint-disable-line no-bitwise + }) + ); +} + +/** + * @method authByScan + * @param {String} appId - the app id + * @param {String} appSecret - the app secret + * @param {Function} onQRGet - (qrcode: string) => void + * @return {Promise} + */ +export function authByScan(appId, appSecret, onQRGet) { + return new Promise(async (resolve, reject) => { + const accessToken = await getAccessToken(appId, appSecret); + const ticket = await getSDKTicket(accessToken); + const nonceStr = generateObjectId(); + const timestamp = String(Math.round(Date.now() / 1000)); + const signature = createSignature(appId, nonceStr, ticket, timestamp); + + const qrcodeEmitter = new NativeEventEmitter(NativeModules.WeChat); + + const subscription = qrcodeEmitter.addListener('onAuthGotQrcode', (res) => + onQRGet && onQRGet(res.qrcode) + ); + + const ret = await nativeScan(appId, nonceStr, timestamp, 'snsapi_userinfo', signature, ''); + // console.log('扫码结果', ret) + subscription.remove(); + if (!ret?.authCode) { + reject(new WechatError({ + errStr: 'Auth code 获取失败', + errCode: -1 + })) + return; + } + getUserInfo(appId, appSecret, ret?.authCode, (result) => { + // console.log('扫码登录结果', result) + if (!result.error) { + resolve(result) + } else { + reject(new WechatError({ + errStr: '扫码登录失败' + JSON.stringify(e), + errCode: -2 + })) + } + }); + }); +} /** * @method sendAuthRequest @@ -224,8 +371,8 @@ export function chooseInvoice(data = {}) { const cardItemList = JSON.parse(resp.cardItemList); resp.cards = cardItemList ? cardItemList.map((item) => ({ - cardId: item.card_id, - encryptCode: item.encrypt_code, + cardId: item.card_id, + encryptCode: item.encrypt_code, })) : []; }