├── .coveralls.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── README_EN.md ├── assets └── jason-wx-reward-code.png ├── demo ├── Order.js ├── index.html ├── index.js ├── oauth.html ├── wechat-config-sample.js ├── wechat-jssdk-demo-legacy.gif └── wechat-jssdk-demo-new.gif ├── lib ├── Card.js ├── JSSDK.js ├── MiniProgram.js ├── OAuth.js ├── Payment.js ├── Wechat.js ├── client.js ├── code.js ├── config.js ├── index.js ├── store │ ├── FileStore.js │ ├── MongoStore.js │ └── Store.js └── utils.js ├── package.json └── test ├── Card.test.js ├── JSSDK.test.js ├── MiniProgram.test.js ├── OAuth.test.js ├── Payment.test.js ├── Store.test.js ├── bootstrap.js ├── config.test.js ├── data-2.xml ├── data.xml ├── db.test.js ├── index.test.js ├── utils.test.js └── xml.test.js /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: iUSiEgqT1o1NNeHUvMQz2bxetC1NzTbZG -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | .nyc_output 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | node_modules 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | *.map 37 | --no-cache 38 | .idea 39 | config.json 40 | .Ds_Store 41 | wechat-info*.json 42 | Gemfile 43 | Gemfile.lock 44 | _site/ 45 | package-lock.json 46 | cert 47 | db_*.json 48 | demo/wechat-config.js 49 | dist 50 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | _site 4 | cert 5 | coverage 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | repo_token: iUSiEgqT1o1NNeHUvMQz2bxetC1NzTbZG 3 | services: mongodb 4 | node_js: 5 | - 8 6 | - 10 7 | - 12 8 | - stable 9 | script: npm run coverage 10 | after_success: 'npm run coveralls' 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-present jason 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wechat-jssdk 2 | [![npm](https://img.shields.io/npm/v/wechat-jssdk.svg?style=flat-square)](https://www.npmjs.com/package/wechat-jssdk) 3 | [![node](https://img.shields.io/node/v/wechat-jssdk.svg?style=flat-square)](https://nodejs.org/) 4 | [![Coverage Status](https://img.shields.io/coveralls/github/JasonBoy/wechat-jssdk.svg?style=flat-square)](https://coveralls.io/github/JasonBoy/wechat-jssdk) 5 | [![npm](https://img.shields.io/npm/l/wechat-jssdk.svg?style=flat-square)](https://www.npmjs.com/package/wechat-jssdk) 6 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 7 | 8 | 微信JSSDK与NodeJS及Web端整合 9 | WeChat JS-SDK integration with NodeJS and Web. 10 | 11 | [English](https://github.com/JasonBoy/wechat-jssdk/blob/master/README_EN.md) 12 | | [Release Notes](https://github.com/JasonBoy/wechat-jssdk/releases) 13 | 14 | ![wechat-jssdk-demo](https://raw.githubusercontent.com/JasonBoy/wechat-jssdk/master/demo/wechat-jssdk-demo-new.gif) 15 | 16 | ## 主要功能 17 | 18 | - [:heartbeat:服务端](#使用方法) 19 | - [:heartpulse:浏览器端](#浏览器端) 20 | - [:unlock:OAuth网页授权](#oauth) 21 | - [:fries:微信卡券](#微信卡券) 22 | - [:credit_card:微信支付](#微信支付) 23 | - [:baby_chick:微信小程序](#小程序) 24 | - [:cd:使用Stores](#使用Stores) 25 | - [:movie_camera:完整 Demo](#demo) 26 | 27 | ## 使用方法 28 | ```bash 29 | npm install wechat-jssdk --save 30 | # 或者 31 | yarn add wechat-jssdk 32 | ``` 33 | 34 | ```javascript 35 | const {Wechat} = require('wechat-jssdk'); 36 | const wx = new Wechat(wechatConfig); 37 | ``` 38 | ### Wechat 配置项 39 | 40 | `wechatConfig` 为以下格式: 41 | ```javascript 42 | { 43 | //第一个为设置网页授权回调地址 44 | wechatRedirectUrl: "http://yourdomain.com/wechat/oauth-callback", 45 | wechatToken: "xxx", //第一次在微信控制台保存开发者配置信息时使用 46 | appId: "xxx", 47 | appSecret: "xxx", 48 | card: true, //开启卡券支持,默认关闭 49 | payment: true, //开启支付支持,默认关闭 50 | merchantId: '', //商户ID 51 | paymentSandBox: true, //沙箱模式,验收用例 52 | paymentKey: '', //必传,验签密钥,TIP:获取沙箱密钥也需要真实的密钥,所以即使在沙箱模式下,真实验签密钥也需要传入。 53 | //pfx 证书 54 | paymentCertificatePfx: fs.readFileSync(path.join(process.cwd(), 'cert/apiclient_cert.p12')), 55 | //默认微信支付通知地址 56 | paymentNotifyUrl: `http://your.domain.com/api/wechat/payment/`, 57 | //小程序配置 58 | "miniProgram": { 59 | "appId": "mp_appid", 60 | "appSecret": "mp_app_secret", 61 | } 62 | } 63 | ``` 64 | 65 | 其他支持的设置都有默认值,基本都是微信API的地址,且基本不会改变, 可以查看 `./lib/config.js`. 66 | 67 | ## 设置微信环境 68 | 1.去微信公众平台 69 | 70 | 下载类似 `MP_verify_XHZon7GAGRdcAFxx.txt` 这样的文件放到网站根目录, 如`http://yourdomain.com/MP_verify_XHZon7GAGRdcAFxx.txt`,微信会验证这个链接. 71 | 72 | 2.然后在你的express/koa app中提供一个接口给浏览器获取验证信息, @see [demo](#demo) 73 | 74 | ```javascript 75 | //express app: 76 | router.get('/get-signature', (req, res) => { 77 | wx.jssdk.getSignature(req.query.url).then(signatureData => { 78 | res.json(signatureData); 79 | }); 80 | }); 81 | //koa2/koa-router app: 82 | router.get('/get-signature', async ctx => { 83 | ctx.body = await wx.jssdk.getSignature(ctx.request.query.url); 84 | }); 85 | ``` 86 | 3.获取签名后,进入下一步浏览器端使用方法. 87 | 88 | ## 浏览器端 89 | ```javascript 90 | const WechatJSSDK = require('wechat-jssdk/dist/client.umd'); 91 | //ES6 import 92 | import WechatJSSDK from 'wechat-jssdk/dist/client.umd'; 93 | 94 | //没有打包的话直接script扔到html,然后从`window`获取, e.g: 95 | const wechatObj = new window.WechatJSSDK(config) 96 | ``` 97 | 98 | `config`应该为: 99 | 100 | ```javascript 101 | const config = { 102 | //前4个是微信验证签名必须的参数,第2-4个参数为类似上面 '/get-signature' 从node端获取的结果 103 | 'appId': 'xxx', 104 | 'nonceStr': 'xxx', 105 | 'signature': 'xxx', 106 | 'timestamp': 'xxx', 107 | //下面为可选参数 108 | 'debug': true, //开启 debug 模式 109 | 'jsApiList': [], //设置所有想要使用的微信jsapi列表, 默认值为 ['updateAppMessageShareData','updateTimelineShareData','onMenuShareTimeline', 'onMenuShareAppMessage'],分享到朋友圈及聊天记录 110 | 'customUrl': '' //自定义微信js链接 111 | } 112 | const wechatObj = new WechatJSSDK(config); 113 | wechatObj.initialize() 114 | .then(w => { 115 | //set up your share info, "w" is the same instance as "wechatObj" 116 | }) 117 | .catch(err => { 118 | console.error(err); 119 | }); 120 | ``` 121 | 验证签名成功后, 就可以自定义你的分享内容了: 122 | > sdk默认只注册了`updateAppMessageShareData`,`updateTimelineShareData`,`onMenuShareTimeline(wx即将废弃)`,`onMenuShareAppMessage(wx即将废弃)` 123 | ```javascript 124 | //自定义分享到聊天窗口 125 | //内部调用 `wechatObj.callWechatApi('updateAppMessageShareData', {...})`, 语法糖而已 126 | wechatObj.updateAppMessageShareData({ 127 | type: 'link', 128 | title: 'title', 129 | link: location.href, 130 | imgUrl: '/logo.png', 131 | desc: 'description', 132 | success: function (){}, 133 | fail: function (){}, 134 | complete: function (){}, 135 | cancel: function (){} 136 | }); 137 | //自定义分享到朋友圈 138 | //语法糖 139 | wechatObj.updateTimelineShareData({ 140 | type: 'link', 141 | title: 'title', 142 | link: location.href, 143 | imgUrl: '/logo.png' 144 | }); 145 | ``` 146 | 要获取原始的微信对象 `wx`,可以通过`wechatObj.getOriginalWx()`来获取。 147 | 如果第一次验证失败,可以在`error`回调里更新签名信息,并重新发验证请求: 148 | `wechatObj.signSignature(newSignatureConfig);`, `newSignatureConfig`只需包含: 149 | ``` 150 | { 151 | 'nonceStr': 'xxx', 152 | 'signature': 'xxx', 153 | 'timestamp': 'xxx', 154 | } 155 | ``` 156 | 157 | 调用其他微信接口: 158 | `wechatObj.callWechatApi(apiName, apiConfig)` 159 | `apiName`和`apiConfig`请参考微信官方接口文档 160 | 161 | ## OAuth 162 | 默认生成微信授权URL为 `wx.oauth.snsUserInfoUrl` 和 `wx.oauth.snsUserBaseUrl`,其中的默认回调URL为 `wechatConfig` 中配置的 `wechatRedirectUrl`. 163 | 你也可以通过调用 `wx.oauth. generateOAuthUrl(customUrl, scope, state)`来自定义回调地址 164 | ```javascript 165 | //callback url handler 166 | //如"wechatRedirectUrl"配置为 "http://127.0.0.1/wechat/oauth-callback", 你的路由需要为: 167 | router.get('/wechat/oauth-callback', function (req, res) { 168 | //得到code,获取用户信息 169 | wx.oauth.getUserInfo(req.query.code) 170 | .then(function(userProfile) { 171 | console.log(userProfile) 172 | res.render("demo", { 173 | wechatInfo: userProfile 174 | }); 175 | }); 176 | }); 177 | ``` 178 | TIP: 确保上面的重定向地址域名已经在微信里的授权回调地址设置里设置过。 179 | ![](https://cloud.githubusercontent.com/assets/2911620/23061999/f95da3d4-f53e-11e6-9022-29ea33adb126.png) 180 | 181 | ## 微信卡券 182 | 183 | 在wechatConfig设置 `card: true` 来支持卡券功能的服务端支持, 参考[demo](#demo). 184 | 要查看卡券 APIs, 参考 [cards apis](https://github.com/JasonBoy/wechat-jssdk/wiki/API#card-apis) 185 | 186 | ## 微信支付 187 | 188 | 在wechatConfig设置 `payment: true` 来支持微信支付功能的服务端支持, 其他一些支付必须的配置也需要一同设置. 189 | 参考 [demo](#demo). 190 | 要查看支付 APIs, 参考 [payment apis](https://github.com/JasonBoy/wechat-jssdk/wiki/API#payment-apis) 191 | 192 | ## 小程序 193 | 194 | 使用小程序的服务端支持([看接口](https://github.com/JasonBoy/wechat-jssdk/wiki/API#mini-programv4)), 在配置里设置小程序的`appId` 和 `appSecret`: 195 | ```javascript 196 | const { Wechat, MiniProgram } = require('wechat-jssdk'); 197 | const wechatConfig = { 198 | "appId": "appid", 199 | "appSecret": "app_secret", 200 | //...other configs 201 | //... 202 | //小程序配置 203 | "miniProgram": { 204 | "appId": "mp_appid", 205 | "appSecret": "mp_app_secret", 206 | } 207 | }; 208 | const wx = new Wechat(wechatConfig); 209 | //调用小程序接口 210 | wx.miniProgram.getSession('code'); 211 | 212 | //手动实例化 MiniProgram 213 | const miniProgram = new MiniProgram({ 214 | miniProgram: { 215 | "appId": "mp_appid", 216 | "appSecret": "mp_app_secret", 217 | } 218 | }) 219 | ``` 220 | 221 | ## 使用Stores 222 | Store用来自定义存储token持久化(如文件,数据库等待),实现自己的Store, 请查看[API](https://github.com/JasonBoy/wechat-jssdk/wiki/Store) 223 | 自带 Store: `FileStore`, `MongoStore`,默认为`FileStore`, 存储到`wechat-info.json`文件. 224 | 225 | ## APIs 226 | 查看 [API wiki](https://github.com/JasonBoy/wechat-jssdk/wiki/API) 227 | 228 | ## Demo 229 | 230 | 在v3.1.0后,demo页面增加卡券和支付的用例测试, 231 | Copy `demo/wechat-config-sample.js` 到 `demo/wechat-config.js`, 232 | 然后在里面里面修改 `appId`, `appSecret`, 及其他的[配置](#wechat-config) 如支付的其他配置如果需要使用支付功能的话. 233 | 234 | 在`./demo/index.js`中设置你自己的`appId`, `appSecret`, 然后 `npm start` 或 `npm run dev`, 使用微信开发者工具测试。 235 | 236 | ## Buy me a coffee 237 | 如果您觉得这个项目对您有用,可以请我喝杯咖啡 238 | ![reward-me](https://raw.githubusercontent.com/JasonBoy/wechat-jssdk/master/assets/jason-wx-reward-code.png) 239 | 240 | ## LICENSE 241 | 242 | MIT @ 2016-present [jason](http://blog.lovemily.me) 243 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # wechat-jssdk 2 | [![npm](https://img.shields.io/npm/v/wechat-jssdk.svg?style=flat-square)](https://www.npmjs.com/package/wechat-jssdk) 3 | [![node](https://img.shields.io/node/v/wechat-jssdk.svg?style=flat-square)](https://nodejs.org/) 4 | [![Building Status](https://img.shields.io/travis/JasonBoy/wechat-jssdk.svg?style=flat-square)](https://travis-ci.org/JasonBoy/wechat-jssdk) 5 | [![Coverage Status](https://img.shields.io/coveralls/github/JasonBoy/wechat-jssdk.svg?style=flat-square)](https://coveralls.io/github/JasonBoy/wechat-jssdk) 6 | [![npm](https://img.shields.io/npm/l/wechat-jssdk.svg?style=flat-square)](https://www.npmjs.com/package/wechat-jssdk) 7 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 8 | 9 | 10 | WeChat JS-SDK integration with NodeJS and Web. 11 | 12 | [中文使用文档](https://github.com/JasonBoy/wechat-jssdk/wiki/%E4%B8%AD%E6%96%87%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3) 13 | [Changelog](https://github.com/JasonBoy/wechat-jssdk/releases) 14 | 15 | ![wechat-jssdk-demo](https://raw.githubusercontent.com/JasonBoy/wechat-jssdk/master/demo/wechat-jssdk-demo-new.gif) 16 | 17 | ## Features 18 | 19 | - [:heartbeat:Server-Side](#usage) 20 | - [:heartpulse:Browser-Side](#browser-side-usage) 21 | - [:unlock:OAuth](#oauth) 22 | - [:fries:Cards and Offers](#cards-and-offers) 23 | - [:credit_card:Wechat Payment](#payment) 24 | - [:baby_chick:Wechat Mini Program](#mini-program) 25 | - [:cd:Using Stores](#using-stores) 26 | - [:movie_camera:Full Featured Demo](#demo) 27 | 28 | ## Usage 29 | 30 | ```bash 31 | npm install wechat-jssdk --save 32 | # or 33 | yarn add wechat-jssdk 34 | ``` 35 | 36 | ```javascript 37 | const {Wechat} = require('wechat-jssdk'); 38 | const wx = new Wechat(wechatConfig); 39 | ``` 40 | 41 | ## Wechat Config 42 | 43 | `wechatConfig` info: 44 | 45 | ```javascript 46 | { 47 | //set your oauth redirect url, defaults to localhost 48 | "wechatRedirectUrl": "http://yourdomain.com/wechat/oauth-callback", 49 | //"wechatToken": "wechat_token", //not necessary required 50 | "appId": "appid", 51 | "appSecret": "app_secret", 52 | card: true, //enable cards 53 | payment: true, //enable payment support 54 | merchantId: '', // 55 | paymentSandBox: true, //dev env 56 | paymentKey: '', //API key to gen payment sign 57 | paymentCertificatePfx: fs.readFileSync(path.join(process.cwd(), 'cert/apiclient_cert.p12')), 58 | //default payment notify url 59 | paymentNotifyUrl: `http://your.domain.com/api/wechat/payment/`, 60 | //mini program config 61 | "miniProgram": { 62 | "appId": "mp_appid", 63 | "appSecret": "mp_app_secret", 64 | } 65 | } 66 | ``` 67 | 68 | ### Setup your Wechat ENV 69 | 1.Set your own URL in [Wechat Website](https://mp.weixin.qq.com) 70 | 71 | Usually wechat will provide you a `MP_verify_XHZon7GAGRdcAFxx.txt` like file to ask you to put that on your website root, 72 | which will be accessed by wechat on `http://yourdomain.com/MP_verify_XHZon7GAGRdcAFxx.txt` to verify that you own the domain. 73 | 74 | 2.You should also provide a api for your browser to get token for the current url, see [demo](#demo) 75 | 76 | ```javascript 77 | //express app for example: 78 | router.get('/get-signature', (req, res) => { 79 | wx.jssdk.getSignature(req.query.url).then(signatureData => { 80 | res.json(signatureData); 81 | }); 82 | }); 83 | //koa2/koa-router app for example: 84 | router.get('/get-signature', async ctx => { 85 | ctx.body = await wx.jssdk.getSignature(ctx.request.query.url); 86 | }); 87 | ``` 88 | 3.Now you can get to the next step in your browser to pass the verification. 89 | 90 | 91 | ## Browser Side Usage 92 | 93 | You can use it from the browser side as follows. Since we have [configured the `browser` field in package.json](https://github.com/yuezk/wechat-jssdk/blob/3ab192a5a67e8db65b2ae6cd9978013eef363b73/package.json#L7), the bundlers (e.g., webpack or rollup, etc.) will resolve the module to `wechat-jssdk/dist/client.umd.js`. 94 | 95 | ```javascript 96 | const WechatJSSDK = require('wechat-jssdk'); 97 | //ES6 import 98 | import WechatJSSDK from 'wechat-jssdk'; 99 | const wechatObj = new WechatJSSDK(config) 100 | 101 | // or if you do not have a bundle process, just add the script tag, and access "WechatJSSDK" from window, e.g: 102 | const wechatObj = new window.WechatJSSDK(config) 103 | ``` 104 | where config will be: 105 | ```javascript 106 | const config = { 107 | //below are mandatory options to finish the wechat signature verification 108 | //the 4 options below should be received like api '/get-signature' above 109 | 'appId': 'app_id', 110 | 'nonceStr': 'your_nonceStr', 111 | 'signature': 'url_signature', 112 | 'timestamp': 'your_timestamp', 113 | //below are optional 114 | //enable debug mode, same as debug 115 | 'debug': true, 116 | 'jsApiList': [], //optional, pass all the jsapi you want, the default will be ['onMenuShareTimeline', 'onMenuShareAppMessage'] 117 | 'customUrl': '' //set custom weixin js script url, usually you don't need to add this js manually 118 | } 119 | const wechatObj = new WechatJSSDK(config); 120 | wechatObj.initialize() 121 | .then(w => { 122 | //set up your share info, "w" is the same instance as "wechatObj" 123 | }) 124 | .catch(err => { 125 | console.error(err); 126 | }); 127 | ``` 128 | after signature signed successfully, you can customize the share information: 129 | 130 | ```javascript 131 | //customize share on chat info 132 | //sugar method for `wechatObj.callWechatApi('onMenuShareAppMessage', {...})` 133 | wechatObj.shareOnChat({ 134 | type: 'link', 135 | title: 'title', 136 | link: location.href, 137 | imgUrl: '/logo.png', 138 | desc: 'description', 139 | success: function (){}, 140 | cancel: function (){} 141 | }); 142 | //customize share on timeline info 143 | //sugar method 144 | wechatObj.shareOnMoment({ 145 | type: 'link', 146 | title: 'title', 147 | link: location.href, 148 | imgUrl: '/logo.png' 149 | }); 150 | ``` 151 | You can also access the original wechat object `wx` from `wechatObj.getOriginalWx()`. 152 | 153 | Call other wechat apis: `wechatObj.callWechatApi(apiName, config)`: 154 | 155 | ```javascript 156 | wechatObj.callWechatApi('onMenuShareAppMessage', { 157 | type: 'link', 158 | title: 'title', 159 | link: location.href, 160 | imgUrl: '/logo.png', 161 | desc: 'description', 162 | success: function (){}, 163 | cancel: function (){} 164 | }); 165 | ``` 166 | or with the original one: 167 | `wechatObj.getOriginalWx().onMenuShareAppMessage(config)` 168 | 169 | ## OAuth 170 | Wechat support web OAuth to get user profile in wechat app. 171 | In your page, provide a link, which you can get by `wx.oauth.snsUserInfoUrl` which is the default oauth url, to the wechat OAuth page, 172 | also you need provide a callback url(as show below) to get the wechat code after user click Agree button, the callback url is configured in the `wechatConfig` object above while initializing, 173 | but you can customize your own callback url by using `wx.oauth.generateOAuthUrl(customUrl, scope, state)` api. 174 | ```javascript 175 | //in node: 176 | const wx = new Wechat(config); 177 | const url = wx.oauth.generateOAuthUrl('http://mycustom.com/oauth-callback', 'snsapi_userinfo', 'custom_state'); 178 | res.render("oauth-page", { 179 | wechatOAuthUrl: url, 180 | }); 181 | //insert "wechatOAuthUrl" into your html: 182 | 183 | //custom callback url, agree clicked by user, come back here: 184 | router.get('/oauth-callback', function (req, res) { 185 | wx.oauth.getUserInfo(req.query.code) 186 | .then(function(userProfile) { 187 | console.log(userProfile) 188 | res.render("demo", { 189 | wechatInfo: userProfile 190 | }); 191 | }); 192 | }); 193 | ``` 194 | 195 | ## Cards and Offers 196 | 197 | since(v3.1) 198 | Set `card: true` in config to enable the cards support on server side, see [demo](#demo). 199 | For cards APIs, see [cards apis](https://github.com/JasonBoy/wechat-jssdk/wiki/API#card-apis) 200 | 201 | ## Payment 202 | 203 | since(v3.1) 204 | Set `payment: true` in config to enable the payment support on server side, you should also provide payment related info. 205 | See [demo](#demo). 206 | For payment APIs, see [payment apis](https://github.com/JasonBoy/wechat-jssdk/wiki/API#payment-apis) 207 | 208 | ## Mini Program 209 | 210 | since(v4) 211 | To enable mini program support([see API](https://github.com/JasonBoy/wechat-jssdk/wiki/API#mini-programv4)), you can just set mini program `appId` & `appSecret` in config: 212 | ```javascript 213 | const { Wechat, MiniProgram } = require('wechat-jssdk'); 214 | const wechatConfig = { 215 | "appId": "appid", 216 | "appSecret": "app_secret", 217 | //...other configs 218 | //... 219 | //mini program config 220 | "miniProgram": { 221 | "appId": "mp_appid", 222 | "appSecret": "mp_app_secret", 223 | } 224 | }; 225 | const wx = new Wechat(wechatConfig); 226 | //access mini program instance 227 | wx.miniProgram.getSession('code'); 228 | 229 | //Use MiniProgram directly 230 | const miniProgram = new MiniProgram({ 231 | miniProgram: { 232 | "appId": "mp_appid", 233 | "appSecret": "mp_app_secret", 234 | } 235 | }) 236 | ``` 237 | 238 | ## Using Stores 239 | 240 | [Store](https://github.com/JasonBoy/wechat-jssdk/wiki/Store) are used to save url signatures into files, dbs, etc..., but also keep a copy in memory for better performence. 241 | The default store used is `FileStore` which will persist tokens and signatures into `wechat-info.json` file every 10 minutes, also it will load these info from the file in next initialization. 242 | Built in Stores: `FileStore`, `MongoStore`, 243 | ### Using Custom Stores: 244 | 245 | ```javascript 246 | ... 247 | const {Wechat, MongoStore, FileStore} = require('wechat-jssdk'); 248 | const wx = new Wechat({ 249 | appId: 'xxx', 250 | ..., 251 | //file store 252 | //store: new FileStore(), 253 | //====== 254 | //pass the MongoStore instance to config 255 | //default 127.0.0.1:27017/wechat db, no need to pass anything to constructor 256 | store: new MongoStore({ 257 | //dbName: 'myWechat', //default wechat 258 | dbAddress: 'mongodb://127.0.0.1:27017/wechat', //set the whole connection uri by yourself 259 | dbOptions: {}, //set mongoose connection config 260 | }) 261 | }) 262 | 263 | ``` 264 | 265 | ### Create your own Store 266 | 267 | You can also create own Store to store tokens anywhere you want, by doing that, you may need to extend the base `Store` class, and reimplement the [apis](https://github.com/JasonBoy/wechat-jssdk/wiki/Store) you need: 268 | 269 | ```javascript 270 | const {Store} = require('wechat-jssdk'); 271 | class CustomStore extends Store { 272 | constructor (options) { 273 | super(); 274 | console.log('using my own store!'); 275 | } 276 | } 277 | ``` 278 | 279 | ## APIs 280 | see [API wiki](https://github.com/JasonBoy/wechat-jssdk/wiki/API) 281 | 282 | [A Blog About This](http://blog.lovemily.me/next-generation-wechat-jssdk-integration-with-nodejs/) 283 | 284 | ## Debug 285 | 286 | Add `DEBUG=wechat*` when start your app to enable wechat-jssdk debug 287 | `DEBUG=wechat* node your-app.js` 288 | 289 | ## Demo 290 | 291 | In v3.1+, the demo page is updated to test the new added `Cards & Offers` and `Payment` support. 292 | Copy the `demo/wechat-config-sample.js` to `demo/wechat-config.js`, 293 | and use your own `appId`, `appSecret`, and other [configs](#wechat-config) like payment if your want to enable them. 294 | 295 | Use `npm start` or `npm run dev` to start the demo. 296 | 297 | ## LICENSE 298 | 299 | MIT @ 2016-present [jason](http://blog.lovemily.me) 300 | -------------------------------------------------------------------------------- /assets/jason-wx-reward-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonBoy/wechat-jssdk/331c8722a89732e22e7910207c8092d332202ca1/assets/jason-wx-reward-code.png -------------------------------------------------------------------------------- /demo/Order.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('wechat-Order'); 4 | const path = require('path'); 5 | const low = require('lowdb'); 6 | const FileSync = require('lowdb/adapters/FileSync'); 7 | const isEmpty = require('lodash.isempty'); 8 | 9 | const adapter = new FileSync(path.join(__dirname, '../db_demo.json')); 10 | const db = low(adapter); 11 | 12 | const utils = require('../lib/utils'); 13 | const Payment = require('../lib/Payment'); 14 | 15 | db.defaults({ 16 | //our own system orders 17 | orders: [], 18 | //wechat unified orders 19 | unifiedOrders: [], 20 | //wechat notified order states 21 | wechatOrders: [], 22 | //refund 23 | refundOrders: [], 24 | //save wechat payment notify result 25 | wechatNotifyOrders: [], 26 | wechatNotifyRefunds: [], 27 | }).write(); 28 | 29 | const defaultInfo = { 30 | device_info: 'wechat_test_web', 31 | body: `ORDER_测试`, 32 | detail: JSON.stringify({ 33 | goods_detail: [ 34 | { 35 | goods_id: 'iphone6s_16G', 36 | wxpay_goods_id: '1001', 37 | goods_name: 'iPhone6s 16G', 38 | quantity: 1, 39 | price: 528800, 40 | goods_category: '123456', 41 | body: '苹果手机', 42 | }, 43 | ], 44 | }), 45 | attach: '上海分店', 46 | total_fee: '101', 47 | spbill_create_ip: '127.0.0.1', 48 | // time_start: utils.simpleDate(now), 49 | // time_expire: utils.simpleDate(nowPlusTwoHours), 50 | // goods_tag: 'wx_test', 51 | trade_type: Payment.PAYMENT_TYPE.JSAPI, 52 | // notify_url: 'http://beautytest.yjyyun.com/payment/', 53 | // product_id: '', 54 | // limit_pay: '', 55 | // openid: info.openId, 56 | scene_info: JSON.stringify({ 57 | id: 'SH001', 58 | name: '上大餐厅', 59 | area_code: '200100', 60 | address: '广中路引力楼1楼', 61 | }), 62 | }; 63 | 64 | /** 65 | * A demo implementation for order & payment 66 | */ 67 | class Order { 68 | constructor(options) { 69 | this.payment = options.payment; 70 | } 71 | 72 | createOrderCase1(info) { 73 | const now = new Date(); 74 | const order = Object.assign( 75 | {}, 76 | defaultInfo, 77 | { 78 | body: defaultInfo.body + '_1', 79 | time_start: utils.simpleDate(now), 80 | total_fee: '101', 81 | // goods_tag: 'wx_test', 82 | }, 83 | info 84 | ); 85 | return this.createOrder(order); 86 | } 87 | 88 | createOrderCase2(info) { 89 | const now = new Date(); 90 | const order = Object.assign( 91 | {}, 92 | defaultInfo, 93 | { 94 | body: defaultInfo.body + '_2', 95 | time_start: utils.simpleDate(now), 96 | total_fee: '102', 97 | goods_tag: 'wx_test', 98 | }, 99 | info 100 | ); 101 | return this.createOrder(order); 102 | } 103 | 104 | createOrderCase3(info) { 105 | const now = new Date(); 106 | const order = Object.assign( 107 | {}, 108 | defaultInfo, 109 | { 110 | body: defaultInfo.body + '_3', 111 | time_start: utils.simpleDate(now), 112 | total_fee: '130', 113 | goods_tag: 'wx_test', 114 | }, 115 | info 116 | ); 117 | return this.createOrder(order); 118 | } 119 | 120 | createOrderCase4(info) { 121 | const now = new Date(); 122 | const order = Object.assign( 123 | {}, 124 | defaultInfo, 125 | { 126 | body: defaultInfo.body + '_4', 127 | time_start: utils.simpleDate(now), 128 | total_fee: '131', 129 | goods_tag: 'wx_test', 130 | }, 131 | info 132 | ); 133 | return this.createOrder(order); 134 | } 135 | 136 | createOrderCase5(info) { 137 | const now = new Date(); 138 | const order = Object.assign( 139 | {}, 140 | defaultInfo, 141 | { 142 | body: defaultInfo.body + '_5', 143 | time_start: utils.simpleDate(now), 144 | total_fee: '132', 145 | goods_tag: 'wx_test', 146 | }, 147 | info 148 | ); 149 | return this.createOrder(order); 150 | } 151 | 152 | createOrderCase6(info) { 153 | const now = new Date(); 154 | const order = Object.assign( 155 | {}, 156 | defaultInfo, 157 | { 158 | body: defaultInfo.body + '_6', 159 | time_start: utils.simpleDate(now), 160 | total_fee: '133', 161 | goods_tag: 'wx_test', 162 | }, 163 | info 164 | ); 165 | return this.createOrder(order); 166 | } 167 | 168 | createOrderCase7(info) { 169 | const now = new Date(); 170 | const order = Object.assign( 171 | {}, 172 | defaultInfo, 173 | { 174 | body: defaultInfo.body + '_7', 175 | time_start: utils.simpleDate(now), 176 | total_fee: '134', 177 | goods_tag: 'wx_test', 178 | }, 179 | info 180 | ); 181 | return this.createOrder(order); 182 | } 183 | 184 | //optional 185 | createOrderCase8(info) { 186 | const now = new Date(); 187 | const order = Object.assign( 188 | {}, 189 | defaultInfo, 190 | { 191 | body: defaultInfo.body + '_8', 192 | time_start: utils.simpleDate(now), 193 | total_fee: '179', 194 | goods_tag: 'wx_test', 195 | }, 196 | info 197 | ); 198 | return this.createOrder(order); 199 | } 200 | 201 | createOrder(order) { 202 | return this.payment 203 | .unifiedOrder(order) 204 | .then(result => { 205 | const requestData = Object.assign( 206 | { id: result.requestData.out_trade_no }, 207 | result.requestData 208 | ); 209 | const responseData = Object.assign( 210 | { id: result.responseData.out_trade_no }, 211 | result.responseData 212 | ); 213 | const hasOrder = db 214 | .get('orders') 215 | .find({ id: requestData.id }) 216 | .has('id') 217 | .value(); 218 | if (hasOrder) { 219 | } else { 220 | db.get('orders') 221 | .push(requestData) 222 | .write(); 223 | db.get('unifiedOrders') 224 | .push(responseData) 225 | .write(); 226 | debug('add new order & unified order finished!'); 227 | } 228 | return Promise.resolve(responseData); 229 | }) 230 | .then(data => { 231 | return this.payment 232 | .generateChooseWXPayInfo(data.prepay_id) 233 | .then(chooseWXPayData => { 234 | console.log('parsed data:', data); 235 | console.log('WXpaydata data:', chooseWXPayData); 236 | return Promise.resolve({ 237 | orderId: data.out_trade_no, 238 | chooseWXPay: chooseWXPayData, 239 | }); 240 | }); 241 | }); 242 | } 243 | 244 | queryOrder(tradeNo) { 245 | return this.payment 246 | .queryOrder({ 247 | out_trade_no: tradeNo, 248 | }) 249 | .then(result => { 250 | const temp = Object.assign( 251 | { id: result.responseData.out_trade_no }, 252 | result.responseData 253 | ); 254 | db.get('wechatOrders') 255 | .push(temp) 256 | .write(); 257 | debug('write wechat query order finished!'); 258 | return Promise.resolve(result); 259 | }); 260 | } 261 | 262 | getOrderFromDB(tradeNo) { 263 | return db 264 | .get('orders') 265 | .find({ id: tradeNo }) 266 | .value(); 267 | } 268 | 269 | updateNotifyResult(data) { 270 | const order = db 271 | .get('wechatNotifiesOrders') 272 | .find({ id: data.out_trade_no }) 273 | .value(); 274 | db.get('orders') 275 | .find({ id: data.out_trade_no }) 276 | .assign({ processed: true }) 277 | .value(); 278 | 279 | if (!isEmpty(order)) { 280 | if (order.processed) return; 281 | //update existing order info 282 | db.get('wechatNotifiesOrders') 283 | .find({ id: data.out_trade_no }) 284 | .assign(data) 285 | .write(); 286 | return; 287 | } 288 | const temp = Object.assign( 289 | { id: data.out_trade_no, processed: true }, 290 | data 291 | ); 292 | db.get('wechatNotifiesOrders') 293 | .push(temp) 294 | .write(); 295 | } 296 | 297 | updateNotifyRefundResult(data) { 298 | const order = db 299 | .get('wechatNotifyRefunds') 300 | .find({ id: data.out_trade_no }) 301 | .value(); 302 | if (!isEmpty(order)) { 303 | if (order.processed) return; 304 | //update existing order info 305 | db.get('wechatNotifyRefunds') 306 | .find({ id: data.out_trade_no }) 307 | .assign(data) 308 | .write(); 309 | return; 310 | } 311 | const temp = Object.assign( 312 | { id: data.out_trade_no, processed: true }, 313 | data 314 | ); 315 | db.get('wechatNotifyRefunds') 316 | .push(temp) 317 | .write(); 318 | } 319 | } 320 | 321 | module.exports = Order; 322 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Wechat JSSDK DEMO 8 | 9 | 10 | 11 |
=====点击右上角分享!=====
12 |
=====OAuth 网页授权=====
13 |

OAuth 用户信息授权

14 |

Implicit OAuth 静默授权

15 |

OAuth without new code 用缓存code直接拿用户信息

16 |
=====CARD 卡券=====
17 |

ChooseCard 选择卡券并打开

18 |

OpenCard 打开卡券

19 |

AddCard 添加卡券

20 |
=====Payment 支付=====
21 |
22 | 23 | 24 |
25 |

QueryOrder 查询上一个创建的订单

26 |

UnifiedOrder 创建订单用例1

27 |

UnifiedOrder 创建订单用例2

28 |

UnifiedOrder 创建订单用例3

29 |

UnifiedOrder 创建订单用例4

30 |

UnifiedOrder 创建订单用例5

31 |

UnifiedOrder 创建订单用例6

32 |

UnifiedOrder 创建订单用例7

33 |

UnifiedOrder 创建订单用例8(可选)

34 |

下载对账单

35 |

chooseWXPay 微信支付上一个订单

36 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const http = require('http'); 5 | const nunjucks = require('nunjucks'); 6 | const { Wechat, Payment } = require('../lib'); 7 | const path = require('path'); 8 | const debug = require('debug')('wechat-demo'); 9 | const bodyParser = require('body-parser'); 10 | const isEmpty = require('lodash.isempty'); 11 | const utils = require('../lib/utils'); 12 | 13 | const cookieParser = require('cookie-parser'); 14 | const session = require('express-session'); 15 | 16 | const wechatConfig = require('./wechat-config'); 17 | const Order = require('./Order'); 18 | 19 | const DOMAIN = wechatConfig.domain; 20 | 21 | const wx = new Wechat(wechatConfig); 22 | 23 | const order = new Order({ payment: wx.payment }); 24 | 25 | const app = express(); 26 | 27 | nunjucks.configure(__dirname, { 28 | autoescape: true, 29 | express: app, 30 | noCache: true, 31 | }); 32 | 33 | // app.engine('html', nunjucks); 34 | app.set('view engine', 'html'); 35 | app.enable('trust proxy'); 36 | app.set('views', __dirname); 37 | 38 | app.use(cookieParser()); 39 | app.use( 40 | session({ 41 | name: 'sid', 42 | secret: 'wechat-app', 43 | saveUninitialized: true, 44 | resave: true, 45 | }) 46 | ); 47 | 48 | app.use(function(req, res, next) { 49 | res.locals.appId = wechatConfig.appId; 50 | res.locals.domain = DOMAIN; 51 | next(); 52 | }); 53 | 54 | app.get('/', function(req, res) { 55 | //also you can generate one at runtime: 56 | const implicitOAuthUrl = wx.oauth.generateOAuthUrl( 57 | DOMAIN + '/implicit-oauth', 58 | 'snsapi_base' 59 | ); 60 | res.render('index.html', { 61 | oauthUrl: wx.oauth.snsUserInfoUrl, 62 | implicitOAuth: implicitOAuthUrl, 63 | }); 64 | }); 65 | 66 | app.get('/api/wechat', function(req, res) { 67 | if (wx.jssdk.verifySignature(req.query)) { 68 | res.send(req.query.echostr); 69 | return; 70 | } 71 | res.send('error'); 72 | }); 73 | 74 | app.get('/get-signature', function(req, res) { 75 | console.log(req.query); 76 | wx.jssdk.getSignature(req.query.url).then( 77 | data => { 78 | console.log('OK', data); 79 | res.json(data); 80 | }, 81 | reason => { 82 | console.error(reason); 83 | res.json(reason); 84 | } 85 | ); 86 | }); 87 | 88 | /** 89 | * @see wechatRedirectUrl in Wechat config 90 | */ 91 | app.get('/oauth', function(req, res) { 92 | //use default openid as the key 93 | const key = req.session.openid; 94 | 95 | //use custom key for oauth token store 96 | // const key = req.sessionID; 97 | // console.log('oauth sessionID: %s', key); 98 | wx.oauth.getUserInfo(req.query.code, key).then(function(userProfile) { 99 | console.log('userProfile:', userProfile); 100 | //set openid to session to use in following request 101 | req.session.openid = userProfile.openid; 102 | console.log(req.session.openid); 103 | res.render('oauth.html', { 104 | wechatInfo: JSON.stringify(userProfile), 105 | }); 106 | }); 107 | }); 108 | 109 | app.get('/implicit-oauth', function(req, res) { 110 | const redirect = req.query.from; 111 | wx.oauth.getUserBaseInfo(req.query.code).then(function(tokenInfo) { 112 | console.log('implicit oauth: ', tokenInfo); 113 | // console.log('implicit oauth: ', JSON.stringify(tokenInfo)); 114 | req.session.openid = tokenInfo.openid; 115 | if (redirect) { 116 | res.redirect(redirect); 117 | return; 118 | } 119 | res.render('oauth.html', { 120 | wechatInfo: JSON.stringify(tokenInfo, null, 2), 121 | }); 122 | }); 123 | }); 124 | 125 | app.get('/oauth-cache', function(req, res) { 126 | const key = req.session.openid; 127 | console.log('openid: ', key); 128 | 129 | // const sid = req.sessionID; 130 | // console.log('sessionID: %s', sid); 131 | 132 | //get user info without code, but with cached access token, 133 | //if cached token is expired, or cannot refresh the token, 134 | //it will redirect to the "/oauth" router above in catch handler to get new code 135 | wx.oauth 136 | .getUserInfo(null, key) 137 | .then(function(userProfile) { 138 | console.log(userProfile); 139 | res.render('oauth.html', { 140 | wechatInfo: JSON.stringify(userProfile), 141 | }); 142 | }) 143 | .catch(() => { 144 | //need to get new code 145 | res.redirect(wx.oauth.snsUserInfoUrl); 146 | }); 147 | }); 148 | 149 | app.get('/choose-card', function(req, res) { 150 | const qs = req.query; 151 | wx.card 152 | .getCardSignature(qs.shopId, qs.cardType, qs.cardId) 153 | .then(sigInfo => { 154 | res.json(sigInfo); 155 | }) 156 | .catch(reason => { 157 | res.json(reason); 158 | }); 159 | }); 160 | 161 | app.get('/get-card-ext', function(req, res) { 162 | const qs = req.query; 163 | wx.card 164 | .getCardExt(qs.cardId, '', '', '', 'wechat-jssdk') 165 | .then(sigInfo => { 166 | res.json({ data: sigInfo }); 167 | }) 168 | .catch(reason => { 169 | res.json(reason); 170 | }); 171 | }); 172 | 173 | app.get('/decode-card-code', function(req, res) { 174 | wx.card 175 | .decryptCardCode(req.query.encryptCode) 176 | .then(data => { 177 | res.json(data); 178 | }) 179 | .catch(data => { 180 | res.json(data); 181 | }); 182 | }); 183 | 184 | app.get('/client.js', function(req, res) { 185 | res.sendFile(path.join(__dirname, '../dist/client.umd.js')); 186 | }); 187 | 188 | app.get('/create-order', function(req, res) { 189 | const openid = req.session.openid; 190 | console.log('req.session.openid:', openid); 191 | const orderCase = req.query.case; 192 | const orderInfo = { 193 | openid: req.session.openid || 'oy5F1wQTfhx4-V3L5TcUn5V9v2Lo', 194 | spbill_create_ip: '104.247.128.2', //req.ip, 195 | }; 196 | let p = undefined; 197 | switch (orderCase) { 198 | case '1': 199 | p = order.createOrderCase1(orderInfo); 200 | break; 201 | case '2': 202 | p = order.createOrderCase2(orderInfo); 203 | break; 204 | case '3': 205 | p = order.createOrderCase3(orderInfo); 206 | break; 207 | case '4': 208 | p = order.createOrderCase4(orderInfo); 209 | break; 210 | case '5': 211 | p = order.createOrderCase5(orderInfo); 212 | break; 213 | case '6': 214 | p = order.createOrderCase6(orderInfo); 215 | break; 216 | case '7': 217 | p = order.createOrderCase7(orderInfo); 218 | break; 219 | case '8': 220 | p = order.createOrderCase8(orderInfo); 221 | break; 222 | default: 223 | p = order.createOrderCase1(orderInfo); 224 | break; 225 | } 226 | 227 | p.then(data => { 228 | console.log(data.orderId); 229 | req.session.orderId = data.orderId; 230 | res.json(data.chooseWXPay); 231 | }).catch(err => { 232 | res.json(err); 233 | }); 234 | }); 235 | 236 | app.get('/query-order', function(req, res) { 237 | const orderId = req.query.tradeNo || req.session.orderId; 238 | if (!orderId) { 239 | res.json({ 240 | msg: 'no available out_trade_no!', 241 | }); 242 | return; 243 | } 244 | 245 | order 246 | .queryOrder(orderId) 247 | .then(data => { 248 | res.json(data); 249 | }) 250 | .catch(err => { 251 | res.json(err); 252 | }); 253 | }); 254 | 255 | app.get('/download-bill', function(req, res) { 256 | const query = req.query; 257 | wx.payment 258 | .downloadBill(query.billDate, Payment.DOWNLOAD_BILL_TYPE.SUCCESS) 259 | .then(result => { 260 | if (result.body) { 261 | res.type('zip'); 262 | res.send(result.body); 263 | } 264 | }) 265 | .catch(err => { 266 | console.error(err); 267 | res.json(err); 268 | }); 269 | }); 270 | 271 | app.get('/settlements', function(req, res) { 272 | const { 273 | usetag, //int 1 -> settled, 2 -> unsettled 274 | offset, //int 275 | limit, //int 276 | date_start, //string e.g. '20180701' 277 | date_end, //string e.g. '20180820' 278 | // visit https://pay.weixin.qq.com/wiki/doc/api/external/jsapi.php?chapter=9_14&index=9 for more info 279 | } = req.query; 280 | wx.payment 281 | .querySettlement({ 282 | usetag, 283 | offset, 284 | limit, 285 | date_start, 286 | date_end, 287 | }) 288 | .then(result => { 289 | res.json(result.responseData); 290 | }) 291 | .catch(err => { 292 | console.error(err); 293 | res.json(err); 294 | }); 295 | }); 296 | 297 | app.get('/exchange-rate', (req, res) => { 298 | const { 299 | fee_type, //string 'USD' 300 | date, //string '20180801' 301 | // visit https://pay.weixin.qq.com/wiki/doc/api/external/jsapi.php?chapter=9_15&index=10 for more info 302 | } = req.query; 303 | wx.payment 304 | .queryExchangeRate({ 305 | fee_type: fee_type || 'GBP', 306 | date: date || utils.simpleDate(new Date(), 'YYYYMMDD'), 307 | }) 308 | .then(result => { 309 | res.json(result.responseData); 310 | }) 311 | .catch(err => { 312 | console.error(err); 313 | res.json(err); 314 | }); 315 | }); 316 | 317 | //demo: unified order pay result notify_url goes here 318 | app.post('/pay-result-notify', bodyParser.text(), function(req, res) { 319 | wx.payment 320 | .parseNotifyData(req.body) 321 | .then(data => { 322 | const sign = data.sign; 323 | data.sign = undefined; 324 | const genSignData = wx.payment.generateSignature(data, data.sign_type); 325 | //case test, only case 6 will return sign 326 | if (!sign || (sign && sign === genSignData.sign)) { 327 | const tradeNo = data.out_trade_no; 328 | if (tradeNo) { 329 | const order = order.getOrderFromDB(tradeNo); 330 | //order info inconsistent 331 | if (isEmpty(order) || order.total_fee != data.total_fee) { 332 | return Promise.reject(new Error('notify data not consistent!')); 333 | } 334 | //already processed 335 | if (order && order.processed) { 336 | return; 337 | } 338 | } 339 | 340 | order.updateNotifyResult(data); 341 | //sign check and send back 342 | wx.payment.replyData(true).then(ret => { 343 | res.send(ret); 344 | }); 345 | return; 346 | } 347 | return Promise.reject(new Error('notify sign not matched!')); 348 | }) 349 | .catch(err => { 350 | console.error(err); 351 | // wx.payment.replyData(false).then(ret => { 352 | // res.send(ret); 353 | // }); 354 | }); 355 | }); 356 | //process refund notify result 357 | app.post('/refund-result-notify', bodyParser.text(), function(req, res) { 358 | wx.payment 359 | .decryptRefundNotifyResult(req.body) 360 | .then(result => { 361 | const parsedXMLData = result.parsedXMLData; 362 | const decryptedReqInfoData = result.decryptedData; 363 | order.updateNotifyRefundResult( 364 | Object.assign(parsedXMLData, decryptedReqInfoData) 365 | ); 366 | 367 | wx.payment.replyData(true).then(ret => { 368 | res.send(ret); 369 | }); 370 | }) 371 | .catch(err => { 372 | console.error(err); 373 | wx.payment.replyData(false).then(ret => { 374 | res.send(ret); 375 | }); 376 | }); 377 | }); 378 | 379 | const server = http.createServer(app); 380 | const port = process.env.PORT || 3000; 381 | //should use like nginx to proxy the request to 3000, the signature domain must be on PORT 80. 382 | server.listen(port); 383 | server.on('listening', function() { 384 | debug('Express listening on port %d', port); 385 | }); 386 | 387 | process.on('exit', function() { 388 | wx.store.flush(); 389 | }); 390 | -------------------------------------------------------------------------------- /demo/oauth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Wechat JSSDK DEMO 7 | 8 | 9 |

10 | User profile: 11 |

12 |

13 | {{ wechatInfo }} 14 |

15 |

16 | Back to Home 17 |

18 | 19 | -------------------------------------------------------------------------------- /demo/wechat-config-sample.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const Wechat = require('../lib'); 5 | const MongoStore = Wechat.MongoStore; 6 | const FileStore = Wechat.FileStore; 7 | 8 | const DOMAIN = 'http://your.domain.com'; 9 | 10 | module.exports = { 11 | //=====a service account test===== 12 | domain: DOMAIN, 13 | wechatToken: '', 14 | appId: '', 15 | appSecret: '', 16 | wechatRedirectUrl: `${DOMAIN}/oauth`, 17 | // store: new MongoStore({limit: 5}), 18 | store: new FileStore({ interval: 1000 * 60 * 3 }), 19 | card: true, 20 | payment: true, 21 | merchantId: '', 22 | paymentSandBox: true, //dev env 23 | paymentKey: '', 24 | // paymentSandBoxKey: '', 25 | paymentCertificatePfx: fs.readFileSync( 26 | path.join(process.cwd(), 'cert/apiclient_cert.p12') 27 | ), 28 | paymentNotifyUrl: `${DOMAIN}/api/wechat/payment/`, 29 | }; 30 | -------------------------------------------------------------------------------- /demo/wechat-jssdk-demo-legacy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonBoy/wechat-jssdk/331c8722a89732e22e7910207c8092d332202ca1/demo/wechat-jssdk-demo-legacy.gif -------------------------------------------------------------------------------- /demo/wechat-jssdk-demo-new.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JasonBoy/wechat-jssdk/331c8722a89732e22e7910207c8092d332202ca1/demo/wechat-jssdk-demo-new.gif -------------------------------------------------------------------------------- /lib/Card.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('wechat-Card'); 4 | 5 | const isEmpty = require('lodash.isempty'); 6 | 7 | const utils = require('./utils'); 8 | const config = require('./config'); 9 | 10 | const Store = require('./store/Store'); 11 | const FileStore = require('./store/FileStore'); 12 | const codeUtils = require('./code'); 13 | 14 | const wxConfig = config.getDefaultConfiguration(); 15 | 16 | const CODE_TYPE = { 17 | CODE_TYPE_QRCODE: 'CODE_TYPE_QRCODE', 18 | CODE_TYPE_BARCODE: 'CODE_TYPE_BARCODE', 19 | CODE_TYPE_ONLY_QRCODE: 'CODE_TYPE_ONLY_QRCODE', 20 | CODE_TYPE_TEXT: 'CODE_TYPE_TEXT', 21 | CODE_TYPE_NONE: 'CODE_TYPE_NONE', 22 | }; 23 | 24 | const CARD_TYPE = { 25 | GROUPON: 'GROUPON', 26 | CASH: 'CASH', 27 | DISCOUNT: 'DISCOUNT', 28 | GIFT: 'GIFT', 29 | GENERAL_COUPON: 'GENERAL_COUPON', 30 | }; 31 | 32 | const TOKEN_TYPE = 'wx_card'; 33 | 34 | class Card { 35 | /** 36 | * Wechat Card/Coupons class 37 | * @constructor 38 | * @param options 39 | * @return {Card} Card instance 40 | */ 41 | constructor(options) { 42 | config.checkPassedConfiguration(options); 43 | 44 | this.wechatConfig = isEmpty(options) 45 | ? /* istanbul ignore next */ wxConfig 46 | : Object.assign({}, wxConfig, options); 47 | 48 | /* istanbul ignore if */ 49 | if (!options.store || !(options.store instanceof Store)) { 50 | debug('[Card]Store not provided, using default FileStore...'); 51 | this.store = new FileStore(options.storeOptions); 52 | } else { 53 | this.store = options.store; 54 | } 55 | } 56 | 57 | /* istanbul ignore next */ 58 | static get CODE_TYPE() { 59 | return CODE_TYPE; 60 | } 61 | /* istanbul ignore next */ 62 | static get CARD_TYPE() { 63 | return CARD_TYPE; 64 | } 65 | 66 | /** 67 | * Get Card api_ticket 68 | * @param {string} accessToken 69 | * @return {Promise} 70 | */ 71 | getApiTicketRemotely(accessToken) { 72 | const params = { 73 | access_token: accessToken, 74 | type: TOKEN_TYPE, 75 | }; 76 | return utils 77 | .sendWechatRequest(this.wechatConfig.ticketUrl, { 78 | query: params, 79 | }) 80 | .then(data => { 81 | data = Object.assign({ modifyDate: new Date() }, data); 82 | data.errcode = undefined; 83 | data.errmsg = undefined; 84 | return this.store.updateCardTicket(data); 85 | }) 86 | .catch(reason => { 87 | /* istanbul ignore next */ 88 | debug('get card api_ticket failed!'); 89 | return Promise.reject(reason); 90 | }); 91 | } 92 | 93 | /** 94 | * Get global access token 95 | * @param {Boolean} force if should check for cached token 96 | * @return {Promise} 97 | */ 98 | getGlobalToken(force) { 99 | const cfg = this.wechatConfig; 100 | /* istanbul ignore if */ 101 | if (force) { 102 | return utils 103 | .getGlobalAccessToken(cfg.appId, cfg.appSecret, cfg.accessTokenUrl) 104 | .then(globalToken => { 105 | const info = { 106 | modifyDate: new Date(), 107 | accessToken: globalToken.access_token, 108 | }; 109 | return this.store.updateGlobalToken(info); 110 | }); 111 | } 112 | 113 | return this.store.getGlobalToken().then(globalToken => { 114 | if ( 115 | !globalToken || 116 | !globalToken.accessToken || 117 | utils.isExpired(globalToken.modifyDate) 118 | ) { 119 | debug( 120 | 'global access token was expired, getting new global access token now...' 121 | ); 122 | return utils 123 | .getGlobalAccessToken(cfg.appId, cfg.appSecret, cfg.accessTokenUrl) 124 | .then(globalToken => { 125 | const info = { 126 | modifyDate: new Date(), 127 | accessToken: globalToken.access_token, 128 | }; 129 | return this.store.updateGlobalToken(info); 130 | }); 131 | // .then(info => Promise.reject(info)); 132 | } 133 | return Promise.resolve(globalToken); 134 | }); 135 | } 136 | 137 | /** 138 | * Get card api_ticket 139 | * @return {Promise} 140 | */ 141 | getApiTicket() { 142 | return this.store 143 | .getCardTicket() 144 | .then(ticketInfo => { 145 | if ( 146 | ticketInfo && 147 | ticketInfo.ticket && 148 | !utils.isExpired(ticketInfo.modifyDate) 149 | ) { 150 | return Promise.resolve(ticketInfo); 151 | } 152 | return this.getGlobalToken().then(info => Promise.reject(info)); 153 | }) 154 | .catch(globalToken => { 155 | // console.log(globalToken); 156 | return this.getApiTicketRemotely(globalToken.accessToken); 157 | }); 158 | } 159 | 160 | /** 161 | * Generate card signature info for chooseCard function 162 | * @param {string=} shopId, aka: location_id 163 | * @param {string=} cardType 164 | * @param {string=} cardId 165 | * @return {Promise} 166 | */ 167 | getCardSignature(shopId, cardType, cardId) { 168 | const infoForCardSign = { 169 | shopId: shopId || /* istanbul ignore next */ '', //location_id 170 | cardType: cardType || /* istanbul ignore next */ '', 171 | cardId: cardId || /* istanbul ignore next */ '', 172 | timestamp: utils.timestamp(), 173 | nonceStr: utils.nonceStr(), 174 | appid: this.wechatConfig.appId, 175 | api_ticket: '', 176 | }; 177 | return this.getApiTicket() 178 | .then(ticketInfo => { 179 | infoForCardSign.api_ticket = ticketInfo.ticket; 180 | const keys = Object.keys(infoForCardSign); 181 | const values = keys.map(key => infoForCardSign[key]); 182 | values.sort(); 183 | infoForCardSign.cardSign = utils.genSHA1(values.join('')); 184 | infoForCardSign.appid = undefined; 185 | infoForCardSign.api_ticket = undefined; 186 | infoForCardSign.signType = 'SHA1'; 187 | return Promise.resolve(infoForCardSign); 188 | }) 189 | .catch(reason => { 190 | /* istanbul ignore next */ 191 | return Promise.reject(reason); 192 | }); 193 | } 194 | 195 | /** 196 | * Generate cardExt 197 | * @param {string} cardId 198 | * @param {string=} code 199 | * @param {string=} openid 200 | * @param {string=} fixed_begintimestamp 201 | * @param {string=} outer_str 202 | * @return {Promise} 203 | */ 204 | getCardExt(cardId, code, openid, fixed_begintimestamp, outer_str) { 205 | const infoForCardExt = { 206 | // card_id: cardId || '', 207 | // code: code || '', 208 | // openid: openid || '', 209 | timestamp: utils.timestamp(), 210 | nonce_str: utils.nonceStr(), 211 | // fixed_begintimestamp: fixed_begintimestamp || '', 212 | // outer_str: outer_str || '', 213 | // signature: '', 214 | }; 215 | /* istanbul ignore else */ 216 | if (cardId) { 217 | infoForCardExt.card_id = cardId; 218 | } 219 | /* istanbul ignore else */ 220 | if (code) { 221 | infoForCardExt.code = code; 222 | } 223 | /* istanbul ignore else */ 224 | if (openid) { 225 | infoForCardExt.openid = openid; 226 | } 227 | return this.getApiTicket() 228 | .then(ticketInfo => { 229 | infoForCardExt.api_ticket = ticketInfo.ticket; 230 | const keys = Object.keys(infoForCardExt); 231 | const values = keys.map(key => infoForCardExt[key]); 232 | infoForCardExt.signature = utils.genSHA1(values.sort().join('')); 233 | fixed_begintimestamp && 234 | (infoForCardExt.fixed_begintimestamp = fixed_begintimestamp); 235 | outer_str && (infoForCardExt.outer_str = outer_str); 236 | infoForCardExt.api_ticket = undefined; 237 | return Promise.resolve(JSON.stringify(infoForCardExt)); 238 | }) 239 | .catch(reason => { 240 | /* istanbul ignore next */ 241 | return Promise.reject(reason); 242 | }); 243 | } 244 | 245 | /** 246 | * Simply send decode card encrypt_code api 247 | * @param {String} encryptCode encrypt_code of real card code 248 | * @param {object} qs querystring object to send with the request 249 | * @return {Promise} 250 | */ 251 | sendDecodeRequest(encryptCode, qs) { 252 | return utils.sendWechatRequest(this.wechatConfig.decodeCardCodeUrl, { 253 | query: qs, 254 | method: 'POST', 255 | json: true, 256 | body: { 257 | encrypt_code: encryptCode, 258 | }, 259 | }); 260 | } 261 | 262 | /** 263 | * Decode/Decrypt card encrypt_code to get real card code 264 | * @param {string} encryptCode 265 | * @return {Promise} 266 | */ 267 | decryptCardCode(encryptCode) { 268 | return this.getGlobalToken().then(info => { 269 | const accessToken = info.accessToken; 270 | const params = { 271 | access_token: accessToken, 272 | }; 273 | return this.sendDecodeRequest(encryptCode, params).catch(reason => { 274 | debug('decode card encrypt_code failed!'); 275 | //retry when access token error 276 | if (codeUtils.errorByAccessTokenRelated(reason.errcode)) { 277 | return this.getGlobalToken(true).then(info => { 278 | const accessToken = info.accessToken; 279 | const params = { 280 | access_token: accessToken, 281 | }; 282 | return this.sendDecodeRequest(encryptCode, params).catch(reason => { 283 | debug( 284 | 'decode card encrypt_code failed again, tray again later!!!' 285 | ); 286 | return Promise.reject(reason); 287 | }); 288 | }); 289 | } 290 | return Promise.reject(reason); 291 | }); 292 | }); 293 | } 294 | } 295 | 296 | module.exports = Card; 297 | -------------------------------------------------------------------------------- /lib/JSSDK.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('wechat-JSSDK'); 4 | 5 | const isEmpty = require('lodash.isempty'); 6 | const urlParser = require('url'); 7 | 8 | const utils = require('./utils'); 9 | const config = require('./config'); 10 | 11 | const Store = require('./store/Store'); 12 | const FileStore = require('./store/FileStore'); 13 | 14 | const wxConfig = config.getDefaultConfiguration(); 15 | 16 | class JSSDK { 17 | /** 18 | * Pass custom wechat config for the instance 19 | * @constructor 20 | * @param {object=} options 21 | * @see ./config.js 22 | * @return {JSSDK} JSSDK instance 23 | */ 24 | constructor(options) { 25 | config.checkPassedConfiguration(options); 26 | this.refreshedTimes = 0; 27 | this.wechatConfig = isEmpty(options) 28 | ? /* istanbul ignore next */ wxConfig 29 | : Object.assign({}, wxConfig, options); 30 | 31 | //no custom store provided, using default FileStore 32 | /* istanbul ignore if */ 33 | if (!options.store || !(options.store instanceof Store)) { 34 | debug('[JSSDK]Store not provided, using default FileStore...'); 35 | this.store = new FileStore(options.storeOptions); 36 | } else { 37 | this.store = options.store; 38 | } 39 | 40 | //clear the counter every 2 hour 41 | setInterval( 42 | () => (this.refreshedTimes = 0), 43 | options.clearCountInterval || 1000 * 7200 44 | ); 45 | } 46 | 47 | /** 48 | * Check if token is expired starting from the modify date 49 | * @param modifyDate 50 | * @static 51 | * @return {boolean} 52 | */ 53 | static isTokenExpired(modifyDate) { 54 | return utils.isExpired(modifyDate); 55 | } 56 | 57 | /** 58 | * Create NonceStr before generating the signature 59 | * @static 60 | * @return {string} 61 | */ 62 | static createNonceStr() { 63 | return utils.nonceStr(); 64 | } 65 | 66 | /** 67 | * Filter the signature for the client 68 | * @param {object} originalSignatureObj original signature information 69 | * @return {object} filtered signature object 70 | */ 71 | filterSignature(originalSignatureObj) { 72 | if (!originalSignatureObj) { 73 | return {}; 74 | } 75 | return { 76 | appId: this.wechatConfig.appId, 77 | timestamp: originalSignatureObj.timestamp, 78 | nonceStr: originalSignatureObj.nonceStr, 79 | signature: originalSignatureObj.signature, 80 | url: originalSignatureObj.url, 81 | }; 82 | } 83 | 84 | /** 85 | * Remove hash from the url, wechat signature does not need it 86 | * @param {string} url original url 87 | * @static 88 | * @return {string} 89 | */ 90 | static normalizeUrl(url) { 91 | const temp = urlParser.parse(url); 92 | const hashIndex = url.indexOf(temp.hash); 93 | //remove hash from url 94 | return hashIndex > 0 ? url.substring(0, hashIndex) : url; 95 | } 96 | 97 | /** 98 | * Generate the url signature with the provided info 99 | * @param {string} url current url 100 | * @param {string} accessToken 101 | * @param {string} ticket js ticket 102 | * @static 103 | * @returns {object} generated wechat signature info 104 | */ 105 | static generateSignature(url, accessToken, ticket) { 106 | const ret = { 107 | jsapi_ticket: ticket, 108 | nonceStr: JSSDK.createNonceStr(), 109 | timestamp: utils.timestamp(), 110 | url: JSSDK.normalizeUrl(url), 111 | }; 112 | const originalStr = utils.paramsToString(ret); 113 | ret.signature = utils.genSHA1(originalStr); 114 | ret.accessToken = accessToken; 115 | return ret; 116 | } 117 | 118 | /** 119 | * Need to verify before you are a wechat developer 120 | * @param {object} query url query sent by the wechat server to do the validation 121 | * @return {boolean} 122 | */ 123 | verifySignature(query) { 124 | const keys = [ 125 | this.wechatConfig.wechatToken, 126 | query['timestamp'], 127 | query['nonce'], 128 | ]; 129 | let str = keys.sort().join(''); 130 | str = utils.genSHA1(str); 131 | return str === query.signature; 132 | } 133 | 134 | /** 135 | * Send request to get wechat access token 136 | * @return {Promise} 137 | */ 138 | getAccessToken() { 139 | const cfg = this.wechatConfig; 140 | return utils.getGlobalAccessToken( 141 | cfg.appId, 142 | cfg.appSecret, 143 | cfg.accessTokenUrl 144 | ); 145 | } 146 | 147 | /** 148 | * Get wechat ticket with the accessToken 149 | * @param {string} accessToken token received from the @see getAccessToken above 150 | * @return {Promise} 151 | */ 152 | getJsApiTicket(accessToken) { 153 | const params = { 154 | access_token: accessToken, 155 | type: 'jsapi', 156 | }; 157 | return utils 158 | .sendWechatRequest(this.wechatConfig.ticketUrl, { 159 | query: params, 160 | }) 161 | .then(data => data) 162 | .catch(reason => { 163 | debug('get ticket failed!'); 164 | return Promise.reject(reason); 165 | }); 166 | } 167 | 168 | /** 169 | * Update the global token or js_ticket, we should cache this to prevent requesting too often 170 | * @param {string} token 171 | * @param {string} ticket 172 | * @return {Promise} resolved with the updated globalToken object 173 | */ 174 | updateAccessTokenOrTicketGlobally(token, ticket) { 175 | const info = { modifyDate: new Date() }; 176 | token && (info.accessToken = token); 177 | ticket && (info.jsapi_ticket = ticket); 178 | return this.store.updateGlobalToken(info); 179 | } 180 | 181 | /** 182 | * Get new access token and ticket from wechat server, and update that to cache 183 | * @param {boolean=} force force update, by default it will only get at most 5 times within 2 hours, 184 | * cause the wechat server limits the access_token requests number 185 | * @return {Promise} 186 | */ 187 | getGlobalTokenAndTicket(force) { 188 | force || this.refreshedTimes++; 189 | /* istanbul ignore if */ 190 | if (!force && this.refreshedTimes > 5) { 191 | return Promise.reject( 192 | new Error('maximum manual refresh threshold reached!') 193 | ); 194 | } 195 | let accessToken = ''; 196 | return this.getAccessToken() 197 | .then(result => { 198 | accessToken = result.access_token; 199 | return accessToken; 200 | }) 201 | .catch(reason => { 202 | debug('get new global token failed!'); 203 | return Promise.reject(reason); 204 | }) 205 | .then(receivedAccessToken => { 206 | return this.getJsApiTicket(receivedAccessToken); 207 | }) 208 | .then(ticketResult => { 209 | return this.updateAccessTokenOrTicketGlobally( 210 | accessToken, 211 | ticketResult.ticket 212 | ); 213 | }) 214 | .catch(ticketReason => { 215 | debug('get new global ticket failed!'); 216 | debug(ticketReason); 217 | return Promise.reject(ticketReason); 218 | }); 219 | } 220 | 221 | /** 222 | * Get or generate global token info for signature generating process 223 | * @return {Promise} 224 | */ 225 | prepareGlobalToken() { 226 | return this.store.getGlobalToken().then(globalToken => { 227 | if ( 228 | !globalToken || 229 | !globalToken.accessToken || 230 | JSSDK.isTokenExpired(globalToken.modifyDate) 231 | ) { 232 | debug( 233 | 'global access token was expired, getting new global access token and ticket now...' 234 | ); 235 | return this.getGlobalTokenAndTicket(true); 236 | } 237 | debug('global ticket exists, use cached access token'); 238 | return Promise.resolve(globalToken); 239 | }); 240 | } 241 | 242 | /** 243 | * Save or update the signature 244 | * @param {object} info signature information to save 245 | * @return {Promise} 246 | */ 247 | saveSignature(info) { 248 | const signature = Object.assign({}, info); 249 | signature.createDate = new Date(); 250 | signature.modifyDate = signature.createDate; 251 | return this.store 252 | .isSignatureExisting(signature.url) 253 | .then(existing => { 254 | if (existing) { 255 | debug('wechat url signature existed, try updating the signature...'); 256 | return this.updateSignature(signature.url, signature); 257 | } 258 | return this.store.saveSignature(signature.signatureName, signature); 259 | }) 260 | .then(sig => { 261 | debug('create/update wechat signature finished'); 262 | return Promise.resolve(sig); 263 | }); 264 | } 265 | 266 | /** 267 | * Update the signature for existing url 268 | * @param {string} url signature of url need to update 269 | * @param {object} info update info need to be updated to the existing url signature info 270 | * @return {Promise} 271 | */ 272 | updateSignature(url, info) { 273 | url = JSSDK.normalizeUrl(url); 274 | info.modifyDate = new Date(); 275 | delete info.createDate; 276 | delete info.url; //prevent changing the original url 277 | delete info.signatureName; //prevent changing the original name 278 | return this.store.updateSignature(url, info).then(sig => { 279 | debug('update wechat signature finished'); 280 | return Promise.resolve(sig); 281 | }); 282 | } 283 | 284 | /** 285 | * Get the signature from cache or create a new one 286 | * @param {string} url 287 | * @param {boolean=} forceNewSignature if true, generate a new signature rather than getting from cache 288 | * @return {Promise} 289 | */ 290 | getSignature(url, forceNewSignature) { 291 | url = JSSDK.normalizeUrl(url); 292 | return this.store.getSignature(url).then(signature => { 293 | if ( 294 | !forceNewSignature && 295 | signature && 296 | !JSSDK.isTokenExpired(signature.modifyDate) 297 | ) { 298 | signature = this.filterSignature(signature); 299 | return Promise.resolve(signature); 300 | } 301 | return this.createSignature(url); 302 | }); 303 | } 304 | 305 | /** 306 | * Create a new signature now, and save to store 307 | * @param {string} url signature will be created for the url 308 | * @return {Promise} resolved with filtered signature results 309 | */ 310 | createSignature(url) { 311 | return this.prepareGlobalToken() 312 | .then(data => { 313 | const ret = JSSDK.generateSignature( 314 | url, 315 | data.accessToken, 316 | data.jsapi_ticket 317 | ); 318 | ret.signatureName = ret.url; 319 | return this.saveSignature(ret); 320 | }) 321 | .then(sig => this.filterSignature(sig)); 322 | } 323 | 324 | /** 325 | * Just get url signature from cache 326 | * @param {string} url 327 | * @return {Promise} filtered signature info 328 | */ 329 | getCachedSignature(url) { 330 | url = JSSDK.normalizeUrl(url); 331 | return this.store.getSignature(url).then(signature => { 332 | return Promise.resolve(this.filterSignature(signature)); 333 | }); 334 | } 335 | } 336 | 337 | module.exports = JSSDK; 338 | -------------------------------------------------------------------------------- /lib/MiniProgram.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('wechat-MiniProgram'); 4 | const crypto = require('crypto'); 5 | const isEmpty = require('lodash.isempty'); 6 | 7 | const utils = require('./utils'); 8 | const config = require('./config'); 9 | 10 | const Store = require('./store/Store'); 11 | const FileStore = require('./store/FileStore'); 12 | 13 | const wxConfig = config.getDefaultConfiguration(); 14 | 15 | class MiniProgram { 16 | /** 17 | * Wechat mini program class, must have "options.miniProgram" option 18 | * @constructor 19 | * @param options 20 | * @return {MiniProgram} MiniProgram instance 21 | */ 22 | constructor(options = {}) { 23 | // config.checkPassedConfiguration(options); 24 | 25 | let miniOptions = options.miniProgram || /* istanbul ignore next */ {}; 26 | 27 | /* istanbul ignore if */ 28 | if (!miniOptions.appId) { 29 | throw new Error('wechat mini program appId not found'); 30 | } 31 | 32 | /* istanbul ignore if */ 33 | if (!miniOptions.appSecret) { 34 | throw new Error('wechat mini program appSecret not found'); 35 | } 36 | 37 | this.miniProgramOptions = miniOptions = Object.assign( 38 | {}, 39 | wxConfig.miniProgram, 40 | miniOptions 41 | ); 42 | options.miniProgram = miniOptions; 43 | 44 | this.wechatConfig = isEmpty(options) 45 | ? /* istanbul ignore next */ wxConfig 46 | : Object.assign({}, wxConfig, options); 47 | //alias 48 | this.appId = miniOptions.appId; 49 | this.appSecret = miniOptions.appSecret; 50 | 51 | /* istanbul ignore else */ 52 | if ( 53 | !options.store || 54 | /* istanbul ignore next */ !(options.store instanceof Store) 55 | ) { 56 | debug('[MiniProgram]Store not provided, using default FileStore...'); 57 | this.store = new FileStore(options.storeOptions); 58 | } else { 59 | this.store = options.store; 60 | } 61 | } 62 | 63 | /** 64 | * Get the new session from wechat 65 | * @param code - code from wx.login() 66 | * @param key - key used to store the session data, default will use the openid 67 | * @return {Promise} 68 | */ 69 | getSession(code, key) { 70 | return utils 71 | .sendWechatRequest(this.miniProgramOptions.GET_SESSION_KEY_URL, { 72 | query: { 73 | appid: this.appId, 74 | secret: this.appSecret, 75 | js_code: code, 76 | grant_type: 'authorization_code', 77 | }, 78 | }) 79 | .then(data => { 80 | return this.store.setMPSession(key || data.openid, data).then(() => { 81 | return Promise.resolve(data); 82 | }); 83 | }) 84 | .catch(err => { 85 | debug(err); 86 | return Promise.reject(err); 87 | }); 88 | } 89 | 90 | /** 91 | * Generate mini program signature with raw data and session key 92 | * @param {string} rawDataString 93 | * @param sessionKey 94 | * @return {Promise} Promise - generated signature 95 | */ 96 | genSignature(rawDataString, sessionKey) { 97 | return Promise.resolve(utils.genSHA1(rawDataString + sessionKey)); 98 | 99 | // return this.store.getMPSessionKey().then(sessionKey => { 100 | // return Promise.resolve(utils.genSHA1(rawDataString + sessionKey)); 101 | // }); 102 | } 103 | 104 | /** 105 | * Verify the provided signature and generated signature with the rawData 106 | * @param {object|string} rawData - raw data on which the signature will be generated 107 | * @param {string} signature - on which the generated signature will be compared upon 108 | * @param sessionKey 109 | * @return {Promise} Promise - resolved if signatures match, otherwise reject 110 | */ 111 | verifySignature(rawData, signature, sessionKey) { 112 | if ('object' === typeof rawData) { 113 | rawData = JSON.stringify(rawData); 114 | } 115 | return this.genSignature(rawData, sessionKey).then(genSig => { 116 | if (genSig === signature) { 117 | return Promise.resolve(); 118 | } 119 | return Promise.reject( 120 | new Error( 121 | `verify signature failed: 122 | expected: ${signature} 123 | generated: ${genSig} 124 | ` 125 | ) 126 | ); 127 | }); 128 | } 129 | 130 | /** 131 | * Decrypt data from wechat 132 | * @param {string} encryptedData 133 | * @param {string} iv 134 | * @param {string=} sessionKey - session_key used to decrypt encryptedData 135 | * @param {string=} key - get the session_key with key(usually is openid) from Store if the above "sessionKey" is not provided 136 | * @return {Promise} Promise - resolved/rejected with decrypted data or Error 137 | */ 138 | decryptData(encryptedData, iv, sessionKey, key) { 139 | /* istanbul ignore if */ 140 | if (!sessionKey && !key) { 141 | return Promise.reject( 142 | new Error('one of "sessionKey" or "key" must be provided!') 143 | ); 144 | } 145 | const data = utils.createBufferFromBase64(encryptedData); 146 | let p; 147 | /* istanbul ignore if */ 148 | if (!sessionKey && key) { 149 | p = this.store.getMPSessionKey(key); 150 | } else { 151 | p = Promise.resolve(sessionKey); 152 | } 153 | 154 | return p 155 | .then(sessionKey => { 156 | const aesKey = utils.createBufferFromBase64(sessionKey); 157 | const aesIV = utils.createBufferFromBase64(iv); 158 | let decoded; 159 | try { 160 | const decipher = crypto.createDecipheriv( 161 | 'aes-128-cbc', 162 | aesKey, 163 | aesIV 164 | ); 165 | decipher.setAutoPadding(true); 166 | decoded = decipher.update(data, 'binary', 'utf8'); 167 | decoded += decipher.final('utf8'); 168 | decoded = JSON.parse(decoded); 169 | } catch (err) { 170 | /* istanbul ignore next */ 171 | debug(err); 172 | return Promise.reject(err); 173 | } 174 | /* istanbul ignore if */ 175 | if (!decoded.watermark || decoded.watermark.appid !== this.appId) { 176 | const msg = 'appId not match in watermark'; 177 | debug(msg); 178 | return Promise.reject(new Error(msg)); 179 | } 180 | return Promise.resolve(decoded); 181 | }) 182 | .catch(err => { 183 | /* istanbul ignore next */ 184 | return Promise.reject(err); 185 | }); 186 | } 187 | } 188 | 189 | module.exports = MiniProgram; 190 | -------------------------------------------------------------------------------- /lib/OAuth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('wechat-OAuth'); 4 | 5 | const qs = require('querystring'); 6 | const isEmpty = require('lodash.isempty'); 7 | 8 | const utils = require('./utils'); 9 | const config = require('./config'); 10 | 11 | const Store = require('./store/Store'); 12 | const FileStore = require('./store/FileStore'); 13 | 14 | const wxConfig = config.getDefaultConfiguration(); 15 | 16 | const REDIRECT_HASH = '#wechat_redirect'; 17 | const oauthScope = { 18 | BASE: 'snsapi_base', 19 | USER_INFO: 'snsapi_userinfo', 20 | }; 21 | 22 | const oAuthDefaultParams = { 23 | redirect_uri: '', 24 | response_type: 'code', 25 | }; 26 | 27 | class OAuth { 28 | /** 29 | * OAuth class 30 | * @constructor 31 | * @param {object=} options 32 | * @return {OAuth} OAuth instance 33 | */ 34 | constructor(options) { 35 | config.checkPassedConfiguration(options); 36 | 37 | this.wechatConfig = isEmpty(options) 38 | ? /* istanbul ignore next */ wxConfig 39 | : Object.assign({}, wxConfig, options); 40 | 41 | this.oAuthUrl = this.wechatConfig.oAuthUrl + '?'; 42 | 43 | this.setDefaultOAuthUrl(); 44 | 45 | //no custom store provided, using default FileStore 46 | /* istanbul ignore if */ 47 | if (!options.store || !(options.store instanceof Store)) { 48 | debug('[OAuth]Store not provided, using default FileStore...'); 49 | this.store = new FileStore(options.storeOptions); 50 | } else { 51 | this.store = options.store; 52 | } 53 | } 54 | 55 | /** 56 | * Get wechat user profile based on the access token 57 | * @param {object} tokenInfo access token info received based on the code(passed by the wechat server to the redirect_uri) 58 | * @param {boolean=} withToken if true, the access token info will be merged to the resolved user profile object 59 | * @return {Promise} 60 | */ 61 | getUserInfoRemotely(tokenInfo, withToken) { 62 | return utils 63 | .sendWechatRequest('/sns/userinfo', { 64 | baseUrl: this.wechatConfig.apiUrl, 65 | query: { 66 | access_token: tokenInfo.access_token, 67 | openid: tokenInfo.openid, 68 | lang: 'zh_CN', 69 | }, 70 | }) 71 | .then(data => { 72 | debug('user info received'); 73 | return withToken ? Object.assign({}, tokenInfo, data) : data; 74 | }) 75 | .catch(reason => { 76 | debug('get user info failed!'); 77 | return Promise.reject(reason); 78 | }); 79 | } 80 | 81 | /** 82 | * Set the expire time starting from now for the cached access token 83 | * @param {object} tokenInfo 84 | * @static 85 | * @return {object} tokenInfo updated token info 86 | */ 87 | static setAccessTokenExpirationTime(tokenInfo) { 88 | if (!tokenInfo.expires_in) return tokenInfo; 89 | const now = Date.now(); 90 | tokenInfo.expirationTime = now + (tokenInfo.expires_in - 60) * 1000; //minus 60s to expire 91 | return tokenInfo; 92 | } 93 | 94 | /** 95 | * Generate redirect url for use wechat oauth page 96 | * @param {string} redirectUrl 97 | * @param {string=} scope pass custom scope 98 | * @param {string=} state pass custom state 99 | * @return {string} generated oauth uri 100 | */ 101 | generateOAuthUrl(redirectUrl, scope, state) { 102 | let url = this.oAuthUrl; 103 | const tempObj = { 104 | appid: this.wechatConfig.appId, 105 | }; 106 | const oauthState = state || this.wechatConfig.oAuthState || 'userAuth'; 107 | const tempOAuthParams = Object.assign(tempObj, oAuthDefaultParams, { 108 | redirect_uri: redirectUrl, 109 | state: oauthState, 110 | }); 111 | tempOAuthParams.scope = scope 112 | ? scope 113 | : /* istanbul ignore next */ oauthScope.USER_INFO; 114 | 115 | const keys = Object.keys(tempOAuthParams); 116 | //sort the keys for correct order on url query 117 | keys.sort(); 118 | const oauthParams = {}; 119 | keys.forEach(key => (oauthParams[key] = tempOAuthParams[key])); 120 | 121 | url += qs.stringify(oauthParams); 122 | url += REDIRECT_HASH; 123 | return url; 124 | } 125 | 126 | /** 127 | * Get wechat user base info, aka, get openid and token 128 | * @param {*} code code included in the redirect url 129 | * @param {string} [key] key to store the oauth token 130 | * @return {Promise} 131 | */ 132 | getUserBaseInfo(code, key) { 133 | return this.getAccessToken(code, key).then(data => data); 134 | } 135 | 136 | /** 137 | * Get wechat user info, including nickname, openid, avatar, etc... 138 | * @param {*} code 139 | * @param {string} [key] key to store oauth token 140 | * @param {boolean} [withToken] return token info together with the profile 141 | * @return {Promise} 142 | */ 143 | getUserInfo(code, key, withToken) { 144 | return this.getAccessToken(code, key).then(tokenInfo => { 145 | return this.getUserInfoRemotely(tokenInfo, withToken); 146 | }); 147 | } 148 | 149 | /** 150 | * Get oauth access token 151 | * @param {*} code 152 | * @param {string} key custom user session id to identify cached token 153 | * @return {Promise} 154 | */ 155 | getAccessToken(code, key) { 156 | if (code) { 157 | return this.getAccessTokenRemotely(code, key); 158 | } 159 | return this.store.getOAuthAccessToken(key).then(tokenInfo => { 160 | //nothing in cache, or token is expired 161 | if (!tokenInfo) { 162 | const err = new Error('please get new code!'); 163 | debug(err); 164 | return Promise.reject(err); 165 | } 166 | if (OAuth.isAccessTokenExpired(tokenInfo)) { 167 | return this.refreshAccessToken(key, tokenInfo); 168 | } 169 | return tokenInfo; 170 | }); 171 | } 172 | 173 | /** 174 | * Get access token from wechat server 175 | * @param {*} code 176 | * @param {string} key 177 | * @return {Promise} 178 | */ 179 | getAccessTokenRemotely(code, key) { 180 | debug('getting new oauth access token...'); 181 | return utils 182 | .sendWechatRequest('/sns/oauth2/access_token', { 183 | baseUrl: this.wechatConfig.apiUrl, 184 | query: { 185 | appid: this.wechatConfig.appId, 186 | secret: this.wechatConfig.appSecret, 187 | code: code, 188 | grant_type: 'authorization_code', 189 | }, 190 | }) 191 | .then(data => { 192 | OAuth.setAccessTokenExpirationTime(data); 193 | const oauthKey = key || data.openid; 194 | data.key = oauthKey; 195 | data.createDate = new Date(); 196 | data.modifyDate = data.createDate; 197 | return this.store.saveOAuthAccessToken(oauthKey, data); 198 | }) 199 | .catch(reason => { 200 | debug('get oauth access token failed!'); 201 | return Promise.reject(reason); 202 | }); 203 | } 204 | 205 | /** 206 | * Refresh access token with the cached refresh_token over the wechat server 207 | * @param {string} key 208 | * @param {object} tokenInfo 209 | * @return {Promise} 210 | */ 211 | refreshAccessToken(key, tokenInfo) { 212 | return utils 213 | .sendWechatRequest('/sns/oauth2/refresh_token', { 214 | baseUrl: this.wechatConfig.apiUrl, 215 | query: { 216 | appid: this.wechatConfig.appId, 217 | refresh_token: tokenInfo.refresh_token, 218 | grant_type: 'refresh_token', 219 | }, 220 | }) 221 | .then(data => { 222 | OAuth.setAccessTokenExpirationTime(data); 223 | const oauthKey = key || data.openid; 224 | data.modifyDate = new Date(); 225 | return this.store.updateOAuthAccessToken(oauthKey, data); 226 | }) 227 | .catch(err => { 228 | debug('please get the new code!'); 229 | return Promise.reject(err); 230 | }); 231 | } 232 | 233 | /** 234 | * Check if cached token is valid over the wechat server 235 | * @param {object} tokenInfo 236 | * @return {Promise} 237 | */ 238 | isAccessTokenValid(tokenInfo) { 239 | return utils.sendWechatRequest('/sns/auth', { 240 | baseUrl: this.wechatConfig.apiUrl, 241 | query: { 242 | appid: this.wechatConfig.appId, 243 | access_token: tokenInfo.access_token, 244 | }, 245 | }); 246 | } 247 | 248 | /** 249 | * Set default wechat oauth url for the instance 250 | */ 251 | setDefaultOAuthUrl() { 252 | let temp = this.wechatConfig.wechatRedirectUrl; 253 | /* istanbul ignore else */ 254 | if (!temp) { 255 | temp = this.wechatConfig.wechatRedirectHost + '/wechat/oauth-callback'; 256 | } 257 | this.snsUserInfoUrl = this.generateOAuthUrl(temp, oauthScope.USER_INFO); 258 | this.snsUserBaseUrl = this.generateOAuthUrl(temp, oauthScope.BASE); 259 | } 260 | 261 | /** 262 | * Check if cached token is expired 263 | * @param {object} tokenInfo 264 | * @return {boolean} 265 | */ 266 | static isAccessTokenExpired(tokenInfo) { 267 | if (!tokenInfo.expirationTime) return true; 268 | return Date.now() - tokenInfo.expirationTime >= 0; 269 | } 270 | } 271 | 272 | module.exports = OAuth; 273 | -------------------------------------------------------------------------------- /lib/Payment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('wechat-Payment'); 4 | 5 | const isEmpty = require('lodash.isempty'); 6 | const crypto = require('crypto'); 7 | const https = require('https'); 8 | 9 | const utils = require('./utils'); 10 | const config = require('./config'); 11 | 12 | const Store = require('./store/Store'); 13 | const FileStore = require('./store/FileStore'); 14 | const got = require('got'); 15 | 16 | const wxConfig = config.getDefaultConfiguration(); 17 | 18 | let paymentUrls = wxConfig.paymentUrls; 19 | 20 | const SIGN_TYPE = { 21 | MD5: 'MD5', 22 | HMAC_SHA256: 'HMAC-SHA256', 23 | }; 24 | 25 | const RESULT_CODE = { 26 | SUCCESS: 'SUCCESS', 27 | FAIL: 'FAIL', 28 | }; 29 | 30 | const PAYMENT_TYPE = { 31 | JSAPI: 'JSAPI', 32 | NATIVE: 'NATIVE', 33 | APP: 'APP', 34 | MICROPAY: 'MICROPAY', 35 | }; 36 | 37 | const TRADE_STATE = { 38 | SUCCESS: '支付成功', 39 | REFUND: '转入退款', 40 | NOTPAY: '未支付', 41 | CLOSED: '已关闭', 42 | REVOKED: '已撤销(刷卡支付)', 43 | USERPAYING: '用户支付中', 44 | PAYERROR: '支付失败', 45 | }; 46 | 47 | const BILL_TYPE = { 48 | ALL: 'ALL', 49 | SUCCESS: 'SUCCESS', 50 | REFUND: 'REFUND', 51 | RECHARGE_REFUND: 'RECHARGE_REFUND', 52 | }; 53 | 54 | const REFUND_STATUS = { 55 | SUCCESS: 'SUCCESS', 56 | REFUNDCLOSE: 'REFUNDCLOSE', 57 | PROCESSING: 'PROCESSING', 58 | CHANGE: 'CHANGE', 59 | }; 60 | 61 | const FUND_ACCOUNT_TYPE = { 62 | BASIC: 'Basic', 63 | OPERATION: 'Operation', 64 | FEES: 'Fees', 65 | }; 66 | 67 | const COUPON_TYPE = { 68 | CASH: 'CASH', 69 | NO_CASH: 'NO_CASH', 70 | }; 71 | 72 | const SANDBOX_SIGN_KEY_ERROR_MSG = '沙箱验证签名失败'; 73 | const MAX_SANDBOX_SIGN_KEY_ERROR_ATTEMPTS = 2; 74 | 75 | class Payment { 76 | /** 77 | * Wechat Payment class 78 | * @constructor 79 | * @param options 80 | * @return {Payment} Payment instance 81 | */ 82 | constructor(options) { 83 | config.checkPassedConfiguration(options); 84 | 85 | this.wechatConfig = isEmpty(options) 86 | ? /* istanbul ignore next */ wxConfig 87 | : Object.assign({}, wxConfig, options); 88 | // if(!this.wechatConfig.paymentKey) { 89 | // throw new Error('Payment key not found, pls go to wechat payment dashboard to get the key!'); 90 | // } 91 | /* istanbul ignore if */ 92 | if (!this.wechatConfig.paymentCertificatePfx) { 93 | throw new Error( 94 | 'Payment certificate key not found, pls provide pkcs12 key!' 95 | ); 96 | } 97 | /* istanbul ignore if */ 98 | if (!this.wechatConfig.merchantId) { 99 | throw new Error('Payment merchant id not found!'); 100 | } 101 | 102 | this.paymentUrls = Object.assign( 103 | {}, 104 | paymentUrls, 105 | this.wechatConfig.paymentUrls 106 | ); 107 | /* istanbul ignore else */ 108 | if (this.wechatConfig.paymentSandBox) { 109 | this.paymentUrls = utils.paymentUrlsWithSandBox(this.paymentUrls); 110 | this.getSandboxSignKey(); 111 | } 112 | 113 | this.notifyUrl = options.paymentNotifyUrl || wxConfig.paymentNotifyUrl; 114 | 115 | /* istanbul ignore if */ 116 | if (!options.store || !(options.store instanceof Store)) { 117 | debug('[Payment]Store not provided, using default FileStore...'); 118 | this.store = new FileStore(options.storeOptions); 119 | } else { 120 | this.store = options.store; 121 | } 122 | } 123 | 124 | /** 125 | * 1000fen -> 10RMB 126 | * @param value 127 | * @return {number} 128 | */ 129 | static fenToYuan(value) { 130 | return Number(value) / 100; //to yuan 131 | } 132 | 133 | /** 134 | * 10RMB -> 1000fen, 10.123RMB -> 1012fen 135 | * @param value 136 | * @return {number} 137 | */ 138 | static yuanToFen(value) { 139 | return Math.round(Number(value) * 100); 140 | } 141 | 142 | /** 143 | * RMB yuan, 10.123 => 10.12, 10 => 10.00, 10.456 => 10.45 144 | * @param value 145 | * @return {string} formatted currency 146 | */ 147 | static formatCurrency(value) { 148 | const fen = Payment.yuanToFen(value); 149 | const yuan = Payment.fenToYuan(fen); 150 | return yuan.toFixed(2); 151 | } 152 | 153 | /* istanbul ignore next */ 154 | static get DOWNLOAD_BILL_TYPE() { 155 | return BILL_TYPE; 156 | } 157 | 158 | /* istanbul ignore next */ 159 | static get TRADE_TYPE() { 160 | return PAYMENT_TYPE; 161 | } 162 | 163 | /* istanbul ignore next */ 164 | static get TRADE_STATE() { 165 | return TRADE_STATE; 166 | } 167 | 168 | /* istanbul ignore next */ 169 | static get REFUND_STATUS() { 170 | return REFUND_STATUS; 171 | } 172 | 173 | /* istanbul ignore next */ 174 | static get FUND_ACCOUNT_TYPE() { 175 | return FUND_ACCOUNT_TYPE; 176 | } 177 | 178 | /* istanbul ignore next */ 179 | static get SIGN_TYPE() { 180 | return SIGN_TYPE; 181 | } 182 | 183 | /* istanbul ignore next */ 184 | static get PAYMENT_TYPE() { 185 | return PAYMENT_TYPE; 186 | } 187 | 188 | /* istanbul ignore next */ 189 | static get COUPON_TYPE() { 190 | return COUPON_TYPE; 191 | } 192 | 193 | /** 194 | * Generate payment signature 195 | * @param {object} params 196 | * @param {string=} signType 197 | * @param {Boolean=} sandbox gen sign for retrieve sandbox sign key 198 | * @return {object} signature object 199 | */ 200 | generateSignature(params, signType, sandbox) { 201 | const data = this.generateGeneralPaymentSignature( 202 | params, 203 | signType || /* istanbul ignore next */ SIGN_TYPE.MD5, 204 | sandbox 205 | ); 206 | return Object.assign({}, data.params, { sign: data.paySign }); 207 | } 208 | 209 | /** 210 | * Generate paySign info for jssdk to invoke wechat payment 211 | * @param {string} prepayId received from unifiedOrder() 212 | * @param {string=} signType MD5 or SHA1, default MD5 213 | * @return {Promise} 214 | */ 215 | generateChooseWXPayInfo(prepayId, signType) { 216 | const params = { 217 | appId: this.wechatConfig.appId, 218 | timeStamp: utils.timestamp(), 219 | nonceStr: utils.nonceStr(), 220 | package: 'prepay_id=' + prepayId, 221 | signType: signType || SIGN_TYPE.MD5, 222 | }; 223 | const ret = this.generateGeneralPaymentSignature(params, params.signType); 224 | return Promise.resolve({ 225 | timestamp: params.timeStamp, 226 | nonceStr: params.nonceStr, 227 | package: params.package, 228 | signType: params.signType, 229 | paySign: ret.paySign, 230 | }); 231 | } 232 | 233 | /** 234 | * General payment sign generator 235 | * @param {object} params data used to gen payment sign 236 | * @param {string} signType 237 | * @param {boolean=} sandbox if gen the sign to get sandbox api key 238 | * @return {object} 239 | */ 240 | generateGeneralPaymentSignature(params, signType, sandbox) { 241 | const originalKeys = Object.keys(params); 242 | const keys = originalKeys.filter(key => { 243 | const val = params[key]; 244 | return ( 245 | typeof key === 'string' && 246 | val !== undefined && 247 | val !== '' && 248 | val !== null 249 | ); 250 | }); 251 | const newParams = {}; 252 | keys.forEach(key => { 253 | newParams[key] = params[key]; 254 | }); 255 | // console.log(utils.buildXML(Object.assign({key: this.wechatConfig.paymentKey}, params)) 256 | // .then(xml => console.log(xml))); 257 | let str = utils.paramsToString(newParams, true); 258 | const key = this.getAPISignKey(sandbox); 259 | str += '&key=' + key; 260 | // console.log(params); 261 | // console.log(str); 262 | let paySign = 263 | signType === SIGN_TYPE.HMAC_SHA256 264 | ? utils.genHmacSHA256(str, key) 265 | : utils.genMD5(str); 266 | paySign = paySign.toUpperCase(); 267 | return { 268 | params: newParams, 269 | paySign: paySign, 270 | }; 271 | } 272 | 273 | /** 274 | * Get extra options when pfx needed 275 | * @return {object} 276 | */ 277 | getPaymentAgent() { 278 | if (this.paymentAgent) return this.paymentAgent; 279 | const wc = this.wechatConfig; 280 | this.paymentAgent = new https.Agent({ 281 | host: this.wechatConfig.PAYMENT_HOST, 282 | port: this.wechatConfig.PAYMENT_HOST_PORT, 283 | pfx: wc.paymentCertificatePfx, 284 | passphrase: wc.paymentPassphrase || wc.merchantId, 285 | }); 286 | return this.paymentAgent; 287 | } 288 | 289 | /** 290 | * Generate simple trade id 291 | * @return {string} 292 | */ 293 | simpleTradeNo() { 294 | return utils.simpleDate() + utils.nonceStr().toUpperCase(); 295 | } 296 | 297 | /** 298 | * Generate unified order from wechat 299 | * @param {object} orderInfo 300 | * @return {Promise} 301 | */ 302 | unifiedOrder(orderInfo) { 303 | const data = Object.assign( 304 | { 305 | // appid: wechatConfig.appId, 306 | // mch_id: wechatConfig.merchantId, 307 | // nonce_str: utils.nonceStr(), 308 | // sign_type: SIGN_TYPE.MD5, 309 | out_trade_no: this.simpleTradeNo(), 310 | notify_url: this.notifyUrl, 311 | trade_type: PAYMENT_TYPE.JSAPI, 312 | device_info: 'WEB', 313 | }, 314 | orderInfo 315 | ); 316 | return this.simpleRequest(this.paymentUrls.UNIFIED_ORDER, data) 317 | .then(result => { 318 | const responseData = result.responseData; 319 | if (!responseData.out_trade_no) { 320 | responseData.out_trade_no = data.out_trade_no; 321 | } 322 | debug('unified result ok'); 323 | return Promise.resolve(result); 324 | }) 325 | .catch(reason => { 326 | console.error(reason); 327 | debug('get unified order failed!'); 328 | return Promise.reject(reason); 329 | }); 330 | } 331 | 332 | /** 333 | * Query specific order status from wechat 334 | * @param {object} queryInfo 335 | * @return {Promise} 336 | */ 337 | queryOrder(queryInfo) { 338 | return this.simpleRequest(this.paymentUrls.QUERY_ORDER, queryInfo).catch( 339 | reason => { 340 | debug('query order failed!'); 341 | return Promise.reject(reason); 342 | } 343 | ); 344 | } 345 | 346 | /** 347 | * Close order from wechat 348 | * @param {string} orderId wechat out_trade_no 349 | * @return {Promise} 350 | */ 351 | closeOrder(orderId) { 352 | return this.simpleRequest(this.paymentUrls.CLOSE_ORDER, { 353 | out_trade_no: orderId, 354 | }).catch(reason => { 355 | debug('close order failed!'); 356 | return Promise.reject(reason); 357 | }); 358 | } 359 | 360 | /** 361 | * Request refund from wechat 362 | * @param {object} info: 363 | * { 364 | * transaction_id: '123', 365 | * out_trade_no: '3210', //only one of 'transaction_id' or 'out_trade_no' is required 366 | * out_refund_no: '1234', //required, merchant order refund id, similar with 'out_trade_no' 367 | * total_fee: '100', //required 368 | * refund_fee: '100', //required 369 | * refund_fee_type: 'CNY', //optional 370 | * refund_desc: '', //optional 371 | * refund_account: '', //optional, one of ['REFUND_SOURCE_UNSETTLED_FUNDS', 'REFUND_SOURCE_RECHARGE_FUNDS'] 372 | * notify_url: '', //optional 373 | * } 374 | * @return {Promise} 375 | */ 376 | refund(info) { 377 | const data = this.generateSignature(this.mergeParams(info)); 378 | return utils 379 | .buildXML(data) 380 | .then(xmlData => { 381 | return utils.sendWechatPaymentRequest(this.paymentUrls.REFUND, { 382 | body: xmlData, 383 | agent: { https: this.getPaymentAgent() }, 384 | }); 385 | }) 386 | .then(rawData => { 387 | return utils.parseXML(rawData); 388 | }) 389 | .catch(reason => { 390 | debug('request refund operation failed!'); 391 | return Promise.reject(reason); 392 | }); 393 | } 394 | 395 | /** 396 | * Query refund status from wechat 397 | * @param {object} info: 398 | * { 399 | * transaction_id: '', 400 | * out_trade_no: '', 401 | * out_refund_no: '', 402 | * refund_id: '', //only one of four above is required 403 | * offset: 15, //optional, start from number 16 404 | * } 405 | * @return {Promise} 406 | */ 407 | queryRefund(info) { 408 | return this.simpleRequest(this.paymentUrls.QUERY_REFUND, info).catch( 409 | reason => { 410 | debug('query refund failed!'); 411 | return Promise.reject(reason); 412 | } 413 | ); 414 | } 415 | 416 | /** 417 | * Download bill from wechat 418 | * @param {string} billDate e.g: 20180603 419 | * @param {string} billType e.g: FUND_ACCOUNT_TYPE.BASIC 420 | * @param {boolean=} noGzip if download stream is gziped 421 | * @return {Promise} 422 | */ 423 | downloadBill(billDate, billType, noGzip) { 424 | const data = this.generateSignature( 425 | this.mergeParams({ 426 | bill_date: billDate, 427 | bill_type: billType || BILL_TYPE.ALL, 428 | tar_type: noGzip ? '' : 'GZIP', 429 | }) 430 | ); 431 | return this.download( 432 | data, 433 | { 434 | decompress: !noGzip, 435 | encoding: null, // get zip file as buffer 436 | }, 437 | this.paymentUrls.DOWNLOAD_BILL 438 | ); 439 | } 440 | 441 | /** 442 | * Download fund flow 443 | * @param {string} billDate e.g: 20180603 444 | * @param {string} accountType e.g: BILL_TYPE.ALL 445 | * @param {boolean=} noGzip if download stream is gziped 446 | * @return {Promise} 447 | */ 448 | downloadFundFlow(billDate, accountType, noGzip) { 449 | const data = this.generateSignature( 450 | this.mergeParams( 451 | { 452 | bill_date: billDate, 453 | account_type: accountType, 454 | tar_type: noGzip ? '' : 'GZIP', 455 | }, 456 | SIGN_TYPE.HMAC_SHA256 457 | ), 458 | SIGN_TYPE.HMAC_SHA256 459 | ); 460 | return this.download( 461 | data, 462 | { 463 | agent: { https: this.getPaymentAgent() }, 464 | decompress: !noGzip, 465 | }, 466 | this.paymentUrls.DOWNLOAD_FUND_FLOW 467 | ); 468 | } 469 | 470 | /** 471 | * Download functionality 472 | * @param {object} data request data 473 | * @param {object=} requestOptions options send to request 474 | * @param url to wechat api endpoint 475 | * @return {Promise} resolve or reject with: 476 | * { 477 | * //an error object, rejected 478 | * error: new Error(), 479 | * //error msg, , rejected 480 | * msg: '', 481 | * //resolved with download stream, 482 | * //can be piped to other writable stream, e.g: result.data.pipe(fs.createWritableStream('./bill.txt')) 483 | * data: Stream, 484 | * //sign info from response header 485 | * digest: 'SHA=ec45d7c24492dcd62d92472b0f2816c8d9a2d773', 486 | * } 487 | */ 488 | download(data, requestOptions, url) { 489 | return utils.buildXML(data).then(xmlData => { 490 | const myOptions = Object.assign( 491 | { 492 | json: false, 493 | decompress: true, 494 | method: 'POST', 495 | body: xmlData, 496 | }, 497 | requestOptions 498 | ); 499 | return new Promise((resolve, reject) => { 500 | const stream = got.stream(url, myOptions); 501 | const chunks = []; 502 | let body = ''; 503 | let response; 504 | stream.on('response', res => { 505 | response = res; 506 | }); 507 | stream.on('data', chunk => { 508 | chunks.push(chunk); 509 | }); 510 | stream.on('end', () => { 511 | const ret = Buffer.concat(chunks); 512 | body = ret.toString(); 513 | if (!response || response.statusCode != 200) { 514 | let str = 'request failed'; 515 | if (response) { 516 | str += ' with status code: ' + response.statusCode; 517 | } 518 | debug(str); 519 | reject({ error: new Error(str) }); 520 | return; 521 | } 522 | if (body && String(body).indexOf('return_code') >= 0) { 523 | const msg = 'download failed!'; 524 | debug(msg); 525 | debug(body); 526 | reject({ 527 | error: new Error(msg), 528 | msg: body, 529 | }); 530 | return; 531 | } 532 | return resolve({ 533 | //return the request stream 534 | data: stream, 535 | body, 536 | digest: response.headers['digest'], 537 | }); 538 | }); 539 | stream.on('error', (error, body, response) => { 540 | debug(error); 541 | reject({ error, body, response }); 542 | }); 543 | }); 544 | }); 545 | } 546 | 547 | /** 548 | * Report wechat services status to wechat 549 | * @param {object} info 550 | * @return {Promise} 551 | */ 552 | reportToWechat(info) { 553 | return this.simpleRequest(this.paymentUrls.REPORT, info).catch(reason => { 554 | debug('report status to wechat failed!'); 555 | return Promise.reject(reason); 556 | }); 557 | } 558 | 559 | getSandboxSignKey() { 560 | const params = { 561 | mch_id: this.wechatConfig.merchantId, 562 | nonce_str: utils.nonceStr(), 563 | }; 564 | const data = this.generateSignature(params, SIGN_TYPE.MD5, true); 565 | return utils 566 | .buildXML(data) 567 | .then(xmlData => { 568 | // console.log('sandbox key request:', xmlData); 569 | return utils.sendWechatPaymentRequest(this.paymentUrls.SIGN_KEY, { 570 | body: xmlData, 571 | }); 572 | }) 573 | .then(rawData => { 574 | // console.log(rawData); 575 | return utils.parseXML(rawData); 576 | }) 577 | .then(jsonData => { 578 | this.wechatConfig.paymentSandBoxKey = jsonData.sandbox_signkey; 579 | return Promise.resolve(jsonData); 580 | }) 581 | .catch(reason => { 582 | debug('get sandbox sign key failed!'); 583 | return Promise.reject(reason); 584 | }); 585 | } 586 | 587 | /** 588 | * Simplified request wrapper 589 | * @param {string} apiUrl 590 | * @param {object} info 591 | * @param {Number=} attempts, sandbox key error retry count 592 | * @return {Promise} 593 | */ 594 | simpleRequest(apiUrl, info, attempts) { 595 | if (attempts > MAX_SANDBOX_SIGN_KEY_ERROR_ATTEMPTS) { 596 | const msg = 'maximum sandbox key error attempts reached!'; 597 | debug(msg); 598 | return Promise.reject(new Error(msg)); 599 | } 600 | if (!attempts) { 601 | attempts = 1; 602 | } 603 | const data = this.generateSignature(this.mergeParams(info)); 604 | // console.log(data); 605 | // return utils.buildXML(data) 606 | // .then(data => { 607 | // console.log(data); 608 | // return utils.parseXML(data); 609 | // }); 610 | return utils 611 | .buildXML(data) 612 | .then(xmlData => { 613 | // console.log(xmlData); 614 | return utils.sendWechatPaymentRequest(apiUrl, { 615 | body: xmlData, 616 | }); 617 | }) 618 | .then(rawData => { 619 | // console.log('wechat response:', rawData); 620 | return utils.parseXML(rawData); 621 | }) 622 | .then(jsonData => { 623 | if (jsonData.return_code == RESULT_CODE.FAIL) { 624 | debug(jsonData.return_msg); 625 | //sandbox key error, try get new key and try one more time 626 | if ( 627 | String(jsonData.return_msg).indexOf(SANDBOX_SIGN_KEY_ERROR_MSG) >= 0 628 | ) { 629 | debug( 630 | 'sandbox sign key error, try get new key and try one more time...' 631 | ); 632 | return this.getSandboxSignKey().then(() => { 633 | return this.simpleRequest(apiUrl, info, attempts + 1); 634 | }); 635 | } 636 | return Promise.reject(jsonData); 637 | } 638 | if (jsonData.result_code == RESULT_CODE.FAIL) { 639 | debug('ErrorCode[%s]: %s', jsonData.err_code, jsonData.err_code_des); 640 | return Promise.reject(jsonData); 641 | } 642 | return Promise.resolve({ 643 | requestData: data, 644 | responseData: jsonData, 645 | }); 646 | }); 647 | } 648 | 649 | /** 650 | * Merge custom params with default params 651 | * @param customParams 652 | * @param signType 653 | * @return {object} 654 | */ 655 | mergeParams(customParams, signType) { 656 | const wechatConfig = this.wechatConfig; 657 | return Object.assign( 658 | { 659 | appid: wechatConfig.appId, 660 | mch_id: wechatConfig.merchantId, 661 | nonce_str: utils.nonceStr(), 662 | sign_type: signType || SIGN_TYPE.MD5, 663 | }, 664 | customParams 665 | ); 666 | } 667 | 668 | /** 669 | * Parse xml data notified by wechat server 670 | * @param data 671 | * @return {Promise} 672 | */ 673 | parseNotifyData(data) { 674 | return utils.parseXML(data); 675 | } 676 | 677 | /** 678 | * Get xml reply data based on success or fail 679 | * @param {boolean} isSuccess 680 | * @return {Promise} 681 | */ 682 | replyData(isSuccess) { 683 | const result = { 684 | return_code: RESULT_CODE.FAIL, 685 | }; 686 | if (isSuccess) { 687 | result.return_code = RESULT_CODE.SUCCESS; 688 | result.return_msg = 'OK'; 689 | } 690 | return utils.buildXML(result); 691 | } 692 | 693 | /** 694 | * Format download bill date to format like: 20170101 695 | * @param {string|Date} date 696 | * @return {string} 697 | */ 698 | getDownloadBillDate(date) { 699 | return utils.simpleDate(date, 'YYYYMMDD'); 700 | } 701 | 702 | /** 703 | * Get api key based on env 704 | * @param {Boolean=} getSandboxKey the sandbox api key should also use the original payment api key 705 | * @return {*} 706 | */ 707 | getAPISignKey(getSandboxKey) { 708 | return getSandboxKey || 709 | /* istanbul ignore next */ !this.wechatConfig.paymentSandBox 710 | ? this.wechatConfig.paymentKey 711 | : /* istanbul ignore next */ this.wechatConfig.paymentSandBoxKey; 712 | } 713 | 714 | /** 715 | * Decrypt wechat refund notify result 716 | * @see https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_16&index=10 717 | * @param {string} xmlResult notify xml data 718 | * @return {Promise} 719 | */ 720 | decryptRefundNotifyResult(xmlResult) { 721 | return utils.parseXML(xmlResult).then(data => { 722 | const originalData = data; 723 | const md5Key = utils.genMD5(this.getAPISignKey()); 724 | data = utils.createBufferFromBase64(data.req_info); 725 | let decoded; 726 | const decipher = crypto.createDecipheriv('aes-256-ecb', md5Key, Buffer.alloc(0)); 727 | decipher.setAutoPadding(true); 728 | decoded = decipher.update(data, 'binary', 'utf8'); 729 | decoded += decipher.final('utf8'); 730 | return utils.parseXML(decoded).then(ret =>{ 731 | return Promise.resolve({ 732 | parsedXMLData: originalData, 733 | decryptedData: ret, 734 | }); 735 | }); 736 | }); 737 | } 738 | 739 | /** 740 | * Batch query user comments 741 | * @param {string} beginTime in format 'YYYYMMDDHHmmss' 742 | * @param {string} endTime same as beginTime 743 | * @param {number=} offset integer 744 | * @param {number=} limit integer 745 | * @return {Promise} 746 | */ 747 | queryComments(beginTime, endTime, offset, limit) { 748 | const data = this.generateSignature( 749 | this.mergeParams( 750 | { 751 | begin_time: beginTime, 752 | end_time: endTime, 753 | offset: offset || 0, 754 | limit: limit, 755 | }, 756 | SIGN_TYPE.HMAC_SHA256 757 | ), 758 | SIGN_TYPE.HMAC_SHA256 759 | ); 760 | return utils 761 | .buildXML(data) 762 | .then(xmlData => { 763 | return utils.sendWechatPaymentRequest( 764 | this.paymentUrls.BATCH_QUERY_COMMENT, 765 | { 766 | body: xmlData, 767 | agent: { https: this.getPaymentAgent() }, 768 | } 769 | ); 770 | }) 771 | .then(data => { 772 | //not ok if has xml tag 773 | if (String(data).indexOf('') >= 0) { 774 | return utils.parseXML(data).then(parsedData => { 775 | return Promise.reject(parsedData); 776 | }); 777 | } 778 | return Promise.resolve(data); 779 | }) 780 | .catch(reason => { 781 | debug('query user comments operation failed!'); 782 | return Promise.reject(reason); 783 | }); 784 | } 785 | 786 | /** 787 | * International merchant only 788 | * Retrieve foreign currency settlements within the specified date range 789 | * @param {object} query 790 | * @see https://pay.weixin.qq.com/wiki/doc/api/external/jsapi.php?chapter=9_14&index=9 791 | * @return {Promise} 792 | */ 793 | querySettlement(query) { 794 | return this.simpleRequest(this.paymentUrls.QUERY_SETTLEMENT, query).catch( 795 | reason => { 796 | debug('query settlement failed!'); 797 | return Promise.reject(reason); 798 | } 799 | ); 800 | } 801 | 802 | /** 803 | * International merchant only 804 | * Retrieve exchange rate for given foreign currency and date 805 | * @param {object} query 806 | * @see https://pay.weixin.qq.com/wiki/doc/api/external/jsapi.php?chapter=9_15&index=10 807 | * @return {Promise} 808 | */ 809 | queryExchangeRate(query) { 810 | return this.simpleRequest( 811 | this.paymentUrls.QUERY_EXCHANGE_RATE, 812 | Object.assign({}, query, { 813 | nonce_str: null, 814 | sign_type: null, 815 | }) 816 | ).catch(reason => { 817 | debug('query exchange rate failed!'); 818 | return Promise.reject(reason); 819 | }); 820 | } 821 | } 822 | 823 | module.exports = Payment; 824 | -------------------------------------------------------------------------------- /lib/Wechat.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('wechat'); 4 | const { COMPARE_CONFIG_KEYS } = require('./config'); 5 | const JSSDK = require('./JSSDK'); 6 | const OAuth = require('./OAuth'); 7 | const Card = require('./Card'); 8 | const Payment = require('./Payment'); 9 | const MiniProgram = require('./MiniProgram'); 10 | const Store = require('./store/Store'); 11 | const FileStore = require('./store/FileStore'); 12 | 13 | class Wechat { 14 | /** 15 | * @constructor 16 | * @param options custom wechat configuration 17 | * @return {Wechat} 18 | */ 19 | constructor(options) { 20 | options = options || {}; 21 | //no custom store provided, using default FileStore 22 | if (!options.store || !(options.store instanceof Store)) { 23 | debug('Store not provided, using default FileStore...'); 24 | options.store = new FileStore(options.storeOptions, options); 25 | } 26 | 27 | //create a JSSDK instance 28 | this.jssdk = new JSSDK(options); 29 | //create a OAuth instance 30 | this.oauth = new OAuth(options); 31 | /* istanbul ignore if */ 32 | if (options.card) { 33 | //create a Card instance 34 | this.card = new Card(options); 35 | } 36 | /* istanbul ignore if */ 37 | if (options.payment) { 38 | //create a Payment instance 39 | this.payment = new Payment(options); 40 | } 41 | /* istanbul ignore if */ 42 | if (options.miniProgram) { 43 | //create a MiniProgram instance 44 | this.miniProgram = new MiniProgram(options); 45 | } 46 | 47 | this.store = options.store; 48 | } 49 | } 50 | 51 | module.exports = Wechat; 52 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @license MIT 3 | * Client side js to use wechat-jssdk, also works with other server side service. 4 | * https://github.com/JasonBoy/wechat-jssdk 5 | */ 6 | 7 | 'use strict'; 8 | 9 | //default wechat script url 10 | let defaultScriptUrl = '//res.wx.qq.com/open/js/jweixin-1.6.0.js'; 11 | 12 | //default apis with share-on-moment and share-on-chat 13 | const defaultApiList = [ 14 | 'updateAppMessageShareData', 15 | 'updateTimelineShareData', 16 | 'onMenuShareTimeline', 17 | 'onMenuShareAppMessage', 18 | ]; 19 | 20 | // default opentags wx-open-subscribe and wx-open-audio 21 | const defaultopenTagList = ['wx-open-subscribe', 'wx-open-audio']; 22 | 23 | class WechatJSSDK { 24 | /** 25 | * Initialize the WechatJSSDK instance 26 | * @constructor 27 | * @param {object} wechatConfig, should contain like: 28 | * { 29 | * appId: 'xxxx', 30 | * timestamp: '', 31 | * nonceStr: '', 32 | * signature: '', 33 | * jsApiList: ['onMenuShareTimeline', 'onMenuShareAppMessage', ...], 34 | * customUrl: 'http://res.wx.qq.com/open/js/jweixin-1.0.0.js' // set custom weixin script url 35 | * } 36 | * @returns {WechatJSSDK} 37 | */ 38 | constructor(wechatConfig) { 39 | //using new WechatJSSDK(config); 40 | if (this instanceof WechatJSSDK) { 41 | this.sdkUrl = defaultScriptUrl; 42 | this.config = wechatConfig || {}; 43 | if (this.config.customUrl) { 44 | this.sdkUrl = this.config.customUrl; 45 | } 46 | const apiList = this.config.jsApiList; 47 | const tagList = this.config.openTagList; 48 | //add default list 49 | if (!apiList || apiList.length <= 0) { 50 | this.config.jsApiList = defaultApiList; 51 | } 52 | //add default tags 53 | if (!tagList || tagList.length <= 0) { 54 | this.config.openTagList = defaultopenTagList; 55 | } 56 | this.debug = !!this.config.debug; 57 | return this; 58 | } 59 | return new WechatJSSDK(wechatConfig); 60 | } 61 | 62 | /** 63 | * Initialize wechat config 64 | * @return {Promise} 65 | */ 66 | initialize() { 67 | return this.loadScript(); 68 | } 69 | 70 | /** 71 | * Sign the signature now 72 | * @param {object} [newSignConfig], debug mode, appId, jsApiList cannot be changed!!! 73 | * , should only provide new signature specific config 74 | * @returns {Promise} 75 | */ 76 | signSignature(newSignConfig) { 77 | const selfConfig = this.config; 78 | const config = newSignConfig || selfConfig; 79 | const signConfig = { 80 | debug: this.debug, 81 | appId: selfConfig.appId, 82 | timestamp: config.timestamp || selfConfig.timestamp, 83 | nonceStr: config.nonceStr || selfConfig.nonceStr, 84 | signature: config.signature || selfConfig.signature, 85 | jsApiList: selfConfig.jsApiList.slice(0, selfConfig.jsApiList.length), 86 | openTagList: selfConfig.openTagList.slice( 87 | 0, 88 | selfConfig.openTagList.length 89 | ), 90 | }; 91 | const debug = this.debug; 92 | return new Promise((resolve, reject) => { 93 | if (!window.wx) { 94 | return reject(new Error('wx js not defined')); 95 | } 96 | const wx = window.wx; 97 | //export original wx object 98 | this.setOriginWx(); 99 | wx.config(signConfig); 100 | wx.ready(() => { 101 | console.log('sign signature finished...'); 102 | this.setOriginWx(); 103 | resolve(this); 104 | }); 105 | wx.error(err => { 106 | debug && alert('sign error: ' + JSON.stringify(err)); 107 | this.setOriginWx(); 108 | reject(err); 109 | }); 110 | }); 111 | } 112 | 113 | /** 114 | * Load wechat js script and sign the signature 115 | * @returns {Promise} 116 | */ 117 | loadScript() { 118 | return new Promise((resolve, reject) => { 119 | const ele = document.createElement('script'); 120 | ele.type = 'text/javascript'; 121 | ele.async = true; 122 | ele.onload = () => { 123 | console.log('Wechat script loaded successfully!'); 124 | //init the wechat config 125 | this.signSignature() 126 | .then(instance => { 127 | resolve(instance); 128 | }) 129 | .catch(err => { 130 | reject(err); 131 | }); 132 | }; 133 | ele.onerror = err => { 134 | console.error('Failed to load wechat script!'); 135 | console.error(err); 136 | this.debug && alert('Cannot load wechat script!'); 137 | reject(err); 138 | }; 139 | const linkEle = document.getElementsByTagName('script')[0]; 140 | linkEle.parentNode.insertBefore(ele, linkEle); 141 | ele.src = this.sdkUrl; 142 | }); 143 | } 144 | 145 | /** 146 | * Quick way to set custom moment share configs 147 | * @param {object} info 148 | * @deprecated 'onMenuShareTimeline is deprecated in jweixin-v1.4+, 149 | * use #updateAppMessageShareData() instead 150 | * @returns {WechatJSSDK} 151 | */ 152 | shareOnMoment(info) { 153 | if (!info) return this; 154 | return this.callWechatApi('onMenuShareTimeline', info); 155 | } 156 | 157 | updateAppMessageShareData(info) { 158 | if (!info) return this; 159 | return this.callWechatApi('updateAppMessageShareData', info); 160 | } 161 | 162 | /** 163 | * Quick way to set custom chat share configs 164 | * @param {object} info 165 | * @deprecated 'onMenuShareAppMessage is deprecated in jweixin-v1.4+, 166 | * use #updateTimelineShareData() instead 167 | * @returns {WechatJSSDK} 168 | */ 169 | shareOnChat(info) { 170 | if (!info) return this; 171 | return this.callWechatApi('onMenuShareAppMessage', info); 172 | } 173 | updateTimelineShareData(info) { 174 | if (!info) return this; 175 | return this.callWechatApi('updateTimelineShareData', info); 176 | } 177 | 178 | /** 179 | * Call any wechat api 180 | * @param {string} apiName 181 | * @param {object} config specific api config 182 | * @param {function=} cb wx api callback in v1.4 183 | * @returns {WechatJSSDK} 184 | */ 185 | callWechatApi(apiName, config, cb) { 186 | if (!apiName) return this; 187 | const debug = this.debug; 188 | if (this.config.jsApiList.indexOf(apiName) < 0) { 189 | debug && 190 | alert( 191 | 'the wechat api [' + 192 | apiName + 193 | '] you call was not registered, \npls add the api into your [jsApiList] config' 194 | ); 195 | return this; 196 | } 197 | const wx = this.getOriginalWx(); 198 | let customAPI = wx[apiName]; 199 | if (!customAPI || 'function' !== typeof customAPI) { 200 | debug && alert('no such api [' + apiName + '] found!'); 201 | return this; 202 | } 203 | customAPI(config, cb); 204 | return this; 205 | } 206 | 207 | /** 208 | * get the original wx object directly 209 | * @return {*} 210 | */ 211 | getOriginalWx() { 212 | return this.wx || window.wx; 213 | } 214 | 215 | /** 216 | * check and set the original wx to this 217 | * @returns {WechatJSSDK} 218 | */ 219 | setOriginWx() { 220 | if (!this.wx) { 221 | this.wx = window.wx; 222 | } 223 | return this; 224 | } 225 | } 226 | 227 | export default WechatJSSDK; 228 | -------------------------------------------------------------------------------- /lib/code.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const code = (exports.CODE = { 4 | SERVER_ERROR: -1, 5 | //access token invalid related 6 | APP_SECRET_INVALID: 40001, 7 | ACCESS_TOKEN_INVALID: 40014, 8 | ACCESS_TOKEN_EXPIRED: 42001, 9 | ACCESS_TOKEN_REFRESH_NEEDED: 42007, 10 | }); 11 | 12 | const accessTokenRelatedCodes = [ 13 | code.APP_SECRET_INVALID, 14 | code.ACCESS_TOKEN_INVALID, 15 | code.ACCESS_TOKEN_EXPIRED, 16 | code.ACCESS_TOKEN_REFRESH_NEEDED, 17 | ]; 18 | 19 | /* istanbul ignore next */ 20 | exports.errorByAccessTokenRelated = errorCode => { 21 | return accessTokenRelatedCodes.indexOf(errorCode) >= 0; 22 | }; 23 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isEmpty = require('lodash.isempty'); 4 | 5 | const wechatConfig = { 6 | //redirect host in oauth redirect 7 | wechatRedirectHost: 'http://127.0.0.1', 8 | //full redirect url in oauth redirect, e.g http://127.0.0.1/wechat/oauth-callback 9 | wechatRedirectUrl: '', 10 | //your wechat token set in your 11 | // https://mp.weixin.qq.com/advanced/advanced?action=dev&t=advanced/dev&token=1244756112&lang=zh_CN 12 | wechatToken: '', 13 | //your wechat appId 14 | appId: '', 15 | // your wechat appSecret 16 | appSecret: '', 17 | ticketUrl: 'https://api.weixin.qq.com/cgi-bin/ticket/getticket', 18 | accessTokenUrl: 'https://api.weixin.qq.com/cgi-bin/token', 19 | oAuthUrl: 'https://open.weixin.qq.com/connect/oauth2/authorize', 20 | apiUrl: 'https://api.weixin.qq.com', 21 | //state in oauth callback query 22 | oAuthState: '', 23 | paymentNotifyUrl: 'http://127.0.0.1/api/wechat/payment/', 24 | paymentSandBox: false, 25 | paymentKey: '', 26 | PAYMENT_HOST: 'api.mch.weixin.qq.com', 27 | PAYMENT_HOST_PORT: 443, 28 | paymentUrls: { 29 | UNIFIED_ORDER: 'https://api.mch.weixin.qq.com/pay/unifiedorder', 30 | QUERY_ORDER: 'https://api.mch.weixin.qq.com/pay/orderquery', 31 | CLOSE_ORDER: 'https://api.mch.weixin.qq.com/pay/closeorder', 32 | REFUND: 'https://api.mch.weixin.qq.com/secapi/pay/refund', 33 | QUERY_REFUND: 'https://api.mch.weixin.qq.com/pay/refundquery', 34 | DOWNLOAD_BILL: 'https://api.mch.weixin.qq.com/pay/downloadbill', 35 | SHORT_URL: 'https://api.mch.weixin.qq.com/tools/shorturl', 36 | REPORT: 'https://api.mch.weixin.qq.com/payitil/report', 37 | SIGN_KEY: 'https://api.mch.weixin.qq.com/pay/getsignkey', 38 | DOWNLOAD_FUND_FLOW: 'https://api.mch.weixin.qq.com/pay/downloadfundflow', 39 | BATCH_QUERY_COMMENT: 40 | 'https://api.mch.weixin.qq.com/billcommentsp/batchquerycomment', 41 | QUERY_SETTLEMENT: 'https://api.mch.weixin.qq.com/pay/settlementquery', 42 | // yes this is correct, spelling "exchange" correctly is difficult 🤷️ 43 | QUERY_EXCHANGE_RATE: 'https://api.mch.weixin.qq.com/pay/queryexchagerate', 44 | }, 45 | decodeCardCodeUrl: 'https://api.weixin.qq.com/card/code/decrypt', 46 | miniProgram: { 47 | //your mini program appId 48 | appId: '', 49 | // your mini program appSecret 50 | appSecret: '', 51 | GET_SESSION_KEY_URL: 'https://api.weixin.qq.com/sns/jscode2session', 52 | }, 53 | }; 54 | 55 | const COMPARE_CONFIG_KEYS = ['appId', 'wechatRedirectUrl', 'paymentSandBox']; 56 | 57 | exports.COMPARE_CONFIG_KEYS = COMPARE_CONFIG_KEYS; 58 | 59 | exports.getConfigFromCompareKeys = ( 60 | wechatConfig, 61 | compareKeys = COMPARE_CONFIG_KEYS 62 | ) => { 63 | const ret = {}; 64 | compareKeys.forEach(k => (ret[k] = wechatConfig[k])); 65 | return ret; 66 | }; 67 | 68 | /** 69 | * Check if user passed necessary configuration 70 | * @param {object} options user custom wechat config 71 | */ 72 | exports.checkPassedConfiguration = options => { 73 | if (isEmpty(options)) { 74 | throw new Error('you need to pass the wechat configuration'); 75 | } 76 | if (!options.appId) { 77 | throw new Error('wechat appId not found'); 78 | } 79 | if (!options.appSecret) { 80 | throw new Error('wechat appSecret not found'); 81 | } 82 | return true; 83 | }; 84 | 85 | /** 86 | * get default wechat configuration 87 | * @return {object} wechatConfig 88 | */ 89 | exports.getDefaultConfiguration = () => wechatConfig; 90 | 91 | /** 92 | * Check if the new main wechat config values are the same as the previous one 93 | * @param {object} newConfig 94 | * @param {object} oldConfig 95 | * @param {Array=} compareKeys - custom keys to compare 96 | * @return {boolean} 97 | */ 98 | exports.isBreakingConfigChange = ( 99 | newConfig, 100 | oldConfig, 101 | compareKeys = COMPARE_CONFIG_KEYS 102 | ) => { 103 | if (!newConfig || !oldConfig) { 104 | return true; 105 | } 106 | let isBreaking = false; 107 | for (let i = 0; i < compareKeys.length; i++) { 108 | const key = compareKeys[i]; 109 | if (newConfig[key] != oldConfig[key]) { 110 | isBreaking = true; 111 | break; 112 | } 113 | } 114 | return isBreaking; 115 | }; 116 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { COMPARE_CONFIG_KEYS } = require('./config'); 4 | 5 | exports.COMPARE_CONFIG_KEYS = COMPARE_CONFIG_KEYS; 6 | 7 | exports.Wechat = require('./Wechat'); 8 | exports.JSSDK = require('./JSSDK'); 9 | exports.OAuth = require('./OAuth'); 10 | exports.Card = require('./Card'); 11 | exports.Payment = require('./Payment'); 12 | exports.MiniProgram = require('./MiniProgram'); 13 | exports.Store = require('./store/Store'); 14 | exports.FileStore = require('./store/FileStore'); 15 | exports.MongoStore = require('./store/MongoStore'); 16 | -------------------------------------------------------------------------------- /lib/store/FileStore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('wechat-FileStore'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | const Store = require('./Store'); 8 | const { 9 | getConfigFromCompareKeys, 10 | isBreakingConfigChange, 11 | } = require('../config'); 12 | 13 | /** 14 | * Simple Store using json file 15 | */ 16 | class FileStore extends Store { 17 | constructor(options = {}, wechatConfig = {}) { 18 | super(options); 19 | 20 | this.fileStorePath = options.fileStorePath 21 | ? path.resolve(options.fileStorePath) 22 | : path.resolve(process.cwd(), 'wechat-info.json'); 23 | 24 | this.initFileStore(options, wechatConfig); 25 | } 26 | 27 | initFileStore(options, wechatConfig) { 28 | debug('using FileStore[%s]...', this.fileStorePath); 29 | 30 | const emptyStore = Object.assign({}, this.store); 31 | let hasExistFile = true; 32 | let storeWechatConfig = getConfigFromCompareKeys( 33 | wechatConfig, 34 | options.compareConfigKeys 35 | ); 36 | 37 | try { 38 | fs.statSync(this.fileStorePath); 39 | } catch (e) { 40 | //write the default empty store object to file 41 | emptyStore.wechatConfig = storeWechatConfig; 42 | hasExistFile = false; 43 | fs.writeFileSync(this.fileStorePath, JSON.stringify(emptyStore, null, 2)); 44 | debug('create wechat info file finished'); 45 | } finally { 46 | const storeStr = fs.readFileSync(this.fileStorePath); 47 | /* istanbul ignore else */ 48 | if (storeStr) { 49 | try { 50 | this.store = JSON.parse(storeStr); 51 | } catch (e) { 52 | /* istanbul ignore next */ 53 | debug('wechat json file invalid! Will use empty store instead'); 54 | } 55 | } 56 | /* istanbul ignore if */ 57 | if ( 58 | (hasExistFile && options.clearStore) || 59 | isBreakingConfigChange( 60 | wechatConfig, 61 | this.store.wechatConfig, 62 | options.compareConfigKeys 63 | ) 64 | ) { 65 | this.store = emptyStore; 66 | this.store.wechatConfig = options; 67 | this.flush(); 68 | debug('wechat config change, resetting wechat info...'); 69 | } 70 | } 71 | } 72 | 73 | flush() { 74 | const temp = Object.assign({}, this.store); 75 | if (temp.wechatConfig) { 76 | temp.wechatConfig.store = `${this.constructor.name}_${ 77 | this.fileStorePath 78 | }`; 79 | } 80 | // console.log('this.store: ', temp); 81 | fs.writeFile(this.fileStorePath, JSON.stringify(temp, null, 2), err => { 82 | if (err) { 83 | debug('ERROR: export wechat info to file failed!'); 84 | debug(err); 85 | return; 86 | } 87 | super.flush(); 88 | debug('export wechat info to file finished'); 89 | }); 90 | } 91 | 92 | destroy() { 93 | super.destroy(); 94 | debug('fileStore destroyed!'); 95 | } 96 | } 97 | 98 | module.exports = FileStore; 99 | -------------------------------------------------------------------------------- /lib/store/MongoStore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('wechat-MongoStore'); 4 | const isEmpty = require('lodash.isempty'); 5 | const mongoose = require('mongoose'); 6 | 7 | mongoose.Promise = Promise; 8 | 9 | const Schema = mongoose.Schema; 10 | 11 | const Store = require('./Store'); 12 | const GID = 'GID'; 13 | 14 | const SignatureSchema = new Schema({ 15 | url: { 16 | type: String, 17 | index: true, 18 | unique: true, 19 | }, 20 | signatureName: String, 21 | jsapi_ticket: String, 22 | nonceStr: String, 23 | timestamp: String, 24 | signature: String, 25 | accessToken: String, 26 | createDate: Date, 27 | modifyDate: Date, 28 | }); 29 | const GlobalTokenSchema = new Schema({ 30 | gid: { 31 | type: String, 32 | default: GID, 33 | }, 34 | count: Number, 35 | modifyDate: Date, 36 | accessToken: { 37 | type: String, 38 | index: true, 39 | unique: true, 40 | }, 41 | jsapi_ticket: String, 42 | }); 43 | const OAuthTokenSchema = new Schema({ 44 | key: { 45 | type: String, 46 | index: true, 47 | unique: true, 48 | }, 49 | access_token: String, 50 | expires_in: Number, 51 | refresh_token: String, 52 | openid: { 53 | type: String, 54 | index: true, 55 | unique: true, 56 | }, 57 | scope: String, 58 | expirationTime: Number, 59 | }); 60 | const CardTicketSchema = new Schema({ 61 | ticket: String, 62 | expires_in: Number, 63 | modifyDate: Date, 64 | }); 65 | 66 | /** 67 | * Simple Store using MongoDB 68 | */ 69 | class MongoStore extends Store { 70 | /** 71 | * Simple mongodb store 72 | * @param options 73 | * @constructor 74 | */ 75 | constructor(options) { 76 | super(options); 77 | 78 | /* istanbul ignore else */ 79 | if (!options) { 80 | options = {}; 81 | } 82 | 83 | debug('using MongoStore...'); 84 | /* istanbul ignore if */ 85 | if (options.hasOwnProperty('cache')) { 86 | this.cache = !!options.cache; 87 | } 88 | this.dbName = options.dbName || 'wechat'; 89 | this.dbHost = options.dbHost || '127.0.0.1'; 90 | this.dbPort = options.dbPort || '27017'; 91 | this.dbAddress = 92 | options.dbAddress || 93 | `mongodb://${this.dbHost}:${this.dbPort}/${this.dbName}`; 94 | 95 | this.initLimit = options.limit || 20; 96 | 97 | // console.log('this.dbAddress: ', this.dbAddress); 98 | 99 | //Connecting to mongodb 100 | const conn = (this.connection = mongoose.createConnection( 101 | this.dbAddress, 102 | Object.assign( 103 | { 104 | useNewUrlParser: true, 105 | useUnifiedTopology: true, 106 | useCreateIndex: true, 107 | useFindAndModify: false, 108 | }, 109 | options.dbOptions 110 | ) 111 | )); 112 | 113 | conn 114 | .then(() => { 115 | // we're connected! 116 | debug('Mongodb connected!'); 117 | //Models 118 | this.Signature = conn.model('Signature', SignatureSchema); 119 | this.GlobalToken = conn.model('GlobalToken', GlobalTokenSchema); 120 | this.OAuthToken = conn.model('OAuthToken', OAuthTokenSchema); 121 | this.CardTicket = conn.model('CardTicket', CardTicketSchema); 122 | // this.Signature.syncIndexes(); 123 | this.initializeTokenFromDB(); 124 | }) 125 | .catch(err => { 126 | /* istanbul ignore next */ 127 | debug(err); 128 | }); 129 | } 130 | 131 | /** 132 | * Initialize wechat token, signature, etc... from mongodb 133 | */ 134 | initializeTokenFromDB() { 135 | Promise.all([ 136 | this.getGlobalToken(true), 137 | this.getUrlSignatures(), 138 | this.getOAuthTokens(), 139 | this.getCardTicket(true), 140 | ]) 141 | .then(results => { 142 | /* istanbul ignore if */ 143 | if (!isEmpty(results[0])) { 144 | this.store.globalToken = results[0]; 145 | debug('global token initialized from DB!'); 146 | } 147 | /* istanbul ignore if */ 148 | if (!isEmpty(results[1])) { 149 | this.store.urls = results[1]; 150 | debug('signatures initialized from DB!'); 151 | } 152 | /* istanbul ignore if */ 153 | if (!isEmpty(results[2])) { 154 | this.store.oauth = results[2]; 155 | debug('user oauth tokens initialized from DB!'); 156 | } 157 | /* istanbul ignore if */ 158 | if (!isEmpty(results[3])) { 159 | this.store.card = results[3]; 160 | debug('card_ticket initialized from DB!'); 161 | } 162 | this.emit('initialized'); 163 | }) 164 | .catch(reason => { 165 | debug(reason); 166 | this.emit('initialized'); 167 | }); 168 | } 169 | 170 | getGlobalToken() { 171 | /* istanbul ignore if */ 172 | if (this.cache && !arguments[0]) { 173 | return super.getGlobalToken(); 174 | } 175 | debug('getting global token from DB...'); 176 | return this.GlobalToken.findOne().then(token => { 177 | debug('global token received!'); 178 | return Promise.resolve(this.toObject(token)); 179 | }); 180 | } 181 | 182 | getCardTicket() { 183 | /* istanbul ignore if */ 184 | if (this.cache && !arguments[0]) { 185 | return super.getCardTicket(); 186 | } 187 | debug('getting card_ticket from DB...'); 188 | return this.CardTicket.findOne().then(cardTicket => { 189 | debug('card_ticket received!'); 190 | // console.log('cardTicket: ', this.toObject(cardTicket)); 191 | return Promise.resolve(this.toObject(cardTicket)); 192 | }); 193 | } 194 | 195 | getUrlSignatures(limit) { 196 | return this.Signature.find({}) 197 | .limit(limit || this.initLimit) 198 | .then(signatures => { 199 | const temp = {}; 200 | signatures.forEach(sig => { 201 | /* istanbul ignore next */ 202 | temp[sig.url] = this.toObject(sig); 203 | }); 204 | debug(`[${signatures.length}] signatures received!`); 205 | return Promise.resolve(temp); 206 | }); 207 | } 208 | 209 | getOAuthTokens(limit) { 210 | return this.OAuthToken.find({}) 211 | .limit(limit || this.initLimit) 212 | .then(oauthTokens => { 213 | const temp = {}; 214 | oauthTokens.forEach(token => { 215 | /* istanbul ignore next */ 216 | temp[token.key] = this.toObject(token); 217 | }); 218 | debug(`[${oauthTokens.length}] user oauth tokens received!`); 219 | return Promise.resolve(temp); 220 | }); 221 | } 222 | 223 | updateGlobalToken(info) { 224 | //Update to DB 225 | debug('updating global token...'); 226 | this.GlobalToken.findOneAndUpdate({}, Object.assign({}, info), { 227 | new: true, 228 | upsert: true, 229 | }).then(() => { 230 | debug('global token updated!'); 231 | // Promise.resolve(newResult); 232 | }); 233 | //return immediately from cache 234 | // console.log('info: ', info); 235 | return super.updateGlobalToken(info); 236 | } 237 | 238 | updateCardTicket(ticketInfo) { 239 | //Update to DB 240 | debug('saving or updating card_ticket...'); 241 | this.CardTicket.findOneAndUpdate({}, Object.assign({}, ticketInfo), { 242 | new: true, 243 | upsert: true, 244 | }) 245 | .then(() => { 246 | debug('card_ticket updated!'); 247 | // Promise.resolve(newResult); 248 | }) 249 | .catch(err => { 250 | /* istanbul ignore next */ 251 | debug('update card_ticket to DB failed:'); 252 | debug(err); 253 | }); 254 | //return immediately from cache 255 | return super.updateCardTicket(ticketInfo); 256 | } 257 | 258 | saveSignature(url, signatureInfo) { 259 | const newSignature = new this.Signature(signatureInfo); 260 | newSignature 261 | .save() 262 | .then(() => { 263 | debug(`new signature for [${url}] saved to DB`); 264 | }) 265 | .catch(err => { 266 | /* istanbul ignore next */ 267 | debug('save new signature to DB failed:'); 268 | debug(err); 269 | }); 270 | return super.saveSignature(url, signatureInfo); 271 | } 272 | 273 | getSignature(url) { 274 | return super.getSignature(url).then(sig => { 275 | if (!isEmpty(sig)) { 276 | return Promise.resolve(this.toObject(sig)); 277 | } 278 | return this.Signature.findOne({ url: url }).then(sig => { 279 | if (!isEmpty(sig)) { 280 | debug('got signature from db'); 281 | sig = this.toObject(sig); 282 | this.store.urls[url] = sig; 283 | } 284 | return Promise.resolve(sig); 285 | }); 286 | }); 287 | } 288 | 289 | updateSignature(url, newInfo) { 290 | this.Signature.findOneAndUpdate({ url }, Object.assign({}, newInfo), { 291 | upsert: true, 292 | }) 293 | .then(() => { 294 | debug('update signature to DB finished!'); 295 | }) 296 | .catch(err => { 297 | /* istanbul ignore next */ 298 | debug('update error:', err); 299 | }); 300 | return super.updateSignature(url, newInfo); 301 | } 302 | 303 | getOAuthAccessToken(key) { 304 | return super.getOAuthAccessToken(key).then(token => { 305 | if (!isEmpty(token)) { 306 | return Promise.resolve(token); 307 | } 308 | return this.OAuthToken.findOne({ key: key }).then(token => { 309 | if (!isEmpty(token)) { 310 | debug('got oauth token from db'); 311 | token = this.toObject(token); 312 | this.store.oauth[key] = token; 313 | } 314 | return Promise.resolve(token); 315 | }); 316 | }); 317 | } 318 | 319 | saveOAuthAccessToken(key, info) { 320 | const newOAuthToken = new this.OAuthToken(info); 321 | newOAuthToken 322 | .save() 323 | .then(() => { 324 | debug(`new oauth token saved to DB`); 325 | }) 326 | .catch(err => { 327 | /* istanbul ignore next */ 328 | debug(err); 329 | debug('save new oauth token to DB failed, try updating...'); 330 | this.updateOAuthAccessToken(key, info); 331 | }); 332 | return super.saveOAuthAccessToken(key, info); 333 | } 334 | 335 | updateOAuthAccessToken(key, newInfo) { 336 | this.OAuthToken.findOneAndUpdate({ key }, Object.assign({}, newInfo), { 337 | upsert: true, 338 | }) 339 | .then(() => { 340 | debug('update oauth token to DB finished!'); 341 | }) 342 | .catch(err => { 343 | debug('update oauth token error:', err); 344 | }); 345 | return super.updateSignature(key, newInfo); 346 | } 347 | 348 | flushGlobalToken() { 349 | debug('flushing global token...'); 350 | const gt = this.store.globalToken; 351 | // console.log('gt: ', gt); 352 | if (!isEmpty(gt)) { 353 | return this.GlobalToken.findOneAndUpdate({}, Object.assign({}, gt), { 354 | new: true, 355 | upsert: true, 356 | }) 357 | .then(() => { 358 | debug('global token flushed!'); 359 | return Promise.resolve(); 360 | }) 361 | .catch(err => { 362 | debug(err); 363 | }); 364 | } 365 | return Promise.resolve(undefined); 366 | } 367 | 368 | flushCardTicket() { 369 | debug('flushing card_ticket...'); 370 | const ct = this.store.card; 371 | // console.log('ct: ', Object.assign({}, ct)); 372 | if (!isEmpty(ct)) { 373 | return this.CardTicket.updateOne({}, Object.assign({}, ct), { 374 | new: true, 375 | upsert: true, 376 | }).then(() => { 377 | debug('global token flushed!'); 378 | // Promise.resolve(newResult); 379 | }); 380 | } 381 | return Promise.resolve(undefined); 382 | } 383 | 384 | flushSignatures() { 385 | debug('flushing url signatures...'); 386 | const signatures = this.store.urls; 387 | if (!isEmpty(signatures)) { 388 | const keys = Object.keys(signatures); 389 | const bulk = this.Signature.collection.initializeOrderedBulkOp(); 390 | const batchedKeys = []; 391 | keys.forEach(key => { 392 | const sig = signatures[key]; 393 | if (!sig.updated) return; 394 | const temp = Object.assign({}, sig); 395 | temp._id && (temp._id = undefined); 396 | temp.hasOwnProperty('__v') && (temp.__v = undefined); 397 | temp.hasOwnProperty('updated') && (temp.updated = undefined); 398 | batchedKeys.push(key); 399 | bulk.find({ url: temp.url }).updateOne( 400 | { 401 | $set: temp, 402 | }, 403 | { upsert: true } 404 | ); 405 | }); 406 | return new Promise(function(resolve, reject) { 407 | try { 408 | if (batchedKeys.length <= 0) { 409 | return resolve(true); 410 | } 411 | bulk.execute(function(err) { 412 | if (err) { 413 | debug(err); 414 | } else { 415 | debug(`[${keys.length}] signatures flushed!`); 416 | } 417 | resolve(true); 418 | }); 419 | } catch (e) { 420 | debug(e); 421 | reject(e); 422 | } 423 | }); 424 | } 425 | return Promise.resolve(undefined); 426 | } 427 | 428 | flushOAuthTokens() { 429 | debug('flushing oauth tokens...'); 430 | const oauthTokens = this.store.oauth; 431 | if (!isEmpty(oauthTokens)) { 432 | const keys = Object.keys(oauthTokens); 433 | const bulk = this.OAuthToken.collection.initializeOrderedBulkOp(); 434 | const batchedKeys = []; 435 | keys.forEach(key => { 436 | const token = oauthTokens[key]; 437 | if (!token.updated) return; 438 | const temp = Object.assign({}, token); 439 | temp._id && (temp._id = undefined); 440 | temp.hasOwnProperty('__v') && (temp.__v = undefined); 441 | temp.hasOwnProperty('updated') && (temp.updated = undefined); 442 | batchedKeys.push(key); 443 | bulk.find({ key: temp.key }).updateOne( 444 | { 445 | $set: temp, 446 | }, 447 | { upsert: true } 448 | ); 449 | }); 450 | return new Promise(function(resolve, reject) { 451 | try { 452 | if (batchedKeys.length <= 0) { 453 | return resolve(true); 454 | } 455 | bulk.execute(function(err) { 456 | if (err) { 457 | debug(err); 458 | } else { 459 | debug(`[${keys.length}] oauth tokens flushed!`); 460 | } 461 | resolve(true); 462 | }); 463 | } catch (e) { 464 | debug(e); 465 | reject(e); 466 | } 467 | }); 468 | } 469 | return Promise.resolve(undefined); 470 | } 471 | 472 | flush() { 473 | if (!this.cache) return Promise.resolve(true); 474 | 475 | return Promise.all([ 476 | this.flushGlobalToken(), 477 | this.flushSignatures(), 478 | this.flushOAuthTokens(), 479 | this.flushCardTicket(), 480 | ]) 481 | .then(() => super.flush()) 482 | .catch(() => super.flush()); 483 | } 484 | 485 | /* istanbul ignore next */ 486 | destroy() { 487 | this.connection.close(err => { 488 | if (err) { 489 | debug(err); 490 | } 491 | }); 492 | this.Signature = null; 493 | this.GlobalToken = null; 494 | this.OAuthToken = null; 495 | this.CardTicket = null; 496 | super.destroy(); 497 | debug('mongoStore destroyed!'); 498 | } 499 | 500 | toObject(doc) { 501 | if ( 502 | !doc.toObject || 503 | /* istanbul ignore next */ 'function' != typeof doc.toObject 504 | ) { 505 | return doc; 506 | } 507 | return doc.toObject({ 508 | versionKey: false, 509 | }); 510 | } 511 | } 512 | 513 | module.exports = MongoStore; 514 | -------------------------------------------------------------------------------- /lib/store/Store.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('wechat-Store'); 4 | const EventEmitter = require('events'); 5 | 6 | const storeEvents = { 7 | FLUSH_STORE: 'FLUSH_STORE', 8 | STORE_FLUSHED: 'STORE_FLUSHED', 9 | DESTROYED: 'DESTROYED', 10 | DESTROY: 'DESTROY', 11 | }; 12 | 13 | class Store extends EventEmitter { 14 | /** 15 | * Store class constructor 16 | * @param options 17 | * @constructor 18 | */ 19 | constructor(options) { 20 | super(); 21 | 22 | if (!options) { 23 | options = {}; 24 | } 25 | 26 | this.cache = true; 27 | 28 | /** 29 | * Set default empty store object: 30 | * { 31 | * //global token/ticket info 32 | * globalToken: { 33 | * count: 0, 34 | * "modifyDate": "2016-12-01T09:25:43.781Z", 35 | * "accessToken": "accessToken", 36 | * "jsapi_ticket": "jsapi_ticket" 37 | * }, 38 | * //token/ticket info for every url 39 | * urls: { 40 | * "http://localhost/": { 41 | * "jsapi_ticket": "jsapi_ticket", 42 | * "nonceStr": "8iv3478f26vvsz1", 43 | * "timestamp": "1480584343", 44 | * "url": "http://localhost/", 45 | * "signature": "signature", 46 | * "accessToken": "accessToken", 47 | * "signatureName": "http://localhost/", //same as the url key 48 | * "createDate": "2016-12-01T09:25:43.784Z", 49 | * "modifyDate": "2016-12-01T09:25:43.784Z" 50 | * } 51 | * }, 52 | * //oauth cached token for every user 53 | * oauth: { 54 | * //key could be openid, or custom key passed by user 55 | * "key": { 56 | * "key": "unique_identifier", 57 | * "access_token":"ACCESS_TOKEN", 58 | * "expires_in":7200, 59 | * "refresh_token":"REFRESH_TOKEN", 60 | * "openid":"OPENID", 61 | * "scope":"SCOPE", 62 | * } 63 | * }, 64 | * //card api_ticket info 65 | * card: { 66 | * ticket: "api_ticket", 67 | * expires_in: 7200, 68 | * modifyDate: "2016-12-01T09:25:43.781Z" 69 | * } 70 | * //mini program info 71 | * mp: { 72 | * openid_as_key: { 73 | * openid: "OPENID", 74 | * session_key: "SESSIONKEY", 75 | * unionid: "UNIONID", //optional 76 | * }, 77 | * ... 78 | * } 79 | * } 80 | * 81 | */ 82 | this.store = { 83 | wechatConfig: {}, 84 | globalToken: { 85 | count: 0, 86 | }, 87 | urls: {}, 88 | oauth: {}, 89 | card: {}, 90 | mp: {}, 91 | }; 92 | 93 | this.on(storeEvents.FLUSH_STORE, this.flush); 94 | this.on(storeEvents.DESTROY, this.destroy); 95 | 96 | /* istanbul ignore else */ 97 | if (!options.noInterval) { 98 | //store to file every 10 minutes by default 99 | this.wechatInterval = setInterval( 100 | /* istanbul ignore next */ () => this.flush(), 101 | options.interval || 1000 * 60 * 10 102 | ); 103 | } 104 | } 105 | 106 | /* istanbul ignore next */ 107 | static get StoreEvents() { 108 | return storeEvents; 109 | } 110 | 111 | /** 112 | * Get global token info 113 | * @return {Promise} 114 | */ 115 | getGlobalToken() { 116 | return Promise.resolve(this.store.globalToken); 117 | } 118 | 119 | /** 120 | * Update the global token info, as if access_token or jsapi_ticket is refreshed 121 | * @param info new token info should be updated to store 122 | * @return {Promise} updated global token info 123 | */ 124 | updateGlobalToken(info) { 125 | const newToken = Object.assign({}, this.store.globalToken, info); 126 | // console.log('new token: ', newToken); 127 | newToken.count++; 128 | this.store.globalToken = newToken; 129 | this.flush(); 130 | debug('Access Token or jsapi ticket updated'); 131 | return Promise.resolve(newToken); 132 | } 133 | 134 | /** 135 | * Get signature for passed url from store 136 | * @param url 137 | * @return {Promise} 138 | */ 139 | getSignature(url) { 140 | return Promise.resolve(this.store.urls[url]); 141 | } 142 | 143 | /** 144 | * Add signature to store for the new url 145 | * @param url 146 | * @param signatureInfo 147 | * @return {Promise} 148 | */ 149 | saveSignature(url, signatureInfo) { 150 | signatureInfo.updated = true; 151 | this.store.urls[url] = signatureInfo; 152 | return Promise.resolve(signatureInfo); 153 | } 154 | 155 | /** 156 | * Update url signature to store 157 | * @param url 158 | * @param newInfo 159 | * @return {Promise} 160 | */ 161 | updateSignature(url, newInfo) { 162 | newInfo.updated = true; 163 | const newSig = Object.assign({}, this.store.urls[url], newInfo); 164 | this.store.urls[url] = newSig; 165 | return Promise.resolve(newSig); 166 | } 167 | 168 | /** 169 | * Check if signature of the url is existing in store 170 | * @param url 171 | * @return {Promise} 172 | */ 173 | isSignatureExisting(url) { 174 | const ret = url in this.store.urls; 175 | return Promise.resolve(ret); 176 | } 177 | 178 | /** 179 | * Get cached oauth access token info for current user 180 | * should store openid like in current user session 181 | * @param key 182 | * @return {Promise} 183 | */ 184 | getOAuthAccessToken(key) { 185 | return Promise.resolve(this.store.oauth[key]); 186 | } 187 | 188 | /** 189 | * Save new oauth access token info 190 | * @param key 191 | * @param info user oauth access token info 192 | * @return {Promise} 193 | */ 194 | saveOAuthAccessToken(key, info) { 195 | this.store.oauth[key] = info; 196 | return Promise.resolve(info); 197 | } 198 | 199 | /** 200 | * 201 | * @param key 202 | * @param newInfo 203 | * @return {Promise} 204 | */ 205 | updateOAuthAccessToken(key, newInfo) { 206 | newInfo.updated = true; 207 | const newToken = Object.assign({}, this.store.oauth[key], newInfo); 208 | this.store.oauth[key] = newToken; 209 | return Promise.resolve(newToken); 210 | } 211 | 212 | getCardTicket() { 213 | return Promise.resolve(this.store.card); 214 | } 215 | 216 | /** 217 | * 218 | * @param ticketInfo 219 | * @return {Promise} 220 | */ 221 | updateCardTicket(ticketInfo) { 222 | const newTicket = (this.store.card = Object.assign( 223 | {}, 224 | this.store.card, 225 | ticketInfo 226 | )); 227 | return Promise.resolve(newTicket); 228 | } 229 | 230 | /** 231 | * Get mini program session_ket 232 | * @param key - object key for the session, default openid 233 | * @return {Promise} 234 | */ 235 | /* istanbul ignore next: handle by end user */ 236 | getMPSessionKey(key) { 237 | const session = this.store.mp[key] || {}; 238 | return Promise.resolve(session.session_key); 239 | } 240 | 241 | /** 242 | * Get the session data with the key 243 | * @param key 244 | * @return {Promise} 245 | */ 246 | /* istanbul ignore next: handle by end user */ 247 | getMPSession(key) { 248 | return Promise.resolve(this.store.mp[key]); 249 | } 250 | 251 | /** 252 | * Set the session associated with the key 253 | * @param {string} key - object key for the session, default openid 254 | * @param {object} data - session data 255 | * @return {Promise} - resolved with old session data 256 | */ 257 | /* istanbul ignore next: handle by end user */ 258 | setMPSession(key, data) { 259 | /* istanbul ignore if */ 260 | if (!key) { 261 | return Promise.reject(new Error('missing key for the session!')); 262 | } 263 | const oldSession = this.store.mp[key]; 264 | this.store.mp[key] = data; 265 | return Promise.resolve(oldSession); 266 | } 267 | 268 | /** 269 | * Flush cached store object to persistent storage, e.g Database, File, etc... 270 | */ 271 | flush() { 272 | // this.emit(storeEvents.STORE_FLUSHED, true); 273 | // debug('flushed on Store class'); 274 | } 275 | 276 | /** 277 | * Destroy the Store instance 278 | */ 279 | destroy() { 280 | clearInterval(this.wechatInterval); 281 | this.store = null; 282 | // this.emit(storeEvents.DESTROYED, true); 283 | } 284 | 285 | /** 286 | * Clear all old store information, e.g: 287 | * Rewrite file or empty related db 288 | */ 289 | clearStore() {} 290 | } 291 | 292 | module.exports = Store; 293 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('wechat'); 4 | const crypto = require('crypto'); 5 | 6 | const xml2js = require('xml2js'); 7 | const dateFormat = require('date-fns/format'); 8 | const url = require('url'); 9 | const got = require('got'); 10 | 11 | const DEFAULT_FORMAT = 'yyyyMMddHHmmss'; 12 | 13 | const defaultOptions = { 14 | json: true, 15 | decompress: false, 16 | }; 17 | 18 | //1h 59m, token is only valid within 2 hours 19 | const REFRESH_INTERVAL = 1000 * 119 * 60; 20 | 21 | const utils = {}; 22 | 23 | /** 24 | * Generate digest hash based on the content 25 | * @param {*} content content to be digested 26 | * @param {string=} algorithm digest algorithm, default 'sha1' 27 | * @return {string} 28 | */ 29 | utils.genHash = (content, algorithm) => { 30 | const c = crypto.createHash(algorithm); 31 | c.update(content, 'utf8'); 32 | return c.digest('hex'); 33 | }; 34 | 35 | /** 36 | * Generate SHA1 hash 37 | * @param {*} content 38 | * @return {string} 39 | */ 40 | utils.genSHA1 = content => utils.genHash(content, 'sha1'); 41 | 42 | /** 43 | * Generate MD5 hash 44 | * @param {*} content 45 | * @return {string} 46 | */ 47 | utils.genMD5 = content => utils.genHash(content, 'MD5'); 48 | 49 | utils.genHmacSHA256 = (content, key) => { 50 | const hmac = crypto.createHmac('sha256', key); 51 | hmac.update(content, 'utf8'); 52 | return hmac.digest('hex'); 53 | }; 54 | 55 | /** 56 | * Parse the object to query string without encoding based on the ascii key order 57 | * @param {object} args 58 | * @param {boolean} noLowerCase 59 | * @return {string} 60 | */ 61 | utils.paramsToString = (args, noLowerCase) => { 62 | let keys = Object.keys(args); 63 | keys = keys.sort(); 64 | const newArgs = {}; 65 | keys.forEach(key => { 66 | const temp = noLowerCase ? key : key.toLowerCase(); 67 | newArgs[temp] = args[key]; 68 | }); 69 | 70 | let str = ''; 71 | for (let k in newArgs) { 72 | /* istanbul ignore else */ 73 | if (newArgs.hasOwnProperty(k)) { 74 | str += '&' + k + '=' + newArgs[k]; 75 | } 76 | } 77 | str = str.substr(1); 78 | return str; 79 | }; 80 | 81 | /** 82 | * Send the request to wechat server 83 | * @param {string|Object} url 84 | * @param {object} options custom request options 85 | * @return {Promise} 86 | */ 87 | utils.sendWechatRequest = (url, options) => { 88 | const myOptions = Object.assign({}, defaultOptions, options); 89 | return got(url, myOptions) 90 | .then(response => { 91 | const body = response.body; 92 | if (body.hasOwnProperty('errcode') && body.errcode != 0) { 93 | return Promise.reject(body); 94 | } 95 | return Promise.resolve(body); 96 | }) 97 | .catch(err => { 98 | debug(err); 99 | return Promise.reject( 100 | err.response && err.response.body ? err.response.body : err 101 | ); 102 | }); 103 | }; 104 | 105 | /** 106 | * Send the payment request to wechat server 107 | * @param {string|Object} url 108 | * @param {object} options custom request options 109 | * @return {Promise} 110 | */ 111 | utils.sendWechatPaymentRequest = (url, options) => { 112 | const myOptions = Object.assign( 113 | {}, 114 | defaultOptions, 115 | { 116 | json: false, 117 | method: 'POST', 118 | }, 119 | options 120 | ); 121 | return got(url, myOptions) 122 | .then(response => { 123 | return Promise.resolve(response.body); 124 | }) 125 | .catch(err => { 126 | debug(err); 127 | return Promise.reject( 128 | err.response && err.response.body ? err.response.body : err 129 | ); 130 | }); 131 | }; 132 | 133 | /** 134 | * Create nonce string 135 | * @return {string} 136 | */ 137 | utils.nonceStr = function() { 138 | return Math.random() 139 | .toString(36) 140 | .substr(2, 15); 141 | }; 142 | 143 | /** 144 | * Create timestamp string 145 | * @return {string} 146 | */ 147 | utils.timestamp = function() { 148 | return parseInt(new Date().getTime() / 1000) + ''; 149 | }; 150 | 151 | /** 152 | * Check if date is expired 153 | * @param {Date|string} modifyDate 154 | * @param {number=} interval milliseconds custom expires in 155 | * @return {boolean} 156 | */ 157 | utils.isExpired = function(modifyDate, interval) { 158 | /* istanbul ignore else */ 159 | if (interval === undefined) interval = REFRESH_INTERVAL; 160 | return Date.now() - new Date(modifyDate).getTime() > interval; 161 | }; 162 | 163 | /** 164 | * Get global access token from wechat server 165 | * @param {string} appId 166 | * @param {string} appSecret 167 | * @param {string} accessTokenUrl 168 | * @return {Promise} 169 | */ 170 | utils.getGlobalAccessToken = function(appId, appSecret, accessTokenUrl) { 171 | const params = { 172 | grant_type: 'client_credential', 173 | appid: appId, 174 | secret: appSecret, 175 | }; 176 | debug('getting new global token...'); 177 | return utils 178 | .sendWechatRequest(accessTokenUrl, { 179 | query: params, 180 | }) 181 | .then(data => data) 182 | .catch(reason => { 183 | debug('get global wechat access token failed!'); 184 | return Promise.reject(reason); 185 | }); 186 | }; 187 | 188 | /** 189 | * Parse the xml data returned from wechat server 190 | * @param xmlData 191 | * @return {Promise} result promise 192 | */ 193 | utils.parseXML = function(xmlData) { 194 | const parser = new xml2js.Parser({ 195 | normalize: true, 196 | explicitRoot: false, 197 | explicitArray: false, 198 | }); 199 | return new Promise(function(resolve, reject) { 200 | parser.parseString(xmlData, function(err, result) { 201 | /* istanbul ignore if */ 202 | if (err) { 203 | debug('result: ' + result); 204 | debug(err); 205 | reject(result); 206 | return; 207 | } 208 | resolve(result); 209 | }); 210 | }); 211 | }; 212 | 213 | /** 214 | * Build xml data string from the JSON object 215 | * @param {object} objData 216 | * @return {Promise} 217 | */ 218 | utils.buildXML = function(objData) { 219 | const builder = new xml2js.Builder({ 220 | rootName: 'xml', 221 | cdata: true, 222 | headless: true, 223 | allowSurrogateChars: true, 224 | }); 225 | const xml = builder.buildObject(objData); 226 | return Promise.resolve(xml); 227 | }; 228 | 229 | /** 230 | * Simple Date formatter 231 | * @param {(string|Date)=} date 232 | * @param {string=} format 233 | * @return {string} 234 | */ 235 | utils.simpleDate = function(date = new Date(), format = DEFAULT_FORMAT) { 236 | /* istanbul ignore if */ 237 | if (!(date instanceof Date)) { 238 | date = new Date(date); 239 | } 240 | return dateFormat(date, format); 241 | }; 242 | 243 | /** 244 | * Add "/sandboxnew" for payment apis to for testing 245 | * @param paymentUrls 246 | * @return {object} 247 | */ 248 | utils.paymentUrlsWithSandBox = function(paymentUrls) { 249 | const keys = Object.keys(paymentUrls); 250 | const newUrls = {}; 251 | keys.forEach(urlKey => { 252 | const paymentUrl = paymentUrls[urlKey]; 253 | const obj = url.parse(paymentUrl); 254 | newUrls[urlKey] = [ 255 | obj.protocol, 256 | '//', 257 | obj.host, 258 | '/sandboxnew', 259 | obj.pathname, 260 | ].join(''); 261 | }); 262 | return newUrls; 263 | }; 264 | 265 | /*utils.decodeBase64 = function(b64string) { 266 | return utils.createBuffer(b64string, 'base64').toString(); 267 | };*/ 268 | 269 | utils.createBuffer = function(str, encoding) { 270 | const encode = /* istanbul ignore next */ encoding || 'utf8'; 271 | let buf; 272 | /* istanbul ignore else */ 273 | if (typeof Buffer.from === 'function') { 274 | // Node 5.10+ 275 | buf = Buffer.from(str, encode); 276 | } else { 277 | // older Node versions 278 | buf = new Buffer(str, encode); 279 | } 280 | return buf; 281 | }; 282 | 283 | utils.createBufferFromBase64 = function(base64Str) { 284 | return utils.createBuffer(base64Str, 'base64'); 285 | }; 286 | 287 | module.exports = utils; 288 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wechat-jssdk", 3 | "version": "5.1.0", 4 | "description": "WeChat JS-SDK integration with NodeJS and Web", 5 | "main": "./lib/index.js", 6 | "source": "./lib/client.js", 7 | "browser": "./dist/client.umd.js", 8 | "engines": { 9 | "node": ">= 8.6" 10 | }, 11 | "scripts": { 12 | "start": "DEBUG=wechat* node ./demo/index.js", 13 | "dev": "DEBUG=wechat* nodemon -w ./lib -w ./demo ./demo/index.js", 14 | "test": "_mocha --exit test/**/*.js", 15 | "coverage": "nyc npm run test && nyc report --reporter=lcov", 16 | "coveralls": "cat ./coverage/lcov.info | node node_modules/.bin/coveralls", 17 | "build": "microbundle -f umd -o dist/client.js --name WechatJSSDK --no-sourcemap", 18 | "watch": "microbundle --watch -f umd -o dist/client.js --name WechatJSSDK", 19 | "prepublishOnly": "npm run build" 20 | }, 21 | "husky": { 22 | "hooks": { 23 | "pre-commit": "lint-staged" 24 | } 25 | }, 26 | "lint-staged": { 27 | "**/*.js": [ 28 | "prettier --write", 29 | "git add" 30 | ] 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/JasonBoy/wechat-jssdk.git" 35 | }, 36 | "keywords": [ 37 | "wechat", 38 | "weixin", 39 | "node", 40 | "jssdk", 41 | "wechat share", 42 | "mini program" 43 | ], 44 | "author": "Jason Jiang", 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/JasonBoy/wechat-jssdk/issues" 48 | }, 49 | "homepage": "https://github.com/JasonBoy/wechat-jssdk#readme", 50 | "dependencies": { 51 | "date-fns": "2.9.0", 52 | "debug": "4.1.1", 53 | "got": "9.6.0", 54 | "lodash.isempty": "4.4.0", 55 | "mongoose": "5.11.7", 56 | "xml2js": "0.4.23" 57 | }, 58 | "devDependencies": { 59 | "body-parser": "1.19.0", 60 | "chai": "4.2.0", 61 | "cookie-parser": "1.4.4", 62 | "coveralls": "3.0.9", 63 | "express": "4.17.1", 64 | "express-session": "1.17.0", 65 | "husky": "3.0.9", 66 | "lint-staged": "9.4.2", 67 | "lowdb": "1.0.0", 68 | "microbundle": "0.12.0-next.3", 69 | "mocha": "6.2.1", 70 | "nodemon": "2.0.2", 71 | "nunjucks": "3.2.0", 72 | "nyc": "15.0.0", 73 | "prettier": "1.19.1" 74 | }, 75 | "files": [ 76 | "dist/client.umd.js", 77 | "lib", 78 | "test", 79 | ".coveralls.yml", 80 | ".travis.yml", 81 | "LICENSE", 82 | "package.json", 83 | "README.md" 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /test/Card.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const bootstrap = require('./bootstrap'); 4 | 5 | const config = bootstrap.config; 6 | const should = bootstrap.should; 7 | 8 | const { Wechat } = require('../lib'); 9 | 10 | const util = require('../lib/utils'); 11 | 12 | const wx = new Wechat( 13 | Object.assign({}, config, { 14 | card: true, 15 | }) 16 | ); 17 | 18 | describe('Card', function() { 19 | describe('#getCardSignature()', function() { 20 | this.timeout(20000); 21 | it('should get cardSign', function(done) { 22 | wx.card 23 | .getCardSignature('myShopId', 'myCardType', 'myCardId') 24 | .then(sigInfo => { 25 | // console.log(sigInfo); 26 | sigInfo.should.have.property('cardSign'); 27 | sigInfo.should.have.property('timestamp'); 28 | sigInfo.should.have.property('nonceStr'); 29 | }) 30 | .catch(reason => { 31 | // console.log(reason); 32 | }) 33 | .then(() => done()); 34 | }); 35 | }); 36 | 37 | describe('#getApiTicket()', function() { 38 | setTimeout(() => { 39 | it('should get api ticket', function(done) { 40 | const store = wx.card.store; 41 | store.store.card = {}; 42 | store.store.globalToken.modifyDate = new Date(0); 43 | wx.card 44 | .getApiTicket() 45 | .then(ticket => { 46 | ticket.should.have.property('ticket'); 47 | done(); 48 | }) 49 | .catch(() => done()); 50 | }); 51 | }, 1000); 52 | }); 53 | 54 | describe('#getCardExt()', function() { 55 | this.timeout(20000); 56 | it('should get cardExt', function(done) { 57 | wx.card 58 | .getCardExt( 59 | 'myCardId', 60 | 'myCode', 61 | 'myOpenId', 62 | util.timestamp(), 63 | util.nonceStr() 64 | ) 65 | .then(cardExt => { 66 | // console.log(cardExt); 67 | const extObj = JSON.parse(cardExt); 68 | extObj.should.have.property('signature'); 69 | extObj.should.have.property('timestamp'); 70 | extObj.should.have.property('nonce_str'); 71 | }) 72 | .catch(reason => { 73 | // console.log(reason); 74 | }) 75 | .then(() => done()); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/JSSDK.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const bootstrap = require('./bootstrap'); 4 | 5 | const config = bootstrap.config; 6 | const should = bootstrap.should; 7 | 8 | const { Wechat, JSSDK, MongoStore } = require('../lib'); 9 | 10 | const wx = new Wechat(config); 11 | const wxMongo = new Wechat( 12 | Object.assign( 13 | { 14 | store: new MongoStore(), 15 | }, 16 | config 17 | ) 18 | ); 19 | 20 | describe('JSSDK', function() { 21 | describe('@constructor()', function() { 22 | this.timeout(20000); 23 | it('should use custom store', function(done) { 24 | new Wechat( 25 | Object.assign( 26 | { 27 | store: new MongoStore(), 28 | clearCountInterval: 1000, 29 | }, 30 | config 31 | ) 32 | ); 33 | setTimeout(() => done(), 2000); 34 | }); 35 | }); 36 | 37 | describe('#getAccessToken()', function() { 38 | this.timeout(20000); 39 | it('should get wechat token', function(done) { 40 | wx.jssdk 41 | .getAccessToken() 42 | .then(function(data) { 43 | data.should.have.property('access_token'); 44 | done(); 45 | }) 46 | .catch(() => done()); 47 | }); 48 | 49 | it('should get wechat token failed', function(done) { 50 | //use a custom instance 51 | const wx = new Wechat(config); 52 | wx.jssdk.wechatConfig.appId = 'invalid_app_id'; 53 | wx.jssdk 54 | .getAccessToken() 55 | .catch(reason => { 56 | reason.should.not.have.property('access_token'); 57 | }) 58 | .then(() => done()); 59 | }); 60 | }); 61 | 62 | describe('#getWechatTicket()', function() { 63 | this.timeout(20000); 64 | it('should not get ticket with the invalid token', function(done) { 65 | wx.jssdk 66 | .getJsApiTicket('invalid_access_token') 67 | .catch(result => { 68 | result.should.not.have.property('ticket'); 69 | }) 70 | .then(() => done()); 71 | }); 72 | }); 73 | 74 | describe('#getSignature()', function() { 75 | this.timeout(20000); 76 | it('should get signature', function(done) { 77 | const url = 'http://localhost?test_signature'; 78 | wx.jssdk 79 | .getSignature(url) 80 | .then(function(signature) { 81 | signature.should.be.an('object'); 82 | signature.should.have.property('url').equal(url); 83 | signature.should.have.property('nonceStr'); 84 | signature.should.have.property('signature'); 85 | signature.should.have.property('timestamp'); 86 | done(); 87 | }) 88 | .catch(() => done()); 89 | }); 90 | it('@MongoStore should get signature', function(done) { 91 | const url = 'http://localhost?' + Math.random(); 92 | wxMongo.jssdk 93 | .getSignature(url) 94 | .then(function(signature) { 95 | signature.should.be.an('object'); 96 | signature.should.have.property('url').equal(url); 97 | signature.should.have.property('nonceStr'); 98 | signature.should.have.property('signature'); 99 | signature.should.have.property('timestamp'); 100 | done(); 101 | }) 102 | .catch(() => done()); 103 | }); 104 | 105 | it('should get new signature even if signature is not expired', function(done) { 106 | const url = 'http://localhost?' + Math.random(); 107 | wx.jssdk 108 | .getSignature(url, true) 109 | .then(function(signature) { 110 | signature.should.be.an('object'); 111 | signature.should.have.property('url').equal(url); 112 | signature.should.have.property('nonceStr'); 113 | signature.should.have.property('signature'); 114 | signature.should.have.property('timestamp'); 115 | done(); 116 | }) 117 | .catch(() => done()); 118 | }); 119 | it('@MongoStore should get new signature even if signature is not expired', function(done) { 120 | const url = 'http://localhost?test_signature'; 121 | wxMongo.jssdk 122 | .getSignature(url, true) 123 | .then(function(signature) { 124 | signature.should.be.an('object'); 125 | signature.should.have.property('url').equal(url); 126 | signature.should.have.property('nonceStr'); 127 | signature.should.have.property('signature'); 128 | signature.should.have.property('timestamp'); 129 | }) 130 | .then(() => done()) 131 | .catch(() => done()); 132 | }); 133 | }); 134 | 135 | describe('#isTokenExpired()', function() { 136 | it('should be expired for the token', function() { 137 | const modifyDate = new Date(2016, 11, 11).getTime(); 138 | const expired = JSSDK.isTokenExpired(modifyDate); 139 | expired.should.be.equal(true); 140 | }); 141 | }); 142 | 143 | describe('#signatureResult()', function() { 144 | it('should return empty object', function() { 145 | const result = wx.jssdk.filterSignature(undefined); 146 | result.should.be.deep.equal({}); 147 | }); 148 | it('should return filtered object', function() { 149 | const result = wx.jssdk.filterSignature({ 150 | timestamp: 'aaa', 151 | nonceStr: 'bbb', 152 | signature: 'ccc', 153 | url: 'http://localhost', 154 | extra: 'should not include this', 155 | }); 156 | result.should.not.have.property('extra'); 157 | result.should.have.property('appId'); 158 | result.should.have.property('signature').be.equal('ccc'); 159 | }); 160 | }); 161 | 162 | describe('#normalizeUrl()', function() { 163 | it('should return url without hash', function() { 164 | const baseUrl = 'http://localhost?a=b'; 165 | const url = baseUrl + '#hash'; 166 | const result = JSSDK.normalizeUrl(url); 167 | result.should.be.equal(baseUrl); 168 | }); 169 | }); 170 | 171 | describe('#verifySignature()', function() { 172 | it('should pass the signature verification', function() { 173 | const query = { 174 | timestamp: 'abc', 175 | nonce: 'xyz', 176 | signature: '4241c4733092f8733df37930576473d51aa2cbcc', 177 | }; 178 | const verified = wx.jssdk.verifySignature(query); 179 | verified.should.be.equal(true); 180 | }); 181 | }); 182 | 183 | describe('#getCachedUrlSignature()', function() { 184 | this.timeout(20000); 185 | it('should get the specified url signature', function(done) { 186 | const url = 'http://localhost?' + Math.random(); 187 | wx.jssdk 188 | .createSignature(url) 189 | .then(() => { 190 | return wx.jssdk.getCachedSignature(url); 191 | }) 192 | .then(result => { 193 | result.should.have.property('signature'); 194 | result.url.should.be.equal(url); 195 | done(); 196 | }) 197 | .catch(() => done()); 198 | }); 199 | }); 200 | 201 | const mockUrl = 'http://localhost/saveNewSignature?' + Math.random(); 202 | const mongoMockUrl = 'http://localhost/saveNewSignature?' + Math.random(); 203 | 204 | describe('#saveNewSignature()', function() { 205 | this.timeout(20000); 206 | it('should save new signature', function(done) { 207 | const mock = { 208 | url: mockUrl, 209 | signature: 'old_mock_signature', 210 | }; 211 | wx.jssdk.store 212 | .saveSignature(mock.url, mock) 213 | .then(() => { 214 | return wx.jssdk.saveSignature({ 215 | url: mockUrl, 216 | signature: 'new_mock_signature', 217 | }); 218 | }) 219 | .then(() => wx.jssdk.store.getSignature(mockUrl)) 220 | .then(newSig => { 221 | newSig.signature.should.be.equal('new_mock_signature'); 222 | newSig.url.should.be.equal(mockUrl); 223 | done(); 224 | }) 225 | .catch(() => done()); 226 | }); 227 | 228 | it('@MongoStore should save new signature', function(done) { 229 | const mock = { 230 | url: mongoMockUrl, 231 | signature: 'old_mock_signature', 232 | }; 233 | wxMongo.jssdk.store 234 | .saveSignature(mock.url, mock) 235 | .then(() => { 236 | return wxMongo.jssdk.saveSignature({ 237 | url: mock.url, 238 | signature: 'new_mock_signature', 239 | }); 240 | }) 241 | .then(() => wxMongo.jssdk.store.getSignature(mongoMockUrl)) 242 | .then(newSig => { 243 | newSig.signature.should.be.equal('new_mock_signature'); 244 | newSig.url.should.be.equal(mongoMockUrl); 245 | done(); 246 | }) 247 | .catch(() => done()); 248 | }); 249 | }); 250 | }); 251 | -------------------------------------------------------------------------------- /test/MiniProgram.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const bootstrap = require('./bootstrap'); 4 | 5 | const config = bootstrap.config; 6 | const should = bootstrap.should; 7 | 8 | const { MiniProgram } = require('../lib'); 9 | const mpConfig = { 10 | appId: 'wxeb9dbcfcd5015935', 11 | appSecret: '23aeb29179539e43dea19fbdc409a117', 12 | }; 13 | 14 | const mp = new MiniProgram({ 15 | appId: 'gongzhonghao_appId', 16 | appSecret: 'gongzhonghao_appSecret', 17 | miniProgram: mpConfig, 18 | }); 19 | 20 | const mock = { 21 | errMsg: 'getUserInfo:ok', 22 | rawData: 23 | '{"nickName":"jason","gender":1,"language":"en","city":"Jing","province":"Shanghai","country":"China","avatarUrl":"https://wx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTJTl0Fa4Pzo4jMq5DoIZia7geHq15C1iapVWE57mIyqSoqq3T8LzavQtEpabaKn6UzSKxogEvs8hic0A/132"}', 24 | userInfo: { 25 | nickName: 'jason', 26 | gender: 1, 27 | language: 'en', 28 | city: 'Jing', 29 | province: 'Shanghai', 30 | country: 'China', 31 | avatarUrl: 32 | 'https://wx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTJTl0Fa4Pzo4jMq5DoIZia7geHq15C1iapVWE57mIyqSoqq3T8LzavQtEpabaKn6UzSKxogEvs8hic0A/132', 33 | }, 34 | signature: '59f8338219f036c32d048b6ff28e36f5bb70589e', 35 | encryptedData: 36 | 'Itpx4bsAwofdR2mDIjuA1LKh8iBLxxq6UKmuH498QU7kAjNekkBAVUNCENq/urgvH1Hm47gevYWnZaTj1dOZIoHq1eueKtdpaFKD+akBbm+c7HP/PjRR87O99cJk0zgVogplnKkZWJT6IX4rai7IaSG5fIZNTAHflCmXcmSQPQtV4+kEvRPtigiTfgseLGhKhzBgM5JbeyYhYH6kqCYfigUgG88o1cENCYCylD5dSh6+zTvvpqHsgsCOtLTHunQClvuhmHiMWgx6Z+DPq5kQg+IcpelxbIlW1DLsRkqk8LWKeYRrR0AesGDDn9Pb34sOznIF5Ii2UsMtameGMEZeUMtxDAVDPf46GwAqxcRP2LtOx3xW1OZow/FJgyFmG7T1BHZahGd0Ge+j1bG/hyZDEGm62+hCI2NXkQkGxtgss21EJgyLBEPnZ/mrIGFPYN3qiZk0zLLgDiMmuSFc+UCeGw==', 37 | iv: 's8gf9aH5iVIJmmpowjrrUw==', 38 | }; 39 | 40 | const code = '081LGZDT0Aad3X1hvMET0Ze7ET0LGZDW'; 41 | 42 | const mockSession = { 43 | session_key: 'Bs/KPAI6f2l8UfxSBRVPxA==', 44 | openid: 'o0ZEQ5YfjRe33Z_ncEI34FAgQX7s', 45 | }; 46 | 47 | describe('MiniProgram', function() { 48 | describe('@constructor', function() { 49 | it('should successfully init the MiniProgram instance', function(done) { 50 | mp.appId.should.equal(mpConfig.appId); 51 | mp.appSecret.should.equal(mpConfig.appSecret); 52 | done(); 53 | }); 54 | }); 55 | 56 | // describe('#getSession()', function() { 57 | // it('should get the session_key & openid', function(cb) { 58 | // mp.getSession(code) 59 | // .then(ret => { 60 | // console.log(ret); 61 | // cb(); 62 | // }) 63 | // .catch(err => { 64 | // cb(err); 65 | // }) 66 | // }); 67 | // }); 68 | 69 | // describe('#genSignature()', function() { 70 | // it('should generate signature', function(cb) { 71 | // 72 | // }); 73 | // }); 74 | 75 | describe('#verifySignature()', function() { 76 | it('should verify the signature', function(cb) { 77 | mp.genSignature(mock.rawData, mockSession.session_key) 78 | .then(sig => { 79 | // console.log('sig: ', sig); 80 | sig.should.equal(mock.signature); 81 | cb(); 82 | }) 83 | .catch(err => { 84 | console.error(err); 85 | cb(err); 86 | }); 87 | }); 88 | }); 89 | 90 | describe('#decryptData()', function() { 91 | it('should decrypt the received encrypted data', function(cb) { 92 | mp.decryptData(mock.encryptedData, mock.iv, mockSession.session_key) 93 | .then(ret => { 94 | // console.log(ret); 95 | ret.watermark.appid.should.equal(mpConfig.appId); 96 | ret.openId.should.equal(mockSession.openid); 97 | cb(); 98 | }) 99 | .catch(err => { 100 | console.error(err); 101 | cb(err); 102 | }); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/OAuth.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const bootstrap = require('./bootstrap'); 4 | 5 | const config = bootstrap.config; 6 | const should = bootstrap.should; 7 | 8 | const { Wechat, OAuth, MongoStore } = require('../lib'); 9 | 10 | const wx = new Wechat(config); 11 | const wxMongo = new Wechat( 12 | Object.assign( 13 | { 14 | store: new MongoStore(), 15 | }, 16 | config 17 | ) 18 | ); 19 | 20 | const mockToken = { 21 | access_token: 'ACCESS_TOKEN', 22 | expires_in: 7200, 23 | refresh_token: 'REFRESH_TOKEN', 24 | openid: 'OPENID', 25 | scope: 'SCOPE', 26 | }; 27 | 28 | //set some mock data 29 | const key = 'mock_key_' + Math.random(); 30 | const mockToken2 = { 31 | key: key, 32 | access_token: 'ACCESS_TOKEN' + Math.random(), 33 | expires_in: 7200, 34 | refresh_token: 'REFRESH_TOKEN' + Math.random(), 35 | openid: 'OPENID', 36 | scope: 'SCOPE', 37 | }; 38 | 39 | describe('OAuth', function() { 40 | describe('@constructor()', function() { 41 | this.timeout(20000); 42 | it('should instantiate with custom refresh interval', function(done) { 43 | new Wechat( 44 | Object.assign( 45 | { 46 | refreshAccessTokenInterval: 1000, 47 | }, 48 | config 49 | ) 50 | ); 51 | setTimeout(done, 2000); 52 | }); 53 | }); 54 | 55 | describe('#getUserInfo()', function() { 56 | this.timeout(20000); 57 | it('should fail getting user info', function(done) { 58 | wx.oauth.getUserInfo('invalid_code').catch(result => { 59 | result.errcode.should.not.equal(0); 60 | result.errmsg.should.not.equal('ok'); 61 | done(); 62 | }); 63 | }); 64 | }); 65 | 66 | describe('#getUserInfoRemotely()', function() { 67 | this.timeout(20000); 68 | it('should fail getting user info with token', function(done) { 69 | wx.oauth 70 | .getUserInfoRemotely( 71 | { 72 | access_token: 'invalid_code', 73 | openid: 'openid', 74 | }, 75 | true 76 | ) 77 | .catch(result => { 78 | result.errcode.should.not.equal(0); 79 | result.errmsg.should.not.equal('ok'); 80 | done(); 81 | }); 82 | }); 83 | }); 84 | 85 | describe('#getUserBaseInfo()', function() { 86 | this.timeout(20000); 87 | it('should fail getting base info', function(done) { 88 | wx.oauth.getUserBaseInfo('invalid_code').catch(result => { 89 | result.errcode.should.not.equal(0); 90 | result.errmsg.should.not.equal('ok'); 91 | done(); 92 | }); 93 | }); 94 | }); 95 | 96 | describe('#getAccessToken()', function() { 97 | this.timeout(20000); 98 | it('should fail getting access token', function(done) { 99 | wx.oauth 100 | .getAccessToken('invalid_code', 'mock_key_' + Math.random()) 101 | .catch(result => { 102 | result.errcode.should.not.equal(0); 103 | result.errmsg.should.not.equal('ok'); 104 | done(); 105 | }); 106 | }); 107 | 108 | it('should save access token to store', function(done) { 109 | OAuth.setAccessTokenExpirationTime(mockToken2); 110 | wx.oauth.store 111 | .saveOAuthAccessToken(key, mockToken2) 112 | .then(tokenInfo => { 113 | tokenInfo.should.have 114 | .property('access_token') 115 | .equal(mockToken2.access_token); 116 | tokenInfo.should.have.property('openid').equal(mockToken2.openid); 117 | }) 118 | .then(() => done()); 119 | }); 120 | 121 | it('@mongoStore should save access token to store', function(done) { 122 | OAuth.setAccessTokenExpirationTime(mockToken2); 123 | wxMongo.oauth.store 124 | .saveOAuthAccessToken(key, mockToken2) 125 | .then(tokenInfo => { 126 | tokenInfo.should.have 127 | .property('access_token') 128 | .equal(mockToken2.access_token); 129 | tokenInfo.should.have.property('openid').equal(mockToken2.openid); 130 | }) 131 | .then(() => done()); 132 | }); 133 | 134 | it('should get access token from store', function(done) { 135 | wx.oauth.store 136 | .getOAuthAccessToken(key) 137 | .then(tokenInfo => { 138 | tokenInfo.should.have 139 | .property('access_token') 140 | .equal(mockToken2.access_token); 141 | tokenInfo.should.have.property('openid').equal(mockToken2.openid); 142 | }) 143 | .then(() => done()); 144 | }); 145 | 146 | it('@mongoStore should get access token from store', function(done) { 147 | wxMongo.oauth.store 148 | .getOAuthAccessToken(key) 149 | .then(tokenInfo => { 150 | tokenInfo.should.have 151 | .property('access_token') 152 | .equal(mockToken2.access_token); 153 | tokenInfo.should.have.property('openid').equal(mockToken2.openid); 154 | }) 155 | .then(() => done()); 156 | }); 157 | 158 | it('should get needed access token from store by user', function(done) { 159 | wx.oauth 160 | .getAccessToken('', key) 161 | .then(tokenInfo => { 162 | tokenInfo.should.have 163 | .property('access_token') 164 | .equal(mockToken2.access_token); 165 | tokenInfo.should.have.property('openid').equal(mockToken2.openid); 166 | }) 167 | .then(() => done()); 168 | }); 169 | 170 | it('@mongoStore should get needed access token from store by user', function(done) { 171 | wxMongo.oauth 172 | .getAccessToken('', key) 173 | .then(tokenInfo => { 174 | tokenInfo.should.have 175 | .property('access_token') 176 | .equal(mockToken2.access_token); 177 | tokenInfo.should.have.property('openid').equal(mockToken2.openid); 178 | }) 179 | .then(() => done()); 180 | }); 181 | 182 | it('should not get access token', function(done) { 183 | wx.oauth 184 | .getAccessToken('', 'mock_key_' + Math.random()) 185 | // .should.be.rejected() 186 | .catch(err => { 187 | err.should.be.Error(); 188 | }) 189 | .then(() => done()) 190 | .catch(() => done()); 191 | }); 192 | 193 | it('@mongoStore should not get access token', function(done) { 194 | wxMongo.oauth 195 | .getAccessToken('', 'mock_key_' + Math.random()) 196 | .catch(err => { 197 | err.should.be.Error(); 198 | }) 199 | .then(() => done()) 200 | .catch(() => done()); 201 | }); 202 | 203 | it('should refresh access token when it is expired', function(done) { 204 | wx.oauth.store 205 | .updateOAuthAccessToken(key, { 206 | expirationTime: Date.now() - 3600000, 207 | }) 208 | .then(() => { 209 | return wx.oauth.getAccessToken('', key); 210 | }) 211 | .catch(tokenInfo => { 212 | should.not.exist(tokenInfo.access_token); 213 | should.not.exist(tokenInfo.openid); 214 | }) 215 | .then(() => done()); 216 | }); 217 | }); 218 | 219 | describe('#refreshAccessToken()', function() { 220 | this.timeout(20000); 221 | it('should not get new access_token', function(done) { 222 | wx.oauth.refreshAccessToken('', {}).catch(result => { 223 | should.not.exist(result.access_token); 224 | should.not.exist(result.openid); 225 | done(); 226 | }); 227 | }); 228 | it('should refresh access token directly to store', function(done) { 229 | const newAccessToken = 'new_access_token'; 230 | wx.oauth.store 231 | .updateOAuthAccessToken(key, { access_token: newAccessToken }) 232 | .then(newToken => { 233 | newToken.access_token.should.be.equal(newAccessToken); 234 | newToken.updated.should.be.equal(true); 235 | }) 236 | .then(() => done()); 237 | }); 238 | 239 | it('@mongoStore should refresh access token directly to store', function(done) { 240 | const newAccessToken = 'new_access_token'; 241 | wxMongo.oauth.store 242 | .updateOAuthAccessToken(key, { access_token: newAccessToken }) 243 | .then(newToken => { 244 | newToken.access_token.should.be.equal(newAccessToken); 245 | newToken.updated.should.be.equal(true); 246 | }) 247 | .then(() => done()); 248 | }); 249 | }); 250 | 251 | describe('#isAccessTokenValid()', function() { 252 | this.timeout(20000); 253 | it('should get error message', function(done) { 254 | wx.oauth.isAccessTokenValid({}).catch(result => { 255 | result.errcode.should.not.equal(0); 256 | result.errmsg.should.not.equal('ok'); 257 | done(); 258 | }); 259 | }); 260 | }); 261 | 262 | describe('OAuth.setExpirationTime()', function() { 263 | it('should just return if token info not specified', function() { 264 | OAuth.setAccessTokenExpirationTime({}); 265 | }); 266 | it('should set the expiration time', function() { 267 | const temp = Object.assign({}, mockToken); 268 | OAuth.setAccessTokenExpirationTime(temp); 269 | temp.should.have.property('expirationTime'); 270 | }); 271 | }); 272 | 273 | describe('OAuth.isAccessTokenExpired()', function() { 274 | //expirationTime: 1481601476688 275 | it('should return true if expirationTime not exist', function() { 276 | const expired = OAuth.isAccessTokenExpired({}); 277 | expired.should.be.equal(true); 278 | }); 279 | it('should return true if expirationTime less than now', function() { 280 | const expired = OAuth.isAccessTokenExpired( 281 | Object.assign( 282 | { 283 | expirationTime: 1481601476600, 284 | }, 285 | mockToken 286 | ) 287 | ); 288 | expired.should.be.equal(true); 289 | }); 290 | it('should return false if expirationTime greater than now', function() { 291 | const expired = OAuth.isAccessTokenExpired( 292 | Object.assign( 293 | { 294 | expirationTime: Date.now() + 3600 * 1000, 295 | }, 296 | mockToken 297 | ) 298 | ); 299 | expired.should.be.equal(false); 300 | }); 301 | }); 302 | }); 303 | -------------------------------------------------------------------------------- /test/Payment.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const bootstrap = require('./bootstrap'); 4 | 5 | const config = bootstrap.config; 6 | // const should = bootstrap.should; 7 | 8 | const { Payment } = require('../lib'); 9 | // const utils = require('../lib/utils'); 10 | 11 | describe('Payment', function() { 12 | const customNotifyUrl = 'http://custom.com/api/wechat/payment/'; 13 | const sandboxUnifiedOrder = 14 | 'https://api.mch.weixin.qq.com/sandboxnew/pay/unifiedorder'; 15 | const payment = new Payment( 16 | Object.assign({}, config, { 17 | appId: 'wx2421b1c4370ec43b', 18 | paymentSandBox: true, 19 | paymentNotifyUrl: customNotifyUrl, 20 | // paymentKey: 'test_key', 21 | // paymentKey: '192006250b4c09247ec02edce69f6a2d', 22 | paymentKey: 'c790c109f688b01807617821ed9f1193', 23 | paymentCertificatePfx: 'test_certificate', 24 | merchantId: 'test_merchant_id', 25 | }) 26 | ); 27 | // console.log(payment.paymentUrls); 28 | 29 | describe('@constructor', function() { 30 | it('should successfully init the Payment instance', function(done) { 31 | payment.notifyUrl.should.equal(customNotifyUrl); 32 | payment.paymentUrls.UNIFIED_ORDER.should.equal(sandboxUnifiedOrder); 33 | done(); 34 | }); 35 | }); 36 | 37 | describe('#generateSignature()', function() { 38 | const params2 = { 39 | appid: 'wxd930ea5d5a258f4f', 40 | mch_id: 10000100, 41 | device_info: 1000, 42 | body: 'test', 43 | nonce_str: 'ibuaiVcKdpRxkhJA', 44 | }; 45 | 46 | const params = { 47 | appid: 'wx2421b1c4370ec43b', 48 | attach: '支付测试', 49 | body: 'JSAPI支付测试', 50 | mch_id: '10000100', 51 | detail: 52 | '{ "goods_detail":[ { "goods_id":"iphone6s_16G", "wxpay_goods_id":"1001", "goods_name":"iPhone6s 16G", "quantity":1, "price":528800, "goods_category":"123456", "body":"苹果手机" }, { "goods_id":"iphone6s_32G", "wxpay_goods_id":"1002", "goods_name":"iPhone6s 32G", "quantity":1, "price":608800, "goods_category":"123789", "body":"苹果手机" } ] }', 53 | nonce_str: '1add1a30ac87aa2db72f57a2375d8fec', 54 | notify_url: 'http://wxpay.wxutil.com/pub_v2/pay/notify.v2.php', 55 | openid: 'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o', 56 | out_trade_no: '1415659990', 57 | spbill_create_ip: '14.23.150.211', 58 | total_fee: '1', 59 | trade_type: 'JSAPI', 60 | }; 61 | it('should generate signature for passed parameters', function() { 62 | const data = payment.generateSignature( 63 | params2, 64 | Payment.SIGN_TYPE.MD5, 65 | true 66 | ); 67 | // console.log(data); 68 | // data.sign.should.equal('EC00CE08DD7396EF70AE7D659D2A1D3A'); 69 | data.sign.should.equal('925DDC574F094D26A39C4A1952B645D1'); 70 | // utils.buildXML(data).then(info => console.log(info)); 71 | // utils.buildXML(params).then(info => console.log(info)); 72 | }); 73 | }); 74 | 75 | const paySignData = { 76 | appId: 'wxee7f6cc5d88ceae6', 77 | timeStamp: '1521537075', 78 | nonceStr: '5qrp1lghxau', 79 | package: 'prepay_id=wx20180320171114464556', 80 | }; 81 | 82 | describe('#generateGeneralPaymentSignature(MD5)', function() { 83 | it('should generate paySign in MD5', function() { 84 | //paymentKey: '192006250b4c09247ec02edce69f6a2d', 85 | const data = payment.generateGeneralPaymentSignature( 86 | Object.assign( 87 | { 88 | signType: Payment.SIGN_TYPE.MD5, 89 | }, 90 | paySignData 91 | ), 92 | Payment.SIGN_TYPE.MD5, 93 | true 94 | ); 95 | // console.log(data); 96 | data.should.have 97 | .property('paySign') 98 | .equal('AA044AE801114117CD5008F586A0F32F'); 99 | }); 100 | }); 101 | 102 | describe('#generateGeneralPaymentSignature(HMAC-SHA256)', function() { 103 | it('should generate paySign in HMAC-SHA256', function() { 104 | //paymentKey: '192006250b4c09247ec02edce69f6a2d', 105 | const data = payment.generateGeneralPaymentSignature( 106 | Object.assign( 107 | { 108 | signType: Payment.SIGN_TYPE.HMAC_SHA256, 109 | }, 110 | paySignData 111 | ), 112 | Payment.SIGN_TYPE.HMAC_SHA256, 113 | true 114 | ); 115 | // console.log(data); 116 | data.should.have 117 | .property('paySign') 118 | .equal( 119 | 'AD97A811D2762EEB24F9565C2EA2176616702627DE606A52C59787D878C52B0C' 120 | ); 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /test/Store.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('./bootstrap'); 3 | 4 | const { FileStore, MongoStore } = require('../lib'); 5 | const util = require('../lib/utils'); 6 | 7 | const fileStore = new FileStore({ 8 | fileStorePath: './wechat-info-' + Math.random() + '.json', 9 | }); 10 | 11 | describe('FileStore', function() { 12 | it('should flush the store', function() { 13 | fileStore.flush(); 14 | }); 15 | 16 | it('should failed to flush file store', function(done) { 17 | fileStore.fileStorePath = './invalid/invalid_path'; 18 | fileStore.flush(); 19 | done(); 20 | }); 21 | 22 | it('should destroy the store', function() { 23 | fileStore.destroy(); 24 | }); 25 | }); 26 | 27 | describe('MongoStore', function() { 28 | const mongoStore = new MongoStore(); 29 | let p = new Promise(resolve => { 30 | mongoStore.on('initialized', function() { 31 | resolve(); 32 | }); 33 | }); 34 | 35 | before(function() { 36 | return p; 37 | }); 38 | this.timeout(20000); 39 | it('should flush the store', function(done) { 40 | mongoStore.flush().then(() => done()); 41 | }); 42 | 43 | it('should destroy the store', function() { 44 | mongoStore.destroy(); 45 | }); 46 | }); 47 | 48 | describe('MongoStore2', function() { 49 | const mockUrl = 'http://localhost/?' + Math.random(); 50 | const mockSignature = { 51 | jsapi_ticket: 52 | 'kgt8ON7yVITDhtdwci0qeX1SvqeQa19KqAy_FSXg0gdhb70VCiKAcZAuaBPolQ5MrotbgGTfKvtOVmcuQ2I4GA', 53 | nonceStr: 'xxxxx94fufvbmhteeam0', 54 | timestamp: 'you', 55 | url: mockUrl, 56 | signature: 'decc3f47253e4408c1cab0bc1a52ae4edbc6f9b4', 57 | accessToken: 58 | 'Yfi8sx-m1CPnPA7mjw-UpCNFiRgW_LZl8YMoGyEy0l8I7qPhgJwQCTGWF2LUhZdbRpfZIDI-Tq597LXp7zf0DSz2Wcvsjan9zfZw_rewWczX0fpIM4aE7nPC8nr4g92zNSSeAAAHMM', 59 | signatureName: mockUrl, 60 | createDate: '2017-01-01T09:59:11.559Z', 61 | modifyDate: '2017-01-01T09:59:11.559Z', 62 | }; 63 | const mockOAuthToken = { 64 | key: 'mock_key_' + Math.random(), 65 | access_token: Math.random() + '', 66 | expires_in: 7200, 67 | refresh_token: 'REFRESH_TOKEN0.7769256954162849', 68 | openid: 'OPENID', 69 | scope: 'SCOPE', 70 | expirationTime: 1482390009626.0, 71 | }; 72 | const mockCardTicket = { 73 | ticket: 'api_ticket_' + Math.random(), 74 | expires_in: 7200, 75 | modifyDate: '2016-12-01T09:25:43.781Z', 76 | }; 77 | 78 | const mongoStore2 = new MongoStore(); 79 | let p = new Promise(resolve => { 80 | mongoStore2.on('initialized', function() { 81 | resolve(); 82 | }); 83 | }); 84 | 85 | before(function() { 86 | return p; 87 | }); 88 | 89 | this.timeout(20000); 90 | 91 | it('should save new signature to mongodb', function(done) { 92 | mongoStore2.saveSignature(mockUrl, mockSignature).then(sig => { 93 | sig.should.have.property('jsapi_ticket'); 94 | done(); 95 | }); 96 | }); 97 | 98 | it('should get the saved signature', function(done) { 99 | setTimeout(() => { 100 | mongoStore2.getSignature(mockUrl).then(sig => { 101 | sig.should.have.property('jsapi_ticket'); 102 | sig.should.have.property('url'); 103 | sig.url.should.be.equal(mockUrl); 104 | done(); 105 | }); 106 | }, 1500); 107 | }); 108 | 109 | it('should get the saved signature from db', function(done) { 110 | setTimeout(() => { 111 | delete mongoStore2.store.urls[mockUrl]; 112 | mongoStore2.getSignature(mockUrl).then(sig => { 113 | sig.should.have.property('jsapi_ticket'); 114 | sig.should.have.property('url'); 115 | sig.url.should.be.equal(mockUrl); 116 | done(); 117 | }); 118 | }, 2000); 119 | }); 120 | 121 | it('should save new oauth access token to mongodb', function(done) { 122 | mongoStore2 123 | .saveOAuthAccessToken(mockOAuthToken.key, mockOAuthToken) 124 | .then(sig => { 125 | sig.should.have.property('access_token'); 126 | done(); 127 | }); 128 | }); 129 | 130 | it('should update signature to mongodb', function(done) { 131 | const newSignature = util.nonceStr(); 132 | const newSig = Object.assign({}, mockUrl, { signature: newSignature }); 133 | setTimeout(() => { 134 | mongoStore2 135 | .updateSignature(mockUrl, newSig) 136 | .then(sig => { 137 | sig.should.have.property('signature'); 138 | sig.signature.should.be.equal(newSignature); 139 | done(); 140 | }) 141 | .catch(() => done()); 142 | }, 1500); 143 | }); 144 | 145 | it('should get saved oauth access token', function(done) { 146 | setTimeout(() => { 147 | mongoStore2 148 | .getOAuthAccessToken(mockOAuthToken.key) 149 | .then(tokenInfo => { 150 | tokenInfo.should.have.property('access_token'); 151 | tokenInfo.key.should.be.equal(mockOAuthToken.key); 152 | tokenInfo.access_token.should.be.equal(mockOAuthToken.access_token); 153 | done(); 154 | }) 155 | .catch(() => done()); 156 | }, 1000); 157 | }); 158 | 159 | it('should get saved oauth access token from db', function(done) { 160 | setTimeout(() => { 161 | delete mongoStore2.store.oauth[mockOAuthToken.key]; 162 | mongoStore2 163 | .getOAuthAccessToken(mockOAuthToken.key) 164 | .then(tokenInfo => { 165 | tokenInfo.should.have.property('access_token'); 166 | tokenInfo.key.should.be.equal(mockOAuthToken.key); 167 | tokenInfo.access_token.should.be.equal(mockOAuthToken.access_token); 168 | done(); 169 | }) 170 | .catch(() => done()); 171 | }, 1500); 172 | }); 173 | 174 | it('should update the oauth access token', function(done) { 175 | const newToken = util.nonceStr(); 176 | setTimeout(() => { 177 | mongoStore2 178 | .updateOAuthAccessToken(mockOAuthToken.key, { 179 | access_token: newToken, 180 | }) 181 | .then(newTokenInfo => { 182 | newTokenInfo.access_token.should.be.equal(newToken); 183 | done(); 184 | }) 185 | .catch(() => done()); 186 | }, 1500); 187 | }); 188 | 189 | it('should update the global token', function(done) { 190 | mongoStore2 191 | .getGlobalToken() 192 | .then(oldToken => { 193 | const newToken = { 194 | accessToken: 'mock_access_token', 195 | }; 196 | return mongoStore2.updateGlobalToken(newToken).then(updatedToken => { 197 | updatedToken.should.have.property('accessToken'); 198 | updatedToken.accessToken.should.be.equal('mock_access_token'); 199 | const newCount = oldToken.count + 1; 200 | newCount.should.be.equal(updatedToken.count); 201 | }); 202 | }) 203 | .then(() => done()) 204 | .catch(() => done()); 205 | }); 206 | 207 | it('should save the card ticket', function(done) { 208 | mongoStore2 209 | .updateCardTicket(mockCardTicket) 210 | .then(cardTicket => { 211 | cardTicket.ticket.should.be.equal(mockCardTicket.ticket); 212 | done(); 213 | }) 214 | .catch(() => done()); 215 | }); 216 | 217 | it('should get the card ticket', function(done) { 218 | setTimeout(() => { 219 | mongoStore2 220 | .getCardTicket() 221 | .then(cardTicket => { 222 | cardTicket.ticket.should.be.equal(mockCardTicket.ticket); 223 | done(); 224 | }) 225 | .catch(() => done()); 226 | }, 1500); 227 | }); 228 | 229 | it('should update the card ticket', function(done) { 230 | const newTicket = Object.assign({}, mockCardTicket, { 231 | ticket: util.nonceStr(), 232 | }); 233 | setTimeout(() => { 234 | mongoStore2 235 | .updateCardTicket(newTicket) 236 | .then(cardTicket => { 237 | cardTicket.ticket.should.be.equal(newTicket.ticket); 238 | done(); 239 | }) 240 | .catch(() => done()); 241 | }, 2000); 242 | }); 243 | 244 | it('should flush in memory data', function(done) { 245 | setTimeout(() => { 246 | const store = mongoStore2.store; 247 | const signatureKeys = Object.keys(store.urls); 248 | const oauthKeys = Object.keys(store.oauth); 249 | signatureKeys.forEach(url => { 250 | store.urls[url].updated = true; 251 | }); 252 | oauthKeys.forEach(key => { 253 | store.oauth[key].updated = true; 254 | }); 255 | mongoStore2 256 | .flush() 257 | .then(() => done()) 258 | .catch(() => done()); 259 | }, 4000); 260 | }); 261 | }); 262 | -------------------------------------------------------------------------------- /test/bootstrap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const config = { 4 | wechatToken: '6mwdIm9p@Wg7$Oup', 5 | appId: 'wxfc9c5237ebf480aa', 6 | appSecret: '2038576336804a90992b8dbe46cd5948', 7 | }; 8 | 9 | exports.config = config; 10 | exports.should = require('chai').should(); 11 | -------------------------------------------------------------------------------- /test/config.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const bootstrap = require('./bootstrap'); 4 | const config = bootstrap.config; 5 | const should = bootstrap.should; 6 | 7 | const configCheck = require('../lib/config').checkPassedConfiguration; 8 | 9 | describe('config', function() { 10 | it('should throw error with empty options', function() { 11 | (function() { 12 | configCheck({}); 13 | }.should.throw(/wechat configuration/)); 14 | }); 15 | 16 | it('should throw error with empty wechat appid', function() { 17 | const temp = Object.assign({}, config); 18 | delete temp.appId; 19 | (function() { 20 | configCheck(temp); 21 | }.should.throw(/wechat appId/)); 22 | }); 23 | 24 | it('should throw error with empty wechat appSecret', function() { 25 | const temp = Object.assign({}, config); 26 | delete temp.appSecret; 27 | (function() { 28 | configCheck(temp); 29 | }.should.throw(/wechat appSecret/)); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/data-2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 1 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/data.xml: -------------------------------------------------------------------------------- 1 | 2 | wx2421b1c4370ec43b 3 | 支付测试 4 | JSAPI支付测试 5 | 10000100 6 | 7 | 1add1a30ac87aa2db72f57a2375d8fec 8 | http://wxpay.wxutil.com/pub_v2/pay/notify.v2.php 9 | oUpF8uMuAJO_M2pxb1Q9zNjWeS6o 10 | 1415659990 11 | 14.23.150.211 12 | 1 13 | JSAPI 14 | 0CB01533B8C1EF103065174F50BCA001 15 | -------------------------------------------------------------------------------- /test/db.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const low = require('lowdb'); 3 | const FileSync = require('lowdb/adapters/FileSync'); 4 | 5 | const adapter = new FileSync(path.join(__dirname, '../db_test.json')); 6 | const db = low(adapter); 7 | 8 | const utils = require('../lib/utils'); 9 | 10 | function init() { 11 | // Set some defaults (required if your JSON file is empty) 12 | db.defaults({ posts: [], orders: [], user: {}, count: 0 }).write(); 13 | 14 | // Add a post 15 | db.get('posts') 16 | .push({ id: utils.nonceStr(), title: 'lowdb is awesome2' }) 17 | .write(); 18 | 19 | db.get('orders') 20 | .push({ id: utils.nonceStr(), title: 'order 1' }) 21 | .write(); 22 | 23 | // Set a user using Lodash shorthand syntax 24 | db.set('user.name', 'typicode2').write(); 25 | 26 | // Increment count 27 | db.update('count', n => n + 1).write(); 28 | } 29 | 30 | function getState() { 31 | /*console.log(db.getState()); 32 | console.log(db.has('orders').value()); 33 | console.log(db.has('orders2').value()); 34 | console.log( 35 | db 36 | .get('posts') 37 | .size() 38 | .value() 39 | ); 40 | console.log(db.get('posts[0].title').value()); 41 | console.log( 42 | db 43 | .get('posts') 44 | .cloneDeep() 45 | .value() 46 | ); 47 | console.log( 48 | db 49 | .get('posts') 50 | .find({ id: 1 }) 51 | .has('id2') 52 | .value() 53 | );*/ 54 | } 55 | 56 | getState(); 57 | // init(); 58 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const bootstrap = require('./bootstrap'); 4 | const config = bootstrap.config; 5 | 6 | const { OAuth, JSSDK, Store } = require('../lib'); 7 | 8 | describe('index', function() { 9 | it('should get OAuth class', function() { 10 | OAuth.constructor.name.should.be.equal('Function'); 11 | new OAuth(config).constructor.name.should.be.equal('OAuth'); 12 | }); 13 | 14 | it('should get JSSDK class', function() { 15 | JSSDK.constructor.name.should.be.equal('Function'); 16 | new JSSDK(config).constructor.name.should.be.equal('JSSDK'); 17 | }); 18 | 19 | it('should get Store class', function() { 20 | Store.constructor.name.should.be.equal('Function'); 21 | new Store(config).constructor.name.should.be.equal('Store'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const utils = require('../lib/utils'); 5 | const bootstrap = require('./bootstrap'); 6 | 7 | const should = bootstrap.should; 8 | 9 | describe('Utils', function() { 10 | describe('#genMD5()', function() { 11 | it('should get hash from the content', function() { 12 | const hash = utils.genMD5('md5_test'); 13 | hash.should.equal('c74318b61a3024520c466f828c043c79'); 14 | }); 15 | }); 16 | describe('#genSHA1()', function() { 17 | it('should get hash from the content', function() { 18 | const hash = utils.genSHA1('sha1_test'); 19 | hash.should.equal('18cb41714af7239d6a6b28dc1df74603eebc4da8'); 20 | }); 21 | }); 22 | describe('#simpleDate()', function() { 23 | it('should return the date with default format', function() { 24 | const date = new Date(2017, 0, 1, 10, 10, 8); 25 | const result = utils.simpleDate(date); 26 | result.should.equal('20170101101008'); 27 | }); 28 | it('should return the current date with default format', function() { 29 | const result = utils.simpleDate(); 30 | result.length.should.equal(14); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/xml.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const utils = require('../lib/utils'); 5 | const bootstrap = require('./bootstrap'); 6 | 7 | const should = bootstrap.should; 8 | 9 | describe('XML', function() { 10 | describe('#parseXML()', function() { 11 | it('should parse the xml data without root', function(done) { 12 | const xmlData = fs.readFileSync(path.join(__dirname, 'data.xml')); 13 | utils 14 | .parseXML(xmlData) 15 | .then(data => { 16 | data.should.have.property('appid').equal('wx2421b1c4370ec43b'); 17 | data.should.have 18 | .property('sign') 19 | .equal('0CB01533B8C1EF103065174F50BCA001'); 20 | }) 21 | .catch(() => {}) 22 | .then(() => done()); 23 | }); 24 | }); 25 | describe('#buildXML()', function() { 26 | it('should build the xml data with xml root', function(done) { 27 | const objData = { 28 | appid: 'appid', 29 | sign: 30 | '{ "goods_detail":[ { "goods_id":"iphone6s_16G", "wxpay_goods_id":"1001", "goods_name":"iPhone6s 16G", "quantity":1, "price":528800, "goods_category":"123456", "body":"苹果手机" }, { "goods_id":"iphone6s_32G", "wxpay_goods_id":"1002", "goods_name":"iPhone6s 32G", "quantity":1, "price":608800, "goods_category":"123789", "body":"苹果手机" } ] }', 31 | }; 32 | utils 33 | .buildXML(objData) 34 | .then(data => { 35 | // console.log(data); 36 | return utils.parseXML(data); 37 | }) 38 | .then(result => { 39 | result.should.have.property('appid').equal(objData.appid); 40 | result.should.have.property('sign').equal(objData.sign); 41 | }) 42 | .catch(() => {}) 43 | .then(() => done()); 44 | }); 45 | }); 46 | }); 47 | --------------------------------------------------------------------------------