├── .gitignore
├── .jshintrc
├── .travis.yml
├── Gruntfile.js
├── LICENSE.txt
├── README.md
├── bower.json
├── build
└── .gitkeep
├── dist
├── fxpay.debug.js
├── fxpay.min.js
└── fxpay.min.js.map
├── example
├── README.rst
├── hosted-paid-app
│ ├── README.rst
│ ├── manifest.webapp
│ ├── media
│ │ ├── css
│ │ │ └── index.css
│ │ ├── index.html
│ │ └── js
│ │ │ └── index.js
│ ├── package.json
│ └── server.js
├── hosted
│ ├── README.rst
│ ├── manifest.webapp
│ ├── package.json
│ └── server.js
├── packaged-web
│ ├── README.rst
│ └── manifest.webapp
├── packaged
│ ├── README.rst
│ └── manifest.webapp
└── shared
│ ├── css
│ └── index.css
│ ├── img
│ ├── cheese_128.png
│ ├── cheese_32.png
│ ├── cheese_48.png
│ ├── cheese_64.png
│ ├── kiwi_128.png
│ ├── kiwi_32.png
│ ├── kiwi_48.png
│ └── kiwi_64.png
│ ├── index.html
│ └── js
│ ├── index.js
│ └── lib
│ └── jquery-1.9.1.min.js
├── jsdoc.conf.json
├── karma.conf.js
├── lib
└── fxpay
│ ├── adapter.js
│ ├── api.js
│ ├── errors.js
│ ├── fxpay.js
│ ├── jwt.js
│ ├── pay.js
│ ├── products.js
│ ├── receipts.js
│ ├── settings.js
│ └── utils.js
├── package.json
├── stackato.yml
├── tests
├── helper.js
├── test-api.js
├── test-errors.js
├── test-fxpay.js
├── test-get-products.js
├── test-jwt.js
├── test-main.js
├── test-product-receipt-validation.js
├── test-products.js
├── test-purchase.js
├── test-receipts-all.js
├── test-receipts-verify-app-data.js
├── test-receipts-verify-data.js
├── test-receipts-verify-inapp-data.js
├── test-settings.js
├── test-utils.js
├── test-validate-app-receipt.js
├── test-web-purchase.js
└── test-webrt-purchase.js
└── umd
├── end.frag
└── start.frag
/.gitignore:
--------------------------------------------------------------------------------
1 | *.sw[po]
2 | *~
3 | .DS_Store
4 | build/*
5 | bower_components
6 |
7 | node_modules
8 |
9 | # For gh-pages
10 | .ghpages
11 |
12 | .grunt/*
13 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "boss": true,
3 | "browser": true,
4 | "camelcase": false,
5 | "eqeqeq": true,
6 | "eqnull": true,
7 | "es5": false,
8 | "esnext": true,
9 | "expr": true,
10 | "forin": false,
11 | "globals": {
12 | "define": false
13 | },
14 | "indent": 2,
15 | "laxbreak": true,
16 | "laxcomma": true,
17 | "maxerr": 100,
18 | "maxlen": 80,
19 | "node": true,
20 | "noarg": true,
21 | "passfail": false,
22 | "predef": [
23 | "assert",
24 | "beforeEach",
25 | "afterEach",
26 | "describe",
27 | "exports",
28 | "fxpay",
29 | "helper",
30 | "it",
31 | "process",
32 | "Promise",
33 | "requirejs",
34 | "sinon"
35 | ],
36 | "shadow": false,
37 | "strict": false,
38 | "supernew": false,
39 | "undef": true,
40 | "unused": true,
41 | "white": false
42 | }
43 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | sudo: false
3 | node_js:
4 | - 0.10
5 | before_script:
6 | - npm rebuild
7 | - "export DISPLAY=:99.0"
8 | - "sh -e /etc/init.d/xvfb start"
9 | script: npm test
10 | notifications:
11 | irc:
12 | channels:
13 | - "irc.mozilla.org#payments"
14 | on_success: change
15 | on_failure: always
16 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 |
3 | module.exports = function(grunt) {
4 | var testOption = grunt.option('tests');
5 |
6 | var nodeModulesPath = __dirname + '/node_modules';
7 | var almondPath = nodeModulesPath + '/almond/almond.js';
8 | var almondInclude = path.relative(
9 | __dirname + '/lib/fxpay', almondPath).replace(/\.js$/, '');
10 |
11 | grunt.initConfig({
12 | pkg: grunt.file.readJSON('package.json'),
13 |
14 | jshint: {
15 | options: { jshintrc: __dirname + '/.jshintrc' },
16 | files: [
17 | 'Gruntfile.js',
18 | 'lib/*.js',
19 | 'lib/*/*.js',
20 | 'tests/*.js',
21 | ],
22 | },
23 |
24 | karma: {
25 | options: {
26 | files: [
27 | 'tests/test-main.js',
28 | {pattern: 'lib/fxpay/*.js', included: false},
29 | {pattern: 'tests/helper.js', included: false},
30 | {pattern: testOption || 'tests/test*.js', included: false},
31 | {pattern: 'lib/bower_components/es6-promise/promise.js',
32 | included: false}
33 | ],
34 | logLevel: grunt.option('log-level') || 'ERROR',
35 | },
36 | dev: {
37 | configFile: 'karma.conf.js',
38 | autoWatch: true
39 | },
40 | run: {
41 | configFile: 'karma.conf.js',
42 | singleRun: true
43 | },
44 | },
45 |
46 | usebanner: {
47 | chaff: {
48 | options: {
49 | position: 'top',
50 | banner: ")]}'",
51 | linebreak: true
52 | },
53 | files: {
54 | src: ['build/lib/fxpay.min.js.map']
55 | }
56 | }
57 | },
58 |
59 | bower: {
60 | default: {
61 | options: {
62 | targetDir: './lib/bower_components',
63 | layout: 'byType',
64 | bowerOptions: {
65 | // Do not install project devDependencies
66 | production: true,
67 | }
68 | }
69 | }
70 | },
71 |
72 | bump: {
73 | options: {
74 | // The pattern 'version': '..' will be updated in all these files.
75 | files: ['bower.json', 'lib/fxpay/settings.js', 'package.json'],
76 | commit: false,
77 | createTag: false,
78 | push: false,
79 | }
80 | },
81 |
82 | clean: {
83 | build: [
84 | 'build/**/*',
85 | '!build/.gitkeep'
86 | ],
87 | dist: [
88 | 'dist/*',
89 | '!dist/.gitkeep'
90 | ],
91 | },
92 |
93 | copy: {
94 | lib: {
95 | cwd: 'build/lib/',
96 | src: ['*.js', '*.map'],
97 | dest: 'dist/',
98 | filter: 'isFile',
99 | expand: true,
100 | },
101 | 'example-packaged': {
102 | cwd: 'example/packaged/',
103 | src: '*',
104 | dest: 'build/app/',
105 | expand: true,
106 | },
107 | 'example-packaged-web': {
108 | cwd: 'example/packaged-web/',
109 | src: '*',
110 | dest: 'build/app-web/',
111 | expand: true,
112 | },
113 | 'example-shared-to-pkg': {
114 | cwd: 'example/shared/',
115 | src: '**/*',
116 | dest: 'build/app/',
117 | expand: true,
118 | },
119 | 'example-shared-to-web-pkg': {
120 | cwd: 'example/shared/',
121 | src: '**/*',
122 | dest: 'build/app-web/',
123 | expand: true,
124 | },
125 | 'lib-to-package': {
126 | cwd: 'build/lib/',
127 | src: '*',
128 | dest: 'build/app/',
129 | expand: true,
130 | },
131 | 'lib-to-web-package': {
132 | cwd: 'build/lib/',
133 | src: '*',
134 | dest: 'build/app-web/',
135 | expand: true,
136 | },
137 | },
138 |
139 | zip: {
140 | app: {
141 | cwd: 'build/app/',
142 | src: 'build/app/**',
143 | dest: 'build/app.zip',
144 | compression: 'DEFLATE',
145 | },
146 | appWeb: {
147 | cwd: 'build/app-web/',
148 | src: 'build/app-web/**',
149 | dest: 'build/app-web.zip',
150 | compression: 'DEFLATE',
151 | },
152 | },
153 |
154 | jsdoc : {
155 | docs: {
156 | src: ['lib/fxpay/*.js'],
157 | options: {
158 | destination: 'build/docs',
159 | template: 'node_modules/jsdoc-simple-template',
160 | readme: 'README.md',
161 | configure: 'jsdoc.conf.json',
162 | },
163 | }
164 | },
165 |
166 | 'gh-pages': {
167 | options: {
168 | base: 'build/docs',
169 | message: 'Updating docs',
170 | repo: 'git@github.com:mozilla/fxpay.git'
171 | },
172 | src: ['**']
173 | },
174 |
175 | // Builds an unminned optimized combined
176 | // library file.
177 | requirejs: {
178 | debug: {
179 | options: {
180 | include: [almondInclude],
181 | findNestedDependencies: true,
182 | name: 'fxpay',
183 | optimize: 'none',
184 | out: 'build/lib/fxpay.debug.js',
185 | baseUrl: 'lib/fxpay',
186 | normalizeDirDefines: 'all',
187 | skipModuleInsertion: true,
188 | paths: {
189 | promise: '../bower_components/es6-promise/promise',
190 | },
191 | wrap: {
192 | start: grunt.file.read('umd/start.frag'),
193 | end: grunt.file.read('umd/end.frag')
194 | },
195 | }
196 | }
197 | },
198 |
199 | // Takes the requirejs optimized debug file
200 | // and compresses it and creates the sourcemap.
201 | uglify: {
202 | options: {
203 | sourceMap: true
204 | },
205 | minned: {
206 | files: {
207 | 'build/lib/fxpay.min.js': 'build/lib/fxpay.debug.js',
208 | }
209 | }
210 | },
211 |
212 | fileExists: {
213 | almond: {
214 | src: [almondPath],
215 | errorMessage: 'Please run npm install first',
216 | }
217 | }
218 |
219 | });
220 |
221 | grunt.loadNpmTasks('grunt-bower-task');
222 | grunt.loadNpmTasks('grunt-banner');
223 | grunt.loadNpmTasks('grunt-bump');
224 | grunt.loadNpmTasks('grunt-contrib-clean');
225 | grunt.loadNpmTasks('grunt-contrib-copy');
226 | grunt.loadNpmTasks('grunt-contrib-jshint');
227 | grunt.loadNpmTasks('grunt-contrib-requirejs');
228 | grunt.loadNpmTasks('grunt-contrib-uglify');
229 | grunt.loadNpmTasks('grunt-gh-pages');
230 | grunt.loadNpmTasks('grunt-jsdoc');
231 | grunt.loadNpmTasks('grunt-karma');
232 | grunt.loadNpmTasks('grunt-zip');
233 |
234 | grunt.registerMultiTask('fileExists', 'Check files are present', function (){
235 | var files = grunt.file.expand({
236 | nonull: true
237 | }, this.data.src);
238 | var len = files.length;
239 | grunt.log.writeln(
240 | 'Checking existence of %d file%s', len, (len === 1 ? '' : 's'));
241 | var filesExist = files.every(function (file) {
242 | grunt.verbose.writeln('Checking file: %s', file);
243 | var fileExists = grunt.file.exists(file);
244 | if (!fileExists) {
245 | grunt.log.error("%s doesn't exist", file);
246 | }
247 | return fileExists;
248 | });
249 | if (filesExist) {
250 | grunt.log.ok();
251 | } else {
252 | var errorMessage = this.data.errorMessage;
253 | if (errorMessage) {
254 | grunt.log.error(errorMessage);
255 | }
256 | }
257 | return filesExist;
258 | });
259 |
260 | grunt.registerTask('package', [
261 | 'clean:build',
262 | 'compress',
263 | 'copy:example-packaged',
264 | 'copy:example-packaged-web',
265 | 'copy:example-shared-to-pkg',
266 | 'copy:example-shared-to-web-pkg',
267 | 'copy:lib-to-package',
268 | 'copy:lib-to-web-package',
269 | 'zip',
270 | ]);
271 |
272 | // The `compress` step builds a debug version first and then uses that as
273 | // the source for the minified version.
274 | grunt.registerTask('compress', [
275 | 'fileExists:almond',
276 | 'bower',
277 | 'requirejs',
278 | 'uglify:minned',
279 | 'usebanner:chaff'
280 | ]);
281 |
282 | grunt.registerTask('publish-docs', ['docs', 'gh-pages']);
283 | grunt.registerTask('test', ['jshint', 'karma:run']);
284 | grunt.registerTask('release', ['clean', 'compress', 'copy:lib']);
285 | grunt.registerTask('docs', ['clean:build', 'jsdoc']);
286 | grunt.registerTask('default', 'test');
287 | };
288 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014, Mozilla Corporation
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | * Neither the name of the Mozilla Corporation nor the names of its contributors
15 | may be used to endorse or promote products derived from this software without
16 | specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # fxpay
2 |
3 | JavaScript library to support [Firefox Marketplace][mkt] payments in
4 | a web application.
5 |
6 |
7 | [](https://travis-ci.org/mozilla/fxpay)
8 | 
9 | [](https://david-dm.org/mozilla/fxpay#info=devDependencies)
10 |
11 |
12 | ## Usage
13 |
14 | This is a complete [guide to fxpay usage on MDN][mdn-docs]
15 |
16 | ## Examples
17 |
18 | You can find working code in the [example][example] directory of this repository
19 |
20 | ## FxPay Developers
21 |
22 | To hack on this library you need [NodeJS][node] and [npm][npm] installed.
23 | After cloning the source, cd to the root and install all dependencies:
24 |
25 | npm install
26 |
27 | To execute scripts, you should add the local `.bin` directory to
28 | your `$PATH`:
29 |
30 | PATH="./node_modules/.bin:${PATH}"
31 | export PATH
32 |
33 | This is pretty standard for any Node project so you you might already have it.
34 |
35 | To test that you have your path set up, type `which grunt` and make
36 | sure you see a path to the executable.
37 |
38 | Before going any further, you'll need to install the bower components
39 | used for development. Run this:
40 |
41 | grunt bower
42 |
43 | ### Compression
44 |
45 | To build yourself a compressed version of `fxpay.js`, run this:
46 |
47 | grunt compress
48 |
49 | The compressed source file will appear in the `build` directory
50 | as `fxpay.min.js`. You'll also get a [source map][sourcemaps] file in
51 | the same directory as `fxpay.min.js.map`.
52 |
53 | **IMPORTANT**: To use this library in a web page you have to
54 | compress it first because the source code spans multiple files.
55 | The usage instructions above explain how to install public releases from
56 | Bower which is of course easier.
57 |
58 |
59 | ### Running Tests
60 |
61 | From a source checkout, run all tests and lint checks like this:
62 |
63 | grunt test
64 |
65 | To run the JavaScript unit tests continuously while you are developing, type:
66 |
67 | grunt karma:dev
68 |
69 | This opens a web browser and will report test results to your [console][console].
70 | As you edit a code file, it will re-run the tests.
71 | **NOTE**: this can be buggy sometimes.
72 |
73 | To fire off a single test run with a browser and see the results, type:
74 |
75 | grunt karma:run
76 |
77 | Here's how to run a specific test file:
78 |
79 | grunt karma:run --tests tests/test-get-products.js
80 |
81 | You can also use grep patterns to match files:
82 |
83 | grunt karma:run --tests 'tests/test-get-*'
84 |
85 | If you want to run a specific test function, you can use
86 | a grep pattern to match the name in the `describe()` or `it()`
87 | definition. For example, run all tests under
88 | `describe('fxpay.purchase()')` like this:
89 |
90 | grunt karma:run --grep='fxpay.purchase()'
91 |
92 | or run a test defined as `it('should open a payment window on the web')`
93 | like this:
94 |
95 | grunt karma:run --grep='should open a payment window on the web'
96 |
97 | If you should need to change the karma log-level (default is ERROR)
98 | you can do so as follows:
99 |
100 | grunt test --log-level=DEBUG
101 |
102 |
103 | ### Check For Lint
104 |
105 | To check for syntax errors (lint), run:
106 |
107 | grunt jshint
108 |
109 | ### Create A Release
110 |
111 | You have to do a couple things to create a release:
112 |
113 | * Run `grunt release`. This compresses the files and copies them to the dist dir.
114 |
115 | * Commit and push your changes to master.
116 |
117 | * Publish the pending [github release][releases] (or create one) which will tag master
118 | at the version string.
119 |
120 | * Make sure all release notes in the draft are up to date.
121 | * If no release exists yet, create one and title it as the pending
122 | version number. For example: `0.0.1`.
123 | * Alternatively, you could manually tag the release with git by running
124 | `git tag 0.0.1 && git push upstream 0.0.1`.
125 |
126 | * Bump the version for the next release. Library version numbers are
127 | managed in multiple files.
128 | To increment the version number and update all files at once,
129 | run `grunt bump`.
130 |
131 | * Commit and push your changes.
132 |
133 |
134 | ### Build the docs
135 |
136 | To build the JSDoc API docs locally run `grunt docs`. The built docs can be found
137 | in `build/docs`.
138 |
139 | For anyone with the commit bit that wants to publish the docs to the gh-pages branch
140 | of this repo run: `grunt publish-docs`.
141 |
142 | The current API docs are available here: [FxPay API Docs](https://mozilla.github.io/fxpay/) *(Note: they are currently under development).*
143 |
144 |
145 | ## Changelog
146 |
147 | See the [release page][releases]
148 |
149 |
150 | [mkt]: https://marketplace.firefox.com
151 | [node]: http://nodejs.org/
152 | [npm]: https://www.npmjs.org/
153 | [console]: https://developer.mozilla.org/en-US/docs/Web/API/console
154 | [mdn-docs]: https://developer.mozilla.org/en-US/Marketplace/Monetization/In-app_payments_section/fxPay_iap
155 | [example]: https://github.com/mozilla/fxpay/tree/master/example/
156 | [sourcemaps]: http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/
157 | [releases]: https://github.com/mozilla/fxpay/releases
158 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fxpay",
3 | "description": "Adds Firefox Marketplace payments to a web application",
4 | "version": "0.0.18",
5 | "homepage": "https://github.com/mozilla/fxpay",
6 | "repository": {
7 | "type": "git",
8 | "url": "git://github.com/mozilla/fxpay.git"
9 | },
10 | "dependencies": {
11 | "es6-promise": "~2.0.1"
12 | },
13 | "main": "dist/fxpay.min.js",
14 | "ignore": [
15 | "**/.*",
16 | "Gruntfile.js",
17 | "bower.json",
18 | "build",
19 | "example",
20 | "karma.conf.js",
21 | "lib",
22 | "node_modules",
23 | "package.json",
24 | "stackato.yml",
25 | "tests",
26 | "jsdoc.conf.json"
27 | ],
28 | "license": "MPL-2.0"
29 | }
30 |
--------------------------------------------------------------------------------
/build/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/fxpay/be655d21af6544c60c6b7ae55ad28b38b1737ddd/build/.gitkeep
--------------------------------------------------------------------------------
/example/README.rst:
--------------------------------------------------------------------------------
1 | ============
2 | Example Apps
3 | ============
4 |
5 | These are some examples of `Firefox OS`_ web apps that work with the
6 | fxpay library. Each app has its own README for how to install and
7 | play around with them.
8 |
9 | `hosted app with in-app purchases `_
10 | A hosted web app that can sell and restore in-app products.
11 |
12 | `hosted paid app `_
13 | A hosted web app that validates its receipt to ensure the user paid for it.
14 |
15 | `packaged web app with in-app purchases `_
16 | A packaged app (``type: web``) that can sell and restore in-app products.
17 |
18 | `privileged packaged app with in-app purchases `_
19 | A packaged app (``type: privileged``) that can sell and restore in-app products.
20 |
21 | Using A Custom Webpay
22 | ---------------------
23 |
24 | If you want to test payments against your local `Webpay`_ server
25 | using these example apps then you'll also need to
26 | `build a custom profile`_ with payments
27 | preferences. To use the `Firefox OS Simulator`_ with your custom profile,
28 | go into about:addons, click on Preferences for the
29 | Firefox OS Simulator addon, and set the Gaia path to your custom built
30 | profile.
31 |
32 | .. _`Firefox OS`: https://developer.mozilla.org/en-US/Firefox_OS
33 | .. _`Firefox OS Simulator`: https://developer.mozilla.org/en-US/docs/Tools/Firefox_OS_Simulator
34 | .. _Webpay: https://github.com/mozilla/webpay
35 | .. _`build a custom profile`: http://marketplace.readthedocs.org/en/latest/topics/payments.html#build-a-custom-b2g-profile
36 |
--------------------------------------------------------------------------------
/example/hosted-paid-app/README.rst:
--------------------------------------------------------------------------------
1 | ===============
2 | Hosted Paid App
3 | ===============
4 |
5 | This is an example of a `hosted app`_ that uses the fxpay library to
6 | validate its receipt to ensure that users paid for the app.
7 |
8 | Installation
9 | ------------
10 |
11 | To run the example you'll need to first compress a
12 | minified fxpay library, install some node modules, and start a web server.
13 | Run these commands from the root of the repository::
14 |
15 | grunt compress
16 | cd example/hosted-paid-app
17 | npm install
18 | npm start
19 |
20 | You can now open http://localhost:3001 to see the example app but by
21 | launching the URL directly, no receipt will be installed.
22 |
23 | To see a more realistic example, try installing the app with the
24 | Firefox Marketplace `receipt tester`_.
25 |
26 | If you install the app as a desktop web app,
27 | the easiest way to debug it is to launch it from the
28 | shell after installation like this::
29 |
30 | /Applications/FxPayHostedPaidApp.app/Contents/MacOS/webapprt -debug 6000
31 |
32 | Then fire up the `WebIDE`_ and hook it up as a `remote runtime`_.
33 | You'll need to accept a prompt and then you'll be able to use
34 | the debugging tools.
35 |
36 | .. _`hosted app`: https://developer.mozilla.org/en-US/Marketplace/Options/Hosted_apps
37 | .. _`receipt tester`: https://marketplace.firefox.com/developers/test/receipts/
38 | .. _`remote runtime`: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Debugging_Firefox_Desktop
39 | .. _`WebIDE`: https://developer.mozilla.org/en-US/docs/Tools/WebIDE
40 |
--------------------------------------------------------------------------------
/example/hosted-paid-app/manifest.webapp:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.0.1",
3 | "name": "FxPayHostedPaidApp",
4 | "description": "Example of a hosted paid app that does receipt validation",
5 | "developer": {
6 | "name": "Kumar McMillan"
7 | },
8 | "icons": {
9 | "128": "/shared/img/kiwi_128.png",
10 | "64": "/shared/img/kiwi_64.png",
11 | "48": "/shared/img/kiwi_48.png",
12 | "32": "/shared/img/kiwi_32.png"
13 | },
14 | "launch_path": "/index.html",
15 | "installs_allowed_from": [
16 | "https://marketplace.firefox.com",
17 | "https://marketplace-dev.allizom.org",
18 | "https://marketplace.allizom.org",
19 | "https://payments-alt.allizom.org"
20 | ],
21 | "type": "web"
22 | }
23 |
--------------------------------------------------------------------------------
/example/hosted-paid-app/media/css/index.css:
--------------------------------------------------------------------------------
1 | #results {
2 | margin-top: 3em;
3 | }
4 |
5 | #results span {
6 | font-weight: bold;
7 | }
8 |
--------------------------------------------------------------------------------
/example/hosted-paid-app/media/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | fxpay paid app demo
10 |
11 |
12 |
13 |
14 |
Paid App Example
15 |
16 |
17 |
18 | Receipt validation result: working on it...
19 |
20 |
21 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/example/hosted-paid-app/media/js/index.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | "use strict";
3 |
4 | function showResult(result, error) {
5 | $('#error').text(error ? error.toString(): "");
6 | $('#results span').text(result);
7 | }
8 |
9 | function logProductInfo(productInfo) {
10 | console.log('productInfo:', productInfo);
11 | if (productInfo && productInfo.receiptInfo) {
12 | console.log('receipt status:', productInfo.receiptInfo.status);
13 | if (productInfo.receiptInfo.reason) {
14 | console.log('receipt status reason:', productInfo.receiptInfo.reason);
15 | }
16 | }
17 | }
18 |
19 | fxpay.configure({
20 | allowTestReceipts: true,
21 | receiptCheckSites: [
22 | // Allow the production service.
23 | 'https://receiptcheck.marketplace.firefox.com',
24 | 'https://marketplace.firefox.com',
25 |
26 | // The following would not be needed in a live app. These our some test
27 | // services for development of the fxpay library only.
28 |
29 | // Allow our test servers.
30 | 'https://receiptcheck-dev.allizom.org',
31 | 'https://receiptcheck-marketplace-dev.allizom.org',
32 | 'https://receiptcheck-payments-alt.allizom.org',
33 | 'https://marketplace-dev.allizom.org',
34 | 'https://marketplace.allizom.org',
35 | 'https://payments-alt.allizom.org',
36 |
37 | // Allow some common local servers..
38 | 'http://mp.dev',
39 | 'http://fireplace.loc',
40 | ],
41 | });
42 |
43 | fxpay.validateAppReceipt().then(function(productInfo) {
44 | logProductInfo(productInfo);
45 | console.log('receipt is valid; app was purchased');
46 | console.log('product URL:', productInfo.productUrl);
47 | showResult('VALID');
48 | }).catch(function(reason) {
49 | logProductInfo(reason.productInfo);
50 | showResult('INVALID', reason.error || reason);
51 | });
52 |
53 | console.log('initialized hosted paid app');
54 |
55 | })();
56 |
--------------------------------------------------------------------------------
/example/hosted-paid-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fxpay-hosted-paid-app",
3 | "version": "0.0.1",
4 | "engines": {"node": ">=0.10"},
5 | "private": true,
6 | "dependencies": {
7 | "express": "4.10.3",
8 | "morgan": "1.5.0"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/example/hosted-paid-app/server.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var path = require('path');
3 |
4 | var express = require('express');
5 | var morgan = require('morgan');
6 |
7 | var app = express();
8 | var router = express.Router();
9 | var projectDir = path.normalize(__dirname + '/../..');
10 | var hostedDir = projectDir + '/example/hosted-paid-app';
11 | var media = hostedDir + '/media/';
12 | var sharedMedia = projectDir + '/example/shared';
13 | var fxPayRelPath = 'build/lib/fxpay.debug.js';
14 |
15 | if (!fs.existsSync(projectDir + '/' + fxPayRelPath)) {
16 | throw new Error(fxPayRelPath + ' does not exist. ' +
17 | 'You need to run `grunt compress` first');
18 | }
19 |
20 | router.use(morgan('dev')); // logging
21 |
22 | router.get('/fxpay.debug.js:suffix?', function (req, res) {
23 | res.sendFile(fxPayRelPath + (req.params.suffix || ''),
24 | {root: projectDir});
25 | });
26 |
27 | router.get('/lib/fxpay/:sourceFile', function (req, res) {
28 | // Load uncompressed files when debugging with a source map.
29 | res.sendFile('lib/fxpay/' + req.params.sourceFile,
30 | {root: projectDir});
31 | });
32 |
33 | router.get('/manifest.webapp', function (req, res) {
34 | res.sendFile('manifest.webapp', {root: hostedDir});
35 | });
36 |
37 | console.log('Serving /shared from:', sharedMedia);
38 | router.use('/shared/', express.static(sharedMedia));
39 |
40 | console.log('Serving root media from:', media);
41 | router.use('/', express.static(media));
42 |
43 | app.use('/', router);
44 |
45 | var port = process.env.PORT || 3001;
46 | app.listen(port);
47 | console.log('Listening on port ' + port);
48 |
--------------------------------------------------------------------------------
/example/hosted/README.rst:
--------------------------------------------------------------------------------
1 | ================================
2 | Hosted App With In-App Purchases
3 | ================================
4 |
5 | This is an example of a `hosted app`_ that uses the fxpay library to
6 | sell and restore in-app products.
7 |
8 | Installation
9 | ------------
10 |
11 | To run the example you'll need to first compress a
12 | minified fxpay library, install some node modules, and start a web server.
13 | Run these commands from the root of the repository::
14 |
15 | grunt compress
16 | cd example/hosted
17 | npm install
18 | npm start
19 |
20 | You can now open http://localhost:3000 to see the example app.
21 | All features should be working in any standard web browser.
22 |
23 | If you install the app as a desktop web app,
24 | the easiest way to debug it is to launch it from the
25 | shell after installation like this::
26 |
27 | /Applications/FxPayHosted.app/Contents/MacOS/webapprt -debug 6000
28 |
29 | Then fire up the `WebIDE`_ and hook it up as a `remote runtime`_.
30 | You'll need to accept a prompt and then you'll be able to use
31 | the debugging tools.
32 |
33 | On Mozilla's PAAS
34 | -----------------
35 |
36 | The example app is hosted on Mozilla's
37 | `PAAS `_ for convenience.
38 | You can access it at http://fxpay-hosted.paas.allizom.org/ and you can
39 | submit it as an app to your local Firefox Marketplace by uploading the
40 | manifest at http://fxpay-hosted.paas.allizom.org/manifest.webapp .
41 |
42 | To push changes to the app, run this from the fxpay repository root::
43 |
44 | grunt compress
45 | stackato push -n
46 |
47 | Accessing Your Local API
48 | ------------------------
49 |
50 | By default, the example app offers ``http://mp.dev`` as the local
51 | Firefox Marketplace API option.
52 | If you'd like to specify a different local URL, you can do so by
53 | passing it as a query string parameter. For example, to set your
54 | local API URL to ``http://fireplace.local``, load the example app
55 | from this URL:
56 |
57 | http://localhost:3000/?local_api=http%3A%2F%2Ffireplace.local
58 |
59 |
60 | .. _`remote runtime`: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Debugging_Firefox_Desktop
61 | .. _`hosted app`: https://developer.mozilla.org/en-US/Marketplace/Options/Hosted_apps
62 | .. _`WebIDE`: https://developer.mozilla.org/en-US/docs/Tools/WebIDE
63 |
--------------------------------------------------------------------------------
/example/hosted/manifest.webapp:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.0.1",
3 | "name": "FxPayHosted",
4 | "description": "Example of a hosted app that can do server-less in-app payments",
5 | "developer": {
6 | "name": "Kumar McMillan"
7 | },
8 | "icons": {
9 | "128": "/img/kiwi_128.png",
10 | "64": "/img/kiwi_64.png",
11 | "48": "/img/kiwi_48.png",
12 | "32": "/img/kiwi_32.png"
13 | },
14 | "launch_path": "/index.html",
15 | "installs_allowed_from": ["*"],
16 | "type": "web"
17 | }
18 |
--------------------------------------------------------------------------------
/example/hosted/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fxpay-example",
3 | "version": "0.0.1",
4 | "engines": {"node": ">=0.10"},
5 | "private": true,
6 | "dependencies": {
7 | "express": "4.10.3",
8 | "morgan": "1.5.0"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/example/hosted/server.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var path = require('path');
3 |
4 | var express = require('express');
5 | var morgan = require('morgan');
6 |
7 | var app = express();
8 | var router = express.Router();
9 | var projectDir = path.normalize(__dirname + '/../..');
10 | var hostedDir = projectDir + '/example/hosted';
11 | var media = projectDir + '/example/shared';
12 | var fxPayRelPath = 'build/lib/fxpay.debug.js';
13 |
14 | if (!fs.existsSync(projectDir + '/' + fxPayRelPath)) {
15 | throw new Error(fxPayRelPath + ' does not exist. ' +
16 | 'You need to run `grunt compress` first');
17 | }
18 |
19 | router.use(morgan('dev')); // logging
20 |
21 | router.get('/fxpay.debug.js:suffix?', function (req, res) {
22 | res.sendFile(fxPayRelPath + (req.params.suffix || ''),
23 | {root: projectDir});
24 | });
25 |
26 | router.get('/lib/fxpay/:sourceFile', function (req, res) {
27 | // Load uncompressed files when debugging with a source map.
28 | res.sendFile('lib/fxpay/' + req.params.sourceFile,
29 | {root: projectDir});
30 | });
31 |
32 | router.get('/manifest.webapp', function (req, res) {
33 | res.sendFile('manifest.webapp', {root: hostedDir});
34 | });
35 |
36 | console.log('Serving media from:', media);
37 | router.use('/', express.static(media));
38 |
39 | app.use('/', router);
40 |
41 | var port = process.env.PORT || 3000;
42 | app.listen(port);
43 | console.log('Listening on port ' + port);
44 |
--------------------------------------------------------------------------------
/example/packaged-web/README.rst:
--------------------------------------------------------------------------------
1 | ======================================
2 | Packaged Web App With In-App Purchases
3 | ======================================
4 |
5 | This is an example of a `packaged app`_ of `type:web` (meaning it is not
6 | privileged) that uses the fxpay library to sell and restore in-app products.
7 |
8 | By default packaged apps get a random origin but fxpay needs a
9 | reliable origin to look up in-app products. This shows how you can still
10 | use a web package having no origin to do in-app payments.
11 |
12 | Installation
13 | ~~~~~~~~~~~~
14 |
15 | First, build this example into a packaged app. Run this from the root
16 | of the fxpay repository
17 | after you've installed the developer tools listed in the main README::
18 |
19 | grunt package
20 |
21 | This will create ``build/app-web`` and ``build/app-web.zip``.
22 | You can install the app from that directory using the `WebIDE`_.
23 |
24 | .. _`packaged app`: https://developer.mozilla.org/en-US/Marketplace/Options/Packaged_apps
25 | .. _`WebIDE`: https://developer.mozilla.org/en-US/docs/Tools/WebIDE
26 | .. _`Firefox Marketplace`: https://marketplace.firefox.com/
27 |
--------------------------------------------------------------------------------
/example/packaged-web/manifest.webapp:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.0.1",
3 | "name": "FxPay Web Package",
4 | "description": "Example of a packaged app of type:web that can do server-less in-app payments",
5 | "developer": {
6 | "name": "Kumar McMillan"
7 | },
8 | "icons": {
9 | "128": "/img/kiwi_128.png",
10 | "64": "/img/kiwi_64.png",
11 | "48": "/img/kiwi_48.png",
12 | "32": "/img/kiwi_32.png"
13 | },
14 | "launch_path": "/index.html",
15 | "installs_allowed_from": ["*"],
16 | "type": "web"
17 | }
18 |
--------------------------------------------------------------------------------
/example/packaged/README.rst:
--------------------------------------------------------------------------------
1 | ==================================
2 | Packaged App With In-App Purchases
3 | ==================================
4 |
5 | This is an example of a `packaged app`_ that uses the fxpay library to
6 | sell and restore in-app products.
7 |
8 | By default packaged apps get a random origin but fxpay needs a
9 | reliable origin to look up in-app products. Because of this, the packaged
10 | app needs to be `privileged`_ and must define an origin.
11 |
12 | Installation
13 | ~~~~~~~~~~~~
14 |
15 | To install a `privileged`_ app it must be signed by something like
16 | `Firefox Marketplace`_. However, you can use the
17 | `WebIDE`_ to install it as well. First, build this example into a
18 | packaged app. Run this from the root of the fxpay repository
19 | after you've installed the developer tools listed in the main README::
20 |
21 | grunt package
22 |
23 | This will create ``build/app`` and ``build/app.zip``.
24 | You can install the app from that directory using the `WebIDE`_.
25 |
26 | .. _`packaged app`: https://developer.mozilla.org/en-US/Marketplace/Options/Packaged_apps
27 | .. _`privileged`: https://developer.mozilla.org/en-US/Marketplace/Options/Packaged_apps#Privileged_app
28 | .. _`WebIDE`: https://developer.mozilla.org/en-US/docs/Tools/WebIDE
29 | .. _`Firefox Marketplace`: https://marketplace.firefox.com/
30 |
--------------------------------------------------------------------------------
/example/packaged/manifest.webapp:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.0.1",
3 | "name": "FxPay",
4 | "origin": "app://fxpay.allizom.org",
5 | "description": "Example of a packaged app that can do server-less in-app payments",
6 | "developer": {
7 | "name": "Kumar McMillan"
8 | },
9 | "icons": {
10 | "128": "/img/kiwi_128.png",
11 | "64": "/img/kiwi_64.png",
12 | "48": "/img/kiwi_48.png",
13 | "32": "/img/kiwi_32.png"
14 | },
15 | "launch_path": "/index.html",
16 | "installs_allowed_from": ["*"],
17 | "type": "privileged"
18 | }
19 |
--------------------------------------------------------------------------------
/example/shared/css/index.css:
--------------------------------------------------------------------------------
1 | /* http://meyerweb.com/eric/tools/css/reset/
2 | v2.0 | 20110126
3 | License: none (public domain)
4 | */
5 |
6 | html, body, div, span, applet, object, iframe,
7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
8 | a, abbr, acronym, address, big, cite, code,
9 | del, dfn, em, img, ins, kbd, q, s, samp,
10 | small, strike, strong, sub, sup, tt, var,
11 | b, u, i, center,
12 | dl, dt, dd, ol, ul, li,
13 | fieldset, form, label, legend,
14 | table, caption, tbody, tfoot, thead, tr, th, td,
15 | article, aside, canvas, details, embed,
16 | figure, figcaption, footer, header, hgroup,
17 | menu, nav, output, ruby, section, summary,
18 | time, mark, audio, video {
19 | margin: 0;
20 | padding: 0;
21 | border: 0;
22 | font-size: 100%;
23 | font: inherit;
24 | vertical-align: baseline;
25 | }
26 |
27 | body {
28 | text-align: center;
29 | font-size: 80%;
30 | color: #121007;
31 | font-family: Arial, Helvetica;
32 | }
33 |
34 | h2 {
35 | font-size: 2em;
36 | margin-bottom: 1em;
37 | }
38 |
39 | h3 {
40 | font-size: 1.5em;
41 | margin-bottom: 0.5em;
42 | }
43 |
44 | a, button {
45 | color: #121007;
46 | }
47 |
48 | button {
49 | text-decoration: none;
50 | }
51 |
52 | ul li {
53 | list-style: none;
54 | }
55 |
56 | button {
57 | font-size: 1.5em;
58 | padding: 1em;
59 | text-align: center;
60 | text-shadow: 0pt 1px 1px rgba(255, 255, 255, 0.75);
61 | background-color: rgb(245, 245, 245);
62 | background-image: -moz-linear-gradient(center top , rgb(255, 255, 255), rgb(230, 230, 230));
63 | box-shadow: 0pt 1px 0pt rgba(255, 255, 255, 0.2) inset, 0pt 1px 2px rgba(0, 0, 0, 0.05);
64 | border-width: 1px;
65 | border-style: solid;
66 | border-color: rgb(204, 204, 204) rgb(204, 204, 204) rgb(179, 179, 179);
67 | border-radius: 6px 6px 6px 6px;
68 | cursor: pointer;
69 | }
70 |
71 | select {
72 | font-size: 1.3em;
73 | width: 100%;
74 | display: inline-block;
75 | text-overflow: ellipsis;
76 | margin-bottom: 0.5em;
77 | }
78 |
79 | #app {
80 | margin: 1em 17em;
81 | padding: 2em;
82 | border-width: 1px;
83 | border-style: solid;
84 | border-color: rgb(204, 204, 204) rgb(204, 204, 204) rgb(179, 179, 179);
85 | border-radius: 6px 6px 6px 6px;
86 | }
87 |
88 | .clear {
89 | clear: both;
90 | }
91 |
92 | .product img {
93 | float: left;
94 | margin-right: 1em;
95 | }
96 | .product button {
97 | float: right;
98 | }
99 |
100 | .product {
101 | text-align: left;
102 | padding: 1em;
103 | }
104 |
105 | ul {
106 | margin: 0 auto;
107 | width: 25em;
108 | border: 1px solid #C0C0C0;
109 | border-radius: 6px;
110 | }
111 |
112 | ul li {
113 | border-bottom: 1px solid #C0C0C0;
114 | }
115 |
116 | ul li:nth-last-child(1) {
117 | border-bottom: none;
118 | }
119 |
120 | .placeholder {
121 | text-align: center;
122 | }
123 |
124 | #your-products {
125 | margin-top: 3em;
126 | }
127 |
128 | #install-banner {
129 | display: none;
130 | padding: 1em;
131 | background: #777;
132 | border-bottom: 2px solid #000;
133 | }
134 |
135 | #install-banner p {
136 | font-size: 1.2em;
137 | }
138 |
139 | #install-banner button {
140 | margin: 1em;
141 | padding: 0.3em;
142 | }
143 |
144 | #error {
145 | text-align: center;
146 | font-size: 1.5em;
147 | color: red;
148 | }
149 |
150 | #delete-purchases {
151 | display: none;
152 | margin-top: 1em;
153 | }
154 |
155 | /* portrait tablet */
156 | @media (max-width: 768px) {
157 | h2 {
158 | margin-bottom: 0.5em;
159 | }
160 | h3 {
161 | margin-bottom: 0.1em;
162 | }
163 | #app {
164 | margin: 0;
165 | border: 0;
166 | padding: 0.5em;
167 | }
168 | ul {
169 | width: 100%;
170 | }
171 | #your-products {
172 | margin-top: 1em;
173 | }
174 | footer {
175 | margin-top: 1em;
176 | padding-top: 1em;
177 | border-width: 1px 0 0 0;
178 | border-style: solid;
179 | border-color: rgb(204, 204, 204) rgb(204, 204, 204) rgb(179, 179, 179);
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/example/shared/img/cheese_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/fxpay/be655d21af6544c60c6b7ae55ad28b38b1737ddd/example/shared/img/cheese_128.png
--------------------------------------------------------------------------------
/example/shared/img/cheese_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/fxpay/be655d21af6544c60c6b7ae55ad28b38b1737ddd/example/shared/img/cheese_32.png
--------------------------------------------------------------------------------
/example/shared/img/cheese_48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/fxpay/be655d21af6544c60c6b7ae55ad28b38b1737ddd/example/shared/img/cheese_48.png
--------------------------------------------------------------------------------
/example/shared/img/cheese_64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/fxpay/be655d21af6544c60c6b7ae55ad28b38b1737ddd/example/shared/img/cheese_64.png
--------------------------------------------------------------------------------
/example/shared/img/kiwi_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/fxpay/be655d21af6544c60c6b7ae55ad28b38b1737ddd/example/shared/img/kiwi_128.png
--------------------------------------------------------------------------------
/example/shared/img/kiwi_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/fxpay/be655d21af6544c60c6b7ae55ad28b38b1737ddd/example/shared/img/kiwi_32.png
--------------------------------------------------------------------------------
/example/shared/img/kiwi_48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/fxpay/be655d21af6544c60c6b7ae55ad28b38b1737ddd/example/shared/img/kiwi_48.png
--------------------------------------------------------------------------------
/example/shared/img/kiwi_64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/fxpay/be655d21af6544c60c6b7ae55ad28b38b1737ddd/example/shared/img/kiwi_64.png
--------------------------------------------------------------------------------
/example/shared/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | fxpay demo
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
23 |
24 |
25 |
26 |
Products
27 |
31 |
32 |
33 |
34 |
Your Products
35 |
36 | - You haven't bought any yet
37 |
38 |
39 |
40 |
41 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/example/shared/js/index.js:
--------------------------------------------------------------------------------
1 | $(function() {
2 | var apiUrlBase;
3 |
4 | var apiUrls = {
5 | prod: 'https://marketplace.firefox.com',
6 | dev: 'https://marketplace-dev.allizom.org',
7 | stage: 'https://marketplace.allizom.org',
8 | alt: 'https://payments-alt.allizom.org',
9 | local: queryParam('local_api') || 'http://mp.dev',
10 | };
11 |
12 | console.log('local API configured as:', apiUrls.local);
13 |
14 |
15 | // Helper functions:
16 | //
17 | function initApi(env) {
18 | var productsUl = $('#products ul');
19 | if (!env) {
20 | env = $('#api-server option:selected').val();
21 | }
22 | apiUrlBase = apiUrls[env];
23 | if (!apiUrlBase) {
24 | throw new Error('unknown API env: ' + env);
25 | }
26 | console.log('setting API to', apiUrlBase);
27 |
28 | fxpay.configure({
29 | fakeProducts: $('#simulate-checkbox').is(':checked'),
30 | apiUrlBase: apiUrlBase,
31 | adapter: null, // force re-creation of the adapter.
32 | });
33 |
34 | // Reset some state.
35 | clearError();
36 | clearPurchases();
37 | productsUl.empty();
38 |
39 | console.log('getting products from', apiUrlBase);
40 |
41 | fxpay.getProducts().then(function(products) {
42 |
43 | products.forEach(function(product) {
44 | console.info('got product:', product);
45 | addProduct(productsUl, product);
46 |
47 | if (product.hasReceipt()) {
48 | product.validateReceipt().then(function(restoredProduct) {
49 | console.log('product', restoredProduct.productId, restoredProduct,
50 | 'restored from receipt');
51 | productBought(restoredProduct);
52 | }).catch(function(error) {
53 | console.error('error restoring product',
54 | error.productInfo.productId, 'message:',
55 | error.toString());
56 | showError(error);
57 | });
58 | }
59 |
60 | });
61 |
62 | }).catch(function(err) {
63 | console.error('error getting products:', err);
64 | showError(err);
65 | });
66 | }
67 |
68 | function addProduct(parent, prodData, opt) {
69 | opt = opt || {showBuy: true};
70 | var li = $('', {class: 'product'});
71 | if (prodData.smallImageUrl) {
72 | li.append($('
', {src: prodData.smallImageUrl,
73 | height: 64, width: 64}));
74 | }
75 | if (opt.showBuy) {
76 | li.append($('').data({product: prodData}));
77 | }
78 | li.append($('' + encodeHtmlEntities(prodData.name) + '
'));
79 | // TODO bug 1042953:
80 | //li.append($('' + encodeHtmlEntities(prodData.description) + '
'));
81 | li.append($('', {class: 'clear'}));
82 | parent.append(li);
83 | }
84 |
85 | function encodeHtmlEntities(str) {
86 | return str.replace(/[\u00A0-\u9999<>\&]/gim, function(i) {
87 | return '' + i.charCodeAt(0) + ';';
88 | });
89 | }
90 |
91 | function productBought(productInfo) {
92 | $('#your-products ul li.placeholder').hide();
93 | addProduct($('#your-products ul'), productInfo, {showBuy: false});
94 | $('#delete-purchases').show();
95 | }
96 |
97 | function clearPurchases() {
98 | $('#your-products ul li:not(.placeholder)').remove();
99 | $('#your-products ul li.placeholder').show();
100 | }
101 |
102 | function clearError() {
103 | $('#error').text('');
104 | }
105 |
106 | function showError(error) {
107 | console.error(error.toString());
108 | $('#error').text(error.toString());
109 | }
110 |
111 | function queryParam(name) {
112 | // Returns a query string parameter value by `name` or null.
113 | var urlParts = window.location.href.split('?');
114 | var query;
115 | var value = null;
116 |
117 | if (urlParts.length > 1) {
118 | query = urlParts[1].split('&');
119 |
120 | query.forEach(function(nameVal) {
121 | var parts = nameVal.split('=');
122 | if (parts[0] === name) {
123 | value = decodeURIComponent(parts[1]);
124 | }
125 | });
126 | }
127 |
128 | return value;
129 | }
130 |
131 |
132 | // DOM handling:
133 | //
134 | $('ul').on('click', '.product button', function(evt) {
135 | evt.preventDefault();
136 | clearError();
137 | var prod = $(this).data('product');
138 | console.log('purchasing', prod.name, prod.productId);
139 |
140 | fxpay.purchase(prod.productId).then(function(product) {
141 | console.log('product:', product.productId, product, 'purchased');
142 | productBought(product);
143 | }).catch(function (err) {
144 | console.error('error purchasing product',
145 | (err.productInfo && err.productInfo.productId),
146 | 'message:', err.toString());
147 | showError(err);
148 | });
149 |
150 | // TODO: update the UI here with a spinner or something.
151 | });
152 |
153 | $('#delete-purchases').click(function(evt) {
154 | clearPurchases();
155 | console.log('clearing all receipts');
156 | var appSelf = navigator.mozApps && navigator.mozApps.getSelf();
157 | if (appSelf) {
158 | console.log('removing receipts from mozApps');
159 | var num = 0;
160 | appSelf.receipts.forEach(function(receipt) {
161 | var req = appSelf.removeReceipt(receipt);
162 | num++;
163 | req.onsuccess = function() {
164 | console.log('receipt successfully removed');
165 | };
166 | req.onerror = function() {
167 | console.error('could not remove receipt:', this.error.name);
168 | };
169 | });
170 | console.log('number of receipts removed:', num);
171 | } else {
172 | console.log('removing receipts from local storage');
173 | // I guess this is kind of brutal but it's just a demo app :)
174 | window.localStorage.clear();
175 | }
176 | $('#delete-purchases').hide();
177 | });
178 |
179 | $('button.install').click(function(evt) {
180 | var a = document.createElement('a');
181 | a.href = '/manifest.webapp';
182 | var fullManifest = (
183 | a.protocol + '//' + a.hostname + (a.port ? ':' + a.port: '') +
184 | a.pathname);
185 |
186 | var req = window.navigator.mozApps.install(fullManifest);
187 |
188 | req.onsuccess = function() {
189 | var app = this.result;
190 | app.launch();
191 | };
192 |
193 | req.onerror = function() {
194 | console.error('Error installing app:', this.error.name);
195 | };
196 |
197 | });
198 |
199 | $('#api-server').change(function(evt) {
200 | initApi();
201 | });
202 |
203 | $('#simulate-checkbox').change(function(evt) {
204 | initApi();
205 | });
206 |
207 |
208 | // Startup
209 | //
210 | console.log('example app startup');
211 |
212 | fxpay.configure({
213 | receiptCheckSites: [
214 | // Allow the production service.
215 | 'https://receiptcheck.marketplace.firefox.com',
216 | 'https://marketplace.firefox.com',
217 |
218 | // The following would not be needed in a live app. These our some test
219 | // services for development of the fxpay library only.
220 |
221 | // Allow our test servers.
222 | 'https://receiptcheck-dev.allizom.org',
223 | 'https://receiptcheck-marketplace-dev.allizom.org',
224 | 'https://receiptcheck-payments-alt.allizom.org',
225 | 'https://marketplace-dev.allizom.org',
226 | 'https://marketplace.allizom.org',
227 | 'https://payments-alt.allizom.org',
228 |
229 | // Allow some common local servers..
230 | 'http://mp.dev',
231 | 'http://fireplace.loc',
232 | ],
233 | extraProviderUrls: {
234 | // Map some development sites.
235 | 'mozilla-dev/payments/pay/v1':
236 | 'https://marketplace-dev.allizom.org/mozpay/?req={jwt}',
237 | 'mozilla-stage/payments/pay/v1':
238 | 'https://marketplace.allizom.org/mozpay/?req={jwt}',
239 | 'mozilla-alt/payments/pay/v1':
240 | 'https://payments-alt.allizom.org/mozpay/?req={jwt}',
241 | 'mozilla-local/payments/pay/v1':
242 | 'http://fireplace.loc/mozpay/?req={jwt}',
243 | },
244 | // Initially, start by allowing fake products so that test
245 | // receipts can validate. The checkbox in initApi() will
246 | // toggle this setting.
247 | fakeProducts: true
248 | });
249 |
250 |
251 | initApi();
252 |
253 | if (navigator.mozApps && !navigator.mozApps.getSelf()) {
254 | // We're running on Firefox web so provide an option
255 | // to install as an app.
256 | $('#install-banner').show();
257 | }
258 |
259 | });
260 |
--------------------------------------------------------------------------------
/jsdoc.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["plugins/markdown"]
3 | }
4 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // Generated on Wed Mar 05 2014 16:06:01 GMT-0600 (CST)
3 |
4 | module.exports = function(config) {
5 | config.set({
6 |
7 | // base path, that will be used to resolve files and exclude
8 | basePath: '',
9 |
10 |
11 | // frameworks to use
12 | frameworks: [
13 | 'mocha',
14 | 'requirejs',
15 | 'sinon',
16 | 'chai'
17 | ],
18 |
19 |
20 | // list of files / patterns to load in the browser.
21 | // This is defined in Gruntfile.js
22 | files: [],
23 |
24 |
25 | // list of files to exclude
26 | exclude: [],
27 |
28 |
29 | // test results reporter to use
30 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
31 | reporters: ['mocha'],
32 |
33 |
34 | // web server port
35 | port: 9876,
36 |
37 |
38 | // enable / disable colors in the output (reporters and logs)
39 | colors: true,
40 |
41 |
42 | // level of logging
43 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
44 | logLevel: config.LOG_INFO,
45 |
46 |
47 | // enable / disable watching file and executing tests whenever any file changes
48 | autoWatch: true,
49 |
50 |
51 | // Start these browsers, currently available:
52 | // - Chrome
53 | // - ChromeCanary
54 | // - Firefox
55 | // - Opera (has to be installed with `npm install karma-opera-launcher`)
56 | // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`)
57 | // - PhantomJS
58 | // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`)
59 | browsers: ['Firefox'],
60 |
61 |
62 | // If browser does not capture in given timeout [ms], kill it
63 | captureTimeout: 60000,
64 |
65 |
66 | // Continuous Integration mode
67 | // if true, it capture browsers, run tests and exit
68 | singleRun: false
69 | });
70 | };
71 |
--------------------------------------------------------------------------------
/lib/fxpay/adapter.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'exports',
3 | 'api',
4 | 'errors',
5 | 'products',
6 | 'receipts',
7 | 'utils'
8 | ], function(exports, api, errors, products, receipts, utils) {
9 |
10 | 'use strict';
11 |
12 | function FxInappAdapter() {
13 | //
14 | // Adapter for Firefox Marketplace in-app products.
15 | //
16 | // This implements the backend details about how a
17 | // purchase JWT is generated and it has some hooks for
18 | // initialization and finishing up purchases.
19 | //
20 | // This is the default adapter and serves as a guide
21 | // for what public methods you need to implement if you
22 | // were to create your own.
23 | //
24 | // You should avoid setting any adapter properties here
25 | // that might rely on settings. Instead, use the configure()
26 | // hook.
27 | //
28 | }
29 |
30 | FxInappAdapter.prototype.toString = function() {
31 | return '';
32 | };
33 |
34 | FxInappAdapter.prototype.configure = function(settings) {
35 | //
36 | // Adds a configuration hook for when settings change.
37 | //
38 | // This is called when settings are first intialized
39 | // and also whenever settings are reconfigured.
40 | //
41 | this.settings = settings;
42 | this.api = new api.API(settings.apiUrlBase);
43 | settings.log.info('configuring Firefox Marketplace In-App adapter');
44 | };
45 |
46 | FxInappAdapter.prototype.startTransaction = function(opt, callback) {
47 | //
48 | // Start a transaction.
49 | //
50 | // The `opt` object contains the following parameters:
51 | //
52 | // - productId: the ID of the product purchased.
53 | //
54 | // When finished, execute callback(error, transactionData).
55 | //
56 | // - error: an error if one occurred or null if not
57 | // - transactionData: an object that describes the transaction.
58 | // This can be specific to your adapter but must include
59 | // the `productJWT` parameter which is a JSON Web Token
60 | // that can be passed to navigator.mozPay().
61 | //
62 | opt = utils.defaults(opt, {
63 | productId: null
64 | });
65 | var settings = this.settings;
66 | this.api.post(settings.prepareJwtApiUrl, {inapp: opt.productId},
67 | function(err, productData) {
68 | if (err) {
69 | return callback(err);
70 | }
71 | settings.log.debug('requested JWT for ', opt.productId, 'from API; got:',
72 | productData);
73 | return callback(null, {productJWT: productData.webpayJWT,
74 | productId: opt.productId,
75 | productData: productData});
76 | });
77 | };
78 |
79 | FxInappAdapter.prototype.transactionStatus = function(transData, callback) {
80 | //
81 | // Get the status of a transaction.
82 | //
83 | // The `transData` object received is the same one returned by
84 | // startTransaction().
85 | //
86 | // When finished, execute callback(error, isCompleted, productInfo).
87 | //
88 | // - error: an error if one occurred or null if not.
89 | // - isCompleted: true or false if the transaction has been
90 | // completed successfully.
91 | // - productInfo: an object that describes the product purchased.
92 | // If there was an error or the transaction was not completed,
93 | // this can be null.
94 | // A productInfo object should have the propeties described at:
95 | //
96 | // https://developer.mozilla.org/en-US/Marketplace/Monetization
97 | // /In-app_payments_section/fxPay_iap#Product_Info_Object
98 | //
99 | var self = this;
100 | var url = self.api.url(transData.productData.contribStatusURL,
101 | {versioned: false});
102 | self.api.get(url, function(err, data) {
103 | if (err) {
104 | return callback(err);
105 | }
106 |
107 | if (data.status === 'complete') {
108 | self._finishTransaction(data, transData.productId,
109 | function(err, productInfo) {
110 | if (err) {
111 | return callback(err);
112 | }
113 | callback(null, true, productInfo);
114 | });
115 | } else if (data.status === 'incomplete') {
116 | return callback(null, false);
117 | } else {
118 | return callback(errors.ConfigurationError(
119 | 'transaction status ' + data.status + ' from ' +
120 | url + ' was unexpected'));
121 | }
122 | });
123 | };
124 |
125 | FxInappAdapter.prototype._finishTransaction = function(data, productId,
126 | callback) {
127 | //
128 | // Private helper method to finish transactionStatus().
129 | //
130 | var settings = this.settings;
131 | settings.log.info('received completed transaction:', data);
132 |
133 | receipts.add(data.receipt, function(err) {
134 | if (err) {
135 | return callback(err);
136 | }
137 | products.getById(productId, function(err, fullProductInfo) {
138 | if (err) {
139 | return callback(err, fullProductInfo);
140 | }
141 | callback(null, fullProductInfo);
142 | }, {
143 | // If this is a purchase for fake products, only fetch stub products.
144 | fetchStubs: settings.fakeProducts,
145 | });
146 | });
147 | };
148 |
149 | exports.FxInappAdapter = FxInappAdapter;
150 |
151 | });
152 |
--------------------------------------------------------------------------------
/lib/fxpay/api.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'exports',
3 | 'errors',
4 | 'settings',
5 | 'utils'
6 | ], function(exports, errors, settings, utils) {
7 |
8 | 'use strict';
9 |
10 | function API(baseUrl, opt) {
11 | opt = opt || {};
12 | this.baseUrl = baseUrl;
13 | this.log = settings.log;
14 | this.timeoutMs = settings.apiTimeoutMs || 10000;
15 | this.versionPrefix = settings.apiVersionPrefix || undefined;
16 | }
17 |
18 | API.prototype.url = function(path, opt) {
19 | opt = opt || {};
20 | opt.versioned = (typeof opt.versioned !== 'undefined'
21 | ? opt.versioned: true);
22 | var url = this.baseUrl;
23 | if (opt.versioned) {
24 | url += (this.versionPrefix || '');
25 | }
26 | url += path;
27 | return url;
28 | };
29 |
30 | API.prototype.request = function(method, path, data, cb, opt) {
31 | opt = opt || {};
32 | var defaultCType = (data ? 'application/x-www-form-urlencoded': null);
33 | opt.contentType = opt.contentType || defaultCType;
34 | var defaultHeaders = {
35 | 'Accept': 'application/json'
36 | };
37 | if (opt.contentType) {
38 | defaultHeaders['Content-Type'] = opt.contentType;
39 | }
40 |
41 | opt.headers = opt.headers || {};
42 | for (var h in defaultHeaders) {
43 | if (!(h in opt.headers)) {
44 | opt.headers[h] = defaultHeaders[h];
45 | }
46 | }
47 | opt.headers['x-fxpay-version'] = settings.libVersion;
48 |
49 | var log = this.log;
50 | var api = this;
51 | var url;
52 | if (!cb) {
53 | cb = function(err, data) {
54 | if (err) {
55 | throw err;
56 | }
57 | log.info('default callback received data:', data);
58 | };
59 | }
60 | if (/^http(s)?:\/\/.*/.test(path)) {
61 | // Requesting an absolute URL so no need to prefix it.
62 | url = path;
63 | } else {
64 | url = this.url(path);
65 | }
66 | var xhr = new XMLHttpRequest();
67 | // This doesn't seem to be supported by sinon yet.
68 | //xhr.responseType = "json";
69 |
70 | var events = {
71 | abort: function() {
72 | cb(errors.ApiRequestAborted('XHR request aborted for path: ' + path));
73 | },
74 | error: function(evt) {
75 | log.debug('XHR error event:', evt);
76 | cb(errors.ApiRequestError('received XHR error for path: ' + path));
77 | },
78 | load: function() {
79 | var data;
80 | if (this.status.toString().slice(0, 1) !== '2') {
81 | // TODO: handle status === 0 ?
82 | // TODO: handle redirects?
83 | var err = errors.BadApiResponse(
84 | 'Unexpected status: ' + this.status + ' for URL: ' + url);
85 | log.debug(err.toString(), 'response:', this.responseText);
86 | return cb(err);
87 | }
88 |
89 | log.debug('XHR load: GOT response:', this.responseText);
90 | try {
91 | // TODO: be smarter about content-types here.
92 | data = JSON.parse(this.responseText);
93 | } catch (parseErr) {
94 | var err = errors.BadJsonResponse(
95 | 'Unparsable JSON for URL: ' + url + '; exception: ' + parseErr);
96 | log.debug(err.toString(), 'response:', this.responseText);
97 | return cb(err);
98 | }
99 |
100 | cb(null, data);
101 | },
102 | timeout: function() {
103 | cb(errors.ApiRequestTimeout(
104 | 'XHR request to ' + url + ' timed out after ' +
105 | api.timeoutMs + 'ms'));
106 | }
107 | };
108 |
109 | for (var k in events) {
110 | xhr.addEventListener(k, events[k], false);
111 | }
112 |
113 | log.debug('opening', method, 'to', url);
114 | xhr.open(method, url, true);
115 |
116 | // Has to be after xhr.open to avoid
117 | // invalidStateError in IE.
118 | xhr.timeout = api.timeoutMs;
119 |
120 | for (var hdr in opt.headers) {
121 | xhr.setRequestHeader(hdr, opt.headers[hdr]);
122 | }
123 | if (opt.contentType === 'application/x-www-form-urlencoded' && data) {
124 | data = utils.serialize(data);
125 | }
126 | xhr.send(data);
127 | };
128 |
129 | API.prototype.get = function(path, cb, opt) {
130 | this.request('GET', path, null, cb, opt);
131 | };
132 |
133 | API.prototype.del = function(path, cb, opt) {
134 | this.request('DELETE', path, null, cb, opt);
135 | };
136 |
137 | API.prototype.post = function(path, data, cb, opt) {
138 | this.request('POST', path, data, cb, opt);
139 | };
140 |
141 | API.prototype.put = function(path, data, cb, opt) {
142 | this.request('PUT', path, data, cb, opt);
143 | };
144 |
145 | exports.API = API;
146 |
147 | });
148 |
--------------------------------------------------------------------------------
/lib/fxpay/errors.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'exports',
3 | 'settings'
4 | ], function(exports, settings) {
5 |
6 | 'use strict';
7 |
8 | exports.createError = createError;
9 |
10 | // All error classes will implicitly inherit from this.
11 | exports.FxPayError = createError('FxPayError');
12 |
13 | exportErrors([
14 | ['ApiError'],
15 | ['ConfigurationError'],
16 | ['FailedWindowMessage'],
17 | ['IncorrectUsage'],
18 | ['InvalidApp'],
19 | ['InvalidJwt'],
20 | ['NotImplementedError'],
21 | ['PaymentFailed'],
22 | ['PayWindowClosedByUser', {code: 'DIALOG_CLOSED_BY_USER'}],
23 | ['PlatformError'],
24 | ['UnknownMessageOrigin'],
25 | ]);
26 |
27 |
28 | exportErrors([
29 | ['InvalidAppOrigin'],
30 | ], {
31 | inherits: exports.InvalidApp,
32 | });
33 |
34 |
35 | exportErrors([
36 | ['AppReceiptMissing'],
37 | ['InvalidReceipt'],
38 | ['PurchaseTimeout'],
39 | ['TestReceiptNotAllowed'],
40 | ], {
41 | inherits: exports.PaymentFailed,
42 | });
43 |
44 |
45 | exportErrors([
46 | ['AddReceiptError'],
47 | ['PayPlatformError'],
48 | ['PayPlatformUnavailable'],
49 | ], {
50 | inherits: exports.PlatformError,
51 | });
52 |
53 |
54 | exportErrors([
55 | ['ApiRequestAborted'],
56 | ['ApiRequestError'],
57 | ['ApiRequestTimeout'],
58 | ['BadApiResponse'],
59 | ['BadJsonResponse'],
60 | ], {
61 | inherits: exports.ApiError,
62 | });
63 |
64 |
65 | function createError(name, classOpt) {
66 | classOpt = classOpt || {};
67 | var errorParent = classOpt.inherits || exports.FxPayError || Error;
68 |
69 | function CreatedFxPayError(message, opt) {
70 | opt = opt || {};
71 |
72 | if (!(this instanceof CreatedFxPayError)) {
73 | // Let callers create instances without `new`.
74 | var obj = Object.create(CreatedFxPayError.prototype);
75 | return CreatedFxPayError.apply(obj, arguments);
76 | }
77 | this.message = message;
78 | this.stack = (new Error()).stack;
79 | if (opt.code) {
80 | this.code = opt.code;
81 | }
82 |
83 | // Some code will attach a productInfo object
84 | // on to the exception before throwing.
85 | // This object contains information such as
86 | // the exact reason why a receipt was invalid.
87 | this.productInfo = opt.productInfo || {};
88 | var logger = settings.log || console;
89 | logger.error(this.toString());
90 |
91 | return this;
92 | }
93 |
94 | CreatedFxPayError.prototype = Object.create(errorParent.prototype);
95 | CreatedFxPayError.prototype.name = name;
96 |
97 | if (classOpt.code) {
98 | CreatedFxPayError.prototype.code = classOpt.code;
99 | }
100 |
101 | CreatedFxPayError.prototype.toString = function() {
102 | var str = Error.prototype.toString.call(this);
103 | if (this.code) {
104 | str += ' {code: ' + this.code + '}';
105 | }
106 | return str;
107 | };
108 |
109 | return CreatedFxPayError;
110 | }
111 |
112 |
113 | function exportErrors(errList, defaultOpt) {
114 | errList.forEach(function(errArgs) {
115 | var cls = errArgs[0];
116 | var opt = defaultOpt || {};
117 | if (errArgs[1]) {
118 | Object.keys(errArgs[1]).forEach(function(optKey) {
119 | opt[optKey] = errArgs[1][optKey];
120 | });
121 | }
122 | // Export the created error class.
123 | exports[cls] = createError.call(this, cls, opt);
124 | });
125 | }
126 |
127 | });
128 |
--------------------------------------------------------------------------------
/lib/fxpay/fxpay.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'exports',
3 | 'errors',
4 | 'promise',
5 | 'pay',
6 | 'receipts',
7 | 'products',
8 | 'settings',
9 | 'utils'
10 | ], function(exports, errors, promise, pay,
11 | receipts, products, settings, utils) {
12 |
13 | 'use strict';
14 |
15 | //
16 | // publicly exported functions:
17 | //
18 |
19 | var Promise = promise.Promise;
20 |
21 | exports.errors = errors;
22 | exports.receipts = receipts;
23 | exports.settings = settings;
24 |
25 | exports.configure = function() {
26 | return settings.configure.apply(settings, arguments);
27 | };
28 |
29 |
30 | exports.init = function _init(opt) {
31 | settings.initialize(opt);
32 | utils.logDeprecation(
33 | 'fxpay.init() is no longer supported; use ' +
34 | 'fxpay.getProducts()...product.validateReceipt() instead', '0.0.15');
35 |
36 | exports.getProducts()
37 | .then(function(products) {
38 | products.forEach(function(product) {
39 |
40 | if (product.hasReceipt()) {
41 | product.validateReceipt().then(function(productInfo) {
42 | settings.onrestore(null, productInfo);
43 | }).catch(function(error) {
44 | settings.onrestore(error, error.productInfo);
45 | });
46 | }
47 |
48 | });
49 | })
50 | .then(settings.oninit)
51 | .catch(settings.onerror);
52 |
53 | };
54 |
55 |
56 | exports.validateAppReceipt = function validateAppReceipt() {
57 | settings.initialize();
58 | return new Promise(function(resolve, reject) {
59 | utils.getAppSelf(function(error, appSelf) {
60 | if (error) {
61 | return reject(error);
62 | }
63 | if (!appSelf) {
64 | return reject(errors.PayPlatformUnavailable(
65 | 'mozApps.getSelf() required for receipts'));
66 | }
67 | var allAppReceipts = [];
68 |
69 | receipts.all(function(error, allReceipts) {
70 | if (error) {
71 | return reject(error);
72 | }
73 |
74 | allReceipts.forEach(function(receipt) {
75 | var storedata = receipts.checkStoreData(receipt);
76 | if (!storedata) {
77 | settings.log.info(
78 | 'ignoring receipt with missing or unparsable storedata');
79 | return;
80 | }
81 | if (storedata.inapp_id) {
82 | settings.log.info('ignoring in-app receipt with storedata',
83 | storedata);
84 | return;
85 | }
86 | allAppReceipts.push(receipt);
87 | });
88 |
89 | settings.log.info('app receipts found:', allAppReceipts.length);
90 |
91 | var appReceipt;
92 |
93 | if (allAppReceipts.length === 0) {
94 | return reject(errors.AppReceiptMissing(
95 | 'no receipt found in getSelf()'));
96 | } else if (allAppReceipts.length === 1) {
97 | appReceipt = allAppReceipts[0];
98 | settings.log.info('Installed receipt:', appReceipt);
99 | return receipts.validateAppReceipt(appReceipt,
100 | function(error, productInfo) {
101 | settings.log.info('got verification result for', productInfo);
102 | if (error) {
103 | error.productInfo = productInfo;
104 | reject(error);
105 | } else {
106 | resolve(productInfo);
107 | }
108 | });
109 | } else {
110 | // TODO: support multiple app stores? bug 1134739.
111 | // This is an unlikely case where multiple app receipts are
112 | // installed.
113 | return reject(errors.NotImplementedError(
114 | 'multiple app receipts were found which is not yet supported'));
115 | }
116 | });
117 | });
118 | });
119 | };
120 |
121 |
122 | exports.purchase = function _purchase(productId) {
123 | settings.initialize();
124 | var callback;
125 | var opt;
126 |
127 | if (typeof arguments[1] === 'function') {
128 | // Old style: fxpay.purchase(productId, callback, opt)
129 | callback = arguments[1];
130 | opt = arguments[2];
131 | } else {
132 | // New style: fxpay.purchase(productId, opt);
133 | opt = arguments[1];
134 | }
135 |
136 | opt = utils.defaults(opt, {
137 | maxTries: undefined,
138 | managePaymentWindow: undefined,
139 | paymentWindow: undefined,
140 | pollIntervalMs: undefined,
141 | });
142 |
143 | settings.initialize();
144 |
145 | var promise = new Promise(function(resolve, reject) {
146 | if (typeof opt.managePaymentWindow === 'undefined') {
147 | // By default, do not manage the payment window when a custom
148 | // window is defined. This means the client must close its own window.
149 | opt.managePaymentWindow = !opt.paymentWindow;
150 | }
151 |
152 | var partialProdInfo = new products.Product({productId: productId});
153 | settings.log.debug('starting purchase for product', productId);
154 |
155 | if (!settings.mozPay) {
156 | if (!opt.paymentWindow) {
157 | // Open a blank payment window on the same event loop tick
158 | // as the click handler. This avoids popup blockers.
159 | opt.paymentWindow = utils.openWindow();
160 | } else {
161 | settings.log.info('web flow will use client provided payment window');
162 | utils.reCenterWindow(opt.paymentWindow,
163 | settings.winWidth, settings.winHeight);
164 | }
165 | }
166 |
167 | function closePayWindow() {
168 | if (opt.paymentWindow && !opt.paymentWindow.closed) {
169 | if (opt.managePaymentWindow) {
170 | opt.paymentWindow.close();
171 | } else {
172 | settings.log.info('payment window should be closed but client ' +
173 | 'is managing it');
174 | }
175 | }
176 | }
177 |
178 | settings.adapter.startTransaction({productId: productId},
179 | function(err, transData) {
180 | if (err) {
181 | closePayWindow();
182 | err.productInfo = partialProdInfo;
183 | return reject(err);
184 | }
185 | pay.processPayment(transData.productJWT, function(err) {
186 | if (err) {
187 | closePayWindow();
188 | err.productInfo = partialProdInfo;
189 | return reject(err);
190 | }
191 |
192 | // The payment flow has completed and the window has closed.
193 | // Wait for payment verification.
194 |
195 | waitForTransaction(
196 | transData,
197 | function(err, fullProductInfo) {
198 | if (err) {
199 | err.productInfo = partialProdInfo;
200 | reject(err);
201 | } else {
202 | resolve(fullProductInfo);
203 | }
204 | }, {
205 | maxTries: opt.maxTries,
206 | pollIntervalMs: opt.pollIntervalMs
207 | }
208 | );
209 | }, {
210 | managePaymentWindow: opt.managePaymentWindow,
211 | paymentWindow: opt.paymentWindow,
212 | });
213 | });
214 | });
215 |
216 | if (callback) {
217 | utils.logDeprecation(
218 | 'purchase(id, callback) is no longer supported; use the returned ' +
219 | 'promise instead', '0.0.15');
220 | promise.then(function(productInfo) {
221 | callback(null, productInfo);
222 | }).catch(function(error) {
223 | callback(error, error.productInfo || new products.Product());
224 | });
225 | }
226 |
227 | return promise;
228 | };
229 |
230 |
231 | exports.getProduct = function getProduct() {
232 | settings.initialize();
233 | return products.get.apply(products, arguments);
234 | };
235 |
236 |
237 | exports.getProducts = function getProducts() {
238 | settings.initialize();
239 | return products.all.apply(products, arguments);
240 | };
241 |
242 |
243 | //
244 | // private functions:
245 | //
246 |
247 |
248 | // NOTE: if you change this function signature, change the setTimeout below.
249 | function waitForTransaction(transData, cb, opt) {
250 | opt = opt || {};
251 | opt.maxTries = opt.maxTries || 10;
252 | opt.pollIntervalMs = opt.pollIntervalMs || 1000;
253 | opt._tries = opt._tries || 1;
254 |
255 | var log = settings.log;
256 | log.debug('Getting transaction state for', transData,
257 | 'tries=', opt._tries);
258 |
259 | if (opt._tries > opt.maxTries) {
260 | log.error('Giving up on transaction for', transData,
261 | 'after', opt._tries, 'tries');
262 | return cb(errors.PurchaseTimeout(
263 | 'timeout while waiting for completed transaction'));
264 | }
265 |
266 | settings.adapter.transactionStatus(
267 | transData, function(err, isComplete, productInfo) {
268 | if (err) {
269 | return cb(err);
270 | }
271 | if (isComplete) {
272 | return cb(null, productInfo);
273 | } else {
274 | log.debug('Re-trying incomplete transaction in',
275 | opt.pollIntervalMs, 'ms');
276 | window.setTimeout(function() {
277 | waitForTransaction(transData, cb, {
278 | maxTries: opt.maxTries,
279 | pollIntervalMs: opt.pollIntervalMs,
280 | _tries: opt._tries + 1
281 | });
282 | }, opt.pollIntervalMs);
283 | }
284 | });
285 | }
286 |
287 | });
288 |
--------------------------------------------------------------------------------
/lib/fxpay/jwt.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'exports',
3 | 'errors',
4 | 'settings'
5 | ], function(exports, errors, settings) {
6 |
7 | 'use strict';
8 |
9 | // This is a very minimal JWT utility. It does not validate signatures.
10 |
11 | exports.decode = function jwt_decode(jwt, callback) {
12 | var parts = jwt.split('.');
13 |
14 | if (parts.length !== 3) {
15 | settings.log.debug('JWT: not enough segments:', jwt);
16 | return callback(errors.InvalidJwt('JWT does not have 3 segments'));
17 | }
18 |
19 | var jwtData = parts[1];
20 | // Normalize URL safe base64 into regular base64.
21 | jwtData = jwtData.replace("-", "+", "g").replace("_", "/", "g");
22 | var jwtString;
23 | try {
24 | jwtString = atob(jwtData);
25 | } catch (error) {
26 | return callback(errors.InvalidJwt(
27 | 'atob() error: ' + error.toString() +
28 | ' when decoding JWT ' + jwtData));
29 | }
30 | var data;
31 |
32 | try {
33 | data = JSON.parse(jwtString);
34 | } catch (error) {
35 | return callback(errors.InvalidJwt(
36 | 'JSON.parse() error: ' + error.toString() +
37 | ' when parsing ' + jwtString));
38 | }
39 | callback(null, data);
40 | };
41 |
42 |
43 | exports.getPayUrl = function jwt_getPayUrl(encodedJwt, callback) {
44 | exports.decode(encodedJwt, function(err, jwtData) {
45 | if (err) {
46 | return callback(err);
47 | }
48 |
49 | var payUrl = settings.payProviderUrls[jwtData.typ];
50 | if (!payUrl) {
51 | return callback(errors.InvalidJwt(
52 | 'JWT type ' + jwtData.typ +
53 | ' does not map to any known payment providers'));
54 | }
55 | if (payUrl.indexOf('{jwt}') === -1) {
56 | return callback(errors.ConfigurationError(
57 | 'JWT type ' + jwtData.typ +
58 | ' pay URL is formatted incorrectly: ' + payUrl));
59 | }
60 |
61 | payUrl = payUrl.replace('{jwt}', encodedJwt);
62 | settings.log.info('JWT', jwtData.typ, 'resulted in pay URL:', payUrl);
63 | callback(null, payUrl);
64 | });
65 | };
66 |
67 | });
68 |
--------------------------------------------------------------------------------
/lib/fxpay/pay.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'exports',
3 | 'errors',
4 | 'jwt',
5 | 'settings',
6 | 'utils'
7 | ], function(exports, errors, jwt, settings, utils) {
8 |
9 | 'use strict';
10 |
11 | var timer;
12 |
13 | exports.processPayment = function pay_processPayment(jwt, callback, opt) {
14 | opt = utils.defaults(opt, {
15 | managePaymentWindow: true,
16 | paymentWindow: undefined,
17 | });
18 |
19 | if (settings.mozPay) {
20 | settings.log.info('processing payment with mozPay using jwt', jwt);
21 |
22 | var payReq = settings.mozPay([jwt]);
23 |
24 | payReq.onerror = function mozPay_onerror() {
25 | settings.log.error('mozPay: received onerror():', this.error.name);
26 | if (settings.onBrokenWebRT &&
27 | this.error.name === 'USER_CANCELLED') {
28 | // This is a workaround for bug 1133963.
29 | settings.log.warn(
30 | 'webRT: pretending the cancel message is actually a success!');
31 | callback();
32 | } else {
33 | callback(errors.PayPlatformError(
34 | 'mozPay error: ' + this.error.name,
35 | {code: this.error.name}));
36 | }
37 | };
38 |
39 | payReq.onsuccess = function mozPay_onsuccess() {
40 | settings.log.debug('mozPay: received onsuccess()');
41 | callback();
42 | };
43 |
44 | } else {
45 | if (!opt.paymentWindow) {
46 | return callback(errors.IncorrectUsage(
47 | 'Cannot start a web payment without a ' +
48 | 'reference to the payment window'));
49 | }
50 | settings.log.info('processing payment with web flow');
51 | return processWebPayment(opt.paymentWindow, opt.managePaymentWindow,
52 | jwt, callback);
53 | }
54 | };
55 |
56 |
57 | exports.acceptPayMessage = function pay_acceptPayMessage(event,
58 | allowedOrigin,
59 | paymentWindow,
60 | callback) {
61 | settings.log.debug('received', event.data, 'from', event.origin);
62 |
63 | if (event.origin !== allowedOrigin) {
64 | return callback(errors.UnknownMessageOrigin(
65 | 'ignoring message from foreign window at ' + event.origin));
66 | }
67 | var eventData = event.data || {};
68 |
69 | if (eventData.status === 'unloaded') {
70 | // Look for the window having been closed.
71 | if (timer) {
72 | window.clearTimeout(timer);
73 | }
74 | // This delay is introduced so that the closed property
75 | // of the window has time to be updated.
76 | timer = window.setTimeout(function(){
77 | if (!paymentWindow || paymentWindow.closed === true) {
78 | return callback(
79 | errors.PayWindowClosedByUser('Window closed by user'));
80 | }
81 | }, 300);
82 | } else if (eventData.status === 'ok') {
83 | settings.log.info('received pay success message from window at',
84 | event.origin);
85 | return callback();
86 | } else if (eventData.status === 'failed') {
87 | return callback(errors.FailedWindowMessage(
88 | 'received pay fail message with status=' + eventData.status +
89 | ', code=' + eventData.errorCode + ' from window at ' +
90 | event.origin, {code: eventData.errorCode}));
91 | } else {
92 | return callback(errors.FailedWindowMessage(
93 | 'received pay message with unknown status ' +
94 | eventData.status + ' from window at ' +
95 | event.origin));
96 | }
97 | };
98 |
99 |
100 | function processWebPayment(paymentWindow, managePaymentWindow, payJwt,
101 | callback) {
102 | jwt.getPayUrl(payJwt, function(err, payUrl) {
103 | if (err) {
104 | return callback(err);
105 | }
106 | // Now that we've extracted a payment URL from the JWT,
107 | // load it into the freshly created popup window.
108 | paymentWindow.location = payUrl;
109 |
110 | // This interval covers closure of the popup
111 | // whilst on external domains that won't postMessage
112 | // onunload.
113 | var popupInterval = setInterval(function() {
114 | if (!paymentWindow || paymentWindow.closed) {
115 | clearInterval(popupInterval);
116 | return callback(errors.PayWindowClosedByUser(
117 | 'polling detected a closed window'));
118 | }
119 | }, 500);
120 |
121 | function receivePaymentMessage(event) {
122 |
123 | function messageCallback(err) {
124 | if (err instanceof errors.UnknownMessageOrigin) {
125 | // These could come from anywhere so ignore them.
126 | return;
127 | }
128 |
129 | // We know if we're getting messages from our UI
130 | // at this point so we can do away with the
131 | // interval watching for the popup closing
132 | // whilst on 3rd party domains.
133 | if (popupInterval) {
134 | clearInterval(popupInterval);
135 | }
136 |
137 | settings.window.removeEventListener('message',
138 | receivePaymentMessage);
139 | if (managePaymentWindow) {
140 | paymentWindow.close();
141 | } else {
142 | settings.log.info('payment window should be closed but client ' +
143 | 'is managing it');
144 | }
145 | if (err) {
146 | return callback(err);
147 | }
148 | callback();
149 | }
150 |
151 | exports.acceptPayMessage(event, utils.getUrlOrigin(payUrl),
152 | paymentWindow, messageCallback);
153 | }
154 |
155 | settings.window.addEventListener('message', receivePaymentMessage);
156 |
157 | });
158 | }
159 | });
160 |
--------------------------------------------------------------------------------
/lib/fxpay/products.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'exports',
3 | 'api',
4 | 'errors',
5 | 'promise',
6 | 'receipts',
7 | 'settings',
8 | 'utils'
9 | ], function(exports, apiModule, errors, promise, receipts, settings, utils) {
10 |
11 | 'use strict';
12 |
13 | var Promise = promise.Promise;
14 |
15 | exports.get = function(productId, opt) {
16 | return buildProductReceiptMap()
17 | .then(function() {
18 | return new Promise(function(resolve, reject) {
19 | exports.getById(productId, function(error, product) {
20 | if (error) {
21 | settings.log.error(
22 | 'no existing product with productId=' + productId +
23 | '; error: ' + error);
24 | return reject(error);
25 | }
26 |
27 | resolve(product);
28 | }, opt);
29 | });
30 | });
31 | };
32 |
33 |
34 | exports.all = function(callback) {
35 | settings.initialize();
36 | var promise = buildProductReceiptMap()
37 | .then(function() {
38 | return new Promise(function(resolve, reject) {
39 | var allProducts = [];
40 | var origin;
41 | var api = new apiModule.API(settings.apiUrlBase);
42 | var url;
43 |
44 | if (settings.fakeProducts) {
45 | settings.log.warn('about to fetch fake products');
46 | url = '/payments/stub-in-app-products/';
47 | } else {
48 | origin = encodeURIComponent(utils.getSelfOrigin());
49 | settings.log.info('about to fetch real products for app',
50 | origin);
51 | url = '/payments/' + origin + '/in-app/?active=1';
52 | }
53 |
54 | api.get(url, function(err, result) {
55 | if (err) {
56 | return reject(err);
57 | }
58 | if (!result || !result.objects) {
59 | settings.log.debug('unexpected API response', result);
60 | return reject(errors.BadApiResponse(
61 | 'received empty API response'));
62 | }
63 | settings.log.info('total products fetched:', result.objects.length);
64 | for (var i=0; i < result.objects.length; i++) {
65 | var ob = result.objects[i];
66 | var productInfo = createProductFromApi(ob);
67 | allProducts.push(productInfo);
68 | }
69 | resolve(allProducts);
70 | });
71 | });
72 | });
73 |
74 | if (callback) {
75 | utils.logDeprecation(
76 | 'getProducts(callback) is no longer supported; use the returned ' +
77 | 'promise instead', '0.0.15');
78 | promise.then(function(products) {
79 | callback(null, products);
80 | }).catch(function(error) {
81 | callback(error, []);
82 | });
83 | }
84 |
85 | return promise;
86 | };
87 |
88 |
89 | exports.getById = function(productId, onFetch, opt) {
90 | opt = opt || {};
91 | if (typeof opt.fetchStubs === 'undefined') {
92 | opt.fetchStubs = false;
93 | }
94 | if (!opt.api) {
95 | opt.api = new apiModule.API(settings.apiUrlBase);
96 | }
97 | var origin = encodeURIComponent(utils.getSelfOrigin());
98 | var url;
99 |
100 | if (opt.fetchStubs) {
101 | url = '/payments/stub-in-app-products/' + productId.toString() + '/';
102 | } else {
103 | url = '/payments/' + origin + '/in-app/' + productId.toString() + '/';
104 | }
105 | settings.log.info(
106 | 'fetching product info at URL', url, 'fetching stubs?', opt.fetchStubs);
107 |
108 | opt.api.get(url, function(err, productData) {
109 | if (err) {
110 | settings.log.error('Error fetching product info', err.toString());
111 | return onFetch(err, {productId: productId});
112 | }
113 | onFetch(null, createProductFromApi(productData));
114 | });
115 | };
116 |
117 |
118 | function Product(params) {
119 | params = params || {};
120 | this.pricePointId = params.pricePointId;
121 | this.productId = params.productId;
122 | this.name = params.name;
123 | this.smallImageUrl = params.smallImageUrl;
124 | this.receiptInfo = params.receiptInfo || {};
125 | }
126 |
127 | exports.Product = Product;
128 |
129 | Product.prototype.getReceiptMap = function() {
130 | if (!settings.productReceiptMap) {
131 | // Sadly, building a receipt map must be done asynchronously so
132 | // we need to rely on a higher level function to set it up.
133 | throw errors.IncorrectUsage(
134 | 'cannot proceed with this method; receipt map is empty');
135 | }
136 | return settings.productReceiptMap;
137 | };
138 |
139 | Product.prototype.hasReceipt = function() {
140 | return typeof this.getReceiptMap()[this.productId] !== 'undefined';
141 | };
142 |
143 | Product.prototype.validateReceipt = function() {
144 | var receiptMap = this.getReceiptMap();
145 | var product = this;
146 |
147 | return new Promise(function(resolve, reject) {
148 |
149 | var receipt = receiptMap[product.productId];
150 | if (!receipt) {
151 | return reject(errors.InvalidReceipt(
152 | 'could not find installed receipt for productId=' +
153 | product.productId));
154 | }
155 |
156 | receipts.validateInAppProductReceipt(receipt, product,
157 | function(error, product) {
158 | if (error) {
159 | settings.log.error('receipt validation error: ' + error);
160 | error.productInfo = product;
161 | return reject(error);
162 | } else {
163 | return resolve(product);
164 | }
165 | });
166 |
167 | });
168 | };
169 |
170 |
171 | //
172 | // private functions:
173 | //
174 |
175 |
176 | function buildProductReceiptMap() {
177 | return new Promise(function(resolve, reject) {
178 | if (settings.productReceiptMap) {
179 | return resolve(settings.productReceiptMap);
180 | }
181 |
182 | settings.log.debug('building a product->receipt map');
183 |
184 | receipts.all(function(error, allReceipts) {
185 | if (error) {
186 | return reject(error);
187 | }
188 |
189 | settings.productReceiptMap = {};
190 |
191 | allReceipts.forEach(function(receipt) {
192 | var storedata = receipts.checkStoreData(receipt);
193 | if (!storedata) {
194 | settings.log.debug(
195 | 'ignoring receipt with missing or unparsable storedata');
196 | return;
197 | }
198 | if (!storedata.inapp_id) {
199 | return settings.log.debug('ignoring receipt without inapp_id');
200 | }
201 | settings.log.debug('found receipt with inapp_id=',
202 | storedata.inapp_id);
203 | settings.productReceiptMap[storedata.inapp_id] = receipt;
204 | });
205 |
206 | resolve(settings.productReceiptMap);
207 | });
208 | });
209 | }
210 |
211 | function createProductFromApi(ob) {
212 | return new Product({
213 | pricePointId: ob.price_id,
214 | productId: ob.guid,
215 | name: ob.name,
216 | smallImageUrl: ob.logo_url,
217 | });
218 | }
219 |
220 | });
221 |
--------------------------------------------------------------------------------
/lib/fxpay/settings.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'exports',
3 | 'adapter',
4 | 'errors'
5 | ], function(exports, adapter, errors) {
6 |
7 | 'use strict';
8 | var pkgInfo = {"version": "0.0.18"}; // this is updated by `grunt bump`
9 |
10 | var defaultSettings = {
11 |
12 | // Public settings.
13 | //
14 | // Reject test receipts which are generated by the Marketplace
15 | // and do not indicate real purchases.
16 | allowTestReceipts: false,
17 | apiUrlBase: 'https://marketplace.firefox.com',
18 | apiVersionPrefix: '/api/v1',
19 | // When truthy, this will override the API object's default.
20 | apiTimeoutMs: null,
21 | // When defined, this optional map will override or
22 | // append values to payProviderUrls.
23 | extraProviderUrls: null,
24 | // When true, work with fake products and test receipts.
25 | // This implies allowTestReceipts=true.
26 | fakeProducts: false,
27 | // This object is used for all logging.
28 | log: window.console || {
29 | // Shim in a minimal set of the console API.
30 | debug: function() {},
31 | error: function() {},
32 | info: function() {},
33 | log: function() {},
34 | warn: function() {},
35 | },
36 | // Only these receipt check services are allowed.
37 | receiptCheckSites: [
38 | 'https://receiptcheck.marketplace.firefox.com',
39 | 'https://marketplace.firefox.com'
40 | ],
41 |
42 | // Private settings.
43 | //
44 | adapter: null,
45 | // This will be the App object returned from mozApps.getSelf().
46 | // On platforms that do not implement mozApps it will be false.
47 | appSelf: null,
48 | // True if configuration has been run at least once.
49 | alreadyConfigured: false,
50 | // Map of product IDs to installed receipts.
51 | // These receipts may or may not be valid.
52 | productReceiptMap: null,
53 | // Map of JWT types to payment provider URLs.
54 | payProviderUrls: {
55 | 'mozilla/payments/pay/v1':
56 | 'https://marketplace.firefox.com/mozpay/?req={jwt}'
57 | },
58 | // Reference window so tests can swap it out with a stub.
59 | window: window,
60 | // Width for payment window as a popup.
61 | winWidth: 276,
62 | // Height for payment window as a popup.
63 | winHeight: 384,
64 | // Relative API URL that accepts a product ID and returns a JWT.
65 | prepareJwtApiUrl: '/webpay/inapp/prepare/',
66 | onerror: function(err) {
67 | throw err;
68 | },
69 | oninit: function() {
70 | exports.log.info('fxpay version:', exports.libVersion);
71 | exports.log.info('initialization ran successfully');
72 | },
73 | onrestore: function(error, info) {
74 | if (error) {
75 | exports.log.error('error while restoring product:', info.productId,
76 | 'message:', error);
77 | } else {
78 | exports.log.info('product', info.productId,
79 | 'was restored from receipt');
80 | }
81 | },
82 | localStorage: window.localStorage || null,
83 | localStorageKey: 'fxpayReceipts',
84 | // When true, we're running on a broken webRT. See bug 1133963.
85 | onBrokenWebRT: (navigator.mozPay &&
86 | navigator.userAgent.indexOf('Mobile') === -1),
87 | mozPay: navigator.mozPay || null,
88 | mozApps: navigator.mozApps || null,
89 | libVersion: pkgInfo.version,
90 | };
91 |
92 | exports.configure = function settings_configure(newSettings, opt) {
93 | //
94 | // Configure new settings values.
95 | //
96 | opt = opt || {};
97 |
98 | // On first run, we always need to reset.
99 | if (!exports.alreadyConfigured) {
100 | opt.reset = true;
101 | }
102 |
103 | // Reset existing configuration.
104 | if (opt.reset) {
105 | for (var def in defaultSettings) {
106 | exports[def] = defaultSettings[def];
107 | }
108 | }
109 |
110 | // Merge new values into existing configuration.
111 | for (var param in newSettings) {
112 | if (typeof exports[param] === 'undefined') {
113 | return exports.onerror(errors.IncorrectUsage(
114 | 'configure() received an unknown setting: ' + param));
115 | }
116 | exports[param] = newSettings[param];
117 | }
118 |
119 | // Set some implied values from other parameters.
120 | if (exports.extraProviderUrls) {
121 | exports.log.info('adding extra pay provider URLs',
122 | exports.extraProviderUrls);
123 | for (var paySpec in exports.extraProviderUrls) {
124 | exports.payProviderUrls[paySpec] = exports.extraProviderUrls[paySpec];
125 | }
126 | }
127 | if (exports.fakeProducts) {
128 | exports.allowTestReceipts = true;
129 | }
130 |
131 | // Construct our in-app payments adapter.
132 | var DefaultAdapter = adapter.FxInappAdapter;
133 | if (!exports.adapter) {
134 | exports.log.info('creating default adapter');
135 | exports.adapter = new DefaultAdapter();
136 | }
137 |
138 | // Configure the new adapter or re-configure an existing adapter.
139 | exports.adapter.configure(exports);
140 | exports.log.info('using adapter:', exports.adapter.toString());
141 |
142 | exports.log.info('(re)configuration completed; fxpay version:',
143 | exports.libVersion);
144 | exports.alreadyConfigured = true;
145 |
146 | return exports;
147 | };
148 |
149 |
150 | exports.initialize = function(newSettings) {
151 | //
152 | // A hook to ensure that settings have been initialized.
153 | // Any public fxpay method that a user may call should call
154 | // this at the top. It can be called repeatedly without harm.
155 | //
156 | // When a newSettings object is defined, all settings will be
157 | // reconfigured with those values.
158 | //
159 | if (typeof newSettings === 'object' && newSettings) {
160 | exports.configure(newSettings);
161 | } else if (!exports.alreadyConfigured) {
162 | exports.configure();
163 | }
164 | };
165 |
166 | });
167 |
--------------------------------------------------------------------------------
/lib/fxpay/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Utils module.
3 | * @module utils
4 | */
5 |
6 | define([
7 | 'exports',
8 | 'errors',
9 | 'settings'
10 | ], function(exports, errors, settings) {
11 |
12 | 'use strict';
13 |
14 | /**
15 | * Populates an object with defaults if the key is not yet defined.
16 | * Similar to _.defaults except this takes only a single defaults object.
17 | * @param {object} object - the object to populate defaults on
18 | * @param {object} defaults - the defaults to use
19 | * @returns {object}
20 | */
21 | exports.defaults = function(object, defaults) {
22 | object = object || {};
23 | Object.keys(defaults).forEach(function(key) {
24 | if (typeof object[key] === 'undefined') {
25 | object[key] = defaults[key];
26 | }
27 | });
28 | return object;
29 | };
30 |
31 | /**
32 | * Gets the app origin
33 | * @returns {string}
34 | */
35 | exports.getSelfOrigin = function(settingsObj) {
36 | settingsObj = settingsObj || settings;
37 |
38 | if (settingsObj.appSelf) {
39 | // Check the manifest-declared origin (not the auto-generated one).
40 | var hasDeclaredOrigin = !!settingsObj.appSelf.manifest.origin;
41 |
42 | if (settingsObj.appSelf.manifest.type === 'web' || !hasDeclaredOrigin) {
43 | // This package does not have a reliable origin so
44 | // here we look for its marketplace hosted package URL
45 | // to derive a marketplace-specific origin.
46 | var pat = new RegExp(
47 | '^https?://(?:marketplace|mp\\.dev).*/app/([^/]+)/manifest\\.webapp$'
48 | );
49 | var match = pat.exec(settingsObj.appSelf.manifestURL);
50 | if (!match) {
51 | throw new errors.InvalidAppOrigin(
52 | 'Cannot derive marketplace GUID from "' +
53 | settingsObj.appSelf.manifestURL +
54 | '". The package must be installed from the Firefox Marketplace ' +
55 | 'or define an origin. For local testing, use fake products.'
56 | );
57 | }
58 | // Create an origin out of the marketplace GUID.
59 | var marketplaceOrigin = 'marketplace:' + match[1];
60 | settingsObj.log.info('derived marketplace origin as', marketplaceOrigin,
61 | 'from URL', settingsObj.appSelf.manifestURL);
62 | return marketplaceOrigin;
63 | }
64 |
65 | // Trust the declared origin from a privileged/certified app.
66 | return settingsObj.appSelf.origin;
67 |
68 | } else {
69 | // Get the origin from a non-app website.
70 | var win = settingsObj.window;
71 | if (win.location.origin) {
72 | return win.location.origin;
73 | } else {
74 | return win.location.protocol + '//' + win.location.hostname;
75 | }
76 | }
77 | };
78 |
79 | /**
80 | * Gets the the origin of the URL provided.
81 | * @param {string} url - the URL to introspect the origin from
82 | * @returns {string}
83 | */
84 | exports.getUrlOrigin = function(url) {
85 | var a = document.createElement('a');
86 | a.href = url;
87 | return a.origin || (a.protocol + '//' + a.host);
88 | };
89 |
90 | /**
91 | * Gets the center coordinates for a passed width and height.
92 | * Uses centering calcs that work on multiple monitors (bug 1122683).
93 | * @param {number} w - width
94 | * @param {number} h - height
95 | * @returns {list}
96 | */
97 | exports.getCenteredCoordinates = function(w, h) {
98 | var x = window.screenX +
99 | Math.max(0, Math.floor((window.innerWidth - w) / 2));
100 | var y = window.screenY +
101 | Math.max(0, Math.floor((window.innerHeight - h) / 2));
102 | return [x, y];
103 | };
104 |
105 | /**
106 | * Re-center an existing window.
107 | * @param {object} winRef - A reference to an existing window
108 | * @param {number} [w] - width
109 | * @param {number} [h] - height
110 | * @returns {undefined}
111 | */
112 | exports.reCenterWindow = function(winRef, w, h) {
113 | w = w || settings.winWidth;
114 | h = h || settings.winHeight;
115 | var xy = exports.getCenteredCoordinates(w, h);
116 | try {
117 | // Allow for the chrome as resizeTo args are the external
118 | // window dimensions not the internal ones.
119 | w = w + (winRef.outerWidth - winRef.innerWidth);
120 | h = h + (winRef.outerHeight - winRef.innerHeight);
121 | settings.log.log('width: ', w, 'height:', h);
122 | winRef.resizeTo(w, h);
123 | winRef.moveTo(xy[0], xy[1]);
124 | } catch(e) {
125 | settings.log.log("We don't have permission to resize this window");
126 | }
127 | };
128 |
129 | /**
130 | * Open a window
131 | * @param {object} [options] - the settings object
132 | * @param {string} [options.url] - the window url
133 | * @param {string} [options.title] - the window title
134 | * @param {number} [options.w] - the window width
135 | * @param {number} [options.h] - the window height
136 | * @returns {object} windowRef - a window reference.
137 | */
138 | exports.openWindow = function(options) {
139 | var defaults = {
140 | url: '',
141 | title: 'FxPay',
142 | w: settings.winWidth,
143 | h: settings.winHeight,
144 | };
145 |
146 | options = exports.defaults(options, defaults);
147 | var xy = exports.getCenteredCoordinates(options.w, options.h);
148 |
149 | var winOptString = 'toolbar=no,location=yes,directories=no,' +
150 | 'menubar=no,scrollbars=yes,resizable=no,copyhistory=no,' +
151 | 'width=' + options.w + ',height=' + options.h +
152 | ',top=' + xy[1] + ',left=' + xy[0];
153 |
154 | var windowRef = settings.window.open(options.url, options.title,
155 | winOptString);
156 | if (!windowRef) {
157 | settings.log.error('window.open() failed. URL:', options.url);
158 | }
159 | return windowRef;
160 | };
161 |
162 | /**
163 | * Get the App object returned from [`mozApps.getSelf()`](http://goo.gl/x4BDqs)
164 | * @param {module:utils~getAppSelfCallback} callback - the callback function.
165 | * @returns {undefined}
166 | */
167 | exports.getAppSelf = function getAppSelf(callback) {
168 | function storeAppSelf(appSelf) {
169 | if (appSelf === null) {
170 | throw new Error('cannot store a null appSelf');
171 | }
172 | settings.appSelf = appSelf;
173 | return appSelf;
174 | }
175 |
176 | if (settings.appSelf !== null) {
177 | // This means getAppSelf() has already run successfully so let's
178 | // return the value immediately.
179 | return callback(null, settings.appSelf);
180 | }
181 |
182 | if (!settings.mozApps) {
183 | settings.log.info(
184 | 'web platform does not define mozApps, cannot get appSelf');
185 | return callback(null, storeAppSelf(false));
186 | }
187 | var appRequest = settings.mozApps.getSelf();
188 |
189 | appRequest.onsuccess = function() {
190 | var appSelf = this.result;
191 | // In the case where we're in a Firefox that supports mozApps but
192 | // we're not running as an app, this could be falsey.
193 | settings.log.info('got appSelf from mozApps.getSelf():', appSelf);
194 | callback(null, storeAppSelf(appSelf || false));
195 | };
196 |
197 | appRequest.onerror = function() {
198 | var errCode = this.error.name;
199 | settings.log.error('mozApps.getSelf() returned an error', errCode);
200 | // We're not caching an appSelf result here.
201 | // This allows nested functions to report errors better.
202 | callback(errors.InvalidApp('invalid application: ' + errCode,
203 | {code: errCode}), settings.appSelf);
204 | };
205 | };
206 |
207 | /**
208 | * The callback called by {@link module:utils.getAppSelf }
209 | * @callback module:utils~getAppSelfCallback
210 | * @param {object} error - an error object. Will be null if no error.
211 | * @param {object} appSelf - the [appSelf object](http://goo.gl/HilsmA)
212 | */
213 |
214 |
215 | /**
216 | * Log a deprecation message with some extra info.
217 | * @param {string} msg - log message
218 | * @param {string} versionDeprecated - the version when deprecated
219 | * @returns {undefined}
220 | */
221 | exports.logDeprecation = function(msg, versionDeprecated) {
222 | settings.log.warn(
223 | msg + '. This was deprecated in ' + versionDeprecated + '. ' +
224 | 'More info: https://github.com/mozilla/fxpay/releases/tag/' +
225 | versionDeprecated);
226 | };
227 |
228 | /**
229 | * Take an object of key value pairs and serialize it into a url-encoded
230 | * query string.
231 | * @example
232 | * // returns foo=bar&baz=zup
233 | * utils.serialize({"foo": "bar", "baz": "zup"});
234 | * @param {object} obj - object to serialize
235 | * @returns {string}
236 | */
237 | exports.serialize = function serialize(obj) {
238 | var str = [];
239 | for (var p in obj){
240 | if (obj.hasOwnProperty(p)) {
241 | str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
242 | }
243 | }
244 | return str.join("&");
245 | };
246 |
247 | });
248 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fxpay",
3 | "description": "Adds Firefox Marketplace payments to a web application",
4 | "version": "0.0.18",
5 | "homepage": "https://github.com/mozilla/fxpay",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/mozilla/fxpay.git"
9 | },
10 | "dependencies": {},
11 | "devDependencies": {
12 | "almond": "^0.3.1",
13 | "async": "0.9.0",
14 | "chai": "1.9.1",
15 | "grunt": "^0.4.5",
16 | "grunt-banner": "^0.3.1",
17 | "grunt-bower-task": "0.4.0",
18 | "grunt-bump": "0.0.15",
19 | "grunt-cli": "0.1.13",
20 | "grunt-contrib-clean": "^0.6.0",
21 | "grunt-contrib-copy": "^0.8.0",
22 | "grunt-contrib-jshint": "0.6.5",
23 | "grunt-contrib-requirejs": "^0.4.4",
24 | "grunt-contrib-uglify": "0.6.0",
25 | "grunt-gh-pages": "^0.10.0",
26 | "grunt-jsdoc": "^0.6.3",
27 | "grunt-karma": "0.10.1",
28 | "grunt-zip": "^0.16.2",
29 | "jsdoc-simple-template": "0.0.1",
30 | "karma": "0.12.31",
31 | "karma-chai": "0.1.0",
32 | "karma-chrome-launcher": "0.1.4",
33 | "karma-coffee-preprocessor": "0.2.1",
34 | "karma-firefox-launcher": "0.1.3",
35 | "karma-html2js-preprocessor": "0.1.0",
36 | "karma-mocha": "0.1.10",
37 | "karma-mocha-reporter": "1.0.2",
38 | "karma-requirejs": "^0.2.2",
39 | "karma-script-launcher": "0.1.0",
40 | "karma-sinon": "1.0.4",
41 | "mocha": "2.2.1",
42 | "sinon": "1.14.1"
43 | },
44 | "scripts": {
45 | "test": "node -e \"require('grunt').cli()\" null bower test"
46 | },
47 | "author": "Kumar McMillan",
48 | "contributors": [],
49 | "license": "MPL-2.0"
50 | }
51 |
--------------------------------------------------------------------------------
/stackato.yml:
--------------------------------------------------------------------------------
1 | name: fxpay-hosted
2 | instances: 1
3 | framework:
4 | type: node
5 | runtime: node010
6 | document-root: example/hosted
7 | hooks:
8 | pre-staging:
9 | - cd example/hosted && npm install
10 | ignores: [
11 | '.git',
12 | 'node_modules',
13 | 'package.json',
14 | 'example/hosted/node_modules'
15 | ]
16 | mem: 128
17 |
--------------------------------------------------------------------------------
/tests/helper.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'exports',
3 | 'settings',
4 | 'utils'
5 | ], function(exports, settings, utils) {
6 |
7 | exports.server = null;
8 | exports.someAppOrigin = 'app://my-app';
9 | // Product info as returned from a GET request to the API.
10 | exports.apiProduct = {guid: 'server-guid', name: "Name from API",
11 | logo_url: "img.jpg"};
12 |
13 | exports.setUp = function setUp() {
14 | exports.server = sinon.fakeServer.create();
15 | settings.configure({
16 | apiUrlBase: 'http://tests-should-never-hit-this.com',
17 | mozApps: exports.mozAppsStub,
18 | productReceiptMap: null,
19 | }, {
20 | reset: true
21 | });
22 | window.localStorage.clear();
23 | exports.appSelf.init();
24 | };
25 |
26 |
27 | exports.tearDown = function tearDown() {
28 | exports.server.restore();
29 | };
30 |
31 |
32 | exports.makeReceipt = function makeReceipt(data, opt) {
33 | // Generate a pseudo web application receipt.
34 | // https://wiki.mozilla.org/Apps/WebApplicationReceipt
35 | data = data || {};
36 | opt = opt || {};
37 |
38 | // Fill in some defaults:
39 | data.typ = data.typ || 'purchase-receipt';
40 | data.iss = data.iss || 'https://payments-alt.allizom.org';
41 | data.verify = (data.verify ||
42 | 'https://fake-receipt-check-server.net/verify/');
43 | data.product = data.product || {
44 | url: opt.productUrl || exports.someAppOrigin,
45 | storedata: opt.storedata || 'contrib=297&id=500419&inapp_id=1'
46 | };
47 | data.user = data.user || {
48 | type: 'directed-identifier',
49 | value: 'anonymous-user'
50 | };
51 |
52 | // Make up some fake timestamps.
53 | data.iat = data.iat || 1402935236;
54 | data.exp = data.exp || 1418660036;
55 | data.nbf = data.nbf || 1402935236;
56 |
57 | // Make a URL safe base 64 encoded receipt:
58 | var receipt = (btoa(JSON.stringify(data)).replace(/\+/g, '-')
59 | .replace(/\//g, '_').replace(/\=+$/, ''));
60 |
61 | // jwtKey and jwtSig are stubbed out here because they
62 | // are not used by the library.
63 | return 'jwtKey.' + (opt.receipt || receipt) + '.jwtSig';
64 | };
65 |
66 |
67 | exports.resolvePurchase = function(opt) {
68 | opt = utils.defaults(opt, {
69 | receipt: '~',
70 | mozPay: null,
71 | productData: null,
72 | payCompleter: null,
73 | fetchProductsPattern: new RegExp('.*/payments/.*/in-app/.*'),
74 | enableTransPolling: false,
75 | mozPayResolver: function(domRequest) {
76 | domRequest.onsuccess();
77 | },
78 | addReceiptResolver: function(domRequest) {
79 | domRequest.onsuccess();
80 | },
81 | });
82 |
83 | if (!opt.transData) {
84 | opt.transData = exports.transactionData({receipt: opt.receipt});
85 | }
86 |
87 | // Respond to fetching the JWT.
88 | exports.server.respondWith(
89 | 'POST',
90 | settings.apiUrlBase + settings.apiVersionPrefix
91 | + '/webpay/inapp/prepare/',
92 | exports.productData(opt.productData));
93 |
94 | exports.server.respond();
95 |
96 | if (opt.mozPay) {
97 | console.log('Simulate a payment completion with mozPay');
98 | // Resolve DOMRequest for navigator.mozPay().
99 | opt.mozPayResolver(opt.mozPay.returnValues[0]);
100 | } else if (opt.payCompleter) {
101 | console.log('Simulate a payment completion with custom function');
102 | opt.payCompleter();
103 | }
104 |
105 | // Respond to checking the transaction state.
106 | exports.server.autoRespond = !!opt.enableTransPolling;
107 | exports.server.respondWith('GET', settings.apiUrlBase + '/transaction/XYZ',
108 | opt.transData);
109 | exports.server.respond();
110 |
111 | // Resolve DOMRequest for mozApps.getSelf().addReceipt().
112 | opt.addReceiptResolver(exports.receiptAdd);
113 |
114 | // Respond to fetching the product object after successful transaction.
115 | exports.server.respondWith('GET', opt.fetchProductsPattern,
116 | [200, {"Content-Type": "application/json"},
117 | JSON.stringify(exports.apiProduct)]);
118 | exports.server.respond();
119 | };
120 |
121 |
122 | exports.productData = function(overrides, status) {
123 | // Create a JSON exports.server response to a request for product data.
124 | overrides = overrides || {};
125 | var data = {
126 | webpayJWT: '',
127 | contribStatusURL: '/transaction/XYZ',
128 | };
129 | for (var k in data) {
130 | if (overrides[k]) {
131 | data[k] = overrides[k];
132 | }
133 | }
134 | return [status || 200, {"Content-Type": "application/json"},
135 | JSON.stringify(data)];
136 | };
137 |
138 |
139 | exports.transactionData = function(overrides, status) {
140 | // Create a JSON exports.server response to a request for transaction data.
141 | overrides = overrides || {};
142 | var data = {
143 | status: 'complete',
144 | // Pretend this is a real Marketplace receipt.
145 | receipt: '~'
146 | };
147 | for (var k in data) {
148 | if (overrides[k]) {
149 | data[k] = overrides[k];
150 | }
151 | }
152 | return [status || 200, {"Content-Type": "application/json"},
153 | JSON.stringify(data)];
154 | };
155 |
156 |
157 | exports.receiptAdd = {
158 | error: null,
159 | _receipt: null,
160 | onsuccess: function() {},
161 | onerror: function() {},
162 | reset: function() {
163 | this._receipt = null;
164 | this.error = null;
165 | this.onsuccess = function() {};
166 | this.onerror = function() {};
167 | }
168 | };
169 |
170 |
171 | exports.appSelf = {
172 | init: function() {
173 | this.error = null;
174 | this.origin = exports.someAppOrigin;
175 | this.manifest = {
176 | installs_allowed_from: ['*'],
177 | origin: this.origin,
178 | type: 'privileged',
179 | permissions: {
180 | systemXHR: {description: "Required to access payment API"}
181 | },
182 | };
183 | this.receipts = [];
184 | // This is the result of getSelf(). Setting it to this makes
185 | // stubbing easier.
186 | this.result = this;
187 |
188 | this.addReceipt = function(receipt) {
189 | exports.receiptAdd._receipt = receipt;
190 | return exports.receiptAdd;
191 | };
192 | this.onsuccess = function() {};
193 | this.onerror = function() {};
194 | },
195 | };
196 |
197 |
198 | // https://developer.mozilla.org/en-US/docs/Web/API/Apps.getSelf
199 | exports.mozAppsStub = {
200 | getSelf: function() {
201 | return exports.appSelf;
202 | }
203 | };
204 |
205 |
206 | exports.ReceiptValidator = function ReceiptValidator(opt) {
207 | opt = utils.defaults(opt, {
208 | response: {status: 'ok'},
209 | onValidationResponse: undefined,
210 | onRequest: function() {},
211 | verifyUrl: 'https://fake-receipt-check-server.net/verify/',
212 | });
213 |
214 | if (!opt.onValidationResponse) {
215 | opt.onValidationResponse = function(request) {
216 | opt.onRequest(request.requestBody);
217 | request.respond(200, {"Content-Type": "application/json"},
218 | JSON.stringify(opt.response));
219 | };
220 | }
221 |
222 | exports.server.respondWith('POST', opt.verifyUrl, opt.onValidationResponse);
223 | };
224 |
225 | exports.ReceiptValidator.prototype.finish = function() {
226 | // Send the receipt validation response:
227 | exports.server.respond();
228 | };
229 |
230 | exports.mozPayStub = function mozPayStub() {
231 | // https://developer.mozilla.org/en-US/docs/Web/API/Navigator.mozPay
232 | return {
233 | onsuccess: function() {},
234 | onerror: function() {},
235 | };
236 | };
237 |
238 | });
239 |
--------------------------------------------------------------------------------
/tests/test-api.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'api',
3 | 'fxpay',
4 | 'helper',
5 | 'errors',
6 | 'settings'
7 | ], function(apiModule, fxpay, helper, errors, settings) {
8 |
9 | describe('fxpay.API()', function () {
10 | var api;
11 | var baseUrl = 'https://not-a-real-api';
12 | var versionPrefix = '/api/v1';
13 |
14 | beforeEach(function() {
15 | helper.setUp();
16 | fxpay.configure({apiVersionPrefix: versionPrefix});
17 | api = new apiModule.API(baseUrl);
18 | });
19 |
20 | afterEach(function() {
21 | helper.tearDown();
22 | });
23 |
24 | it('should handle POSTs', function (done) {
25 | helper.server.respondWith(
26 | 'POST', /.*\/post/,
27 | function(request) {
28 | assert.equal(request.requestHeaders.Accept, 'application/json');
29 | assert.equal(request.requestHeaders['Content-Type'],
30 | 'application/x-www-form-urlencoded;charset=utf-8');
31 | assert.equal(request.requestBody, 'foo=bar&baz=zop');
32 | request.respond(200, {"Content-Type": "application/json"},
33 | '{"data": "received"}');
34 | });
35 |
36 | api.post('/post', {foo: 'bar', 'baz': 'zop'}, function(err, data) {
37 | assert.equal(data.data, 'received');
38 | done(err);
39 | });
40 |
41 | helper.server.respond();
42 | });
43 |
44 | it('should handle GETs', function (done) {
45 | helper.server.respondWith(
46 | 'GET', /.*\/get/,
47 | function(request) {
48 | assert.equal(request.requestHeaders.Accept, 'application/json');
49 | assert.equal(request.requestHeaders['Content-Type'], undefined);
50 | request.respond(200, {"Content-Type": "application/json"},
51 | '{"data": "received"}');
52 | });
53 |
54 | api.get('/get', function(err, data) {
55 | assert.equal(data.data, 'received');
56 | done(err);
57 | });
58 |
59 | helper.server.respond();
60 | });
61 |
62 | it('should handle PUTs', function (done) {
63 | helper.server.respondWith(
64 | 'PUT', /.*\/put/,
65 | function(request) {
66 | assert.equal(request.requestHeaders.Accept, 'application/json');
67 | assert.equal(request.requestHeaders['Content-Type'],
68 | 'application/x-www-form-urlencoded;charset=utf-8');
69 | request.respond(200, {"Content-Type": "application/json"},
70 | '{"data": "received"}');
71 | });
72 |
73 | api.put('/put', {foo: 'bar'}, function(err, data) {
74 | assert.equal(data.data, 'received');
75 | done(err);
76 | });
77 |
78 | helper.server.respond();
79 | });
80 |
81 | it('should handle DELETEs', function (done) {
82 | helper.server.respondWith(
83 | 'DELETE', /.*\/delete/,
84 | [200, {"Content-Type": "application/json"},
85 | '{"data": "received"}']);
86 |
87 | api.del('/delete', function(err, data) {
88 | assert.equal(data.data, 'received');
89 | done(err);
90 | });
91 |
92 | helper.server.respond();
93 | });
94 |
95 | it('should send the library version with each request', function (done) {
96 | helper.server.respondWith(
97 | 'GET', /.*/,
98 | function(request) {
99 | assert.ok(settings.libVersion); // make sure it's defined.
100 | assert.equal(request.requestHeaders['x-fxpay-version'],
101 | settings.libVersion);
102 | request.respond(200, {"Content-Type": "application/json"}, '{}');
103 | });
104 |
105 | api.get('/get', function(err) {
106 | done(err);
107 | });
108 |
109 | helper.server.respond();
110 | });
111 |
112 | it('should allow custom content-type POSTs', function (done) {
113 | helper.server.respondWith(
114 | 'POST', /.*\/post/,
115 | function(request) {
116 | assert.equal(request.requestHeaders['Content-Type'],
117 | 'text/plain;charset=utf-8');
118 | assert.equal(request.requestBody, 'custom-data');
119 | request.respond(200, {"Content-Type": "application/json"},
120 | '{"data": "received"}');
121 | });
122 |
123 | api.post('/post', 'custom-data', function(err) {
124 | done(err);
125 | }, {contentType: 'text/plain'});
126 |
127 | helper.server.respond();
128 | });
129 |
130 | it('should send custom headers', function (done) {
131 | helper.server.respondWith(
132 | 'GET', /.*\/get/,
133 | function(request) {
134 | assert.equal(request.requestHeaders.Foobar, 'bazba');
135 | assert.equal(request.requestHeaders.Zoopa, 'wonza');
136 | request.respond(200, {"Content-Type": "application/json"},
137 | '{"data": "received"}');
138 | });
139 |
140 | api.get('/get', function(err) {
141 | done(err);
142 | }, {headers: {Foobar: 'bazba', Zoopa: 'wonza'}});
143 |
144 | helper.server.respond();
145 | });
146 |
147 | it('should report XHR abort', function (done) {
148 | helper.server.respondWith(function(xhr) {
149 | // We use a custom event because xhr.abort() triggers load first
150 | // https://github.com/cjohansen/Sinon.JS/issues/432
151 | dispatchXhrEvent(xhr, 'abort');
152 | });
153 |
154 | api.post('/some/path', null, function(err) {
155 | assert.instanceOf(err, errors.ApiRequestAborted);
156 | done();
157 | });
158 |
159 | helper.server.respond();
160 | });
161 |
162 | it('should report XHR errors', function (done) {
163 | helper.server.respondWith(function(xhr) {
164 | dispatchXhrEvent(xhr, 'error');
165 | });
166 |
167 | api.post('/some/path', null, function(err) {
168 | assert.instanceOf(err, errors.ApiRequestError);
169 | done();
170 | });
171 |
172 | helper.server.respond();
173 | });
174 |
175 | it('should report non-200 responses', function (done) {
176 | helper.server.respondWith(
177 | 'POST', /.*\/some\/path/,
178 | [500, {"Content-Type": "application/json"},
179 | JSON.stringify({webpayJWT: '',
180 | contribStatusURL: '/somewhere'})]);
181 |
182 | api.post('/some/path', null, function(err) {
183 | assert.instanceOf(err, errors.BadApiResponse);
184 | done();
185 | });
186 |
187 | helper.server.respond();
188 | });
189 |
190 | it('should report unparsable JSON', function (done) {
191 | helper.server.respondWith(
192 | 'POST', /.*\/some\/path/,
193 | /* jshint -W044 */
194 | [200, {"Content-Type": "application/json"},
195 | "{this\is not; valid JSON'"]);
196 | /* jshint +W044 */
197 |
198 | api.post('/some/path', null, function(err) {
199 | assert.instanceOf(err, errors.BadJsonResponse);
200 | done();
201 | });
202 |
203 | helper.server.respond();
204 | });
205 |
206 | it('should parse and return JSON', function (done) {
207 | helper.server.respondWith(
208 | 'POST', /.*\/some\/path/,
209 | [200, {"Content-Type": "application/json"},
210 | '{"is_json": true}']);
211 |
212 | api.post('/some/path', null, function(err, data) {
213 | assert.equal(data.is_json, true);
214 | done(err);
215 | });
216 |
217 | helper.server.respond();
218 | });
219 |
220 | it('should request a full URL based on a path', function (done) {
221 | helper.server.respondWith(
222 | 'POST', new RegExp(baseUrl + versionPrefix + '/path/check'),
223 | [200, {"Content-Type": "application/json"},
224 | '{"foo":"bar"}']);
225 |
226 | api.post('/path/check', null, function(err) {
227 | // If this is not a 404 then we're good.
228 | done(err);
229 | });
230 |
231 | helper.server.respond();
232 | });
233 |
234 | it('should request an absolute https URL when specified', function (done) {
235 | var absUrl = 'https://secure-site.com/some/page';
236 |
237 | helper.server.respondWith('POST', absUrl,
238 | [200, {"Content-Type": "application/json"},
239 | '{"foo":"bar"}']);
240 |
241 | api.post(absUrl, null, function(err) {
242 | // If this is not a 404 then we're good.
243 | done(err);
244 | });
245 |
246 | helper.server.respond();
247 | });
248 |
249 | it('should request an absolute http URL when specified', function (done) {
250 | var absUrl = 'http://insecure-site.com/some/page';
251 |
252 | helper.server.respondWith('POST', absUrl,
253 | [200, {"Content-Type": "application/json"},
254 | '{"foo":"bar"}']);
255 |
256 | api.post(absUrl, null, function(err) {
257 | // If this is not a 404 then we're good.
258 | done(err);
259 | });
260 |
261 | helper.server.respond();
262 | });
263 |
264 | it('should timeout', function (done) {
265 | helper.server.respondWith(function(xhr) {
266 | // We simulate a timeout event here because Sinon
267 | // doesn't seem to support the XHR.timeout property.
268 | // https://github.com/cjohansen/Sinon.JS/issues/431
269 | dispatchXhrEvent(xhr, 'timeout');
270 | });
271 |
272 | api.post('/timeout', null, function(err) {
273 | assert.instanceOf(err, errors.ApiRequestTimeout);
274 | done();
275 | });
276 |
277 | helper.server.respond();
278 | });
279 |
280 | it('should allow you to get unversioned URLs', function (done) {
281 | assert.equal(api.url('/not/versioned', {versioned: false}),
282 | baseUrl + '/not/versioned');
283 | done();
284 | });
285 |
286 | it('should allow you to get versioned URLs', function (done) {
287 | assert.equal(api.url('/this/is/versioned'),
288 | baseUrl + versionPrefix + '/this/is/versioned');
289 | done();
290 | });
291 |
292 |
293 | function dispatchXhrEvent(xhr, eventName, bubbles, cancelable) {
294 | xhr.dispatchEvent(new sinon.Event(eventName, bubbles, cancelable, xhr));
295 | // Prevent future listening, like, in future tests.
296 | // Maybe this is fixed now?
297 | // See https://github.com/cjohansen/Sinon.JS/issues/430
298 | xhr.eventListeners = {};
299 | }
300 |
301 | });
302 |
303 | });
304 |
--------------------------------------------------------------------------------
/tests/test-errors.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'errors'
3 | ], function(errors) {
4 |
5 | describe('fxpay.errors.createError()', function() {
6 |
7 | var FxPayError = errors.FxPayError;
8 | var createError = errors.createError;
9 |
10 | var CustomError = createError('MyError');
11 | var customError = CustomError();
12 |
13 | it('should create instances of Error', function() {
14 | assert.instanceOf(customError, Error);
15 | });
16 |
17 | it('should create instances of FxPayError', function() {
18 | assert.instanceOf(customError, FxPayError);
19 | });
20 |
21 | it('should set a custom name', function() {
22 | assert.equal(customError.name, 'MyError');
23 | });
24 |
25 | it('should set an empty productInfo object', function() {
26 | assert.typeOf(customError.productInfo, 'object');
27 | });
28 |
29 | it('should accept a productInfo object', function() {
30 | var info = {someProperty: 'foo'};
31 | assert.equal(CustomError('message', {productInfo: info}).productInfo,
32 | info);
33 | });
34 |
35 | it('should stringify the class name', function() {
36 | assert.equal(CustomError().toString(), 'MyError');
37 | });
38 |
39 | it('should stringify with message', function() {
40 | assert.equal(CustomError('message').toString(),
41 | 'MyError: message');
42 | });
43 |
44 | it('should stringify with message and code', function() {
45 | assert.equal(CustomError('message', {code: 'A_CODE'}).toString(),
46 | 'MyError: message {code: A_CODE}');
47 | });
48 |
49 | it('should set a message', function() {
50 | assert.equal(CustomError('some message').message,
51 | 'some message');
52 | });
53 |
54 | it('should set a custom code', function() {
55 | assert.equal(CustomError('message', {code: 'CUSTOM_CODE'}).code,
56 | 'CUSTOM_CODE');
57 | });
58 |
59 | it('should set a default code', function() {
60 | var ErrorWithCode = createError('ErrorWithCode', {code: 'MY_CODE'});
61 | assert.equal(ErrorWithCode().code, 'MY_CODE');
62 | });
63 |
64 | it('should define a stack', function() {
65 | assert.ok(customError.stack);
66 | });
67 |
68 | it('should allow custom parents', function() {
69 | var SubError = createError('NewError', {inherits: CustomError});
70 | var subError = SubError();
71 | assert.instanceOf(subError, CustomError);
72 | assert.instanceOf(subError, FxPayError);
73 | assert.instanceOf(subError, Error);
74 | });
75 |
76 | it('should create PaymentFailed errors', function() {
77 | assert.instanceOf(errors.AppReceiptMissing(), errors.PaymentFailed);
78 | assert.instanceOf(errors.InvalidReceipt(), errors.PaymentFailed);
79 | assert.instanceOf(errors.PurchaseTimeout(), errors.PaymentFailed);
80 | assert.instanceOf(errors.TestReceiptNotAllowed(), errors.PaymentFailed);
81 | });
82 |
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/tests/test-fxpay.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'errors',
3 | 'fxpay',
4 | 'helper',
5 | 'receipts',
6 | 'settings',
7 | ], function(errorsModule, fxpay, helper, receipts, settingsModule) {
8 |
9 | describe('fxpay', function() {
10 |
11 | beforeEach(function() {
12 | helper.setUp();
13 | });
14 |
15 | afterEach(function() {
16 | helper.tearDown();
17 | });
18 |
19 | it('should expose fxpay.errors', function() {
20 | assert.equal(fxpay.errors.FxPayError, errorsModule.FxPayError);
21 | });
22 |
23 | it('should expose fxpay.settings', function() {
24 | assert.equal(fxpay.settings.fakeProducts, settingsModule.fakeProducts);
25 | });
26 |
27 | it('should expose fxpay.receipts', function() {
28 | assert.equal(fxpay.receipts.validateInAppProductReceipt,
29 | receipts.validateInAppProductReceipt);
30 | });
31 |
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/tests/test-get-products.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'errors',
3 | 'fxpay',
4 | 'helper',
5 | 'settings',
6 | 'utils'
7 | ], function(errors, fxpay, helper, settings, utils) {
8 |
9 | describe('fxpay.getProducts()', function() {
10 |
11 | beforeEach(function() {
12 | helper.setUp();
13 | fxpay.configure({
14 | appSelf: helper.appSelf,
15 | });
16 | });
17 |
18 | afterEach(function() {
19 | helper.tearDown();
20 | });
21 |
22 | it('resolves promise with product info', function(done) {
23 |
24 | var serverObjects = setUpApiResponses();
25 |
26 | fxpay.getProducts()
27 | .then(function(products) {
28 | assert.equal(products[0].name, serverObjects[0].name);
29 | assert.equal(products[0].productId, serverObjects[0].guid);
30 | assert.equal(products[0].smallImageUrl, serverObjects[0].logo_url);
31 | assert.equal(products[0].pricePointId, serverObjects[0].price_id);
32 | assert.equal(products[1].name, serverObjects[1].name);
33 | assert.equal(products[1].productId, serverObjects[1].guid);
34 | assert.equal(products[1].smallImageUrl, serverObjects[1].logo_url);
35 | assert.equal(products[1].pricePointId, serverObjects[1].price_id);
36 | assert.equal(products.length, 2);
37 | done();
38 | }).catch(done);
39 |
40 | });
41 |
42 | it('still supports old callback interface', function(done) {
43 |
44 | var serverObjects = setUpApiResponses();
45 |
46 | fxpay.getProducts(function(error, products) {
47 | assert.equal(products[0].name, serverObjects[0].name);
48 | done(error);
49 | });
50 |
51 | });
52 |
53 | it('can retrieve fake products', function(done) {
54 |
55 | fxpay.configure({fakeProducts: true});
56 |
57 | var serverObjects = setUpApiResponses({
58 | serverObjects: [
59 | {"guid": "guid1", "app": "fxpay", "price_id": 1,
60 | "name": "Clown Shoes", "logo_url": "http://site/image1.png"},
61 | {"guid": "guid2", "app": "fxpay", "price_id": 2,
62 | "name": "Belt and Suspenders", "logo_url": "http://site/image2.png"}
63 | ],
64 | url: settings.apiUrlBase + settings.apiVersionPrefix +
65 | '/payments/stub-in-app-products/',
66 | });
67 |
68 | fxpay.getProducts().then(function(products) {
69 | assert.equal(products[0].name, serverObjects[0].name);
70 | assert.equal(products[1].name, serverObjects[1].name);
71 | assert.equal(products.length, 2);
72 | done();
73 | }).catch(done);
74 |
75 | });
76 |
77 | it('calls back with API errors', function(done) {
78 |
79 | helper.server.respondImmediately = true;
80 | helper.server.respondWith('GET', /.*/, [404, {}, '']);
81 |
82 | fxpay.getProducts().then(function() {
83 | done(Error('unexpected success'));
84 | }).catch(function(err) {
85 | assert.instanceOf(err, errors.BadApiResponse);
86 | done();
87 | }).catch(done);
88 |
89 | });
90 |
91 | it('still supports callback interface for errors', function(done) {
92 |
93 | helper.server.respondImmediately = true;
94 | helper.server.respondWith('GET', /.*/, [404, {}, '']);
95 |
96 | fxpay.getProducts(function(error, products) {
97 | assert.instanceOf(error, errors.BadApiResponse);
98 | assert.equal(products.length, 0);
99 | done();
100 | });
101 |
102 | });
103 |
104 | it('requires a JSON response from the server', function(done) {
105 |
106 | setUpApiResponses({serverObjects: null});
107 |
108 | fxpay.getProducts().then(function() {
109 | done(Error('unexpected success'));
110 | }).catch(function(err) {
111 | assert.instanceOf(err, errors.BadApiResponse);
112 | done();
113 | }).catch(done);
114 | });
115 |
116 |
117 | function setUpApiResponses(opt) {
118 | opt = utils.defaults(opt, {
119 | serverObjects: [
120 | {"guid": "guid3", "app": "fxpay", "price_id": 237,
121 | "name": "Virtual Kiwi", "logo_url": "http://site/image1.png"},
122 | {"guid": "guid4", "app": "fxpay", "price_id": 238,
123 | "name": "Majestic Cheese", "logo_url": "http://site/image2.png"}
124 | ],
125 | url: settings.apiUrlBase + settings.apiVersionPrefix +
126 | '/payments/' + encodeURIComponent(helper.someAppOrigin) +
127 | '/in-app/?active=1',
128 | });
129 |
130 | // When working with promises, we need to define responses up front
131 | // and respond as each request comes in.
132 | helper.server.respondImmediately = true;
133 |
134 | helper.server.respondWith(
135 | 'GET', this.url,
136 | [200, {"Content-Type": "application/json"},
137 | JSON.stringify({
138 | meta: {
139 | next: null,
140 | previous: null,
141 | total_count: opt.serverObjects ? opt.serverObjects.length: 0,
142 | offset: 0,
143 | limit: 25,
144 | },
145 | objects: opt.serverObjects,
146 | })]);
147 |
148 | return opt.serverObjects;
149 | }
150 |
151 | });
152 | });
153 |
--------------------------------------------------------------------------------
/tests/test-jwt.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'errors',
3 | 'fxpay',
4 | 'jwt'
5 | ], function(errors, fxpay, jwt) {
6 | describe('jwt.decode()', function() {
7 |
8 | it('should decode JWTs', function(done) {
9 | var fakeJwt = {
10 | aud: 'payments-alt.allizom.org',
11 | request: {simulate: {result: 'postback'},
12 | pricePoint: '10'}
13 | };
14 | var encJwt = '.' + btoa(JSON.stringify(fakeJwt)) + '.';
15 |
16 | jwt.decode(encJwt, function(err, data) {
17 | // Do a quick sanity check that this was decoded.
18 |
19 | assert.deepPropertyVal(data, 'aud', fakeJwt.aud);
20 | assert.deepPropertyVal(data, 'request.simulate.result',
21 | fakeJwt.request.simulate.result);
22 | assert.deepPropertyVal(data, 'request.pricePoint',
23 | fakeJwt.request.pricePoint);
24 | done(err);
25 | });
26 | });
27 |
28 | it('should decode URL safe JWTs', function(done) {
29 | // This JWT has `-` and `_` chars which need to be converted.
30 | var encJwt =
31 | "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJhdWQiOiAibW9j" +
32 | "a3BheXByb3ZpZGVyLnBocGZvZ2FwcC5jb20iLCAiaXNzIjogIkVudGVyI" +
33 | "HlvdSBhcHAga2V5IGhlcmUhIiwgInJlcXVlc3QiOiB7Im5hbWUiOiAiUG" +
34 | "llY2Ugb2YgQ2FrZSIsICJwcmljZSI6ICIxMC41MCIsICJwcmljZVRpZXI" +
35 | "iOiAxLCAicHJvZHVjdGRhdGEiOiAidHJhbnNhY3Rpb25faWQ9ODYiLCAi" +
36 | "Y3VycmVuY3lDb2RlIjogIlVTRCIsICJkZXNjcmlwdGlvbiI6ICJWaXJ0d" +
37 | "WFsIGNob2NvbGF0ZSBjYWtlIHRvIGZpbGwgeW91ciB2aXJ0dWFsIHR1bW" +
38 | "15In0sICJleHAiOiAxMzUyMjMyNzkyLCAiaWF0IjogMTM1MjIyOTE5Miw" +
39 | "gInR5cCI6ICJtb2NrL3BheW1lbnRzL2luYXBwL3YxIn0.QZxc62USCy4U" +
40 | "IyKIC1TKelVhNklvk-Ou1l_daKntaFI";
41 |
42 | jwt.decode(encJwt, function(err, data) {
43 | assert.deepPropertyVal(data, 'request.name', 'Piece of Cake');
44 | assert.deepPropertyVal(
45 | data, 'request.description',
46 | 'Virtual chocolate cake to fill your virtual tummy');
47 | done(err);
48 | });
49 | });
50 |
51 | it('should error on missing segments', function(done) {
52 | jwt.decode('one.two', function(err) {
53 | assert.instanceOf(err, errors.InvalidJwt);
54 | done();
55 | });
56 | });
57 |
58 | it('should error on invalid binary data', function(done) {
59 | jwt.decode('.this{}IS not*base64 encoded.',
60 | function(err) {
61 | assert.instanceOf(err, errors.InvalidJwt);
62 | done();
63 | }
64 | );
65 | });
66 |
67 | it('should error on non JSON data within JWT', function(done) {
68 | var encJwt = '.' + btoa('(not / valid JSON}') + '.';
69 | jwt.decode(encJwt, function(err) {
70 | assert.instanceOf(err, errors.InvalidJwt);
71 | done();
72 | });
73 | });
74 |
75 | });
76 |
77 |
78 | describe('jwt.getPayUrl()', function() {
79 |
80 | it('should pass through parse errors', function(done) {
81 | var encJwt = '.' + btoa('(not / valid JSON}') + '.';
82 | jwt.getPayUrl(encJwt, function(err) {
83 | assert.instanceOf(err, errors.InvalidJwt);
84 | done();
85 | });
86 | });
87 |
88 | it('should error on unknown JWT types', function(done) {
89 | var payRequest = {typ: 'unknown-type'};
90 | var encJwt = '.' + btoa(JSON.stringify(payRequest)) + '.';
91 | jwt.getPayUrl(encJwt, function(err) {
92 | assert.instanceOf(err, errors.InvalidJwt);
93 | done();
94 | });
95 | });
96 |
97 | it('should error on invalid URL templates', function(done) {
98 | fxpay.configure({
99 | payProviderUrls: {
100 | someType: 'https://pay/start?req={nope}' // missing {req}
101 | }
102 | });
103 | var payRequest = {typ: 'someType'};
104 | var encJwt = '.' + btoa(JSON.stringify(payRequest)) + '.';
105 | jwt.getPayUrl(encJwt, function(err) {
106 | assert.instanceOf(err, errors.ConfigurationError);
107 | done();
108 | });
109 | });
110 |
111 | it('should return a JWT formatted URL', function(done) {
112 | fxpay.configure({
113 | payProviderUrls: {
114 | someType: 'https://pay/start?req={jwt}'
115 | }
116 | });
117 |
118 | var payRequest = {typ: 'someType'};
119 | var encJwt = '.' + btoa(JSON.stringify(payRequest)) + '.';
120 |
121 | jwt.getPayUrl(encJwt, function(err, payUrl) {
122 | assert.equal(payUrl, 'https://pay/start?req=' + encJwt);
123 | done(err);
124 | });
125 | });
126 |
127 | });
128 | });
129 |
--------------------------------------------------------------------------------
/tests/test-main.js:
--------------------------------------------------------------------------------
1 | var tests = [];
2 | for (var file in window.__karma__.files) {
3 | if (window.__karma__.files.hasOwnProperty(file)) {
4 | if (/test-.*?\.js$/.test(file) && !/test-main\.js/.test(file)) {
5 | tests.push(file);
6 | }
7 | }
8 | }
9 |
10 | requirejs.config({
11 | // Karma serves files from '/base'
12 | baseUrl: '/base/lib/fxpay/',
13 |
14 | paths: {
15 | 'helper': '../../tests/helper',
16 | 'promise': '/base/lib/bower_components/es6-promise/promise',
17 | },
18 |
19 | // ask Require.js to load these files (all our tests)
20 | deps: tests,
21 |
22 | // start test run, once Require.js is done
23 | callback: window.__karma__.start
24 | });
25 |
--------------------------------------------------------------------------------
/tests/test-product-receipt-validation.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'errors',
3 | 'fxpay',
4 | 'helper',
5 | 'settings',
6 | 'utils'
7 | ], function(errors, fxpay, helper, settings, utils) {
8 |
9 | describe('product.validateReceipt()', function() {
10 | var defaultProductUrl = 'http://boar4485.testmanifest.com';
11 | var receipt = makeReceipt();
12 |
13 | beforeEach(function() {
14 | helper.setUp();
15 | helper.appSelf.origin = defaultProductUrl;
16 | fxpay.configure({
17 | appSelf: helper.appSelf,
18 | receiptCheckSites: [
19 | 'https://fake-receipt-check-server.net',
20 | ]
21 | });
22 | });
23 |
24 | afterEach(function() {
25 | helper.tearDown();
26 | window.localStorage.clear();
27 | });
28 |
29 | it('succeeds for valid receipts', function(done) {
30 | helper.appSelf.receipts = [receipt];
31 |
32 | setUpReceiptCheck({
33 | onValidationRequest: function(requestBody) {
34 | assert.equal(requestBody, receipt);
35 | },
36 | });
37 |
38 | fxpay.getProduct(helper.apiProduct.guid).then(function(product) {
39 | assert.equal(product.productId, helper.apiProduct.guid);
40 | assert.equal(product.name, helper.apiProduct.name);
41 | assert.equal(product.smallImageUrl, helper.apiProduct.logo_url);
42 | assert.typeOf(product.receiptInfo, 'object');
43 | return product.validateReceipt();
44 | }).then(function(product) {
45 | assert.equal(product.receiptInfo.status, 'ok');
46 | assert.equal(product.receiptInfo.receipt, receipt);
47 | done();
48 | }).catch(done);
49 |
50 | });
51 |
52 | it('finds receipts in local storage', function(done) {
53 | window.localStorage.setItem(settings.localStorageKey,
54 | JSON.stringify([receipt]));
55 |
56 | setUpReceiptCheck();
57 |
58 | fxpay.getProduct(helper.apiProduct.guid).then(function(product) {
59 | return product.validateReceipt();
60 | }).then(function(product) {
61 | assert.equal(product.productId, helper.apiProduct.guid);
62 | done();
63 | }).catch(done);
64 |
65 | });
66 |
67 | it('fails on validation errors', function(done) {
68 | helper.appSelf.receipts = [receipt];
69 | var receiptResponse = {status: "invalid", reason: "ERROR_DECODING"};
70 |
71 | setUpReceiptCheck({
72 | validatorResponse: receiptResponse,
73 | });
74 |
75 | fxpay.getProduct(helper.apiProduct.guid).then(function(product) {
76 | return product.validateReceipt();
77 | }).catch(function(error) {
78 | assert.instanceOf(error, errors.InvalidReceipt);
79 | assert.equal(error.productInfo.productId, helper.apiProduct.guid);
80 | assert.equal(error.productInfo.receiptInfo.status,
81 | receiptResponse.status);
82 | assert.equal(error.productInfo.receiptInfo.reason,
83 | receiptResponse.reason);
84 | done();
85 | }).catch(done);
86 |
87 | });
88 |
89 | it('fails on validation service errors', function(done) {
90 | helper.appSelf.receipts = [makeReceipt()];
91 |
92 | setUpReceiptCheck({
93 | onValidationResponse: function(request) {
94 | request.respond(500, {}, 'Internal Error');
95 | },
96 | });
97 |
98 | fxpay.getProduct(helper.apiProduct.guid).then(function(product) {
99 | return product.validateReceipt();
100 | }).catch(function(error) {
101 | assert.instanceOf(error, errors.BadApiResponse);
102 | assert.typeOf(error.productInfo, 'object');
103 | assert.equal(error.productInfo.productId, helper.apiProduct.guid);
104 | done();
105 | }).catch(done);
106 |
107 | });
108 |
109 | it('fails when receipt is missing', function(done) {
110 | helper.appSelf.receipts = [];
111 | setUpReceiptCheck();
112 |
113 | fxpay.getProduct(helper.apiProduct.guid).then(function(product) {
114 | return product.validateReceipt();
115 | }).then(function() {
116 | done(Error('unexpected success'));
117 | }).catch(function(error) {
118 | assert.instanceOf(error, errors.InvalidReceipt);
119 | assert.include(error.message, 'could not find installed receipt');
120 | done();
121 | }).catch(done);
122 |
123 | });
124 |
125 | it('ignores malformed receipts', function(done) {
126 | helper.appSelf.receipts = [''];
127 |
128 | setUpReceiptCheck();
129 |
130 | fxpay.getProduct(helper.apiProduct.guid).then(function(product) {
131 | return product.validateReceipt();
132 | }).catch(function(error) {
133 | assert.instanceOf(error, errors.InvalidReceipt);
134 | assert.typeOf(error.productInfo, 'object');
135 | done();
136 | }).catch(done);
137 |
138 | });
139 |
140 | it('validates test receipts', function(done) {
141 |
142 | var testReceipt = makeReceipt({}, {
143 | typ: 'test-receipt',
144 | iss: 'https://payments-alt.allizom.org',
145 | verify: 'https://fake-receipt-check-server.net/' +
146 | 'developers/test-receipt/',
147 | });
148 | helper.appSelf.receipts = [testReceipt];
149 |
150 | fxpay.configure({fakeProducts: true});
151 |
152 | setUpReceiptCheck({
153 | fetchProductUrl: new RegExp(
154 | 'http(s)?://[^/]+/api/v1/payments/stub-in-app-products/' +
155 | helper.apiProduct.guid + '/'),
156 | verifyUrl: new RegExp(
157 | 'https://fake-receipt-check-server\\.net/developers/test-receipt/'),
158 | onValidationRequest: function(requestBody) {
159 | assert.equal(requestBody, testReceipt);
160 | },
161 | });
162 |
163 | fxpay.getProduct(helper.apiProduct.guid, {
164 | fetchStubs: true,
165 | }).then(function(product) {
166 | return product.validateReceipt();
167 | }).then(function(product) {
168 | assert.equal(product.productId, helper.apiProduct.guid);
169 | assert.equal(product.name, helper.apiProduct.name);
170 | assert.equal(product.smallImageUrl, helper.apiProduct.logo_url);
171 | done();
172 | }).catch(done);
173 |
174 | });
175 |
176 | it('can still succeed from init', function(done) {
177 | helper.appSelf.receipts = [receipt];
178 |
179 | setUpReceiptCheck({
180 | fetchAllProducts: true,
181 | onValidationRequest: function(requestBody) {
182 | assert.equal(requestBody, receipt);
183 | },
184 | });
185 |
186 | fxpay.init({
187 | onerror: function(err) {
188 | done(err);
189 | },
190 | oninit: function() {},
191 | onrestore: function(err, info) {
192 | if (!err) {
193 | assert.equal(info.productId, helper.apiProduct.guid);
194 | assert.equal(info.name, helper.apiProduct.name);
195 | assert.equal(info.smallImageUrl, helper.apiProduct.logo_url);
196 | assert.equal(info.receiptInfo.status, 'ok');
197 | assert.equal(info.receiptInfo.receipt, receipt);
198 | }
199 | done(err);
200 | }
201 | });
202 | });
203 |
204 | it('can still fail from init', function(done) {
205 | helper.appSelf.receipts = [receipt];
206 |
207 | setUpReceiptCheck({
208 | fetchAllProducts: true,
209 | onValidationResponse: function(request) {
210 | request.respond(500, {}, 'Internal Error');
211 | },
212 | });
213 |
214 | fxpay.init({
215 | onerror: done,
216 | oninit: function() {},
217 | onrestore: function(err, info) {
218 | assert.instanceOf(err, errors.BadApiResponse);
219 | assert.typeOf(info, 'object');
220 | done();
221 | }
222 | });
223 |
224 | });
225 |
226 |
227 | function setUpReceiptCheck(opt) {
228 | opt = utils.defaults(opt, {
229 | fetchProductUrl: new RegExp(
230 | 'http(s)?://[^/]+/api/v1/payments/' +
231 | encodeURIComponent(helper.appSelf.origin) + '/in-app/' +
232 | helper.apiProduct.guid + '/'),
233 | fetchAllProducts: false,
234 | validatorResponse: undefined,
235 | onValidationRequest: function() {},
236 | onValidationResponse: undefined,
237 | verifyUrl: undefined,
238 | });
239 |
240 | // When working with promises, we need to define responses up front
241 | // and respond as each request comes in.
242 | helper.server.respondImmediately = true;
243 |
244 | if (opt.fetchAllProducts) {
245 | // When testing init, we need to respond to fxpay.getProducts()
246 | helper.server.respondWith(
247 | 'GET', settings.apiUrlBase + settings.apiVersionPrefix +
248 | '/payments/' + encodeURIComponent(defaultProductUrl) +
249 | '/in-app/?active=1',
250 | [200, {"Content-Type": "application/json"},
251 | JSON.stringify({
252 | meta: {
253 | next: null,
254 | previous: null,
255 | total_count: 1,
256 | offset: 0,
257 | limit: 25,
258 | },
259 | objects: [helper.apiProduct]
260 | })]);
261 | }
262 |
263 | // Respond to fetching the product info related to the receipt.
264 | helper.server.respondWith('GET', opt.fetchProductUrl,
265 | function(request) {
266 | request.respond(200, {"Content-Type": "application/json"},
267 | JSON.stringify(helper.apiProduct));
268 | });
269 |
270 | var validator = new helper.ReceiptValidator({
271 | verifyUrl: opt.verifyUrl,
272 | onRequest: opt.onValidationRequest,
273 | response: opt.validatorResponse,
274 | onValidationResponse: opt.onValidationResponse,
275 | });
276 | validator.finish();
277 |
278 | }
279 |
280 |
281 | function makeReceipt(overrides, receiptData) {
282 | overrides = utils.defaults(overrides, {
283 | productUrl: defaultProductUrl,
284 | storedata: 'contrib=297&id=500419&inapp_id=' + helper.apiProduct.guid,
285 | });
286 | return helper.makeReceipt(receiptData, overrides);
287 | }
288 |
289 | });
290 | });
291 |
--------------------------------------------------------------------------------
/tests/test-products.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'errors',
3 | 'fxpay',
4 | 'helper',
5 | 'products'
6 | ], function(errors, fxpay, helper, products) {
7 |
8 | describe('products.Product', function() {
9 | var Product = products.Product;
10 | var product = new Product({productId: 'some-id'});
11 |
12 | beforeEach(function() {
13 | helper.setUp();
14 | fxpay.configure({
15 | appSelf: helper.appSelf,
16 | });
17 | });
18 |
19 | afterEach(function() {
20 | helper.tearDown();
21 | });
22 |
23 | describe('hasReceipt()', function() {
24 |
25 | it('returns false when no receipts', function() {
26 | fxpay.configure({productReceiptMap: {}});
27 | assert.equal(product.hasReceipt(), false);
28 | });
29 |
30 | it('returns true when receipt exists', function() {
31 | var productReceiptMap = {};
32 | productReceiptMap[product.productId] = helper.makeReceipt();
33 | fxpay.configure({productReceiptMap: productReceiptMap});
34 |
35 | assert.equal(product.hasReceipt(), true);
36 | });
37 |
38 | it('requires re-population of receipt map', function() {
39 | fxpay.configure({productReceiptMap: null});
40 | assert.throws(function() {
41 | product.hasReceipt();
42 | }, errors.IncorrectUsage);
43 | });
44 |
45 | });
46 |
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/tests/test-purchase.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'errors',
3 | 'fxpay',
4 | 'helper',
5 | 'settings'
6 | ], function(errors, fxpay, helper, settings) {
7 |
8 | describe('fxpay.purchase() on B2G', function () {
9 | var mozPay;
10 |
11 | beforeEach(function() {
12 | helper.setUp();
13 | mozPay = sinon.spy(helper.mozPayStub);
14 | fxpay.configure({
15 | appSelf: helper.appSelf,
16 | mozPay: mozPay,
17 | });
18 | });
19 |
20 | afterEach(function() {
21 | helper.tearDown();
22 | mozPay.reset();
23 | helper.receiptAdd.reset();
24 | });
25 |
26 | it('should send a JWT to mozPay', function (done) {
27 | var webpayJWT = '';
28 | var cfg = {
29 | apiUrlBase: 'https://not-the-real-marketplace',
30 | apiVersionPrefix: '/api/v1',
31 | adapter: null,
32 | };
33 | fxpay.configure(cfg);
34 |
35 | fxpay.purchase(helper.apiProduct.guid).then(function(productInfo) {
36 | assert.ok(mozPay.called);
37 | assert.ok(mozPay.calledWith([webpayJWT]), mozPay.firstCall);
38 | assert.equal(productInfo.productId, helper.apiProduct.guid);
39 | done();
40 | }).catch(done);
41 |
42 | helper.resolvePurchase({productData: {webpayJWT: webpayJWT},
43 | mozPay: mozPay});
44 | });
45 |
46 | it('should support the old callback interface', function (done) {
47 |
48 | fxpay.purchase(helper.apiProduct.guid, function(error, productInfo) {
49 | if (!error) {
50 | assert.ok(mozPay.called);
51 | assert.equal(productInfo.productId, helper.apiProduct.guid);
52 | }
53 | done(error);
54 | });
55 |
56 | helper.resolvePurchase({mozPay: mozPay});
57 | });
58 |
59 | it('should timeout polling the transaction', function (done) {
60 | var productId = 'some-guid';
61 |
62 | fxpay.purchase(productId, {
63 | maxTries: 2,
64 | pollIntervalMs: 1
65 | }).then(function() {
66 | done(Error('unexpected success'));
67 | }).catch(function(err) {
68 | assert.instanceOf(err, errors.PurchaseTimeout);
69 | assert.equal(err.productInfo.productId, productId);
70 | done();
71 | }).catch(done);
72 |
73 | helper.resolvePurchase({
74 | mozPay: mozPay,
75 | transData: helper.transactionData({status: 'incomplete'}),
76 | enableTransPolling: true,
77 | });
78 | });
79 |
80 | it('should call back with mozPay error', function (done) {
81 | var productId = 'some-guid';
82 |
83 | fxpay.purchase(productId).then(function() {
84 | done(Error('unexpected success'));
85 | }).catch(function(err) {
86 | assert.instanceOf(err, errors.PayPlatformError);
87 | assert.equal(err.code, 'DIALOG_CLOSED_BY_USER');
88 | assert.equal(err.productInfo.productId, productId);
89 | done();
90 | }).catch(done);
91 |
92 | helper.resolvePurchase({
93 | mozPay: mozPay,
94 | mozPayResolver: function(domRequest) {
95 | domRequest.error = {name: 'DIALOG_CLOSED_BY_USER'};
96 | domRequest.onerror();
97 | },
98 | });
99 | });
100 |
101 | it('should support old callback interface for errors', function (done) {
102 |
103 | fxpay.purchase(helper.apiProduct.guid, function(err, productInfo) {
104 | assert.instanceOf(err, errors.PayPlatformError);
105 | assert.equal(productInfo.productId, helper.apiProduct.guid);
106 | done();
107 | });
108 |
109 | helper.resolvePurchase({
110 | mozPay: mozPay,
111 | mozPayResolver: function(domRequest) {
112 | domRequest.error = {name: 'DIALOG_CLOSED_BY_USER'};
113 | domRequest.onerror();
114 | },
115 | });
116 | });
117 |
118 | it('should report invalid transaction state', function (done) {
119 |
120 | fxpay.purchase(helper.apiProduct.guid).then(function() {
121 | done(Error('unexpected success'));
122 | }).catch(function(err) {
123 | assert.instanceOf(err, errors.ConfigurationError);
124 | done();
125 | }).catch(done);
126 |
127 | helper.resolvePurchase({
128 | mozPay: mozPay,
129 | transData: helper.transactionData(
130 | {status: 'THIS_IS_NOT_A_VALID_STATE'}),
131 | });
132 | });
133 |
134 | it('should add receipt to device with addReceipt', function (done) {
135 | var receipt = '';
136 |
137 | fxpay.purchase(helper.apiProduct.guid).then(function(productInfo) {
138 | assert.equal(helper.receiptAdd._receipt, receipt);
139 | assert.equal(productInfo.productId, helper.apiProduct.guid);
140 | done();
141 | }).catch(done);
142 |
143 | helper.resolvePurchase({receipt: receipt, mozPay: mozPay});
144 | });
145 |
146 | it('should call back with complete product info', function (done) {
147 |
148 | fxpay.purchase(helper.apiProduct.guid).then(function(info) {
149 | assert.equal(info.productId, helper.apiProduct.guid);
150 | assert.equal(info.name, helper.apiProduct.name);
151 | assert.equal(info.smallImageUrl, helper.apiProduct.logo_url);
152 | done();
153 | }).catch(done);
154 |
155 | helper.resolvePurchase({mozPay: mozPay});
156 | });
157 |
158 | it('should fetch stub products when using fake products', function (done) {
159 | fxpay.configure({fakeProducts: true});
160 |
161 | fxpay.purchase(helper.apiProduct.guid).then(function(info) {
162 | assert.equal(info.productId, helper.apiProduct.guid);
163 | assert.equal(info.name, helper.apiProduct.name);
164 | assert.equal(info.smallImageUrl, helper.apiProduct.logo_url);
165 | done();
166 | }).catch(done);
167 |
168 | helper.resolvePurchase({
169 | fetchProductsPattern: new RegExp('.*\/stub-in-app-products\/.*'),
170 | mozPay: mozPay
171 | });
172 | });
173 |
174 | it('should add receipt to device with localStorage', function (done) {
175 | var receipt = '';
176 |
177 | setUpLocStorAddReceipt();
178 |
179 | // Without addReceipt(), receipt should go in localStorage.
180 |
181 | fxpay.purchase(helper.apiProduct.guid).then(function(productInfo) {
182 | assert.equal(
183 | JSON.parse(
184 | window.localStorage.getItem(settings.localStorageKey))[0],
185 | receipt);
186 | assert.equal(productInfo.productId, helper.apiProduct.guid);
187 | done();
188 | }).catch(done);
189 |
190 | helper.resolvePurchase({receipt: receipt, mozPay: mozPay});
191 | });
192 |
193 | it('should error when no storage mechanisms exist', function(done) {
194 | var receipt = '';
195 | delete helper.appSelf.addReceipt; // older FxOSs do not have this.
196 |
197 | fxpay.configure({
198 | localStorage: null, // no fallback.
199 | });
200 |
201 | fxpay.purchase(helper.apiProduct.guid).then(function() {
202 | done(Error('unexpected success'));
203 | }).catch(function(error) {
204 | assert.instanceOf(error, errors.PayPlatformUnavailable);
205 | done();
206 | }).catch(done);
207 |
208 | helper.resolvePurchase({receipt: receipt, mozPay: mozPay});
209 | });
210 |
211 | it('should not add dupes to localStorage', function (done) {
212 | var receipt = '';
213 |
214 | setUpLocStorAddReceipt();
215 |
216 | // Set up an already stored receipt.
217 | window.localStorage.setItem(settings.localStorageKey,
218 | JSON.stringify([receipt]));
219 |
220 | fxpay.purchase(helper.apiProduct.guid).then(function(productInfo) {
221 | var addedReceipts = JSON.parse(
222 | window.localStorage.getItem(settings.localStorageKey));
223 |
224 | // Make sure a new receipt wasn't added.
225 | assert.equal(addedReceipts.length, 1);
226 |
227 | assert.equal(productInfo.productId, helper.apiProduct.guid);
228 | done();
229 | }).catch(done);
230 |
231 | helper.resolvePurchase({receipt: receipt, mozPay: mozPay});
232 | });
233 |
234 | it('should pass through receipt errors', function (done) {
235 |
236 | fxpay.purchase(helper.apiProduct.guid).then(function() {
237 | done(Error('unexpected success'));
238 | }).catch(function(err) {
239 | assert.instanceOf(err, errors.AddReceiptError);
240 | assert.equal(err.code, 'ADD_RECEIPT_ERROR');
241 | assert.equal(err.productInfo.productId, helper.apiProduct.guid);
242 | done();
243 | }).catch(done);
244 |
245 | helper.resolvePurchase({
246 | mozPay: mozPay,
247 | addReceiptResolver: function(domRequest) {
248 | domRequest.error = {name: 'ADD_RECEIPT_ERROR'};
249 | domRequest.onerror();
250 | },
251 | });
252 | });
253 |
254 |
255 | function setUpLocStorAddReceipt() {
256 | // Set up a purchase where mozApps does not support addReceipt().
257 | delete helper.appSelf.addReceipt;
258 |
259 | helper.appSelf.onsuccess();
260 | }
261 |
262 | });
263 | });
264 |
--------------------------------------------------------------------------------
/tests/test-receipts-all.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'errors',
3 | 'fxpay',
4 | 'helper',
5 | 'receipts',
6 | 'settings'
7 | ], function(errors, fxpay, helper, receipts, settings) {
8 |
9 | describe('receipts.all()', function() {
10 |
11 | beforeEach(function() {
12 | helper.setUp();
13 | });
14 |
15 | afterEach(function() {
16 | helper.tearDown();
17 | });
18 |
19 | it('exposes mozApps receipts', function(done) {
20 | var receipt = '';
21 | fxpay.configure({
22 | appSelf: {
23 | receipts: [receipt]
24 | }
25 | });
26 | receipts.all(function(error, fetchedReceipts) {
27 | if (!error) {
28 | assert.equal(fetchedReceipts[0], receipt);
29 | assert.equal(fetchedReceipts.length, 1);
30 | }
31 | done(error);
32 | });
33 | });
34 |
35 | it('ignores missing receipts', function(done) {
36 | fxpay.configure({appSelf: {}}); // no receipts property
37 | receipts.all(function(error, fetchedReceipts) {
38 | if (!error) {
39 | assert.equal(fetchedReceipts.length, 0);
40 | }
41 | done(error);
42 | });
43 | });
44 |
45 | it('gets mozApps receipts and localStorage ones', function(done) {
46 | var receipt1 = '';
47 | var receipt2 = '';
48 |
49 | fxpay.configure({
50 | appSelf: {
51 | receipts: [receipt1]
52 | }
53 | });
54 | window.localStorage.setItem(settings.localStorageKey,
55 | JSON.stringify([receipt2]));
56 |
57 | receipts.all(function(error, fetchedReceipts) {
58 | if (!error) {
59 | assert.equal(fetchedReceipts[0], receipt1);
60 | assert.equal(fetchedReceipts[1], receipt2);
61 | assert.equal(fetchedReceipts.length, 2);
62 | }
63 | done(error);
64 | });
65 | });
66 |
67 | it('filters out dupe receipts', function(done) {
68 | var receipt1 = '';
69 |
70 | fxpay.configure({
71 | appSelf: {
72 | receipts: [receipt1]
73 | }
74 | });
75 | window.localStorage.setItem(settings.localStorageKey,
76 | JSON.stringify([receipt1]));
77 |
78 | receipts.all(function(error, fetchedReceipts) {
79 | if (!error) {
80 | assert.equal(fetchedReceipts[0], receipt1);
81 | assert.equal(fetchedReceipts.length, 1);
82 | }
83 | done(error);
84 | });
85 | });
86 |
87 | it('handles appSelf errors', function(done) {
88 | helper.appSelf.error = {name: 'INVALID_MANIFEST'};
89 | fxpay.configure({
90 | appSelf: null // clear appSelf cache.
91 | });
92 | receipts.all(function(error) {
93 | assert.instanceOf(error, errors.InvalidApp,
94 | 'should be instanceOf errors.InvalidApp');
95 | assert.equal(error.code, 'INVALID_MANIFEST');
96 | done();
97 | });
98 |
99 | helper.appSelf.onerror();
100 | });
101 |
102 | });
103 | });
104 |
--------------------------------------------------------------------------------
/tests/test-receipts-verify-app-data.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'errors',
3 | 'fxpay',
4 | 'helper',
5 | 'receipts'
6 | ], function(errors, fxpay, helper, receipts) {
7 |
8 | describe('receipts.verifyAppData()', function() {
9 | var receiptCheckSite = 'https://niceverifier.org';
10 |
11 | beforeEach(function() {
12 | helper.setUp();
13 | fxpay.configure({
14 | appSelf: helper.appSelf,
15 | receiptCheckSites: [receiptCheckSite]
16 | });
17 | });
18 |
19 | afterEach(function() {
20 | helper.tearDown();
21 | });
22 |
23 | it('fails on missing storedata', function(done) {
24 | receipts.verifyAppData(
25 | makeReceipt({storedata: 'foo=baz&barz=zonk'}),
26 | function(err) {
27 | assert.instanceOf(err, errors.InvalidReceipt);
28 | done();
29 | });
30 | });
31 |
32 | it('passes through receipt data', function(done) {
33 | var productId = '123';
34 | var productUrl = 'app://some-packaged-origin';
35 | var storedata = 'id=' + productId;
36 | helper.appSelf.origin = productUrl;
37 |
38 | receipts.verifyAppData(
39 | makeReceipt({storedata: storedata,
40 | productUrl: productUrl}),
41 | function(err, data, info) {
42 | if (!err) {
43 | assert.equal(info.productId, productId);
44 | assert.equal(info.productUrl, productUrl);
45 | assert.equal(data.product.storedata, storedata);
46 | }
47 | done(err);
48 | });
49 | });
50 |
51 |
52 | function makeReceipt(overrides, receiptData) {
53 | receiptData = receiptData || {};
54 | receiptData.verify = receiptCheckSite + '/verify/';
55 |
56 | return helper.makeReceipt(receiptData, overrides);
57 | }
58 |
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/tests/test-receipts-verify-data.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'errors',
3 | 'fxpay',
4 | 'helper',
5 | 'products',
6 | 'receipts'
7 | ], function(errors, fxpay, helper, products, receipts) {
8 |
9 | describe('receipts.verifyData()', function() {
10 | var someProduct = new products.Product({productId: 'some-uuid'});
11 | var receiptCheckSite = 'https://niceverifier.org';
12 |
13 | beforeEach(function() {
14 | helper.setUp();
15 | fxpay.configure({
16 | appSelf: helper.appSelf,
17 | receiptCheckSites: [receiptCheckSite]
18 | });
19 | });
20 |
21 | afterEach(function() {
22 | helper.tearDown();
23 | });
24 |
25 | it('fails on non-strings', function(done) {
26 | receipts.verifyData({not: 'a receipt'}, someProduct, function(err) {
27 | assert.instanceOf(err, errors.InvalidReceipt);
28 | done();
29 | });
30 | });
31 |
32 | it('fails on too many key segments', function(done) {
33 | receipts.verifyData('one~too~many', someProduct, function(err) {
34 | assert.instanceOf(err, errors.InvalidReceipt);
35 | done();
36 | });
37 | });
38 |
39 | it('fails on not enough JWT segments', function(done) {
40 | receipts.verifyData('one.two', someProduct, function(err) {
41 | assert.instanceOf(err, errors.InvalidReceipt);
42 | done();
43 | });
44 | });
45 |
46 | it('fails on invalid base64 encoding', function(done) {
47 | receipts.verifyData(receipt({receipt: 'not%valid&&base64'}),
48 | someProduct,
49 | function(err) {
50 | assert.instanceOf(err, errors.InvalidReceipt);
51 | done();
52 | });
53 | });
54 |
55 | it('fails on invalid JSON', function(done) {
56 | receipts.verifyData('jwtAlgo.' + btoa('^not valid JSON') + '.jwtSig',
57 | someProduct,
58 | function(err) {
59 | assert.instanceOf(err, errors.InvalidReceipt);
60 | done();
61 | });
62 | });
63 |
64 | it('fails on missing product URL', function(done) {
65 | receipts.verifyData(receipt(null, {
66 | product: {
67 | storedata: 'storedata'
68 | }
69 | }), someProduct, function(err) {
70 | assert.instanceOf(err, errors.InvalidReceipt);
71 | done();
72 | });
73 | });
74 |
75 | it('fails on missing storedata', function(done) {
76 | receipts.verifyData(
77 | 'jwtAlgo.' + btoa(JSON.stringify({product: {}})) + '.jwtSig',
78 | someProduct,
79 | function(err) {
80 | assert.instanceOf(err, errors.InvalidReceipt);
81 | done();
82 | });
83 | });
84 |
85 | it('fails on non-string storedata', function(done) {
86 | receipts.verifyData(receipt({storedata: {}}),
87 | someProduct,
88 | function(err) {
89 | assert.instanceOf(err, errors.InvalidReceipt);
90 | done();
91 | });
92 | });
93 |
94 | it('fails on foreign product URL for packaged app', function(done) {
95 | var data = receipt({productUrl: 'wrong-app'});
96 | receipts.verifyData(data, someProduct, function(err) {
97 | assert.instanceOf(err, errors.InvalidReceipt);
98 | done();
99 | });
100 | });
101 |
102 | it('fails on foreign product URL for hosted app', function(done) {
103 | var webOrigin = 'http://some-site.com';
104 |
105 | fxpay.configure({
106 | window: {location: {origin: webOrigin}},
107 | appSelf: null,
108 | });
109 |
110 | var data = receipt({productUrl: 'http://wrong-site.com'});
111 | receipts.verifyData(data, someProduct, function(err) {
112 | assert.instanceOf(err, errors.InvalidReceipt);
113 | done();
114 | });
115 | });
116 |
117 | it('knows how to validate hosted app product URLs', function(done) {
118 | var webOrigin = 'http://some-site.com';
119 |
120 | fxpay.configure({
121 | window: {location: {origin: webOrigin}},
122 | appSelf: null,
123 | });
124 |
125 | var data = receipt({productUrl: webOrigin});
126 | receipts.verifyData(data, someProduct, function(err) {
127 | done(err);
128 | });
129 | });
130 |
131 | it('handles non-prefixed app origins', function(done) {
132 | helper.appSelf.origin = 'app://the-origin';
133 | // TODO: remove this when fixed in Marketplace. bug 1034264.
134 | var data = receipt({productUrl: 'the-origin'});
135 |
136 | receipts.verifyData(data, someProduct, function(err) {
137 | done(err);
138 | });
139 | });
140 |
141 | it('handles properly prefixed app origins', function(done) {
142 | helper.appSelf.origin = 'app://the-app';
143 | var data = receipt({productUrl: helper.appSelf.origin});
144 |
145 | receipts.verifyData(data, someProduct, function(err) {
146 | done(err);
147 | });
148 | });
149 |
150 | it('handles HTTP hosted app origins', function(done) {
151 | helper.appSelf.origin = 'http://hosted-app';
152 | var data = receipt({productUrl: helper.appSelf.origin});
153 |
154 | receipts.verifyData(data, someProduct, function(err) {
155 | done(err);
156 | });
157 | });
158 |
159 | it('handles HTTPS hosted app origins', function(done) {
160 | helper.appSelf.origin = 'https://hosted-app';
161 | var data = receipt({productUrl: helper.appSelf.origin});
162 |
163 | receipts.verifyData(data, someProduct, function(err) {
164 | done(err);
165 | });
166 | });
167 |
168 | it('allows wrong product URLs for test receipts', function(done) {
169 | // Only allow test receipts when fakeProducts is true.
170 | fxpay.configure({fakeProducts: true});
171 | receipts.verifyData(receipt({productUrl: 'wrong-app'},
172 | {typ: 'test-receipt'}),
173 | someProduct,
174 | function(err) {
175 | done(err);
176 | });
177 | });
178 |
179 | it('fails on disallowed receipt check URLs', function(done) {
180 | receipts.verifyData(receipt(null,
181 | {verify: 'http://mykracksite.ru'}),
182 | someProduct,
183 | function(err) {
184 | assert.instanceOf(err, errors.InvalidReceipt);
185 | done();
186 | });
187 | });
188 |
189 | it('disallows test receipts when not testing', function(done) {
190 | receipts.verifyData(receipt(null, {typ: 'test-receipt'}),
191 | someProduct,
192 | function(err, info) {
193 | assert.instanceOf(err, errors.TestReceiptNotAllowed);
194 | assert.typeOf(info, 'object');
195 | done();
196 | });
197 | });
198 |
199 |
200 | function receipt(overrides, receiptData) {
201 | overrides = overrides || {};
202 | receiptData = receiptData || {};
203 |
204 | receiptData.verify = (receiptData.verify ||
205 | receiptCheckSite + '/verify/');
206 | overrides.productUrl = overrides.productUrl || helper.someAppOrigin;
207 |
208 | return helper.makeReceipt(receiptData, overrides);
209 | }
210 |
211 | });
212 | });
213 |
--------------------------------------------------------------------------------
/tests/test-receipts-verify-inapp-data.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'errors',
3 | 'fxpay',
4 | 'helper',
5 | 'products',
6 | 'receipts'
7 | ], function(errors, fxpay, helper, products, receipts) {
8 |
9 | describe('receipts.verifyInAppProductData()', function() {
10 | var someProduct = new products.Product({productId: 'some-uuid'});
11 | var receiptCheckSite = 'https://niceverifier.org';
12 |
13 | beforeEach(function() {
14 | helper.setUp();
15 | fxpay.configure({
16 | appSelf: helper.appSelf,
17 | receiptCheckSites: [receiptCheckSite]
18 | });
19 | });
20 |
21 | afterEach(function() {
22 | helper.tearDown();
23 | });
24 |
25 | it('fails on missing product', function(done) {
26 | receipts.verifyInAppProductData({}, someProduct, function(err) {
27 | assert.instanceOf(err, errors.InvalidReceipt);
28 | done();
29 | });
30 | });
31 |
32 | it('fails on corrupted storedata', function(done) {
33 | receipts.verifyInAppProductData(
34 | makeReceipt({storedata: 'not%a!valid(string'}),
35 | someProduct,
36 | function(err) {
37 | assert.instanceOf(err, errors.InvalidReceipt);
38 | assert.equal(err.productInfo.productId, someProduct.productId);
39 | done();
40 | });
41 | });
42 |
43 | it('handles malformed storedata', function(done) {
44 | receipts.verifyInAppProductData(makeReceipt({storedata: '&&&'}),
45 | someProduct,
46 | function(err) {
47 | assert.instanceOf(err, errors.InvalidReceipt);
48 | assert.equal(err.productInfo.productId, someProduct.productId);
49 | done();
50 | });
51 | });
52 |
53 | it('fails on missing storedata', function(done) {
54 | receipts.verifyInAppProductData(
55 | makeReceipt({storedata: 'foo=baz&barz=zonk'}),
56 | someProduct,
57 | function(err) {
58 | assert.instanceOf(err, errors.InvalidReceipt);
59 | assert.equal(err.productInfo.productId, someProduct.productId);
60 | done();
61 | });
62 | });
63 |
64 | it('passes through receipt data', function(done) {
65 | var productId = 'receipt-product-guid';
66 | var productUrl = 'app://some-packaged-origin';
67 | var storedata = 'inapp_id=' + productId;
68 | helper.appSelf.origin = productUrl;
69 |
70 | receipts.verifyInAppProductData(
71 | makeReceipt({storedata: storedata,
72 | productUrl: productUrl}),
73 | someProduct,
74 | function(err, data, info) {
75 | if (!err) {
76 | assert.equal(info.productId, productId);
77 | assert.equal(info.productUrl, productUrl);
78 | assert.equal(data.product.storedata, storedata);
79 | }
80 | done(err);
81 | });
82 | });
83 |
84 |
85 | function makeReceipt(overrides, receiptData) {
86 | receiptData = receiptData || {};
87 | receiptData.verify = receiptCheckSite + '/verify/';
88 |
89 | return helper.makeReceipt(receiptData, overrides);
90 | }
91 |
92 | });
93 | });
94 |
--------------------------------------------------------------------------------
/tests/test-settings.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'errors',
3 | 'settings'
4 | ], function(errors, settings) {
5 |
6 | describe('fxpay.settings', function() {
7 | var adapterConfigure;
8 | var adapter;
9 |
10 | var Adapter = function() {};
11 | Adapter.prototype.configure = function() {};
12 |
13 | beforeEach(function() {
14 | settings.configure({}, {reset: true});
15 | adapter = new Adapter();
16 | adapterConfigure = sinon.spy(adapter, 'configure');
17 | });
18 |
19 | afterEach(function() {
20 | settings.configure({}, {reset: true});
21 | });
22 |
23 | it('should allow you to set an adapter', function() {
24 | settings.configure({adapter: adapter});
25 | assert.equal(adapterConfigure.called, true);
26 | });
27 |
28 | it('should reconfigure the adapter', function() {
29 | settings.configure({adapter: adapter});
30 | // The adapter should always be reconfigured.
31 | settings.configure();
32 | assert.equal(adapterConfigure.callCount, 2);
33 | });
34 |
35 | it('should let you merge in new parameters', function() {
36 | assert.equal(settings.allowTestReceipts, false);
37 | settings.configure({allowTestReceipts: true});
38 | assert.equal(settings.allowTestReceipts, true);
39 | // Configuring something else should preserve old values.
40 | settings.configure({apiUrlBase: 'https://mysite.net'});
41 | assert.equal(settings.allowTestReceipts, true);
42 | });
43 |
44 | it('should allow test receipts for fake products', function() {
45 | settings.configure({fakeProducts: true});
46 | assert.equal(settings.allowTestReceipts, true);
47 | });
48 |
49 | it('should merge in new payment providers', function() {
50 | var prodDefault = settings.payProviderUrls['mozilla/payments/pay/v1'];
51 | var newProviders = {
52 | 'mysite/pay/v1': 'https://mysite.net/pay/?req={jwt}',
53 | };
54 | settings.configure({extraProviderUrls: newProviders});
55 | assert.equal(settings.payProviderUrls['mozilla/payments/pay/v1'],
56 | prodDefault);
57 | assert.equal(settings.payProviderUrls['mysite/pay/v1'],
58 | newProviders['mysite/pay/v1']);
59 | });
60 |
61 | it('should let you initialize settings with values', function() {
62 | settings.initialize({allowTestReceipts: true});
63 | assert.equal(settings.allowTestReceipts, true);
64 | });
65 |
66 | it('should only initialize settings once', function() {
67 | settings.alreadyConfigured = false;
68 | settings.initialize();
69 |
70 | var defaultConfigure = sinon.spy(settings.adapter, 'configure');
71 |
72 | settings.initialize();
73 | settings.initialize();
74 |
75 | // Make sure repeated calls do not re-configure.
76 | assert.equal(defaultConfigure.callCount, 0);
77 | });
78 |
79 | it('does not re-initialize settings with null parameters', function() {
80 | settings.alreadyConfigured = false;
81 | settings.initialize();
82 |
83 | var defaultConfigure = sinon.spy(settings.adapter, 'configure');
84 |
85 | settings.initialize(null);
86 |
87 | // Make sure repeated calls do not re-configure.
88 | assert.equal(defaultConfigure.callCount, 0);
89 | });
90 |
91 | it('should only allow you to re-initialize settings', function() {
92 | var defaultConfigure = sinon.spy(settings.adapter, 'configure');
93 |
94 | settings.initialize({allowTestReceipts: true});
95 | settings.initialize({allowTestReceipts: true});
96 |
97 | // Since we passed in new settings, each call should reconfigure.
98 | assert.equal(defaultConfigure.callCount, 2);
99 | });
100 |
101 | it('initializes settings with defaults on first run', function() {
102 | var nonDefaultValue = 'not-a-default-value';
103 | settings.allowTestReceipts = nonDefaultValue;
104 |
105 | settings.alreadyConfigured = false;
106 | settings.initialize();
107 |
108 | // This ensures that the first run resets all settings.
109 | assert.ok(settings.allowTestReceipts !== nonDefaultValue);
110 | });
111 |
112 | it('initializes settings with defaults even with overrides', function() {
113 | var nonDefaultValue = 'not-a-default-value';
114 | settings.allowTestReceipts = nonDefaultValue;
115 |
116 | settings.alreadyConfigured = false;
117 | settings.initialize({log: console});
118 |
119 | // This ensures that the first run resets all settings even
120 | // if the first initialize call overrode some values.
121 | assert.ok(settings.allowTestReceipts !== nonDefaultValue);
122 | });
123 |
124 | it('should error with unknown options', function () {
125 | assert.throws(function() {
126 | settings.configure({notAvalidOption: false});
127 | }, errors.IncorrectUsage);
128 | });
129 |
130 | });
131 | });
132 |
--------------------------------------------------------------------------------
/tests/test-utils.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'errors',
3 | 'fxpay',
4 | 'helper',
5 | 'utils',
6 | ], function(errors, fxpay, helper, utils) {
7 |
8 | describe('utils.defaults()', function() {
9 |
10 | it('should handle merging defaults into object', function() {
11 | var obj = {
12 | bar: false,
13 | foo: 'something',
14 | };
15 | var defaults = {
16 | bar: true,
17 | newKey: 'new-thing'
18 | };
19 | var result = utils.defaults(obj, defaults);
20 | assert.deepEqual(result, {
21 | bar: false,
22 | foo: 'something',
23 | newKey: 'new-thing',
24 | });
25 | });
26 |
27 | it('should handle merging defaults into empty object', function() {
28 | var obj = {};
29 | var defaults = {
30 | bar: true,
31 | newKey: 'new-thing'
32 | };
33 | var result = utils.defaults(obj, defaults);
34 | assert.deepEqual(result, {
35 | bar: true,
36 | newKey: 'new-thing',
37 | });
38 | });
39 |
40 | it('should not override existing props', function() {
41 | var obj = {
42 | bar: true,
43 | newKey: 'new-thing'
44 | };
45 | var defaults = {
46 | bar: false,
47 | newKey: 'other-thing'
48 | };
49 | var result = utils.defaults(obj, defaults);
50 | assert.deepEqual(result, {
51 | bar: true,
52 | newKey: 'new-thing',
53 | });
54 | });
55 |
56 | it('should not override null', function() {
57 | var obj = {
58 | bar: null,
59 | newKey: 'new-thing'
60 | };
61 | var defaults = {
62 | bar: false,
63 | newKey: 'other-thing'
64 | };
65 | var result = utils.defaults(obj, defaults);
66 | assert.deepEqual(result, {
67 | bar: null,
68 | newKey: 'new-thing',
69 | });
70 | });
71 |
72 | it('should override an undefined property', function() {
73 | var obj = {
74 | bar: undefined,
75 | };
76 | var defaults = {
77 | bar: false,
78 | };
79 | var result = utils.defaults(obj, defaults);
80 | assert.deepEqual(result, {
81 | bar: false,
82 | });
83 | });
84 |
85 | it('should handle the object being undefined', function() {
86 | var defaults = {
87 | bar: 'result',
88 | };
89 | var result = utils.defaults(undefined, defaults);
90 | assert.deepEqual(result, {
91 | bar: 'result',
92 | });
93 | });
94 | });
95 |
96 |
97 | describe('utils.openWindow()', function() {
98 |
99 | beforeEach(function(){
100 | this.openWindowSpy = sinon.spy();
101 | fxpay.configure({window: {open: this.openWindowSpy}});
102 | });
103 |
104 | it('should be called with props', function() {
105 | utils.openWindow({
106 | url: 'http://blah.com',
107 | title: 'whatever',
108 | w: 200,
109 | h: 400
110 | });
111 | assert(this.openWindowSpy.calledWithMatch('http://blah.com', 'whatever'));
112 | assert.include(this.openWindowSpy.args[0][2], 'width=200');
113 | assert.include(this.openWindowSpy.args[0][2], 'height=400');
114 | });
115 |
116 | it('should be called with defaults', function() {
117 | utils.openWindow();
118 | assert(this.openWindowSpy.calledWithMatch('', 'FxPay'));
119 | assert.include(this.openWindowSpy.args[0][2], 'width=276');
120 | assert.include(this.openWindowSpy.args[0][2], 'height=384');
121 | });
122 |
123 | it('should be passed a features string with no whitespace', function() {
124 | utils.openWindow();
125 | assert.notInclude(this.openWindowSpy.args[0][2], ' ');
126 | });
127 | });
128 |
129 |
130 | describe('utils.getSelfOrigin', function() {
131 |
132 | it('should return the app origin', function() {
133 | assert.equal(
134 | utils.getSelfOrigin({
135 | appSelf: {
136 | origin: 'app://origin',
137 | manifest: {
138 | origin: 'app://origin',
139 | type: 'privileged',
140 | },
141 | },
142 | }),
143 | 'app://origin');
144 | });
145 |
146 | it('should return the marketplace GUID origin', function() {
147 | assert.equal(
148 | utils.getSelfOrigin({
149 | log: {info: function() {}},
150 | appSelf: {
151 | origin: 'app://unusable-origin',
152 | manifest: {
153 | type: 'web',
154 | },
155 | manifestURL: ('https://marketplace-dev.allizom.org' +
156 | '/app/some-guid/manifest.webapp'),
157 | },
158 | }),
159 | 'marketplace:some-guid');
160 | });
161 |
162 | it('should fall back to marketplace when no declared origin', function() {
163 | assert.equal(
164 | utils.getSelfOrigin({
165 | log: {info: function() {}},
166 | // Set up a privileged app that has not declared an origin.
167 | appSelf: {
168 | origin: 'app://unusable-origin',
169 | manifest: {
170 | type: 'privileged',
171 | origin: null,
172 | },
173 | manifestURL: ('https://marketplace-dev.allizom.org' +
174 | '/app/some-guid/manifest.webapp'),
175 | },
176 | }),
177 | 'marketplace:some-guid');
178 | });
179 |
180 | it('should error on non-marketplace packages', function() {
181 | assert.throws(function() {
182 | utils.getSelfOrigin({
183 | appSelf: {
184 | // This would be a randomly generated origin by the platform.
185 | origin: 'app://unusable-origin',
186 | manifest: {
187 | type: 'web',
188 | origin: null,
189 | },
190 | manifestURL: 'http://some-random-site/f/manifest.webapp',
191 | },
192 | });
193 | }, errors.InvalidAppOrigin);
194 | });
195 |
196 | it('should fall back to location origin', function() {
197 | var stubLocation = {origin: 'http://foo.com:3000'};
198 | assert.equal(
199 | utils.getSelfOrigin({window: {location: stubLocation}}),
200 | 'http://foo.com:3000');
201 | });
202 |
203 | it('should fall back to a derived origin', function() {
204 | var stubLocation = {protocol: 'http:',
205 | hostname: 'foo.com:3000'};
206 | assert.equal(
207 | utils.getSelfOrigin({window: {location: stubLocation}}),
208 | 'http://foo.com:3000');
209 | });
210 |
211 | });
212 |
213 |
214 | describe('utils.getUrlOrigin()', function() {
215 |
216 | it('returns location from URL', function() {
217 | assert.equal(utils.getUrlOrigin('http://foo.com/somewhere.html'),
218 | 'http://foo.com');
219 | });
220 |
221 | it('returns location with port', function() {
222 | assert.equal(utils.getUrlOrigin('http://foo.com:3000/somewhere.html'),
223 | 'http://foo.com:3000');
224 | });
225 |
226 | });
227 |
228 | describe('utils.serialize()', function() {
229 |
230 | it('should serialize object', function() {
231 | assert.equal(utils.serialize({foo: 'bar', baz: 'zup'}),
232 | 'foo=bar&baz=zup');
233 | });
234 |
235 | it('should urlencode keys + values', function() {
236 | assert.equal(
237 | utils.serialize({'album name': 'Back in Black', 'artist': 'AC/DC'}),
238 | 'album%20name=Back%20in%20Black&artist=AC%2FDC');
239 | });
240 |
241 | });
242 |
243 | describe('utils.getAppSelf()', function() {
244 |
245 | beforeEach(function() {
246 | helper.setUp();
247 | });
248 |
249 | afterEach(function() {
250 | helper.tearDown();
251 | });
252 |
253 | it('returns mozApps self', function(done) {
254 | utils.getAppSelf(function(error, appSelf) {
255 | assert.equal(appSelf, helper.appSelf);
256 | done(error);
257 | });
258 |
259 | helper.appSelf.onsuccess();
260 | });
261 |
262 | it('caches and returns mozApps self', function(done) {
263 | utils.getAppSelf(function() {
264 | // Now get the cached version:
265 | utils.getAppSelf(function(error, appSelf) {
266 | assert.equal(appSelf, helper.appSelf);
267 | done(error);
268 | });
269 | });
270 |
271 | helper.appSelf.onsuccess();
272 | });
273 |
274 | it('returns mozApps errors', function(done) {
275 | utils.getAppSelf(function(error, appSelf) {
276 | assert.instanceOf(error, errors.InvalidApp);
277 | assert.equal(error.code, 'SOME_ERROR');
278 | assert.strictEqual(appSelf, null);
279 | done();
280 | });
281 |
282 | helper.appSelf.error = {name: 'SOME_ERROR'};
283 | helper.appSelf.onerror();
284 | });
285 |
286 | it('returns false when mozApps is falsey', function(done) {
287 | // This is what happens when we're running on a non-apps platform.
288 | fxpay.configure({mozApps: null});
289 | utils.getAppSelf(function(error, appSelf) {
290 | assert.strictEqual(appSelf, false);
291 | done(error);
292 | });
293 | });
294 |
295 | it('returns pre-fetched appSelf', function(done) {
296 | fxpay.configure({appSelf: 'some-cached-value'});
297 | utils.getAppSelf(function(error, appSelf) {
298 | assert.equal(appSelf, 'some-cached-value');
299 | done(error);
300 | });
301 | });
302 |
303 | it('returns any non-null appSelf', function(done) {
304 | fxpay.configure({appSelf: false});
305 | utils.getAppSelf(function(error, appSelf) {
306 | assert.strictEqual(appSelf, false);
307 | done(error);
308 | });
309 | });
310 | });
311 |
312 | });
313 |
--------------------------------------------------------------------------------
/tests/test-validate-app-receipt.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'errors',
3 | 'fxpay',
4 | 'helper',
5 | 'utils'
6 | ], function(errors, fxpay, helper, utils) {
7 |
8 | describe('fxpay.validateAppReceipt()', function() {
9 | var defaultProductUrl = 'http://boar4485.testmanifest.com';
10 |
11 | beforeEach(function() {
12 | var receipt = makeReceipt();
13 | helper.setUp();
14 | helper.appSelf.origin = defaultProductUrl;
15 | helper.appSelf.receipts = [receipt];
16 | fxpay.configure({
17 | appSelf: helper.appSelf,
18 | receiptCheckSites: [
19 | 'https://fake-receipt-check-server.net',
20 | 'https://payments-alt.allizom.org'
21 | ]
22 | });
23 | });
24 |
25 | afterEach(function() {
26 | helper.tearDown();
27 | });
28 |
29 | it('calls back for successful server validation', function(done) {
30 | var appId = '1234';
31 | var productUrl = helper.appSelf.origin;
32 | var receipt = makeReceipt({
33 | storedata: 'id=' + appId,
34 | productUrl: productUrl,
35 | });
36 | helper.appSelf.receipts = [receipt];
37 |
38 | var validator = new helper.ReceiptValidator({
39 | onRequest: function(requestBody) {
40 | assert.equal(requestBody, receipt);
41 | },
42 | });
43 |
44 | fxpay.validateAppReceipt().then(function(productInfo) {
45 | assert.equal(productInfo.receiptInfo.status, 'ok');
46 | assert.equal(productInfo.receiptInfo.receipt, receipt);
47 | assert.equal(productInfo.productId, appId);
48 | assert.equal(productInfo.productUrl, productUrl);
49 | done();
50 | }).catch(done);
51 |
52 | validator.finish();
53 | });
54 |
55 | it('calls back for server validation errors', function(done) {
56 | var badResponse = {status: 'invalid', reason: 'ERROR_DECODING'};
57 |
58 | var validator = new helper.ReceiptValidator({
59 | response: badResponse,
60 | });
61 |
62 | fxpay.validateAppReceipt().then(function() {
63 | done(Error('unexpected success'));
64 | }).catch(function(reason) {
65 | assert.equal(reason.productInfo.receiptInfo.status, badResponse.status);
66 | assert.equal(reason.productInfo.receiptInfo.reason, badResponse.reason);
67 | assert.instanceOf(reason, errors.InvalidReceipt);
68 | done();
69 | }).catch(done);
70 |
71 | validator.finish();
72 | });
73 |
74 | it('fails when origin does not match product URL', function(done) {
75 | helper.appSelf.origin = 'http://some-other-origin.net';
76 |
77 | var validator = new helper.ReceiptValidator();
78 |
79 | fxpay.validateAppReceipt().then(function() {
80 | done(Error('unexpected success'));
81 | }).catch(function(reason) {
82 | assert.instanceOf(reason, errors.InvalidReceipt);
83 | assert.typeOf(reason.productInfo, 'object');
84 | done();
85 | }).catch(done);
86 |
87 | validator.finish();
88 | });
89 |
90 | it('fails when receipt was issued by a disallowed store', function(done) {
91 | // Disallow all other stores:
92 | helper.appSelf.manifest.installs_allowed_from = [
93 | 'https://my-benevolent-app-store.net',
94 | ];
95 |
96 | var validator = new helper.ReceiptValidator();
97 |
98 | fxpay.validateAppReceipt().then(function() {
99 | done(Error('unexpected success'));
100 | }).catch(function(reason) {
101 | assert.instanceOf(reason, errors.InvalidReceipt);
102 | assert.typeOf(reason.productInfo, 'object');
103 | done();
104 | }).catch(done);
105 |
106 | validator.finish();
107 | });
108 |
109 | it('fails when no stores are allowed', function(done) {
110 | // This is an unlikely case but we should honor it I suppose.
111 | helper.appSelf.manifest.installs_allowed_from = [];
112 |
113 | var validator = new helper.ReceiptValidator();
114 |
115 | fxpay.validateAppReceipt().then(function() {
116 | done(Error('unexpected success'));
117 | }).catch(function(reason) {
118 | assert.instanceOf(reason, errors.InvalidReceipt);
119 | assert.typeOf(reason.productInfo, 'object');
120 | done();
121 | }).catch(done);
122 |
123 | validator.finish();
124 | });
125 |
126 | it('allows any receipt with splat', function(done) {
127 | helper.appSelf.manifest.installs_allowed_from = ['*'];
128 |
129 | var validator = new helper.ReceiptValidator();
130 |
131 | fxpay.validateAppReceipt().then(function() {
132 | done();
133 | }).catch(done);
134 |
135 | validator.finish();
136 | });
137 |
138 | it('converts empty installs_allowed_from to splat', function(done) {
139 | // Make this imply installs_allowed_from = ['*'].
140 | delete helper.appSelf.manifest.installs_allowed_from;
141 |
142 | var validator = new helper.ReceiptValidator();
143 |
144 | fxpay.validateAppReceipt().then(function() {
145 | done();
146 | }).catch(done);
147 |
148 | validator.finish();
149 | });
150 |
151 | it('fails when test receipts are not allowed', function(done) {
152 | var testReceipt = makeReceipt(null, {
153 | typ: 'test-receipt',
154 | });
155 | helper.appSelf.receipts = [testReceipt];
156 |
157 | fxpay.validateAppReceipt().then(function() {
158 | done(Error('unexpected success'));
159 | }).catch(function(reason) {
160 | assert.instanceOf(reason, errors.TestReceiptNotAllowed);
161 | assert.typeOf(reason.productInfo, 'object');
162 | done();
163 | }).catch(done);
164 |
165 | });
166 |
167 | it('accepts test receipts', function(done) {
168 | fxpay.configure({allowTestReceipts: true});
169 |
170 | var testReceipt = makeReceipt(null, {
171 | typ: 'test-receipt',
172 | iss: 'https://payments-alt.allizom.org',
173 | verify: 'https://payments-alt.allizom.org/developers/test-receipt/',
174 | });
175 | helper.appSelf.receipts = [testReceipt];
176 |
177 | var validator = new helper.ReceiptValidator({
178 | verifyUrl: new RegExp(
179 | 'https://payments-alt\\.allizom\\.org/developers/test-receipt/'),
180 | });
181 |
182 | fxpay.validateAppReceipt().then(function(productInfo) {
183 | assert.equal(productInfo.receiptInfo.status, 'ok');
184 | done();
185 | }).catch(done);
186 |
187 | validator.finish();
188 | });
189 |
190 | it('fails when no receipt is present', function(done) {
191 | helper.appSelf.receipts = [];
192 |
193 | var validator = new helper.ReceiptValidator();
194 |
195 | fxpay.validateAppReceipt().then(function() {
196 | done(Error('unexpected success'));
197 | }).catch(function(reason) {
198 | assert.instanceOf(reason, errors.AppReceiptMissing);
199 | assert.typeOf(reason.productInfo, 'object');
200 | done();
201 | }).catch(done);
202 |
203 | validator.finish();
204 | });
205 |
206 | it('fails when mozApps is null', function(done) {
207 | fxpay.configure({mozApps: null, appSelf: null});
208 |
209 | fxpay.validateAppReceipt().then(function() {
210 | done(Error('unexpected success'));
211 | }).catch(function(reason) {
212 | assert.instanceOf(reason, errors.PayPlatformUnavailable);
213 | assert.typeOf(reason.productInfo, 'object');
214 | done();
215 | }).catch(done);
216 |
217 | });
218 |
219 | it('fails when appSelf is null', function(done) {
220 | fxpay.configure({appSelf: null});
221 | helper.appSelf.result = null;
222 |
223 | fxpay.validateAppReceipt().then(function() {
224 | done(Error('unexpected success'));
225 | }).catch(function(reason) {
226 | assert.instanceOf(reason, errors.PayPlatformUnavailable);
227 | assert.typeOf(reason.productInfo, 'object');
228 | done();
229 | }).catch(done);
230 |
231 | helper.appSelf.onsuccess();
232 | });
233 |
234 | it('fails when multiple receipts are installed', function(done) {
235 | helper.appSelf.receipts = [makeReceipt(), makeReceipt()];
236 |
237 | fxpay.validateAppReceipt().then(function() {
238 | done(Error('unexpected success'));
239 | }).catch(function(reason) {
240 | assert.instanceOf(reason, errors.NotImplementedError);
241 | assert.typeOf(reason.productInfo, 'object');
242 | done();
243 | }).catch(done);
244 | });
245 |
246 | it('fails when receipt is malformed', function(done) {
247 | helper.appSelf.receipts = ['^%%%$$$$garbage'];
248 |
249 | var validator = new helper.ReceiptValidator();
250 |
251 | fxpay.validateAppReceipt().then(function() {
252 | done(Error('unexpected success'));
253 | }).catch(function(reason) {
254 | assert.instanceOf(reason, errors.AppReceiptMissing);
255 | assert.typeOf(reason.productInfo, 'object');
256 | done();
257 | }).catch(done);
258 |
259 | validator.finish();
260 | });
261 |
262 | it('ignores in-app receipts', function(done) {
263 | var appId = '1234';
264 | var appReceipt = makeReceipt({storedata: 'id=' + appId});
265 | var inAppProductReceipt = makeReceipt({storedata: 'id=555&inapp_id=234'});
266 | helper.appSelf.receipts = [
267 | appReceipt,
268 | inAppProductReceipt,
269 | ];
270 |
271 | var validator = new helper.ReceiptValidator();
272 |
273 | fxpay.validateAppReceipt().then(function(productInfo) {
274 | assert.equal(productInfo.productId, appId);
275 | done();
276 | }).catch(done);
277 |
278 | validator.finish();
279 | });
280 |
281 |
282 | function makeReceipt(overrides, receiptData) {
283 | overrides = utils.defaults(overrides, {
284 | productUrl: defaultProductUrl,
285 | storedata: 'id=1234',
286 | });
287 | return helper.makeReceipt(receiptData, overrides);
288 | }
289 | });
290 | });
291 |
--------------------------------------------------------------------------------
/tests/test-web-purchase.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'errors',
3 | 'fxpay',
4 | 'helper',
5 | 'pay',
6 | 'settings',
7 | 'utils',
8 | ], function(errors, fxpay, helper, pay, settings, utils) {
9 |
10 | describe('fxpay.purchase() on the web', function() {
11 | var payReq = {typ: 'mozilla/payments/pay/v1'};
12 | var fakeJwt = '.' + btoa(JSON.stringify(payReq)) + '.';
13 | var productId = 'some-uuid';
14 |
15 | var providerUrlTemplate;
16 | var fakePayWindow;
17 | var windowSpy;
18 | var customPayWindow;
19 | var customWindowSpy;
20 | var handlers;
21 | var clock;
22 |
23 | beforeEach(function() {
24 | helper.setUp();
25 | handlers = {};
26 | fakePayWindow = {
27 | closed: false,
28 | close: function() {},
29 | };
30 | windowSpy = {
31 | close: sinon.spy(fakePayWindow, 'close'),
32 | };
33 | customPayWindow = {
34 | close: function() {},
35 | resizeTo: function() {},
36 | moveTo: function() {},
37 | };
38 | customWindowSpy = {
39 | close: sinon.spy(customPayWindow, 'close'),
40 | resizeTo: sinon.spy(customPayWindow, 'resizeTo'),
41 | moveTo: sinon.spy(customPayWindow, 'moveTo'),
42 | };
43 | providerUrlTemplate = settings.payProviderUrls[payReq.typ];
44 |
45 |
46 | fxpay.configure({
47 | appSelf: null,
48 | mozApps: null,
49 | mozPay: null,
50 | apiUrlBase: 'https://not-the-real-marketplace',
51 | apiVersionPrefix: '/api/v1',
52 | adapter: null,
53 | window: {
54 | location: '',
55 | open: function() {
56 | return fakePayWindow;
57 | },
58 | addEventListener: function(type, handler) {
59 | handlers[type] = handler;
60 | },
61 | removeEventListener: function() {},
62 | },
63 | });
64 |
65 | clock = sinon.useFakeTimers();
66 | // Must be after fakeTimers are setup.
67 | sinon.spy(window, 'clearInterval');
68 | });
69 |
70 | afterEach(function() {
71 | helper.tearDown();
72 | helper.receiptAdd.reset();
73 | // Must be before clock.restare().
74 | window.clearInterval.restore();
75 | clock.restore();
76 | });
77 |
78 | it('should open a payment window and resolve', function (done) {
79 |
80 | fxpay.purchase(productId).then(function() {
81 | assert.equal(
82 | fakePayWindow.location,
83 | providerUrlTemplate.replace('{jwt}', fakeJwt));
84 | assert(windowSpy.close.called);
85 | done();
86 | }).catch(done);
87 |
88 | helper.resolvePurchase({
89 | productData: {webpayJWT: fakeJwt},
90 | payCompleter: function() {
91 | simulatePostMessage({status: 'ok'});
92 | }
93 | });
94 | });
95 |
96 | it('should reject with payment errors', function (done) {
97 |
98 | fxpay.purchase(productId).then(function() {
99 | done(Error('unexpected success'));
100 | }).catch(function(err) {
101 | assert.instanceOf(err, errors.FailedWindowMessage);
102 | assert.equal(err.code, 'DIALOG_CLOSED_BY_USER');
103 | assert(windowSpy.close.called);
104 | done();
105 | }).catch(done);
106 |
107 | helper.resolvePurchase({
108 | productData: {webpayJWT: fakeJwt},
109 | payCompleter: function() {
110 | simulatePostMessage({status: 'failed',
111 | errorCode: 'DIALOG_CLOSED_BY_USER'});
112 | }
113 | });
114 | });
115 |
116 | it('should allow client to specify a custom window', function (done) {
117 |
118 | fxpay.purchase(productId, {
119 | paymentWindow: customPayWindow,
120 | managePaymentWindow: true,
121 | }).then(function() {
122 | assert.equal(
123 | customPayWindow.location,
124 | providerUrlTemplate.replace('{jwt}', fakeJwt));
125 | assert(customWindowSpy.resizeTo.called);
126 | assert(customWindowSpy.moveTo.called);
127 | assert(customWindowSpy.close.called);
128 | done();
129 | }).catch(done);
130 |
131 | helper.resolvePurchase({
132 | productData: {webpayJWT: fakeJwt},
133 | payCompleter: function() {
134 | simulatePostMessage({status: 'ok'});
135 | }
136 | });
137 | });
138 |
139 | it('should not manage custom pay windows by default', function (done) {
140 |
141 | fxpay.purchase(productId, {
142 | paymentWindow: customPayWindow,
143 | }).then(function() {
144 | assert.equal(
145 | customPayWindow.location,
146 | providerUrlTemplate.replace('{jwt}', fakeJwt));
147 | assert(!customWindowSpy.close.called);
148 | done();
149 | }).catch(done);
150 |
151 | helper.resolvePurchase({
152 | productData: {webpayJWT: fakeJwt},
153 | payCompleter: function() {
154 | simulatePostMessage({status: 'ok'});
155 | }
156 | });
157 | });
158 |
159 | it('should close payment window on adapter errors', function (done) {
160 | settings.adapter.startTransaction = function(opt, callback) {
161 | callback('SOME_EARLY_ERROR');
162 | };
163 |
164 | fxpay.purchase(productId).then(function() {
165 | done(Error('unexpected success'));
166 | }).catch(function(err) {
167 | assert.equal(err, 'SOME_EARLY_ERROR');
168 | assert(windowSpy.close.called);
169 | done();
170 | }).catch(done);
171 |
172 | });
173 |
174 | it('should not close managed window on adapter errors', function (done) {
175 | settings.adapter.startTransaction = function(opt, callback) {
176 | callback('SOME_EARLY_ERROR');
177 | };
178 |
179 | fxpay.purchase(productId, {
180 | paymentWindow: customPayWindow,
181 | }).then(function() {
182 | done(Error('unexpected success'));
183 | }).catch(function(err) {
184 | assert.equal(err, 'SOME_EARLY_ERROR');
185 | assert(!customWindowSpy.close.called);
186 | done();
187 | }).catch(done);
188 |
189 | });
190 |
191 | it('should close payment window on pay module errors', function (done) {
192 |
193 | fxpay.purchase(productId).then(function() {
194 | done(Error('unexpected success'));
195 | }).catch(function(err) {
196 | assert.instanceOf(err, errors.InvalidJwt);
197 | assert(windowSpy.close.called);
198 | done();
199 | }).catch(done);
200 |
201 | // Force an unexpected JWT type error.
202 | var req = {typ: 'unknown/provider/id'};
203 | var badJwt = '.' + btoa(JSON.stringify(req)) + '.';
204 |
205 | helper.resolvePurchase({
206 | productData: {webpayJWT: badJwt},
207 | payCompleter: function() {},
208 | });
209 | });
210 |
211 | it('should respond to user closed window', function (done) {
212 |
213 | fxpay.purchase(productId).then(function() {
214 | done(Error('unexpected success'));
215 | }).catch(function(err) {
216 | assert(window.clearInterval.called, 'clearInterval should be called');
217 | assert.instanceOf(err, errors.PayWindowClosedByUser);
218 | assert.equal(err.code, 'DIALOG_CLOSED_BY_USER');
219 | done();
220 | }).catch(done);
221 |
222 | // Respond to fetching the JWT.
223 | helper.server.respondWith('POST', /.*\/webpay\/inapp\/prepare/,
224 | helper.productData({webpayJWT: fakeJwt}));
225 | helper.server.respond();
226 |
227 | fakePayWindow.closed = true;
228 | clock.tick(600);
229 |
230 | });
231 |
232 |
233 | function simulatePostMessage(data) {
234 | handlers.message({data: data,
235 | origin: utils.getUrlOrigin(providerUrlTemplate)});
236 | }
237 |
238 | });
239 |
240 |
241 | describe('pay.processPayment()', function() {
242 |
243 | it('should reject calls without a paymentWindow', function(done) {
244 | fxpay.configure({mozPay: false});
245 | pay.processPayment('', function(error) {
246 | assert.instanceOf(error, errors.IncorrectUsage);
247 | done();
248 | });
249 | });
250 |
251 | });
252 |
253 |
254 | describe('pay.acceptPayMessage()', function() {
255 | var defaultOrigin = 'http://marketplace.firefox.com';
256 | var fakeWindow;
257 | var clock;
258 |
259 | beforeEach(function() {
260 | fakeWindow = {};
261 | clock = sinon.useFakeTimers();
262 | });
263 |
264 | afterEach(function() {
265 | clock.restore();
266 | });
267 |
268 | it('calls back on success', function(done) {
269 | pay.acceptPayMessage(
270 | makeEvent(), defaultOrigin,
271 | fakeWindow, function(err) {
272 | done(err);
273 | }
274 | );
275 | });
276 |
277 | it('calls back with error code on failure', function(done) {
278 | pay.acceptPayMessage(
279 | makeEvent({status: 'failed', errorCode: 'EXTERNAL_CODE'}),
280 | defaultOrigin, fakeWindow, function(err) {
281 | assert.instanceOf(err, errors.FailedWindowMessage);
282 | assert.equal(err.code, 'EXTERNAL_CODE');
283 | done();
284 | }
285 | );
286 | });
287 |
288 | it('rejects unknown statuses', function(done) {
289 | pay.acceptPayMessage(
290 | makeEvent({status: 'cheezborger'}),
291 | defaultOrigin, fakeWindow, function(err) {
292 | assert.instanceOf(err, errors.FailedWindowMessage);
293 | done();
294 | }
295 | );
296 | });
297 |
298 | it('rejects undefined data', function(done) {
299 | pay.acceptPayMessage(
300 | makeEvent({data: null}), defaultOrigin,
301 | fakeWindow, function(err) {
302 | assert.instanceOf(err, errors.FailedWindowMessage);
303 | done();
304 | }
305 | );
306 | });
307 |
308 | it('rejects foreign messages', function(done) {
309 | pay.acceptPayMessage(
310 | makeEvent({origin: 'http://bar.com'}), 'http://foo.com', fakeWindow,
311 | function(err) {
312 | assert.instanceOf(err, errors.UnknownMessageOrigin);
313 | done();
314 | }
315 | );
316 | });
317 |
318 | it('had window closed by user via an unload event', function(done) {
319 | fakeWindow.closed = true;
320 | pay.acceptPayMessage(
321 | makeEvent({status: 'unloaded'}),
322 | defaultOrigin, fakeWindow, function(err) {
323 | assert.instanceOf(err, errors.PayWindowClosedByUser);
324 | assert.equal(err.code, 'DIALOG_CLOSED_BY_USER');
325 | done();
326 | }
327 | );
328 | clock.tick(300);
329 | });
330 |
331 | function makeEvent(param) {
332 | param = utils.defaults(param, {
333 | status: 'ok',
334 | data: undefined,
335 | errorCode: undefined,
336 | origin: defaultOrigin,
337 | });
338 | if (typeof param.data === 'undefined') {
339 | param.data = {status: param.status, errorCode: param.errorCode};
340 | }
341 | return {origin: param.origin, data: param.data};
342 | }
343 |
344 | });
345 | });
346 |
--------------------------------------------------------------------------------
/tests/test-webrt-purchase.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'errors',
3 | 'fxpay',
4 | 'helper',
5 | ], function(errors, fxpay, helper) {
6 |
7 | describe('fxpay.purchase() on broken webRT', function () {
8 | var productId = 'some-uuid';
9 | var mozPay;
10 |
11 | beforeEach(function() {
12 | helper.setUp();
13 | mozPay = sinon.spy(helper.mozPayStub);
14 | fxpay.configure({
15 | appSelf: helper.appSelf,
16 | onBrokenWebRT: true,
17 | mozPay: mozPay,
18 | });
19 | });
20 |
21 | afterEach(function() {
22 | helper.tearDown();
23 | mozPay.reset();
24 | helper.receiptAdd.reset();
25 | });
26 |
27 | it('should pretend a cancel is a success', function(done) {
28 |
29 | fxpay.purchase(productId).then(function() {
30 | done();
31 | }).catch(done);
32 |
33 | helper.resolvePurchase({
34 | mozPay: mozPay,
35 | mozPayResolver: function(domRequest) {
36 | domRequest.error = {name: 'USER_CANCELLED'};
37 | domRequest.onerror();
38 | },
39 | });
40 |
41 | });
42 |
43 | it('should pass through non-cancel errors', function(done) {
44 |
45 | fxpay.purchase(productId).then(function() {
46 | done(Error('unexpected success'));
47 | }).catch(function(err) {
48 | assert.instanceOf(err, errors.PayPlatformError);
49 | assert.equal(err.code, 'SOME_RANDOM_ERROR');
50 | done();
51 | }).catch(done);
52 |
53 | helper.resolvePurchase({
54 | mozPay: mozPay,
55 | mozPayResolver: function(domRequest) {
56 | domRequest.error = {name: 'SOME_RANDOM_ERROR'};
57 | domRequest.onerror();
58 | },
59 | });
60 |
61 | });
62 |
63 | });
64 | });
65 |
--------------------------------------------------------------------------------
/umd/end.frag:
--------------------------------------------------------------------------------
1 |
2 | return require('fxpay');
3 | }));
4 |
5 |
--------------------------------------------------------------------------------
/umd/start.frag:
--------------------------------------------------------------------------------
1 | (function (root, factory) {
2 | 'use strict';
3 |
4 | if (typeof define === 'function') {
5 | define(factory);
6 | } else if (typeof exports === 'object') {
7 | module.exports = factory();
8 | } else {
9 | root.fxpay = factory();
10 | }
11 | }(this, function () {
12 |
--------------------------------------------------------------------------------