зеркало из https://github.com/mozilla/fxa.git
Merge branch 'master' into train-141-merge
This commit is contained in:
Коммит
cd377a5bc8
20
README.md
20
README.md
|
@ -99,7 +99,7 @@ If you get an `error` status for any of the servers please verify that you insta
|
|||
> [libgmp](https://gmplib.org/),
|
||||
> [graphicsmagick](http://www.graphicsmagick.org/),
|
||||
> [docker](https://docs.docker.com/),
|
||||
> [grunt](https://github.com/gruntjs/grunt-cli)
|
||||
> [grunt](https://github.com/gruntjs/grunt-cli),
|
||||
> [gcloud CLI](https://cloud.google.com/sdk/)
|
||||
|
||||
##### OS X (with [Brew](http://brew.sh/)):
|
||||
|
@ -170,7 +170,7 @@ Download from [java.com/en/download/](https://www.java.com/en/download/)
|
|||
|
||||
> Rust Nightly is used for the fxa-email-service
|
||||
|
||||
#### Ubuntu
|
||||
##### Ubuntu and OS X
|
||||
|
||||
```
|
||||
curl https://sh.rustup.rs -sSf | sh
|
||||
|
@ -213,7 +213,19 @@ Available options:
|
|||
|
||||
**The following requires [the JDK](http://www.oracle.com/technetwork/java/javase/downloads/index-jsp-138363.html#javasejdk) and tests the local servers only.**
|
||||
|
||||
**Use `npm test` - all functional tests**
|
||||
To run all functional tests:
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
Note that as of 2019-07-08, running this command at the project root will fail ([see issue #725](https://github.com/mozilla/fxa/issues/725)). Instead, run the command in the server that needs to be tested.
|
||||
|
||||
To run a specific test or tests whose name matches part of a search string:
|
||||
|
||||
```
|
||||
node tests/intern.js --suites=all --grep="Test string to search for"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
@ -289,7 +301,7 @@ Once services have started, you can start MailDev on port 9999. You might have t
|
|||
sudo maildev -s 9999
|
||||
```
|
||||
|
||||
All emails sent can be viewed from `http://localhost:1080`.
|
||||
All emails sent can be viewed from [http://localhost:1080](http://localhost:1080).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -53,20 +53,6 @@
|
|||
"max_restarts": "1",
|
||||
"min_uptime": "2m"
|
||||
},
|
||||
{
|
||||
"name": "fxa-basket-proxy PORT 1114",
|
||||
"script": "./bin/basket-proxy-server.js",
|
||||
"cwd": "packages/fxa-basket-proxy",
|
||||
"max_restarts": "1",
|
||||
"min_uptime": "2m"
|
||||
},
|
||||
{
|
||||
"name": "fxa-basket-proxy fake basket server PORT 10140",
|
||||
"script": "./bin/fake-basket-server.js",
|
||||
"cwd": "packages/fxa-basket-proxy",
|
||||
"max_restarts": "1",
|
||||
"min_uptime": "2m"
|
||||
},
|
||||
{
|
||||
"name": "oauth-server PORT 9010",
|
||||
"script": "../../../_scripts/oauth_mysql.sh",
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -29,7 +29,7 @@
|
|||
"fxa-dev-launcher": "github:vladikoff/fxa-dev-launcher",
|
||||
"husky": "^2.5.0",
|
||||
"internal-ip": "1.2.0",
|
||||
"lerna": "^3.13.1",
|
||||
"lerna": "^3.15.0",
|
||||
"lint-staged": "^8.2.1",
|
||||
"madr": "^2.1.2",
|
||||
"node-fetch": "^2.3.0",
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -158,7 +158,7 @@ module.exports.generateTokens = async function generateTokens(grant) {
|
|||
}
|
||||
// Maybe also generate an idToken?
|
||||
if (grant.scope && grant.scope.contains(SCOPE_OPENID)) {
|
||||
result.id_token = await generateIdToken(grant, access);
|
||||
result.id_token = await generateIdToken(grant, result.access_token);
|
||||
}
|
||||
|
||||
amplitude('token.created', {
|
||||
|
@ -169,7 +169,7 @@ module.exports.generateTokens = async function generateTokens(grant) {
|
|||
return result;
|
||||
};
|
||||
|
||||
async function generateIdToken(grant, access) {
|
||||
async function generateIdToken(grant, accessToken) {
|
||||
var now = Math.floor(Date.now() / 1000);
|
||||
var claims = {
|
||||
sub: await sub(grant.userId, grant.clientId, grant.ppidSeed),
|
||||
|
@ -177,7 +177,7 @@ async function generateIdToken(grant, access) {
|
|||
//iss set in jwt.sign
|
||||
iat: now,
|
||||
exp: now + ID_TOKEN_EXPIRATION,
|
||||
at_hash: util.generateTokenHash(access.token),
|
||||
at_hash: util.generateTokenHash(accessToken),
|
||||
};
|
||||
if (grant.amr) {
|
||||
claims.amr = grant.amr;
|
||||
|
|
|
@ -29,15 +29,15 @@ const base64URLEncode = function base64URLEncode(buf) {
|
|||
* Generates a hash of the access token based on
|
||||
* http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
|
||||
*
|
||||
* This value is the hash of the ascii value of the access token, then the base64url
|
||||
* This hash of the access token, then the base64url
|
||||
* value of the left half.
|
||||
*
|
||||
* @param {Buffer} accessTokenBuf
|
||||
* @param {Buffer} accessToken The access token as seen by the client (hex form)
|
||||
* @returns {String}
|
||||
* @api public
|
||||
*/
|
||||
const generateTokenHash = function generateTokenHash(accessTokenBuf) {
|
||||
const hash = encrypt.hash(accessTokenBuf.toString('ascii'));
|
||||
const generateTokenHash = function generateTokenHash(accessToken) {
|
||||
const hash = encrypt.hash(accessToken);
|
||||
return base64URLEncode(hash.slice(0, hash.length / 2));
|
||||
};
|
||||
|
||||
|
|
|
@ -2272,9 +2272,7 @@ describe('/v1', function() {
|
|||
assert.equal(claims.acr, ACR);
|
||||
assert.equal(claims['fxa-aal'], AAL);
|
||||
|
||||
const at_hash = util.generateTokenHash(
|
||||
Buffer.from(res.result.access_token, 'hex')
|
||||
);
|
||||
const at_hash = util.generateTokenHash(res.result.access_token);
|
||||
assert.equal(claims.at_hash, at_hash);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -63,6 +63,10 @@ To change the default auth server edit `server/config/*.json` on your deployed i
|
|||
|
||||
**Note that testing with Selenium via Docker does _not_ work at present, so all testing must be carried out via your normal operating system's npm & Java tooling.**
|
||||
|
||||
### Unit Tests
|
||||
|
||||
If you want to test only the unit tests (not Selenium/function tests) you can visit http://127.0.0.1:3030/tests/index.html and you can select specific tests with something like http://127.0.0.1:3030/tests/index.html?grep=fxa-client
|
||||
|
||||
---
|
||||
|
||||
## Grunt Commands
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<svg width="141" height="18" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(3 3)" fill="none" fill-rule="evenodd">
|
||||
<circle stroke="#E0E0E6" stroke-width="3" cx="6" cy="6" r="7.5"/>
|
||||
<circle fill="#0090ED" cx="67.75" cy="6" r="3.75"/>
|
||||
<circle stroke="#E0E0E6" stroke-width="3" cx="67.5" cy="6" r="7.5"/>
|
||||
<circle stroke="#E0E0E6" stroke-width="3" cx="129" cy="6" r="7.5"/>
|
||||
<path d="M13.5 6.375H60M75 6.375h46.5" stroke="#E0E0E6" stroke-width="3"/>
|
||||
<g transform="translate(123)">
|
||||
<circle stroke="#005CE3" stroke-width="3" fill="#005CE3" cx="6" cy="6" r="7.5"/>
|
||||
<path
|
||||
d="M4.746 9.75a.626.626 0 0 1-.442-.184L2.426 7.69a.625.625 0 0 1 .885-.884l1.35 1.349 3.953-5.643a.626.626 0 0 1 1.026.717L5.26 9.482a.626.626 0 0 1-.458.267.54.54 0 0 1-.055 0z"
|
||||
fill="#FFF" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 927 B |
|
@ -0,0 +1,22 @@
|
|||
<svg width="141" height="18" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(3 3)" fill="none" fill-rule="evenodd">
|
||||
<circle fill="#0090ED" cx="6" cy="6" r="3.75"/>
|
||||
<circle stroke="#E0E0E6" stroke-width="3" cx="6" cy="6" r="7.5"/>
|
||||
<circle stroke="#E0E0E6" stroke-width="3" cx="67.5" cy="6" r="7.5"/>
|
||||
<circle stroke="#E0E0E6" stroke-width="3" cx="129" cy="6" r="7.5"/>
|
||||
<path d="M13.5 6.375H60" stroke="#E0E0E6" stroke-width="3"/>
|
||||
<path d="M75 6.375h46.5" stroke="#E0E0E6" stroke-width="3"/>
|
||||
<g transform="translate(62)">
|
||||
<circle stroke="#005CE3" stroke-width="3" fill="#005CE3" cx="6" cy="6" r="7.5"/>
|
||||
<path
|
||||
d="M4.746 9.75a.626.626 0 0 1-.442-.184L2.426 7.69a.625.625 0 0 1 .885-.884l1.35 1.349 3.953-5.643a.626.626 0 0 1 1.026.717L5.26 9.482a.626.626 0 0 1-.458.267.54.54 0 0 1-.055 0z"
|
||||
fill="#FFF" fill-rule="nonzero"/>
|
||||
</g>
|
||||
<g transform="translate(123)">
|
||||
<circle stroke="#005CE3" stroke-width="3" fill="#005CE3" cx="6" cy="6" r="7.5"/>
|
||||
<path
|
||||
d="M4.746 9.75a.626.626 0 0 1-.442-.184L2.426 7.69a.625.625 0 0 1 .885-.884l1.35 1.349 3.953-5.643a.626.626 0 0 1 1.026.717L5.26 9.482a.626.626 0 0 1-.458.267.54.54 0 0 1-.055 0z"
|
||||
fill="#FFF" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 1.4 KiB |
|
@ -362,6 +362,7 @@ Start.prototype = {
|
|||
oAuthClientId: this._config.oAuthClientId,
|
||||
profileClient: this._profileClient,
|
||||
sentryMetrics: this._sentryMetrics,
|
||||
subscriptionsConfig: this._config.subscriptions,
|
||||
storage: this._getUserStorageInstance(),
|
||||
uniqueUserId: this._getUniqueUserId(),
|
||||
}));
|
||||
|
|
|
@ -1274,6 +1274,29 @@ FxaClientWrapper.prototype = {
|
|||
* - `keyRotationTimestamp`
|
||||
*/
|
||||
getOAuthScopedKeyData: createClientDelegate('getOAuthScopedKeyData'),
|
||||
|
||||
/**
|
||||
* Get a list of active subscriptions with an OAuth access token.
|
||||
*
|
||||
* @param {String} token A token from the OAuth server.
|
||||
* @returns {Promise} A promise that will be fulfilled with a list of active
|
||||
* subscriptions.
|
||||
*/
|
||||
getActiveSubscriptions: createClientDelegate('getActiveSubscriptions'),
|
||||
|
||||
/**
|
||||
* Create a support ticket.
|
||||
*
|
||||
* @param {String} token A token from the OAuth server.
|
||||
* @param {Object} [supportTicket={}]
|
||||
* @param {String} [supportTicket.topic]
|
||||
* @param {String} [supportTicket.subject] Optional subject
|
||||
* @param {String} [supportTicket.message]
|
||||
* @returns {Promise} A promise that will be fulfilled with:
|
||||
* - `success`
|
||||
* - `ticket` OR `error`
|
||||
*/
|
||||
createSupportTicket: createClientDelegate('createSupportTicket'),
|
||||
};
|
||||
|
||||
export default FxaClientWrapper;
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
'use strict';
|
||||
|
||||
const PaymentServer = {
|
||||
navigateToPaymentServer(view, subscriptionsConfig, redirectPath) {
|
||||
const {
|
||||
managementClientId,
|
||||
managementScopes,
|
||||
managementTokenTTL,
|
||||
managementUrl,
|
||||
} = subscriptionsConfig;
|
||||
return view
|
||||
.getSignedInAccount()
|
||||
.createOAuthToken(managementClientId, {
|
||||
scope: managementScopes,
|
||||
ttl: managementTokenTTL,
|
||||
})
|
||||
.then(accessToken => {
|
||||
const url = `${managementUrl}/${redirectPath}#accessToken=${encodeURIComponent(
|
||||
accessToken.get('token')
|
||||
)}`;
|
||||
view.navigateAway(url);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default PaymentServer;
|
|
@ -51,6 +51,7 @@ import SignUpView from '../views/sign_up';
|
|||
import SmsSendView from '../views/sms_send';
|
||||
import SmsSentView from '../views/sms_sent';
|
||||
import Storage from './storage';
|
||||
import SubscriptionsProductRedirectView from '../views/subscriptions_product_redirect';
|
||||
import TwoStepAuthenticationView from '../views/settings/two_step_authentication';
|
||||
import VerificationReasons from './verification-reasons';
|
||||
import WhyConnectAnotherDeviceView from '../views/why_connect_another_device';
|
||||
|
@ -256,6 +257,9 @@ const Router = Backbone.Router.extend({
|
|||
WhyConnectAnotherDeviceView,
|
||||
SmsSendView
|
||||
),
|
||||
'subscriptions/products/:productId': createViewHandler(
|
||||
SubscriptionsProductRedirectView
|
||||
),
|
||||
'verify_email(/)': createViewHandler(CompleteSignUpView, {
|
||||
type: VerificationReasons.SIGN_UP,
|
||||
}),
|
||||
|
|
|
@ -97,6 +97,7 @@ const Account = Backbone.Model.extend(
|
|||
this._metrics = options.metrics;
|
||||
this._notifier = options.notifier;
|
||||
this._sentryMetrics = options.sentryMetrics;
|
||||
this._subscriptionsConfig = options.subscriptionsConfig;
|
||||
|
||||
// upgrade old `grantedPermissions` to the new `permissions`.
|
||||
this._upgradeGrantedPermissions();
|
||||
|
@ -984,6 +985,59 @@ const Account = Backbone.Model.extend(
|
|||
return this._fxaClient.attachedClients(this.get('sessionToken'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch the account's list of active subscriptions.
|
||||
*
|
||||
* @returns {Promise} - resolves with a list of subscription objects.
|
||||
*/
|
||||
fetchActiveSubscriptions() {
|
||||
return this._fetchShortLivedSubscriptionsOAuthToken().then(
|
||||
accessToken => {
|
||||
return this._fxaClient.getActiveSubscriptions(
|
||||
accessToken.get('token')
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch the account's list of active subscriptions.
|
||||
*
|
||||
* @param {Object} [supportTicket={}]
|
||||
* @param {String} [supportTicket.topic]
|
||||
* @param {String} [supportTicket.subject] Optional subject
|
||||
* @param {String} [supportTicket.message]
|
||||
* @returns {Promise} - resolves with:
|
||||
* - `success`
|
||||
* - `ticket` OR `error`
|
||||
*/
|
||||
createSupportTicket(supportTicket) {
|
||||
return this._fetchShortLivedSubscriptionsOAuthToken().then(
|
||||
accessToken => {
|
||||
return this._fxaClient.createSupportTicket(
|
||||
accessToken.get('token'),
|
||||
supportTicket
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch an access token with subscription management scopes and a lifetime
|
||||
* of 30 seconds.
|
||||
*
|
||||
* @returns {Promise<OAuthToken>}
|
||||
*/
|
||||
_fetchShortLivedSubscriptionsOAuthToken() {
|
||||
return this.createOAuthToken(
|
||||
this._subscriptionsConfig.managementClientId,
|
||||
{
|
||||
scope: this._subscriptionsConfig.managementScopes,
|
||||
ttl: 30,
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Disconnect a client from the account
|
||||
*
|
||||
|
|
|
@ -27,6 +27,7 @@ var User = Backbone.Model.extend({
|
|||
this._fxaClient = options.fxaClient;
|
||||
this._metrics = options.metrics;
|
||||
this._notifier = options.notifier;
|
||||
this._subscriptionsConfig = options.subscriptionsConfig;
|
||||
this._storage = options.storage || Storage.factory();
|
||||
|
||||
this.sentryMetrics = options.sentryMetrics;
|
||||
|
@ -139,6 +140,7 @@ var User = Backbone.Model.extend({
|
|||
oAuthClientId: this._oAuthClientId,
|
||||
profileClient: this._profileClient,
|
||||
sentryMetrics: this.sentryMetrics,
|
||||
subscriptionsConfig: this._subscriptionsConfig,
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<div id="main-content" class="card loading subscriptions-redirect">
|
||||
<section>
|
||||
<div class="spinner"></div>
|
||||
</section>
|
||||
</div>
|
|
@ -8,6 +8,7 @@ import Cocktail from 'cocktail';
|
|||
import FormView from '../form';
|
||||
import SettingsPanelMixin from '../mixins/settings-panel-mixin';
|
||||
import Template from 'templates/settings/subscription.mustache';
|
||||
import PaymentServer from '../../lib/payment-server';
|
||||
|
||||
const View = FormView.extend({
|
||||
template: Template,
|
||||
|
@ -19,30 +20,18 @@ const View = FormView.extend({
|
|||
},
|
||||
|
||||
initialize(options) {
|
||||
this._config = {};
|
||||
this._subscriptionsConfig = {};
|
||||
if (options && options.config && options.config.subscriptions) {
|
||||
this._config = options.config.subscriptions;
|
||||
this._subscriptionsConfig = options.config.subscriptions;
|
||||
}
|
||||
},
|
||||
|
||||
submit() {
|
||||
const {
|
||||
managementClientId,
|
||||
managementScopes,
|
||||
managementTokenTTL,
|
||||
managementUrl,
|
||||
} = this._config;
|
||||
return this.getSignedInAccount()
|
||||
.createOAuthToken(managementClientId, {
|
||||
scope: managementScopes,
|
||||
ttl: managementTokenTTL,
|
||||
})
|
||||
.then(accessToken => {
|
||||
const url = `${managementUrl}/subscriptions#accessToken=${encodeURIComponent(
|
||||
accessToken.get('token')
|
||||
)}`;
|
||||
this.navigateAway(url);
|
||||
});
|
||||
return PaymentServer.navigateToPaymentServer(
|
||||
this,
|
||||
this._subscriptionsConfig,
|
||||
'subscriptions'
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import FormView from './form';
|
||||
import Template from 'templates/subscriptions_redirect.mustache';
|
||||
import PaymentServer from '../lib/payment-server';
|
||||
|
||||
class SubscriptionsProductRedirectView extends FormView {
|
||||
mustAuth = true;
|
||||
template = Template;
|
||||
|
||||
initialize(options) {
|
||||
this._currentPage = options.currentPage;
|
||||
this._subscriptionsConfig = {};
|
||||
if (options && options.config && options.config.subscriptions) {
|
||||
this._subscriptionsConfig = options.config.subscriptions;
|
||||
}
|
||||
}
|
||||
|
||||
afterRender() {
|
||||
const searchQuery = this.window.location.search;
|
||||
const productId = this._currentPage.split('/').pop();
|
||||
const redirectPath = `products/${productId}${searchQuery}`;
|
||||
return PaymentServer.navigateToPaymentServer(
|
||||
this,
|
||||
this._subscriptionsConfig,
|
||||
redirectPath
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SubscriptionsProductRedirectView;
|
|
@ -275,14 +275,26 @@ a[data-visible-url^='http']::after {
|
|||
|
||||
.step-1 {
|
||||
background-image: image-url('trailhead/step-1.svg');
|
||||
|
||||
html[dir='rtl'] & {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
.step-2 {
|
||||
background-image: image-url('trailhead/step-2.svg');
|
||||
|
||||
html[dir='rtl'] & {
|
||||
background-image: image-url('trailhead/step-2-rtl.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.step-3 {
|
||||
background-image: image-url('trailhead/step-3.svg');
|
||||
|
||||
html[dir='rtl'] & {
|
||||
background-image: image-url('trailhead/step-3-rtl.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.step-4 {
|
||||
|
|
|
@ -48,7 +48,7 @@ button {
|
|||
background: $button-background-color-primary;
|
||||
color: $button-text-color-primary;
|
||||
|
||||
&:hover {
|
||||
&:hover:not:disabled {
|
||||
background: $button-background-color-primary-hover;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { assert } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
import Account from 'models/account';
|
||||
import PaymentServer from 'lib/payment-server';
|
||||
|
||||
describe('lib/payment-server-redirect', () => {
|
||||
var account;
|
||||
var view;
|
||||
var tokenMock;
|
||||
var config;
|
||||
|
||||
beforeEach(function() {
|
||||
account = new Account();
|
||||
config = {
|
||||
subscriptions: {
|
||||
managementClientId: 'MOCK_CLIENT_ID',
|
||||
managementScopes: 'MOCK_SCOPES',
|
||||
managementTokenTTL: 900,
|
||||
managementUrl: 'http://example.com',
|
||||
},
|
||||
};
|
||||
tokenMock = {
|
||||
get: () => 'MOCK_TOKEN',
|
||||
};
|
||||
sinon
|
||||
.stub(account, 'createOAuthToken')
|
||||
.callsFake(() => Promise.resolve(tokenMock));
|
||||
view = {
|
||||
getSignedInAccount: sinon.stub().callsFake(() => account),
|
||||
navigateAway: sinon.spy(),
|
||||
};
|
||||
});
|
||||
|
||||
it('redirects as expected', () => {
|
||||
const REDIRECT_PATH = 'example/path';
|
||||
PaymentServer.navigateToPaymentServer(
|
||||
view,
|
||||
config.subscriptions,
|
||||
REDIRECT_PATH
|
||||
).then(() => {
|
||||
assert.lengthOf(view.getSignedInAccount.args, 1);
|
||||
assert.deepEqual(
|
||||
account.createOAuthToken.args[0],
|
||||
[
|
||||
config.subscriptions.managementScopes,
|
||||
{
|
||||
//eslint-disable-next-line camelcase
|
||||
client_id: config.subscriptions.managementClientId,
|
||||
ttl: config.subscriptions.managementTokenTTL,
|
||||
},
|
||||
],
|
||||
'should make the correct call to account.createOAuthToken'
|
||||
);
|
||||
assert.deepEqual(
|
||||
view.navigateAway.args[0][0],
|
||||
`${config.subscriptions.managementUrl}/${REDIRECT_PATH}#accessToken=MOCK_TOKEN`,
|
||||
'should make the correct call to navigateAway'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -28,6 +28,7 @@ describe('models/account', function() {
|
|||
var oAuthClient;
|
||||
var profileClient;
|
||||
var relier;
|
||||
var subscriptionsConfig;
|
||||
|
||||
var CLIENT_ID = 'client_id';
|
||||
var EMAIL = 'user@example.domain';
|
||||
|
@ -63,6 +64,10 @@ describe('models/account', function() {
|
|||
oAuthClient = new OAuthClient();
|
||||
profileClient = new ProfileClient();
|
||||
relier = new Relier();
|
||||
subscriptionsConfig = {
|
||||
managementClientId: 'foxkeh',
|
||||
managementScopes: 'quux',
|
||||
};
|
||||
|
||||
account = new Account(
|
||||
{
|
||||
|
@ -77,6 +82,7 @@ describe('models/account', function() {
|
|||
oAuthClientId: CLIENT_ID,
|
||||
profileClient: profileClient,
|
||||
sentryMetrics: sentryMetrics,
|
||||
subscriptionsConfig,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -2864,4 +2870,59 @@ describe('models/account', function() {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_fetchShortLivedSubscriptionsOAuthToken', () => {
|
||||
it('calls createOAuthToken with the correct arguments', () => {
|
||||
const createOAuthTokenStub = sinon.stub(account, 'createOAuthToken');
|
||||
account._fetchShortLivedSubscriptionsOAuthToken();
|
||||
assert.isTrue(createOAuthTokenStub.calledOnce);
|
||||
assert.isTrue(
|
||||
createOAuthTokenStub.calledWith(
|
||||
subscriptionsConfig.managementClientId,
|
||||
{ scope: subscriptionsConfig.managementScopes, ttl: 30 }
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchActiveSubscriptions', () => {
|
||||
it('delegates to the fxa-client', () => {
|
||||
const token = 'tickettoride';
|
||||
const subs = [{ sid: 'foo' }];
|
||||
sinon.stub(account, 'createOAuthToken').callsFake(function() {
|
||||
return Promise.resolve(new OAuthToken({ token }));
|
||||
});
|
||||
const subsStub = sinon
|
||||
.stub(fxaClient, 'getActiveSubscriptions')
|
||||
.callsFake(function() {
|
||||
return Promise.resolve(subs);
|
||||
});
|
||||
|
||||
account.fetchActiveSubscriptions().then(resp => {
|
||||
assert.isTrue(subsStub.calledWith(token));
|
||||
assert.deepEqual(resp, subs);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSupportTicket', () => {
|
||||
it('delegates to the fxa-client', () => {
|
||||
const token = 'tickettoride';
|
||||
const ticket = { topic: 'TESTO', message: 'testo?' };
|
||||
const ticketResp = { success: true, ticket: 123 };
|
||||
sinon.stub(account, 'createOAuthToken').callsFake(function() {
|
||||
return Promise.resolve(new OAuthToken({ token }));
|
||||
});
|
||||
const ticketStub = sinon
|
||||
.stub(fxaClient, 'createSupportTicket')
|
||||
.callsFake(function() {
|
||||
return Promise.resolve(ticketResp);
|
||||
});
|
||||
|
||||
account.createSupportTicket(ticket).then(resp => {
|
||||
assert.isTrue(ticketStub.calledWith(token, ticket));
|
||||
assert.deepEqual(resp, ticketResp);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,19 +3,13 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import $ from 'jquery';
|
||||
import Account from 'models/account';
|
||||
import { assert } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
import User from 'models/user';
|
||||
import View from 'views/settings/subscription';
|
||||
import WindowMock from '../../../mocks/window';
|
||||
import PaymentServer from 'lib/payment-server';
|
||||
|
||||
describe('views/settings/subscription', function() {
|
||||
var account;
|
||||
var user;
|
||||
var view;
|
||||
var tokenMock;
|
||||
var windowMock;
|
||||
var config;
|
||||
|
||||
function render() {
|
||||
|
@ -23,9 +17,6 @@ describe('views/settings/subscription', function() {
|
|||
}
|
||||
|
||||
beforeEach(function() {
|
||||
user = new User();
|
||||
account = new Account();
|
||||
windowMock = new WindowMock();
|
||||
config = {
|
||||
subscriptions: {
|
||||
managementClientId: 'MOCK_CLIENT_ID',
|
||||
|
@ -34,23 +25,18 @@ describe('views/settings/subscription', function() {
|
|||
managementUrl: 'http://example.com',
|
||||
},
|
||||
};
|
||||
tokenMock = {
|
||||
get: () => 'MOCK_TOKEN',
|
||||
};
|
||||
|
||||
view = new View({ config });
|
||||
|
||||
sinon
|
||||
.stub(account, 'createOAuthToken')
|
||||
.callsFake(() => Promise.resolve(tokenMock));
|
||||
|
||||
view = new View({ config, user, window: windowMock });
|
||||
|
||||
sinon.stub(view, 'getSignedInAccount').callsFake(() => account);
|
||||
sinon.stub(view, 'navigateAway');
|
||||
.stub(PaymentServer, 'navigateToPaymentServer')
|
||||
.callsFake(() => Promise.resolve(true));
|
||||
|
||||
return render();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
PaymentServer.navigateToPaymentServer.restore();
|
||||
$(view.el).remove();
|
||||
view.destroy();
|
||||
view = null;
|
||||
|
@ -63,27 +49,10 @@ describe('views/settings/subscription', function() {
|
|||
});
|
||||
|
||||
describe('submit', () => {
|
||||
it('creates an OAuth token on submit', () => {
|
||||
it('calls PaymentServer.navigateToPaymentServer as expected', () => {
|
||||
return view.submit().then(() => {
|
||||
assert.lengthOf(view.getSignedInAccount.args, 1);
|
||||
assert.deepEqual(
|
||||
account.createOAuthToken.args[0],
|
||||
[
|
||||
config.subscriptions.managementClientId,
|
||||
{
|
||||
scope: config.subscriptions.managementScopes,
|
||||
ttl: config.subscriptions.managementTokenTTL,
|
||||
},
|
||||
],
|
||||
'should make the correct call to account.createOAuthToken'
|
||||
);
|
||||
assert.deepEqual(
|
||||
view.navigateAway.args[0],
|
||||
[
|
||||
`${config.subscriptions.managementUrl}/subscriptions#accessToken=MOCK_TOKEN`,
|
||||
],
|
||||
'should make the correct call to navigateAway'
|
||||
);
|
||||
const args = PaymentServer.navigateToPaymentServer.getCall(0).args;
|
||||
assert.deepEqual(args, [view, config.subscriptions, 'subscriptions']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import $ from 'jquery';
|
||||
import Account from 'models/account';
|
||||
import { assert } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
import User from 'models/user';
|
||||
import View from 'views/subscriptions_product_redirect';
|
||||
import Notifier from 'lib/channels/notifier';
|
||||
import WindowMock from '../../mocks/window';
|
||||
import PaymentServer from 'lib/payment-server';
|
||||
|
||||
const PRODUCT_ID = 'pk_8675309';
|
||||
const SEARCH_QUERY = '?plan_id=plk_12345';
|
||||
|
||||
describe('views/subscriptions_product_redirect', function() {
|
||||
let account;
|
||||
let user;
|
||||
let view;
|
||||
let windowMock;
|
||||
let config;
|
||||
let notifier;
|
||||
|
||||
function render() {
|
||||
return view.render().then(() => view.afterVisible());
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
user = new User();
|
||||
account = new Account();
|
||||
notifier = new Notifier();
|
||||
windowMock = new WindowMock();
|
||||
windowMock.location.search = SEARCH_QUERY;
|
||||
|
||||
config = {
|
||||
subscriptions: {
|
||||
managementClientId: 'MOCK_CLIENT_ID',
|
||||
managementScopes: 'MOCK_SCOPES',
|
||||
managementTokenTTL: 900,
|
||||
managementUrl: 'http://example.com',
|
||||
},
|
||||
};
|
||||
|
||||
sinon.stub(user, 'sessionStatus').callsFake(() => Promise.resolve(account));
|
||||
|
||||
view = new View({
|
||||
config,
|
||||
currentPage: `subscriptions/product/${PRODUCT_ID}`,
|
||||
notifier,
|
||||
user,
|
||||
window: windowMock,
|
||||
});
|
||||
|
||||
sinon.stub(view, 'getSignedInAccount').callsFake(() => account);
|
||||
|
||||
sinon
|
||||
.stub(PaymentServer, 'navigateToPaymentServer')
|
||||
.callsFake(() => Promise.resolve(true));
|
||||
|
||||
return render();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
PaymentServer.navigateToPaymentServer.restore();
|
||||
$(view.el).remove();
|
||||
view.destroy();
|
||||
view = null;
|
||||
});
|
||||
|
||||
describe('render', () => {
|
||||
it('renders correctly', () => {
|
||||
assert.lengthOf(view.$('.subscriptions-redirect'), 1);
|
||||
});
|
||||
|
||||
it('calls PaymentServer.navigateToPaymentServer as expected', () => {
|
||||
assert.deepEqual(PaymentServer.navigateToPaymentServer.args, [
|
||||
[view, config.subscriptions, `products/${PRODUCT_ID}${SEARCH_QUERY}`],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -58,6 +58,7 @@ require('./spec/lib/null-storage');
|
|||
require('./spec/lib/oauth-client');
|
||||
require('./spec/lib/oauth-errors');
|
||||
require('./spec/lib/pairing-channel-client');
|
||||
require('./spec/lib/payment-server');
|
||||
require('./spec/lib/profile-client');
|
||||
require('./spec/lib/router');
|
||||
require('./spec/lib/screen-info');
|
||||
|
@ -255,6 +256,7 @@ require('./spec/views/sign_up_password');
|
|||
require('./spec/views/sms_send');
|
||||
require('./spec/views/sms_sent');
|
||||
require('./spec/views/sub_panels');
|
||||
require('./spec/views/subscriptions_product_redirect');
|
||||
require('./spec/views/tooltip');
|
||||
require('./spec/views/tos');
|
||||
require('./spec/views/why_connect_another_device');
|
||||
|
|
|
@ -287,9 +287,9 @@
|
|||
}
|
||||
},
|
||||
"@babel/helpers": {
|
||||
"version": "7.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.5.2.tgz",
|
||||
"integrity": "sha512-NDkkTqDvgFUeo8djXBOiwO/mFjownznOWvmP9hvNdfiFUmx0nwNOqxuaTTbxjH744eQsD9M5ubC7gdANBvIWPw==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.5.4.tgz",
|
||||
"integrity": "sha512-6LJ6xwUEJP51w0sIgKyfvFMJvIb9mWAfohJp0+m6eHJigkFdcH8duZ1sfhn0ltJRzwUIT/yqqhdSfRpCpL7oow==",
|
||||
"requires": {
|
||||
"@babel/template": "^7.4.4",
|
||||
"@babel/traverse": "^7.5.0",
|
||||
|
@ -340,9 +340,9 @@
|
|||
}
|
||||
},
|
||||
"@babel/plugin-proposal-object-rest-spread": {
|
||||
"version": "7.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.2.tgz",
|
||||
"integrity": "sha512-C/JU3YOx5J4d9s0GGlJlYXVwsbd5JmqQ0AvB7cIDAx7nN57aDTnlJEsZJPuSskeBtMGFWSWU5Q+piTiDe0s7FQ==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.4.tgz",
|
||||
"integrity": "sha512-KCx0z3y7y8ipZUMAEEJOyNi11lMb/FOPUjjB113tfowgw0c16EGYos7worCKBcUAh2oG+OBnoUhsnTSoLpV9uA==",
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.0.0",
|
||||
"@babel/plugin-syntax-object-rest-spread": "^7.2.0"
|
||||
|
@ -823,9 +823,9 @@
|
|||
}
|
||||
},
|
||||
"@babel/runtime": {
|
||||
"version": "7.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.2.tgz",
|
||||
"integrity": "sha512-9M29wrrP7//JBGX70+IrDuD1w4iOYhUGpJNMQJVNAXue+cFeFlMTqBECouIziXPUphlgrfjcfiEpGX4t0WGK4g==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.4.tgz",
|
||||
"integrity": "sha512-Na84uwyImZZc3FKf4aUF1tysApzwf3p2yuFBIyBfbzT5glzKTdvYI4KVW4kcgjrzoGUjC7w3YyCHcJKaRxsr2Q==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.2"
|
||||
}
|
||||
|
@ -1191,9 +1191,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "12.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.0.tgz",
|
||||
"integrity": "sha512-dVeOVH/lhZ2Cki5Emh0aKeXUcWG1+EDTkqyzdgPe0ZjzgvBhzSFlogc6rm8uUd0I+XGK5fcp9DsMv5Wofe0/3w==",
|
||||
"version": "12.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.2.tgz",
|
||||
"integrity": "sha512-gojym4tX0FWeV2gsW4Xmzo5wxGjXGm550oVUII7f7G5o4BV6c7DBdiG1RRQd+y1bvqRyYtPfMK85UM95vsapqQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/platform": {
|
||||
|
@ -2829,9 +2829,9 @@
|
|||
"integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30000981",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000981.tgz",
|
||||
"integrity": "sha512-JTByHj4DQgL2crHNMK6PibqAMrqqb/Vvh0JrsTJVSWG4VSUrT16EklkuRZofurlMjgA9e+zlCM4Y39F3kootMQ=="
|
||||
"version": "1.0.30000983",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000983.tgz",
|
||||
"integrity": "sha512-/llD1bZ6qwNkt41AsvjsmwNOoA4ZB+8iqmf5LVyeSXuBODT/hAMFNVOh84NdUzoiYiSKqo5vQ3ZzeYHSi/olDQ=="
|
||||
},
|
||||
"capture-stack-trace": {
|
||||
"version": "1.0.1",
|
||||
|
@ -5792,9 +5792,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"fxa-common-password-list": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fxa-common-password-list/-/fxa-common-password-list-0.0.2.tgz",
|
||||
"integrity": "sha512-xFhM7lu0A6drtSbXKgs6GsgUbfq6uqY1PhNdZMU5P6jBjLLADEmUuE4LTAL+8F/hQD8GtoMDGflhHzJFJwW43w==",
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fxa-common-password-list/-/fxa-common-password-list-0.0.3.tgz",
|
||||
"integrity": "sha512-lRQX4EfMuiNACW4/L6Yr7gsAB2E1kSZBiP5qDV/l+2RkFazpDdtqBEN5piQYyECr1qw18VLt/+M3agF3ZDFtLA==",
|
||||
"requires": {
|
||||
"bloomfilter": "0.0.18"
|
||||
}
|
||||
|
@ -5830,9 +5830,9 @@
|
|||
}
|
||||
},
|
||||
"fxa-js-client": {
|
||||
"version": "1.0.14",
|
||||
"resolved": "https://registry.npmjs.org/fxa-js-client/-/fxa-js-client-1.0.14.tgz",
|
||||
"integrity": "sha512-Yh5OhC+/WS3fAdWpLC/4zlRGDXRaB86lom72+aqUqMQ2/Rz+0vhdS1dWQKfofkfg5IYkPTULump+r36e/pGvMw==",
|
||||
"version": "1.0.15",
|
||||
"resolved": "https://registry.npmjs.org/fxa-js-client/-/fxa-js-client-1.0.15.tgz",
|
||||
"integrity": "sha512-BEcPZbOB/Yr5zlO+fJgoNKR9T4kk+GO1Ai0X304i0H2/vP0RTzn3Cnq2Ew568M/ltPpcDUfQK0D5p0kl1hi5Lg==",
|
||||
"requires": {
|
||||
"es6-promise": "4.1.1",
|
||||
"sjcl": "git://github.com/bitwiseshiftleft/sjcl.git#a03ea8e",
|
||||
|
@ -8351,9 +8351,9 @@
|
|||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.11",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
||||
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
|
||||
"version": "4.17.13",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.13.tgz",
|
||||
"integrity": "sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA=="
|
||||
},
|
||||
"lodash.assign": {
|
||||
"version": "4.2.0",
|
||||
|
@ -8404,9 +8404,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"lodash.merge": {
|
||||
"version": "4.6.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz",
|
||||
"integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ=="
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||
},
|
||||
"lodash.omit": {
|
||||
"version": "4.5.0",
|
||||
|
@ -12514,9 +12514,9 @@
|
|||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
||||
},
|
||||
"typescript": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz",
|
||||
"integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA=="
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz",
|
||||
"integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g=="
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "git://github.com/vladikoff/ua-parser-js.git#643d1698aef5bed095e1264ae258902bf346175c",
|
||||
|
|
|
@ -61,10 +61,10 @@
|
|||
"express": "4.16.2",
|
||||
"extract-loader": "2.0.1",
|
||||
"file-loader": "1.1.11",
|
||||
"fxa-common-password-list": "0.0.2",
|
||||
"fxa-common-password-list": "^0.0.3",
|
||||
"fxa-crypto-relier": "2.3.0",
|
||||
"fxa-geodb": "1.0.4",
|
||||
"fxa-js-client": "^1.0.14",
|
||||
"fxa-js-client": "^1.0.15",
|
||||
"fxa-mustache-loader": "0.0.2",
|
||||
"fxa-pairing-channel": "1.0.1",
|
||||
"fxa-shared": "1.0.26",
|
||||
|
@ -97,7 +97,7 @@
|
|||
"jsxgettext-recursive-next": "1.1.0",
|
||||
"legal-docs": "git://github.com/mozilla/legal-docs.git#master",
|
||||
"load-grunt-tasks": "3.5.2",
|
||||
"lodash": "4.17.11",
|
||||
"lodash": "4.17.13",
|
||||
"mailcheck": "1.1.1",
|
||||
"mkdirp": "0.5.1",
|
||||
"mocha": "4.0.1",
|
||||
|
|
|
@ -73,6 +73,7 @@ module.exports = function() {
|
|||
'sms/sent',
|
||||
'sms/sent/why',
|
||||
'sms/why',
|
||||
'subscriptions/products/[\\w_]+',
|
||||
'verify_email',
|
||||
'verify_primary_email',
|
||||
'verify_secondary_email',
|
||||
|
|
|
@ -75,6 +75,7 @@ var routes = {
|
|||
'/signup_confirmed': { statusCode: 200 },
|
||||
'/signup_permissions': { statusCode: 200 },
|
||||
'/signup_verified': { statusCode: 200 },
|
||||
'/subscriptions/products/123doneProProduct': { statusCode: 200 },
|
||||
'/sms': { statusCode: 200 },
|
||||
'/sms/sent': { statusCode: 200 },
|
||||
'/sms/sent/why': { statusCode: 200 },
|
||||
|
|
|
@ -3171,9 +3171,9 @@
|
|||
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
|
||||
},
|
||||
"lodash.merge": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.0.tgz",
|
||||
"integrity": "sha1-aYhLoUSsM/5plzemCG3v+t0PicU="
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||
},
|
||||
"lodash.snakecase": {
|
||||
"version": "4.1.1",
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
"ip": "1.1.3",
|
||||
"ip-reputation-js-client": "4.1.0",
|
||||
"lodash.isequal": "4.5.0",
|
||||
"lodash.merge": "4.6.0",
|
||||
"lodash.merge": "4.6.2",
|
||||
"memcached": "2.2.1",
|
||||
"newrelic": "4.1.0",
|
||||
"raven": "2.3.0",
|
||||
|
|
|
@ -751,9 +751,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz",
|
||||
"integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==",
|
||||
"version": "3.13.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
|
||||
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
|
@ -789,9 +789,9 @@
|
|||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.11",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
||||
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
|
||||
"version": "4.17.14",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz",
|
||||
"integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.get": {
|
||||
|
|
|
@ -16,9 +16,11 @@ USER app
|
|||
COPY package.json package.json
|
||||
COPY package-lock.json package-lock.json
|
||||
|
||||
RUN npm ci --production && rm -rf ~app/.npm /tmp/*
|
||||
RUN npm ci && rm -rf ~app/.npm /tmp/*
|
||||
|
||||
COPY . /app
|
||||
RUN npm run build
|
||||
|
||||
USER root
|
||||
RUN chown app:app /app/config
|
||||
|
||||
|
|
|
@ -51,7 +51,28 @@ async function main() {
|
|||
logger,
|
||||
webhookService
|
||||
);
|
||||
await server.start();
|
||||
try {
|
||||
await server.start();
|
||||
} catch (err) {
|
||||
logger.error('startup', { err });
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.on('uncaughtException', err => {
|
||||
logger.error('uncaughtException', { err });
|
||||
process.exit(8);
|
||||
});
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
logger.error('unhandledRejection', { error: reason });
|
||||
process.exit();
|
||||
});
|
||||
process.on('SIGINT', shutdown);
|
||||
|
||||
function shutdown() {
|
||||
server.stop({ timeout: 10_000 }).then(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
|
@ -17,6 +17,10 @@ export default class ProxyController {
|
|||
private readonly jwt: JWT
|
||||
) {}
|
||||
|
||||
public async heartbeat(request: hapi.Request, h: hapi.ResponseToolkit) {
|
||||
return h.response({}).code(200);
|
||||
}
|
||||
|
||||
public async proxyDelivery(request: hapi.Request, h: hapi.ResponseToolkit) {
|
||||
const webhookData = this.webhookService.serviceData();
|
||||
const clientId = request.params.clientId;
|
||||
|
@ -66,6 +70,8 @@ export default class ProxyController {
|
|||
|
||||
let resp = h.response(response.body).code(response.statusCode);
|
||||
|
||||
this.logger.debug('proxyDeliverSuccess', { statusCode: response.statusCode });
|
||||
|
||||
// Copy the headers over to our hapi Response
|
||||
Object.entries(response.headers).map(([name, value]) => {
|
||||
if (!value) {
|
||||
|
|
|
@ -12,6 +12,7 @@ export const proxyPayloadValidator = joi
|
|||
data: joi.string().required(),
|
||||
messageId: joi.string().required()
|
||||
})
|
||||
.unknown(true)
|
||||
.required(),
|
||||
subscription: joi.string().required()
|
||||
})
|
||||
|
|
|
@ -20,15 +20,22 @@ export default function(
|
|||
const proxyController = new ProxyController(logger, webhookService, jwt);
|
||||
server.bind(proxyController);
|
||||
|
||||
server.route({
|
||||
method: 'POST',
|
||||
options: {
|
||||
auth: 'pubsub',
|
||||
handler: proxyController.proxyDelivery,
|
||||
validate: {
|
||||
payload: proxyPayloadValidator as hapiJoi.ObjectSchema
|
||||
}
|
||||
server.route([
|
||||
{
|
||||
handler: proxyController.heartbeat,
|
||||
method: 'GET',
|
||||
path: '/__lbheartbeat__'
|
||||
},
|
||||
path: '/v1/proxy/{clientId}'
|
||||
});
|
||||
{
|
||||
method: 'POST',
|
||||
options: {
|
||||
auth: 'pubsub',
|
||||
handler: proxyController.proxyDelivery,
|
||||
validate: {
|
||||
payload: proxyPayloadValidator as hapiJoi.ObjectSchema
|
||||
}
|
||||
},
|
||||
path: '/v1/proxy/{clientId}'
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -26,6 +26,8 @@ const BASE_MESSAGE_SCHEMA = joi
|
|||
.unknown(true)
|
||||
.required();
|
||||
|
||||
type baseMessageSchema = joi.Literal<typeof BASE_MESSAGE_SCHEMA>;
|
||||
|
||||
const LOGIN_SCHEMA = joi
|
||||
.object()
|
||||
.keys({
|
||||
|
@ -46,6 +48,8 @@ const LOGIN_SCHEMA = joi
|
|||
.unknown(true)
|
||||
.required();
|
||||
|
||||
type loginSchema = joi.Literal<typeof LOGIN_SCHEMA>;
|
||||
|
||||
const SUBSCRIPTION_UPDATE_SCHEMA = joi
|
||||
.object()
|
||||
.keys({
|
||||
|
@ -65,6 +69,8 @@ const SUBSCRIPTION_UPDATE_SCHEMA = joi
|
|||
.unknown(true)
|
||||
.required();
|
||||
|
||||
type subscriptionUpdateSchema = joi.Literal<typeof SUBSCRIPTION_UPDATE_SCHEMA>;
|
||||
|
||||
class ServiceNotificationProcessor {
|
||||
public readonly app: Consumer;
|
||||
private readonly db: Datastore;
|
||||
|
@ -126,16 +132,34 @@ class ServiceNotificationProcessor {
|
|||
|
||||
private async handleMessage(sqsMessage: SQS.Message) {
|
||||
const body = JSON.parse(sqsMessage.Body || '{}');
|
||||
const message = joi.attempt(body, BASE_MESSAGE_SCHEMA);
|
||||
let message: baseMessageSchema;
|
||||
try {
|
||||
message = joi.attempt(body, BASE_MESSAGE_SCHEMA);
|
||||
} catch (err) {
|
||||
this.logger.error('badBaseMessage', { err });
|
||||
return;
|
||||
}
|
||||
switch (message.event) {
|
||||
case LOGIN_EVENT: {
|
||||
const loginMessage = joi.attempt(message, LOGIN_SCHEMA);
|
||||
let loginMessage: loginSchema;
|
||||
try {
|
||||
loginMessage = joi.attempt(message, LOGIN_SCHEMA);
|
||||
} catch (err) {
|
||||
this.logger.error('badLoginMessage', { err });
|
||||
return;
|
||||
}
|
||||
await this.db.storeLogin(loginMessage.uid, loginMessage.clientId);
|
||||
this.logger.debug('sqs.loginEvent', loginMessage);
|
||||
return;
|
||||
}
|
||||
case SUBSCRIPTION_UPDATE_EVENT: {
|
||||
const subMessage = joi.attempt(message, SUBSCRIPTION_UPDATE_SCHEMA);
|
||||
let subMessage: subscriptionUpdateSchema;
|
||||
try {
|
||||
subMessage = joi.attempt(message, SUBSCRIPTION_UPDATE_SCHEMA);
|
||||
} catch (err) {
|
||||
this.logger.error('badSubscriptionUpdateMessage', { err });
|
||||
return;
|
||||
}
|
||||
const clientIds = await this.db.fetchClientIds(subMessage.uid);
|
||||
|
||||
this.logger.debug('sqs.subEvent', subMessage);
|
||||
|
|
|
@ -96,6 +96,14 @@ describe('Proxy Controller', () => {
|
|||
await server.stop();
|
||||
});
|
||||
|
||||
it('has a heartbeat', async () => {
|
||||
const result = await server.inject({
|
||||
method: 'GET',
|
||||
url: '/__lbheartbeat__'
|
||||
});
|
||||
cassert.equal(result.statusCode, 200);
|
||||
});
|
||||
|
||||
it('notifies successfully on subscription state change', async () => {
|
||||
mockWebhook();
|
||||
const message = createValidMessage();
|
||||
|
|
|
@ -124,22 +124,22 @@ describe('ServiceNotificationProcessor', () => {
|
|||
assert.calledWith(db.fetchClientIds as SinonSpy);
|
||||
});
|
||||
|
||||
it('throws an error on invalid login message', async () => {
|
||||
it('logs an error on invalid login message', async () => {
|
||||
updateStubMessage(Object.assign({}, { ...baseLoginMessage, email: false }));
|
||||
consumer.start();
|
||||
await pEvent(consumer.app, 'processing_error');
|
||||
await pEvent(consumer.app, 'message_processed');
|
||||
consumer.stop();
|
||||
assert.calledOnce(logger.error as SinonSpy);
|
||||
cassert.equal((logger.error as SinonSpy).getCalls()[0].args[0], 'processingError');
|
||||
cassert.equal((logger.error as SinonSpy).getCalls()[0].args[0], 'badLoginMessage');
|
||||
});
|
||||
|
||||
it('throws an error on invalid subscription message', async () => {
|
||||
it('logs an error on invalid subscription message', async () => {
|
||||
updateStubMessage(Object.assign({}, { ...baseSubscriptionUpdateMessage, productName: false }));
|
||||
consumer.start();
|
||||
await pEvent(consumer.app, 'processing_error');
|
||||
await pEvent(consumer.app, 'message_processed');
|
||||
consumer.stop();
|
||||
assert.calledOnce(logger.error as SinonSpy);
|
||||
assert.calledWithMatch(logger.error as SinonSpy, 'processingError');
|
||||
assert.calledWithMatch(logger.error as SinonSpy, 'badSubscriptionUpdateMessage');
|
||||
});
|
||||
|
||||
it('logs on message its not interested in', async () => {
|
||||
|
|
|
@ -2528,6 +2528,69 @@ define([
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a user's list of active subscriptions.
|
||||
*
|
||||
* @param {String} token A token from the OAuth server.
|
||||
* @returns {Promise} A promise that will be fulfilled with a list of active
|
||||
* subscriptions.
|
||||
*/
|
||||
FxAccountClient.prototype.getActiveSubscriptions = function(token) {
|
||||
var self = this;
|
||||
|
||||
return Promise.resolve().then(function() {
|
||||
required(token, 'token');
|
||||
const requestOptions = {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
};
|
||||
return self.request.send(
|
||||
'/oauth/subscriptions/active',
|
||||
'GET',
|
||||
null,
|
||||
null,
|
||||
requestOptions
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Submit a support ticket.
|
||||
*
|
||||
* @param {String} authorizationHeader A token from the OAuth server.
|
||||
* @param {Object} [supportTicket={}]
|
||||
* @param {String} [supportTicket.topic]
|
||||
* @param {String} [supportTicket.subject] Optional subject
|
||||
* @param {String} [supportTicket.message]
|
||||
* @returns {Promise} A promise that will be fulfilled with:
|
||||
* - `success`
|
||||
* - `ticket` OR `error`
|
||||
*/
|
||||
FxAccountClient.prototype.createSupportTicket = function(
|
||||
token,
|
||||
supportTicket
|
||||
) {
|
||||
var self = this;
|
||||
|
||||
return Promise.resolve().then(function() {
|
||||
required(token, 'token');
|
||||
required(supportTicket, 'supportTicket');
|
||||
const requestOptions = {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
};
|
||||
return self.request.send(
|
||||
'/support/ticket',
|
||||
'POST',
|
||||
null,
|
||||
supportTicket,
|
||||
requestOptions
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check for a required argument. Exposed for unit testing.
|
||||
*
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "fxa-js-client",
|
||||
"version": "1.0.14",
|
||||
"version": "1.0.15",
|
||||
"description": "Web client that talks to the Firefox Accounts API server",
|
||||
"author": "Mozilla",
|
||||
"license": "MPL-2.0",
|
||||
|
|
|
@ -25,6 +25,7 @@ define([
|
|||
'tests/lib/signIn',
|
||||
'tests/lib/signinCodes',
|
||||
'tests/lib/signUp',
|
||||
'tests/lib/subscriptions',
|
||||
'tests/lib/totp',
|
||||
'tests/lib/tokenCodes',
|
||||
'tests/lib/sms',
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
define([
|
||||
'intern!tdd',
|
||||
'intern/chai!assert',
|
||||
'tests/addons/environment',
|
||||
], function(tdd, assert, Environment) {
|
||||
var env = new Environment();
|
||||
if (env.useRemoteServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
with (tdd) {
|
||||
suite('subscriptions', function() {
|
||||
var accountHelper;
|
||||
var respond;
|
||||
var client;
|
||||
var RequestMocks;
|
||||
|
||||
beforeEach(function() {
|
||||
env = new Environment();
|
||||
accountHelper = env.accountHelper;
|
||||
respond = env.respond;
|
||||
client = env.client;
|
||||
RequestMocks = env.RequestMocks;
|
||||
});
|
||||
|
||||
test('#getActiveSubscriptions - missing token', function() {
|
||||
return accountHelper
|
||||
.newVerifiedAccount()
|
||||
.then(function(account) {
|
||||
return respond(
|
||||
client.getActiveSubscriptions(),
|
||||
RequestMocks.getActiveSubscriptions
|
||||
);
|
||||
})
|
||||
.then(assert.notOk, function(error) {
|
||||
assert.include(error.message, 'Missing token');
|
||||
});
|
||||
});
|
||||
test('#getActiveSubscriptions', function() {
|
||||
return accountHelper
|
||||
.newVerifiedAccount()
|
||||
.then(function(account) {
|
||||
return respond(
|
||||
client.getActiveSubscriptions('saynomore'),
|
||||
RequestMocks.getActiveSubscriptions
|
||||
);
|
||||
})
|
||||
.then(function(resp) {
|
||||
assert.ok(resp);
|
||||
}, assert.notOk);
|
||||
});
|
||||
|
||||
test('#createSupportTicket - missing token', function() {
|
||||
return accountHelper
|
||||
.newVerifiedAccount()
|
||||
.then(function(account) {
|
||||
return respond(
|
||||
client.createSupportTicket(),
|
||||
RequestMocks.createSupportTicket
|
||||
);
|
||||
})
|
||||
.then(assert.notOk, function(error) {
|
||||
assert.include(error.message, 'Missing token');
|
||||
});
|
||||
});
|
||||
test('#createSupportTicket - missing supportTicket', function() {
|
||||
return accountHelper
|
||||
.newVerifiedAccount()
|
||||
.then(function(account) {
|
||||
return respond(
|
||||
client.createSupportTicket('redpandas'),
|
||||
RequestMocks.createSupportTicket
|
||||
);
|
||||
})
|
||||
.then(assert.notOk, function(error) {
|
||||
assert.include(error.message, 'Missing supportTicket');
|
||||
});
|
||||
});
|
||||
test('#createSupportTicket', function() {
|
||||
return accountHelper
|
||||
.newVerifiedAccount()
|
||||
.then(function(account) {
|
||||
return respond(
|
||||
client.createSupportTicket('redpandas', {
|
||||
topic: 'Species',
|
||||
subject: 'Cute & Rare',
|
||||
message: 'Need moar',
|
||||
}),
|
||||
RequestMocks.createSupportTicket
|
||||
);
|
||||
})
|
||||
.then(function(resp) {
|
||||
assert.ok(resp);
|
||||
}, assert.notOk);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
|
@ -509,5 +509,13 @@ define(['client/lib/errors', 'tests/lib/push-constants'], function(
|
|||
status: 200,
|
||||
body: '{"exists": true}',
|
||||
},
|
||||
getActiveSubscriptions: {
|
||||
status: 200,
|
||||
body: '[{"subscriptionId": 9},{"subscriptionId": 12}]',
|
||||
},
|
||||
createSupportTicket: {
|
||||
status: 200,
|
||||
body: '{"success": true, "ticket": "abc123xyz"}',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -8,6 +8,10 @@ This project uses [Storybook](https://storybook.js.org/) to show each screen wit
|
|||
|
||||
You can view the Storybook built from the most recent master at http://mozilla.github.io/fxa/fxa-payments-server/
|
||||
|
||||
## Installation notes
|
||||
|
||||
On Mac OS, `npm run test` may trigger an `EMFILE` error. In this case, to get tests running, you may need to `brew install watchman`. (If the watchman postinstall step fails, follow the instructions [here](https://stackoverflow.com/a/41320226) to change `/usr/local` ownership from root to your user account.)
|
||||
|
||||
## License
|
||||
|
||||
MPL-2.0
|
||||
|
|
Двоичные данные
packages/fxa-payments-server/public/favicon.ico
Двоичные данные
packages/fxa-payments-server/public/favicon.ico
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 66 KiB После Ширина: | Высота: | Размер: 15 KiB |
|
@ -1,6 +1,6 @@
|
|||
#dialogs {
|
||||
.modal {
|
||||
padding: 64px 27px;
|
||||
padding: 25px 27px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,8 @@ function init() {
|
|||
.add('in progress', () => <Subject inProgress={true} />)
|
||||
.add('all invalid', () => {
|
||||
const state = mockValidatorState();
|
||||
|
||||
|
||||
// HACK: pre-seed with some error messages for display purposes
|
||||
state.fields.name.valid = false;
|
||||
state.fields.name.error = 'Please enter your name';
|
||||
state.fields.zip.valid = false;
|
||||
|
@ -24,10 +25,10 @@ function init() {
|
|||
state.fields.creditCardNumber.valid = false;
|
||||
state.fields.creditCardNumber.error = 'Your card number is incomplete';
|
||||
state.fields.expDate.valid = false;
|
||||
state.fields.expDate.error = 'Your card\'s expiration date is incomplete.';
|
||||
state.fields.expDate.error = 'Your card\'s expiration date is incomplete';
|
||||
state.fields.cvc.valid = false;
|
||||
state.fields.cvc.error = 'Your card\'s security code is incomplete.';
|
||||
|
||||
state.fields.cvc.error = 'Your card\'s security code is incomplete';
|
||||
|
||||
return<Subject validatorInitialState={state} />;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -22,8 +22,9 @@ const STRIPE_ELEMENT_STYLES = {
|
|||
base: {
|
||||
//TODO: Figure out what this really should be - I just copied it from computed styles because CSS can't apply through the iframe
|
||||
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
||||
fontSize: '16px',
|
||||
lineHeight: '48px',
|
||||
fontSize: '13px',
|
||||
fontWeight: '500',
|
||||
lineHeight: '45px',
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -32,7 +33,7 @@ export type PaymentFormProps = {
|
|||
confirm?: boolean,
|
||||
plan?: Plan,
|
||||
onCancel?: () => void,
|
||||
onPayment: (tokenResponse: stripe.TokenResponse) => void,
|
||||
onPayment: (tokenResponse: stripe.TokenResponse, name: string) => void,
|
||||
onPaymentError: (error: any) => void,
|
||||
validatorInitialState?: ValidatorState,
|
||||
validatorMiddlewareReducer?: ValidatorMiddlewareReducer,
|
||||
|
@ -63,7 +64,7 @@ export const PaymentForm = ({
|
|||
if (stripe) {
|
||||
stripe
|
||||
.createToken({ name, address_zip: zip })
|
||||
.then(onPayment)
|
||||
.then((tokenResponse: stripe.TokenResponse) => onPayment(tokenResponse, name))
|
||||
.catch(onPaymentError);
|
||||
}
|
||||
}, [ validator, onPayment, onPaymentError, stripe ]);
|
||||
|
@ -85,28 +86,27 @@ export const PaymentForm = ({
|
|||
<FieldGroup>
|
||||
|
||||
<StripeElement component={CardNumberElement}
|
||||
name="creditCardNumber" label="Credit Card Number"
|
||||
name="creditCardNumber" label="Card number"
|
||||
style={STRIPE_ELEMENT_STYLES}
|
||||
className="input-row input-row--xl" required />
|
||||
|
||||
<StripeElement component={CardExpiryElement}
|
||||
name="expDate" label="Exp. Date"
|
||||
name="expDate" label="Exp. date"
|
||||
style={STRIPE_ELEMENT_STYLES} required />
|
||||
|
||||
<StripeElement component={CardCVCElement}
|
||||
name="cvc" label="CVC"
|
||||
style={STRIPE_ELEMENT_STYLES} required />
|
||||
|
||||
<Input type="number" name="zip" label="Zip Code" maxLength={5} required
|
||||
<Input type="number" name="zip" label="Zip code" maxLength={5} required
|
||||
data-testid="zip"
|
||||
onValidate={value => {
|
||||
let error = null;
|
||||
if (value !== null) {
|
||||
value = ('' + value).substr(0, 5);
|
||||
if (! value) {
|
||||
if (!value) {
|
||||
error = 'Zip code is required';
|
||||
}
|
||||
if (value.length !== 5) {
|
||||
} else if (value.length !== 5) {
|
||||
error = 'Zip code is too short';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -157,7 +157,13 @@ export const StripeElement = (props: StripeElementProps) => {
|
|||
(value: stripe.elements.ElementChangeResponse) => {
|
||||
if (value !== null) {
|
||||
if (value.error && value.error.message) {
|
||||
validator.updateField({ name, value, valid: false, error: value.error.message });
|
||||
let error = value.error.message;
|
||||
// Issue #1718 - remove periods from error messages from Stripe
|
||||
// for consistency with our own errors
|
||||
if (error.endsWith('.')) {
|
||||
error = error.slice(0, -1);
|
||||
}
|
||||
validator.updateField({ name, value, valid: false, error });
|
||||
} else if (value.complete) {
|
||||
validator.updateField({ name, value, valid: true });
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import 'jest-dom/extend-expect';
|
||||
import { getErrorMessage, BASIC_ERROR, PAYMENT_ERROR_1 } from './errors';
|
||||
|
||||
it('returns the basic error text if not predefined error type', () => {
|
||||
expect(getErrorMessage("NON_PREDEFINED_ERROR")).toEqual(BASIC_ERROR);
|
||||
});
|
||||
|
||||
it('returns the payment error text for the correct error type', () => {
|
||||
expect(getErrorMessage("approve_with_id")).toEqual(PAYMENT_ERROR_1);
|
||||
});
|
||||
|
|
@ -1,9 +1,110 @@
|
|||
// ref: fxa-auth-server/lib/error.js
|
||||
export const AuthServerErrno = {
|
||||
const AuthServerErrno = {
|
||||
UNKNOWN_SUBSCRIPTION_CUSTOMER: 176,
|
||||
UNKNOWN_SUBSCRIPTION: 177,
|
||||
UNKNOWN_SUBSCRIPTION_PLAN: 178,
|
||||
REJECTED_SUBSCRIPTION_PAYMENT_TOKEN: 179,
|
||||
SUBSCRIPTION_ALREADY_CANCELLED: 180,
|
||||
REJECTED_CUSTOMER_UPDATE: 181,
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* Todos:
|
||||
* - L10N
|
||||
* - handle General SubHub subscription creation failure on submit
|
||||
* (network issue, server error)
|
||||
* - handle Payment token not valid
|
||||
*/
|
||||
|
||||
const BASIC_ERROR = "Hmm, we're having trouble with our system. We're working on fixing it for you and apologize for the inconvenience. Please try again later.";
|
||||
const PAYMENT_ERROR_1 = "Hmm. There was a problem authorizing your payment. Try again or get in touch with your card issuer.";
|
||||
const PAYMENT_ERROR_2 = "Hmm. There was a problem authorizing your payment. Get in touch with your card issuer.";
|
||||
|
||||
let errorMessageIndex: { [key: string]: string } = {
|
||||
"expired_card": "It looks like your credit card has expired. Try another card.",
|
||||
"insufficient_funds": "It looks like your card has insufficient funds. Try another card.",
|
||||
"withdrawal_count_limit_exceeded": "It looks like this transaction will put you over your credit limit. Try another card.",
|
||||
"charge_exceeds_source_limit": "It looks like this transaction will put you over your daily credit limit. Try another card or in 24 hours.",
|
||||
"instant_payouts_unsupported": "It looks like your debit card isn't setup for instant payments. Try another debit or credit card.",
|
||||
"duplicate_transaction": "Hmm. Looks like an identical transaction was just sent. Check your payment history.",
|
||||
"coupon_expired": "It looks like that promo code has expired.",
|
||||
// todo: handle "parameters_exclusive": "Your already subscribed to _product_"
|
||||
};
|
||||
|
||||
const basicErrors = ["api_key_expired",
|
||||
"platform_api_key_expired",
|
||||
"rate_limit",
|
||||
"UNKNOWN", // TODO: General SubHub subscription creation failure on submit (network issue, server error)
|
||||
"api_connection_error",
|
||||
"api_error",
|
||||
"invalid_request_error",
|
||||
"UNKNOWN", // TODO: Payment token not valid
|
||||
"state_unsupported",
|
||||
"invalid_source_usage",
|
||||
"invoice_no_customer_line_items",
|
||||
"invoice_no_subscription_line_items",
|
||||
"invoice_not_editable",
|
||||
"invoice_upcoming_none",
|
||||
"missing",
|
||||
"order_creation_failed",
|
||||
"order_required_settings",
|
||||
"order_status_invalid",
|
||||
"order_upstream_timeout",
|
||||
"payment_intent_incompatible_payment_method",
|
||||
"payment_intent_unexpected_state",
|
||||
"payment_method_unactivated",
|
||||
"payment_method_unexpected_state",
|
||||
"payouts_not_allowed",
|
||||
"resource_already_exists",
|
||||
"resource_missing",
|
||||
"secret_key_required",
|
||||
"sepa_unsupported_account",
|
||||
"shipping_calculation_failed",
|
||||
"tax_id_invalid",
|
||||
"taxes_calculation_failed",
|
||||
"tls_version_unsupported",
|
||||
"token_already_used",
|
||||
"token_in_use",
|
||||
"transfers_not_allowed"];
|
||||
|
||||
const paymentErrors1 = ["approve_with_id",
|
||||
"issuer_not_available",
|
||||
"processing_error",
|
||||
"reenter_transaction",
|
||||
"try_again_later",
|
||||
"payment_intent_authentication_failure",
|
||||
"processing_error",];
|
||||
|
||||
const paymentErrors2 = ["call_issuer",
|
||||
"card_not_supported",
|
||||
"card_velocity_exceeded",
|
||||
"do_not_honor",
|
||||
"do_not_try_again",
|
||||
"fraudulent",
|
||||
"generic_decline",
|
||||
"invalid_account",
|
||||
"lost_card",
|
||||
"merchant_blacklist",
|
||||
"new_account_information_available",
|
||||
"no_action_taken",
|
||||
"not_permitted",
|
||||
"pickup_card",
|
||||
"restricted_card",
|
||||
"revocation_of_all_authorizations",
|
||||
"revocation_of_authorization",
|
||||
"security_violation",
|
||||
"service_not_allowed",
|
||||
"stolen_card",
|
||||
"stop_payment_order",
|
||||
"transaction_not_allowed",];
|
||||
|
||||
basicErrors.forEach(k => errorMessageIndex[k] = BASIC_ERROR);
|
||||
paymentErrors1.forEach(k => errorMessageIndex[k] = PAYMENT_ERROR_1);
|
||||
paymentErrors2.forEach(k => errorMessageIndex[k] = PAYMENT_ERROR_2);
|
||||
|
||||
function getErrorMessage(type: string) {
|
||||
return errorMessageIndex[type] ? errorMessageIndex[type] : BASIC_ERROR;
|
||||
}
|
||||
|
||||
// BASIC_ERROR and PAYMENT_ERROR_1 are exported for errors.test.tsx
|
||||
export { AuthServerErrno, getErrorMessage, BASIC_ERROR, PAYMENT_ERROR_1 };
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState, useCallback, useContext } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AuthServerErrno } from '../../lib/errors';
|
||||
import { AuthServerErrno, getErrorMessage } from '../../lib/errors';
|
||||
import { actions, selectors } from '../../store';
|
||||
import { AppContext } from '../../lib/AppContext';
|
||||
import { LoadingOverlay } from '../../components/LoadingOverlay';
|
||||
|
@ -76,7 +76,7 @@ export const Product = ({
|
|||
activated: accountActivated = false
|
||||
} = queryParams;
|
||||
|
||||
const [ createTokenError, setCreateTokenError ] = useState({ message: null });
|
||||
const [ createTokenError, setCreateTokenError ] = useState({ type: "", error: false });
|
||||
|
||||
// Fetch plans on initial render, change in product ID, or auth change.
|
||||
useEffect(() => {
|
||||
|
@ -97,22 +97,23 @@ export const Product = ({
|
|||
selectedPlan = productPlans[0];
|
||||
}
|
||||
|
||||
const onPayment = useCallback((tokenResponse: stripe.TokenResponse) => {
|
||||
const onPayment = useCallback((tokenResponse: stripe.TokenResponse, name: string) => {
|
||||
if (tokenResponse && tokenResponse.token) {
|
||||
createSubscription(accessToken, {
|
||||
paymentToken: tokenResponse.token.id,
|
||||
planId: selectedPlan.plan_id,
|
||||
displayName: profile.result ? profile.result.displayName : '',
|
||||
displayName: name,
|
||||
});
|
||||
} else {
|
||||
// This shouldn't happen with a successful createToken() call, but let's
|
||||
// display an error in case it does.
|
||||
const error: any = { message: 'No token response received from Stripe' };
|
||||
const error: any = { type: 'api_error', error: true };
|
||||
setCreateTokenError(error);
|
||||
}
|
||||
}, [ accessToken, selectedPlan, createSubscription, setCreateTokenError ]);
|
||||
|
||||
const onPaymentError = useCallback((error: any) => {
|
||||
error.error = true;
|
||||
setCreateTokenError(error);
|
||||
}, [ setCreateTokenError ]);
|
||||
|
||||
|
@ -188,16 +189,16 @@ export const Product = ({
|
|||
error={createSubscriptionStatus.error} />
|
||||
)}
|
||||
|
||||
{createTokenError.message && (
|
||||
{createTokenError.error && (
|
||||
<DialogMessage
|
||||
className="dialog-error"
|
||||
onDismiss={() => {
|
||||
resetCreateSubscriptionError();
|
||||
setCreateTokenError({ message: null });
|
||||
setCreateTokenError({ type: "", error: false });
|
||||
}}
|
||||
>
|
||||
<h4>Payment submission failed</h4>
|
||||
<p>{createTokenError.message}</p>
|
||||
<p>{getErrorMessage(createTokenError.type)}</p>
|
||||
</DialogMessage>
|
||||
)}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { useBooleanState } from '../../lib/hooks';
|
||||
import { getErrorMessage } from '../../lib/errors';
|
||||
import {
|
||||
Customer,
|
||||
UpdatePaymentFetchState,
|
||||
|
@ -31,7 +32,7 @@ export const PaymentUpdateForm = ({
|
|||
plan,
|
||||
}: PaymentUpdateFormProps) => {
|
||||
const [ updateRevealed, revealUpdate, hideUpdate ] = useBooleanState();
|
||||
const [ createTokenError, setCreateTokenError ] = useState({ message: null });
|
||||
const [ createTokenError, setCreateTokenError ] = useState({ type: "", error: false });
|
||||
const onRevealUpdateClick = useCallback(() => {
|
||||
resetUpdatePayment();
|
||||
revealUpdate();
|
||||
|
@ -45,17 +46,18 @@ export const PaymentUpdateForm = ({
|
|||
} else {
|
||||
// This shouldn't happen with a successful createToken() call, but let's
|
||||
// display an error in case it does.
|
||||
const error: any = { message: 'No token response received from Stripe' };
|
||||
const error: any = { type: 'api_error', error: true };
|
||||
setCreateTokenError(error);
|
||||
}
|
||||
}, [ accessToken, updatePayment, setCreateTokenError ]);
|
||||
|
||||
const onPaymentError = useCallback((error: any) => {
|
||||
error.error = true;
|
||||
setCreateTokenError(error);
|
||||
}, [ setCreateTokenError ]);
|
||||
|
||||
const onTokenErrorDismiss = useCallback(() => {
|
||||
setCreateTokenError({ message: null });
|
||||
setCreateTokenError({ type: "", error: false });
|
||||
}, [ setCreateTokenError ]);
|
||||
|
||||
const inProgress =
|
||||
|
@ -76,10 +78,10 @@ export const PaymentUpdateForm = ({
|
|||
return (
|
||||
<div className="payment-update">
|
||||
|
||||
{createTokenError.message && (
|
||||
{createTokenError.error && (
|
||||
<DialogMessage className="dialog-error" onDismiss={onTokenErrorDismiss}>
|
||||
<h4>Payment submission failed</h4>
|
||||
<p>{createTokenError.message}</p>
|
||||
<p>{getErrorMessage(createTokenError.type)}</p>
|
||||
</DialogMessage>
|
||||
)}
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
"dependencies": {
|
||||
"acorn": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "http://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
|
||||
"integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=",
|
||||
"dev": true
|
||||
}
|
||||
|
@ -161,7 +161,7 @@
|
|||
"dependencies": {
|
||||
"chalk": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
|
||||
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@ -916,9 +916,9 @@
|
|||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.11",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
||||
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
|
||||
"version": "4.17.14",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz",
|
||||
"integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==",
|
||||
"dev": true
|
||||
},
|
||||
"lolex": {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
IFS=$'\n'
|
||||
|
|
14
servers.json
14
servers.json
|
@ -45,20 +45,6 @@
|
|||
"max_restarts": "1",
|
||||
"min_uptime": "2m"
|
||||
},
|
||||
{
|
||||
"name": "fxa-basket-proxy PORT 1114",
|
||||
"script": "./bin/basket-proxy-server.js",
|
||||
"cwd": "packages/fxa-basket-proxy",
|
||||
"max_restarts": "1",
|
||||
"min_uptime": "2m"
|
||||
},
|
||||
{
|
||||
"name": "fxa-basket-proxy fake basket server PORT 10140",
|
||||
"script": "./bin/fake-basket-server.js",
|
||||
"cwd": "packages/fxa-basket-proxy",
|
||||
"max_restarts": "1",
|
||||
"min_uptime": "2m"
|
||||
},
|
||||
{
|
||||
"name": "oauth-server PORT 9010",
|
||||
"script": "./bin/server.js",
|
||||
|
|
Загрузка…
Ссылка в новой задаче