Skip to content
This repository was archived by the owner on Mar 15, 2018. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ var libFiles = [
__dirname + '/lib/fxpay/utils.js',
__dirname + '/lib/fxpay/settings.js',
__dirname + '/lib/fxpay/api.js',
__dirname + '/lib/fxpay/jwt.js',
__dirname + '/lib/fxpay/pay.js',
__dirname + '/lib/fxpay/products.js',
__dirname + '/lib/fxpay/receipts.js',
Expand Down
25 changes: 21 additions & 4 deletions example/shared/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ $(function() {

// DOM handling:
//
$('ul').on('click', '.product button', function() {
$('ul').on('click', '.product button', function(evt) {
evt.preventDefault();
clearError();
var prod = $(this).data('product');
console.log('purchasing', prod.name, prod.productId);
Expand Down Expand Up @@ -146,22 +147,38 @@ $(function() {

fxpay.configure({
receiptCheckSites: [
// Whitelist the production service.
// Allow the production service.
'https://receiptcheck.marketplace.firefox.com',
'https://marketplace.firefox.com',

// The following would not be needed in a live app. These our some test
// services for development of the fxpay library only.

// Whitelist our test servers.
// Allow our test servers.
'https://receiptcheck-dev.allizom.org',
'https://marketplace-dev.allizom.org',
'https://receiptcheck-payments-alt.allizom.org',
'https://payments-alt.allizom.org',

// Whitelist a local development server I use.
// Allow some common local servers..
'http://mp.dev',
'http://fireplace.loc',
],
payProviderUrls: {
// Map the production site.
'mozilla/payments/pay/v1':
'https://marketplace.firefox.com/mozpay/?req={jwt}',

// Map some development sites.
'mozilla-dev/payments/pay/v1':
'https://marketplace-dev.allizom.org/mozpay/?req={jwt}',
'mozilla-stage/payments/pay/v1':
'https://marketplace.allizom.org/mozpay/?req={jwt}',
'mozilla-alt/payments/pay/v1':
'https://payments-alt.allizom.org/mozpay/?req={jwt}',
'mozilla-local/payments/pay/v1':
'http://fireplace.loc/mozpay/?req={jwt}',
},
// Initially, start by allowing fake products so that test
// receipts can validate. The checkbox in initApi() will
// toggle this setting.
Expand Down
15 changes: 13 additions & 2 deletions lib/fxpay/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
var settings = require('fxpay/settings');
var products = require('fxpay/products');
var receipts = require('fxpay/receipts');
var utils = require('fxpay/utils');

//
// publicly exported functions:
//


exports.configure = function() {
settings.configure.apply(settings, arguments);
return settings.configure.apply(settings, arguments);
};


Expand Down Expand Up @@ -129,6 +130,16 @@

log.debug('starting purchase for product', productId);

var paymentWindow;
if (!settings.mozPay) {
// Open a blank payment window on the same event loop tick
// as the click handler. This avoids popup blockers.
// TODO: maybe we should inject some HTML/CSS to indicate
// loading/progress. We probably also need to display any
// errors that might occur from the Ajax request to get a JWT.
paymentWindow = utils.openWindow({url: ''});
}

api.post(settings.prepareJwtApiUrl, {inapp: productId},
function(err, productData) {
if (err) {
Expand All @@ -155,7 +166,7 @@
pollIntervalMs: opt.pollIntervalMs
}
);
});
}, {paymentWindow: paymentWindow});
});
}

Expand Down
64 changes: 64 additions & 0 deletions lib/fxpay/jwt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
(function() {
"use strict";
// This is a very minimal JWT utility. It does not validate signatures.

var exports = window.fxpay.utils.namespace('fxpay.jwt');
var settings = require('fxpay/settings');


exports.decode = function jwt_decode(jwt, callback) {
var parts = jwt.split('.');
if (parts.length !== 3) {
settings.log.error('JWT does not have 3 segments:', jwt);
return callback('WRONG_JWT_SEGMENT_COUNT');
}

var jwtData = parts[1];
// Normalize URL safe base64 into regular base64.
jwtData = jwtData.replace("-", "+", "g").replace("_", "/", "g");
var jwtString;
try {
jwtString = atob(jwtData);
} catch (error) {
settings.log.error('atob() error:', error.toString(),
'when decoding JWT', jwtData);
return callback('INVALID_JWT_DATA');
}
var data;

try {
data = JSON.parse(jwtString);
} catch (error) {
settings.log.error('JSON.parse() error:', error.toString(),
'when parsing', jwtString);
return callback('INVALID_JWT_DATA');
}
callback(null, data);
};


exports.getPayUrl = function jwt_getPayUrl(encodedJwt, callback) {
exports.decode(encodedJwt, function(err, jwtData) {
if (err) {
return callback(err);
}

var payUrl = settings.payProviderUrls[jwtData.typ];
if (!payUrl) {
settings.log.error('JWT type', jwtData.typ,
'does not map to any known payment providers');
return callback('UNEXPECTED_JWT_TYPE');
}
if (payUrl.indexOf('{jwt}') === -1) {
settings.log.error('JWT type', jwtData.typ,
'pay URL is formatted incorrectly:', payUrl);
return callback('INVALID_PAY_PROVIDER_URL');
}

payUrl = payUrl.replace('{jwt}', encodedJwt);
settings.log.info('JWT', jwtData.typ, 'resulted in pay URL:', payUrl);
callback(null, payUrl);
});
};

})();
26 changes: 22 additions & 4 deletions lib/fxpay/pay.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
"use strict";

var exports = window.fxpay.utils.namespace('fxpay.pay');
var jwt = require('fxpay/jwt');
var settings = require('fxpay/settings');


exports.processPayment = function pay_processPayment(jwts, callback) {
exports.processPayment = function pay_processPayment(jwts, callback, opt) {
opt = opt || {};
if (settings.mozPay) {
settings.log.info('processing payment with mozPay');

Expand All @@ -22,14 +24,30 @@
};

} else {
if (!opt.paymentWindow) {
throw new Error('Cannot start a web payment without a ' +
'reference to the payment window');
}
settings.log.info('processing payment with web flow');
return processWebPayment(jwts, callback);
return processWebPayment(opt.paymentWindow, jwts[0], callback);
}
};


function processWebPayment(jwts, callback) {
return callback('WEB_FLOW_NOT_IMPLEMENTED');
function processWebPayment(paymentWindow, payJwt, callback) {
jwt.getPayUrl(payJwt, function(err, payUrl) {
if (err) {
return callback(err);
}
// Now that we've extracted a payment URL from the JWT,
// load it into the freshly created popup window.
paymentWindow.location = payUrl;

// TODO: wait for postMessage result.
// https://bugzilla.mozilla.org/show_bug.cgi?id=1101995
// Until then, pretend we have immediate success.
callback();
});
}

})();
2 changes: 1 addition & 1 deletion lib/fxpay/products.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
if (!opt.api) {
opt.api = new API(settings.apiUrlBase);
}
var origin = encodeURIComponent(settings.appSelf.origin);
var origin = encodeURIComponent(utils.getSelfOrigin());
var url;

if (opt.fetchStubs) {
Expand Down
8 changes: 8 additions & 0 deletions lib/fxpay/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@
appSelf: null,
// Boolean flag to tell if we have addReceipt() or not.
hasAddReceipt: null,
// Map of JWT types to payment provider URLs.
payProviderUrls: {
'mozilla/payments/pay/v1':
'https://marketplace.firefox.com/mozpay/?req={jwt}'
},
openWindow: function() {
return window.open.apply(window, arguments);
},
// Relative API URL that accepts a product ID and returns a JWT.
prepareJwtApiUrl: '/webpay/inapp/prepare/',
onerror: function(err) {
Expand Down
8 changes: 7 additions & 1 deletion lib/fxpay/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
},

openWindow: function(options) {
var settings = require('fxpay/settings');
var defaults = {
url: 'about:blank',
title: 'FxPay',
Expand All @@ -64,7 +65,12 @@
'width=' + options.w + ',height=' + options.h +
',top=' + top_ + ',left=' + left;

return window.open(options.url, options.title, winOptString);
var windowRef = settings.openWindow(options.url, options.title,
winOptString);
if (!windowRef) {
settings.log.error('window.open() failed. URL:', options.url);
}
return windowRef;
}
};
})();
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
"grunt-contrib-jshint": "0.6.5",
"grunt-contrib-uglify": "0.6.0",
"grunt-karma": "0.9.0",
"karma": "0.12.23",
"karma": "0.12.28",
"karma-chai": "0.1.0",
"karma-chrome-launcher": "0.1.4",
"karma-coffee-preprocessor": "0.2.1",
"karma-firefox-launcher": "0.1.3",
"karma-html2js-preprocessor": "0.1.0",
"karma-mocha": "0.1.9",
"karma-mocha": "0.1.10",
"karma-mocha-reporter": "0.3.1",
"karma-script-launcher": "0.1.0",
"karma-sinon": "1.0.3",
Expand Down
68 changes: 68 additions & 0 deletions tests/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,74 @@
};


exports.finishPurchaseOk = function(receipt, opt) {
var helper = exports;
opt = fxpay.utils.defaults(opt, {
mozPay: null,
productData: null
});
opt.fetchProductsPattern = (opt.fetchProductsPattern ||
new RegExp('.*/payments/.*/in-app/.*'));

// Respond to fetching the JWT.
helper.server.respondWith('POST', /.*\/webpay\/inapp\/prepare/,
helper.productData(opt.productData));
helper.server.respond();

if (opt.mozPay) {
opt.mozPay.returnValues[0].onsuccess();
}

// Respond to validating the transaction.
helper.server.respondWith('GET', /.*\/transaction\/XYZ/,
helper.transactionData({receipt: receipt}));
helper.server.respond();

// Respond to getting product info.
helper.server.respondWith('GET', opt.fetchProductsPattern,
[200, {"Content-Type": "application/json"},
JSON.stringify(helper.apiProduct)]);

helper.receiptAdd.onsuccess();
helper.server.respond();
};


exports.productData = function(overrides, status) {
// Create a JSON helper.server response to a request for product data.
overrides = overrides || {};
var data = {
webpayJWT: '<jwt>',
contribStatusURL: '/transaction/XYZ',
};
for (var k in data) {
if (overrides[k]) {
data[k] = overrides[k];
}
}
return [status || 200, {"Content-Type": "application/json"},
JSON.stringify(data)];
};


exports.transactionData = function(overrides, status) {
// Create a JSON helper.server response to a request for transaction data.
overrides = overrides || {};
var data = {
status: 'complete',
// Pretend this is a real Marketplace receipt.
receipt: '<keys>~<receipt>'
};
for (var k in data) {
if (overrides[k]) {
data[k] = overrides[k];
}
}
return [status || 200, {"Content-Type": "application/json"},
JSON.stringify(data)];
};


exports.receiptAdd = {
error: null,
_receipt: null,
Expand Down
Loading