├── .bowerrc ├── .csslintrc ├── .eslintrc ├── .gitignore ├── .npmrc ├── .travis.yml ├── Dockerfile ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── karma.conf.js ├── karma.conf.sauce.js ├── karma.shared.js ├── package.json ├── public ├── .eslintrc ├── README.md ├── bower_components │ └── fxa-relier-client │ │ ├── fxa-relier-client.js │ │ ├── fxa-relier-client.min.js │ │ └── fxa-relier-client.min.js.map ├── favicon.ico ├── fonts │ ├── clearsans │ │ ├── clearsans-bold.eot │ │ ├── clearsans-bold.svg │ │ ├── clearsans-bold.ttf │ │ ├── clearsans-bold.woff │ │ ├── clearsans-bolditalic.eot │ │ ├── clearsans-bolditalic.svg │ │ ├── clearsans-bolditalic.ttf │ │ ├── clearsans-bolditalic.woff │ │ ├── clearsans-italic.eot │ │ ├── clearsans-italic.svg │ │ ├── clearsans-italic.ttf │ │ ├── clearsans-italic.woff │ │ ├── clearsans-light.eot │ │ ├── clearsans-light.svg │ │ ├── clearsans-light.ttf │ │ ├── clearsans-light.woff │ │ ├── clearsans-medium.eot │ │ ├── clearsans-medium.svg │ │ ├── clearsans-medium.ttf │ │ ├── clearsans-medium.woff │ │ ├── clearsans-mediumitalic.eot │ │ ├── clearsans-mediumitalic.svg │ │ ├── clearsans-mediumitalic.ttf │ │ ├── clearsans-mediumitalic.woff │ │ ├── clearsans-regular.eot │ │ ├── clearsans-regular.svg │ │ ├── clearsans-regular.ttf │ │ ├── clearsans-regular.woff │ │ ├── clearsans-thin.eot │ │ ├── clearsans-thin.svg │ │ ├── clearsans-thin.ttf │ │ └── clearsans-thin.woff │ └── firasans │ │ ├── firasans-bold.eot │ │ ├── firasans-bold.svg │ │ ├── firasans-bold.ttf │ │ ├── firasans-bold.woff │ │ ├── firasans-bolditalic.eot │ │ ├── firasans-bolditalic.svg │ │ ├── firasans-bolditalic.ttf │ │ ├── firasans-bolditalic.woff │ │ ├── firasans-book.eot │ │ ├── firasans-book.svg │ │ ├── firasans-book.ttf │ │ ├── firasans-book.woff │ │ ├── firasans-bookitalic.eot │ │ ├── firasans-bookitalic.svg │ │ ├── firasans-bookitalic.ttf │ │ ├── firasans-bookitalic.woff │ │ ├── firasans-eight.eot │ │ ├── firasans-eight.svg │ │ ├── firasans-eight.ttf │ │ ├── firasans-eight.woff │ │ ├── firasans-eightitalic.eot │ │ ├── firasans-eightitalic.svg │ │ ├── firasans-eightitalic.ttf │ │ ├── firasans-eightitalic.woff │ │ ├── firasans-extrabold.eot │ │ ├── firasans-extrabold.svg │ │ ├── firasans-extrabold.ttf │ │ ├── firasans-extrabold.woff │ │ ├── firasans-extrabolditalic.eot │ │ ├── firasans-extrabolditalic.svg │ │ ├── firasans-extrabolditalic.ttf │ │ ├── firasans-extrabolditalic.woff │ │ ├── firasans-extralight.eot │ │ ├── firasans-extralight.svg │ │ ├── firasans-extralight.ttf │ │ ├── firasans-extralight.woff │ │ ├── firasans-extralightitalic.eot │ │ ├── firasans-extralightitalic.svg │ │ ├── firasans-extralightitalic.ttf │ │ ├── firasans-extralightitalic.woff │ │ ├── firasans-four.eot │ │ ├── firasans-four.svg │ │ ├── firasans-four.ttf │ │ ├── firasans-four.woff │ │ ├── firasans-fouritalic.eot │ │ ├── firasans-fouritalic.svg │ │ ├── firasans-fouritalic.ttf │ │ ├── firasans-fouritalic.woff │ │ ├── firasans-hair.eot │ │ ├── firasans-hair.svg │ │ ├── firasans-hair.ttf │ │ ├── firasans-hair.woff │ │ ├── firasans-hairitalic.eot │ │ ├── firasans-hairitalic.svg │ │ ├── firasans-hairitalic.ttf │ │ ├── firasans-hairitalic.woff │ │ ├── firasans-heavy.eot │ │ ├── firasans-heavy.svg │ │ ├── firasans-heavy.ttf │ │ ├── firasans-heavy.woff │ │ ├── firasans-heavyitalic.eot │ │ ├── firasans-heavyitalic.svg │ │ ├── firasans-heavyitalic.ttf │ │ ├── firasans-heavyitalic.woff │ │ ├── firasans-italic.eot │ │ ├── firasans-italic.svg │ │ ├── firasans-italic.ttf │ │ ├── firasans-italic.woff │ │ ├── firasans-light.eot │ │ ├── firasans-light.svg │ │ ├── firasans-light.ttf │ │ ├── firasans-light.woff │ │ ├── firasans-lightitalic.eot │ │ ├── firasans-lightitalic.svg │ │ ├── firasans-lightitalic.ttf │ │ ├── firasans-lightitalic.woff │ │ ├── firasans-medium.eot │ │ ├── firasans-medium.svg │ │ ├── firasans-medium.ttf │ │ ├── firasans-medium.woff │ │ ├── firasans-mediumitalic.eot │ │ ├── firasans-mediumitalic.svg │ │ ├── firasans-mediumitalic.ttf │ │ ├── firasans-mediumitalic.woff │ │ ├── firasans-regular.eot │ │ ├── firasans-regular.svg │ │ ├── firasans-regular.ttf │ │ ├── firasans-regular.woff │ │ ├── firasans-regularitalic.eot │ │ ├── firasans-regularitalic.svg │ │ ├── firasans-regularitalic.ttf │ │ ├── firasans-regularitalic.woff │ │ ├── firasans-semibold.eot │ │ ├── firasans-semibold.svg │ │ ├── firasans-semibold.ttf │ │ ├── firasans-semibold.woff │ │ ├── firasans-semibolditalic.eot │ │ ├── firasans-semibolditalic.svg │ │ ├── firasans-semibolditalic.ttf │ │ ├── firasans-semibolditalic.woff │ │ ├── firasans-thin.eot │ │ ├── firasans-thin.svg │ │ ├── firasans-thin.ttf │ │ ├── firasans-thin.woff │ │ ├── firasans-thinitalic.eot │ │ ├── firasans-thinitalic.svg │ │ ├── firasans-thinitalic.ttf │ │ ├── firasans-thinitalic.woff │ │ ├── firasans-two.eot │ │ ├── firasans-two.svg │ │ ├── firasans-two.ttf │ │ ├── firasans-two.woff │ │ ├── firasans-twoitalic.eot │ │ ├── firasans-twoitalic.svg │ │ ├── firasans-twoitalic.ttf │ │ ├── firasans-twoitalic.woff │ │ ├── firasans-ultra.eot │ │ ├── firasans-ultra.svg │ │ ├── firasans-ultra.ttf │ │ ├── firasans-ultra.woff │ │ ├── firasans-ultraitalic.eot │ │ ├── firasans-ultraitalic.svg │ │ ├── firasans-ultraitalic.ttf │ │ ├── firasans-ultraitalic.woff │ │ ├── firasans-ultralight.eot │ │ ├── firasans-ultralight.svg │ │ ├── firasans-ultralight.ttf │ │ ├── firasans-ultralight.woff │ │ ├── firasans-ultralightitalic.eot │ │ ├── firasans-ultralightitalic.svg │ │ ├── firasans-ultralightitalic.ttf │ │ └── firasans-ultralightitalic.woff ├── img │ ├── cardicons-sprite.svg │ ├── firefox.png │ ├── management │ │ ├── history.svg │ │ ├── pay-methods.svg │ │ ├── profile.svg │ │ └── subs.svg │ ├── mgmt-nav-sprite.svg │ ├── pay-method-sprite.svg │ ├── spinnerlight.png │ ├── spinnerlight@2x.png │ ├── spinnerwhite.png │ └── spinnerwhite@2x.png ├── index.html ├── js │ ├── actions │ │ ├── api.js │ │ ├── braintree.js │ │ ├── management.js │ │ ├── notifications.js │ │ ├── pay-methods.js │ │ ├── processing.js │ │ ├── subscriptions.js │ │ ├── transaction.js │ │ └── user.js │ ├── apps │ │ ├── management │ │ │ ├── app.jsx │ │ │ └── main.jsx │ │ └── transaction │ │ │ ├── app.jsx │ │ │ └── main.jsx │ ├── components │ │ ├── card-form.jsx │ │ ├── card-input.jsx │ │ ├── input-error.jsx │ │ ├── modal.jsx │ │ ├── notification-list.jsx │ │ ├── notification.jsx │ │ ├── pay-method-choice.jsx │ │ ├── pay-method-drop-down.jsx │ │ ├── pay-method-icon.jsx │ │ ├── pay-method-item.jsx │ │ ├── pay-method-list.jsx │ │ ├── product-detail.jsx │ │ ├── spinner.jsx │ │ ├── submit-button.jsx │ │ ├── subscription-list.jsx │ │ └── subscription.jsx │ ├── constants │ │ ├── action-types.js │ │ └── error-codes.js │ ├── data-store.js │ ├── products.js │ ├── reducers │ │ ├── app.js │ │ ├── index.js │ │ ├── management.js │ │ ├── processing.js │ │ ├── transaction.js │ │ └── user.js │ ├── settings.js │ ├── tracking.js │ ├── utils.jsx │ └── views │ │ ├── management.jsx │ │ ├── management │ │ ├── add-pay-method.jsx │ │ ├── confirm-del-pay-method.jsx │ │ ├── del-pay-method.jsx │ │ ├── history.jsx │ │ ├── my-account.jsx │ │ ├── pay-methods.jsx │ │ └── subscriptions.jsx │ │ ├── shared │ │ ├── braintree-token.jsx │ │ ├── sign-in.jsx │ │ └── sign-out.jsx │ │ ├── transaction.jsx │ │ └── transaction │ │ ├── complete-payment.jsx │ │ ├── product-pay-chooser.jsx │ │ └── product-pay.jsx ├── management.html ├── media │ ├── README.md │ └── img │ │ ├── tabzilla-static-high-res.png │ │ └── tabzilla-static.png ├── scss │ ├── _base.scss │ ├── _buttons.scss │ ├── _complete-payment.scss │ ├── _email.scss │ ├── _forms.scss │ ├── _global.scss │ ├── _json-table.scss │ ├── _management.scss │ ├── _mgmt-nav-sprite.scss │ ├── _modal.scss │ ├── _notification.scss │ ├── _pay-method-drop-down.scss │ ├── _pay-method-item.scss │ ├── _pay-method-list.scss │ ├── _pay-method-sprite.scss │ ├── _pay-methods.scss │ ├── _product.scss │ ├── _signed-out.scss │ ├── _spinner.scss │ ├── _subscriptions.scss │ ├── _tooltip.scss │ ├── _transaction.scss │ ├── _typography.scss │ ├── _utils.scss │ ├── common.scss │ ├── config │ │ ├── dev │ │ │ └── _settings.scss │ │ └── local │ │ │ └── _settings.scss │ ├── email.scss │ ├── inc │ │ ├── mixins.scss │ │ └── vars.scss │ ├── lib │ │ ├── normalize.scss │ │ └── tabzilla.scss │ ├── management.scss │ └── transaction.scss └── svg │ ├── amex.svg │ ├── diners.svg │ ├── discover.svg │ ├── history.svg │ ├── jcb.svg │ ├── maestro.svg │ ├── mastercard.svg │ ├── pay-methods.svg │ ├── profile.svg │ ├── subs.svg │ └── visa.svg ├── styleguide ├── jsx │ ├── card-form.jsx │ ├── modal.jsx │ └── spinner.jsx ├── pages │ ├── buttons.md │ ├── card-form.md │ ├── index.md │ ├── modal.md │ ├── spinner.md │ └── typography.md └── templates │ ├── base-styleguide.html │ ├── buttons-spinner.html │ ├── buttons.html │ ├── jsx.html │ └── typography.html ├── tasks ├── .eslintrc ├── abideCompile.js ├── abideCreate.js ├── abideExtract.js ├── abideMerge.js ├── clean.js ├── cog.js ├── concurrent.js ├── copy.js ├── csslint.js ├── devserver.js ├── eslint.js ├── gh-pages.js ├── karma.js ├── sass.js ├── svg_sprite.js ├── watch.js ├── webpack-dev-server.js └── webpack.js ├── tests ├── .eslintrc ├── .gitkeep ├── actions │ ├── test.api.js │ ├── test.braintree.js │ ├── test.notification-actions.js │ ├── test.pay-method-actions.js │ ├── test.processing-actions.js │ ├── test.subs-actions.js │ ├── test.transaction.js │ └── test.user-actions.js ├── apps │ ├── test.management-app.jsx │ └── test.transaction-app.jsx ├── components │ ├── test.card-form.jsx │ ├── test.modal.jsx │ ├── test.notification-list.jsx │ ├── test.notification.jsx │ ├── test.pay-method-choice.jsx │ ├── test.pay-method-drop-down.jsx │ ├── test.pay-method-icon.jsx │ ├── test.product-detail.jsx │ ├── test.spinner.jsx │ └── test.submit-button.js ├── helpers.jsx ├── reducers │ ├── test.app-reducer.js │ ├── test.management-reducer.js │ ├── test.processing-reducer.js │ ├── test.trans-reducer.js │ └── test.user-reducer.js ├── test-loader.js ├── test.gh-pages-config.js ├── test.product-data.js ├── test.tracking.js ├── test.utils.jsx └── views │ ├── test.braintree-token.jsx │ ├── test.complete-payment.jsx │ ├── test.del-pay-method-confirmation.jsx │ ├── test.history.jsx │ ├── test.management.jsx │ ├── test.pay-methods.jsx │ ├── test.product-pay-chooser.jsx │ ├── test.product-pay.jsx │ ├── test.sign-in.jsx │ ├── test.sign-out.jsx │ ├── test.subscriptions.jsx │ └── test.transaction.jsx └── webpack.config.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "public/bower_components/" 3 | } 4 | -------------------------------------------------------------------------------- /.csslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "adjoining-classes": false, 3 | "box-model": false, 4 | "box-sizing": false, 5 | "bulletproof-font-face": false, 6 | "compatible-vendor-prefixes": false, 7 | "duplicate-properties": 2, 8 | "fallback-colors": false, 9 | "font-faces": false, 10 | "font-sizes": false, 11 | "floats": false, 12 | "ids": false, 13 | "important": false, 14 | "outline-none": false, 15 | "qualified-headings": false, 16 | "regex-selectors": false, 17 | "unique-headings": false, 18 | "universal-selector": false, 19 | "unqualified-attributes": false, 20 | "zero-units": 2 21 | } 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // Babel parser required to support the spread operator 3 | // in jsx. See: https://github.com/eslint/espree/issues/116 4 | "parser": "babel-eslint", 5 | "env": { 6 | "node": true, 7 | "browser": true, 8 | "es6": true, 9 | }, 10 | "plugins": [ 11 | "react" 12 | ], 13 | "ecmaFeatures": { 14 | "arrowFunctions": true, 15 | "blockBindings": true, 16 | "classes": true, 17 | "destructuring": true, 18 | "defaultParams": true, 19 | "jsx": true, 20 | "modules": true, 21 | "restParams": true, 22 | "spread": true, 23 | }, 24 | "globals": { 25 | "exports": false, 26 | "module": false, 27 | "require": false, 28 | "FxaRelierClient": false, 29 | }, 30 | "rules": { 31 | "block-scoped-var": 0, 32 | "camelcase": 0, 33 | "comma-dangle": [2, "always-multiline"], 34 | "comma-style": [2, "last"], 35 | "curly": [2, "all"], 36 | "dot-notation": [2, {"allowKeywords": true}], 37 | "eqeqeq": [2, "allow-null"], 38 | "guard-for-in": 0, 39 | "jsx-quotes": 1, 40 | "max-len": [2, 80, 2, {ignoreComments: true}], 41 | "new-cap": [2, {"capIsNewExceptions": ["Deferred"]}], 42 | "no-bitwise": 2, 43 | "no-caller": 2, 44 | "no-cond-assign": [2, "except-parens"], 45 | "no-debugger": 2, 46 | "no-empty": 2, 47 | "no-eval": 2, 48 | "no-extend-native": 2, 49 | "no-extra-parens": 0, 50 | "no-extra-semi": 2, 51 | "no-irregular-whitespace": 2, 52 | "no-iterator": 2, 53 | "no-loop-func": 2, 54 | "no-multi-str": 2, 55 | "no-new": 2, 56 | "no-plusplus": 2, 57 | "no-proto": 2, 58 | "no-redeclare": 0, 59 | "no-script-url": 2, 60 | "no-sequences": 2, 61 | "no-undef": 2, 62 | "no-underscore-dangle": 0, 63 | "no-unused-vars": 2, 64 | "no-with": 2, 65 | "quotes": [2, "single", "avoid-escape"], 66 | "quote-props": [1, "consistent-as-needed"], 67 | "react/display-name": 0, 68 | "react/jsx-boolean-value": 1, 69 | "react/jsx-no-undef": 1, 70 | "react/jsx-sort-props": 0, 71 | "react/jsx-sort-prop-types": 1, 72 | "react/jsx-uses-react": 1, 73 | "react/jsx-uses-vars": 1, 74 | "react/no-did-mount-set-state": 1, 75 | "react/no-did-update-set-state": 1, 76 | "react/no-multi-comp": 1, 77 | "react/no-unknown-property": 1, 78 | "react/prop-types": 1, 79 | "react/react-in-jsx-scope": 1, 80 | "react/self-closing-comp": 1, 81 | "react/sort-comp": 1, 82 | "react/wrap-multilines": 1, 83 | "semi": [2, "always"], 84 | "space-infix-ops": 0, 85 | "strict": [2, "never"], 86 | "valid-typeof": 2, 87 | "wrap-iife": [2, "inside"] 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | .grunt 4 | styleguide/build 5 | styleguide/jsx-bundles 6 | npm-debug.log 7 | public/dist/* 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-prefix='' 2 | -------------------------------------------------------------------------------- /.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 | after_success: npm run-script publish-docker > /dev/null 2>&1 17 | env: 18 | global: 19 | - secure: a1fGjKDKp+35F1XbuRYFSePOVgDeUAOHpfVJzrV8PnAl6ONKOr49HQocIAtF4ALevcbYS05IS47RES9Cp+7jkon10Ry2KrFR1or0euWBEukuTwL8n3rc9AhnOBDOp5RO2CWvweocFzLGa5MM4aCCw0wnmhmAUOGDWrwG5DMTwpY= 20 | - secure: L64ENqh7CpB8ziEa4DRzwTUVdsXix5nBBWzXAiZlGQQKrydEEiHpVE7/tYIN/k61IbAISguJuEGahPElOReWF4LxfYSmlSPZeh3QdLO+Pc9HilnSemP7YBXEKXLck32z1bbmV/NGxBM8nA0qpYOcSDg+ljWEmCgpp1eQaXqxwCk= 21 | - secure: SGYIOsqecNHtrWTrHQnibhkZ8NbizuDIbfiNBcZ0raSz7hvGpLrZeXPwuBSdJRUkJ9D6/Q2SZEdCq2niklddXBArAiTg3FiywHL/0Eo/CVHxgX0UQOHemJJx+xAAHkvNFSsXvrGkcjuA3hqKe7iNCBjp044PLgrnY7KNfpEXFsA= 22 | - secure: O7Uc/wE/Nyu0CLTH7EaxYcwYhQC8XoNVe7QcYXk5xcBUhTcuDtxD1uY0fVC7deNEYKDlL1Bu88dqOXN/n2cyExST+gr1L8FKlHXLhkuF35U4HuP+ybagjRtc7oAFOKXR0PFthczJ9wJJ7B+gkAo2JDkjp0goAN5f6Mz47tfDLfc= 23 | - secure: MDZohwJmwnGhXvCDs9ozZqVoT6E+xBQ/XJmh1KiRr4KHztcrOzPRa+yxQkxXDkGLVnVWI6/mgFd8XB49J7f7tVEl3mdOjQPa7RdjF3jIJlmedE3UVlfRpperkM/vHHrbqX0uXwPm2B+HIKH2DPu3eQ+bQQOuFvfYeZ/AYcpkI/4= 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cogniteev/echo 2 | 3 | COPY public /srv/payments-ui 4 | COPY .git/logs/HEAD /srv/payments-ui/git-rev.txt 5 | VOLUME /srv/payments-ui 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. 2 | 3 | If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moz-payments-ui", 3 | "dependencies": { 4 | "fxa-relier-client": "https://github.com/mozilla/fxa-relier-client.git#0.0.8" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var karmaConfig = require('./karma.shared'); 2 | 3 | module.exports = function (config) { 4 | config.set(karmaConfig); 5 | }; 6 | -------------------------------------------------------------------------------- /karma.conf.sauce.js: -------------------------------------------------------------------------------- 1 | var defaults = require('lodash.defaults'); 2 | var karmaConfig = require('./karma.shared'); 3 | var browsers = require('mozilla-payments-saucelabs-browsers'); 4 | 5 | 6 | module.exports = function (config) { 7 | 8 | if (!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) { 9 | console.log('Make sure the SAUCE_USERNAME and ' + 10 | 'SAUCE_ACCESS_KEY environment variables are set.'); 11 | throw new Error('Missing SAUCE_USERNAME and SAUCE_ACCESS_KEY env vars'); 12 | } 13 | 14 | karmaConfig.plugins.push('karma-sauce-launcher'); 15 | karmaConfig.reporters.push('saucelabs'); 16 | 17 | config.set(defaults({ 18 | // Increase timeout in case connection in CI is slow 19 | browserDisconnectTimeout: 10000, 20 | browserDisconnectTolerance: 2, 21 | browserNoActivityTimeout: 30000, 22 | captureTimeout: 120000, 23 | customLaunchers: browsers, 24 | browsers: Object.keys(browsers), 25 | port: 9876, 26 | sauceLabs: { 27 | testName: 'Payments UI Unit Tests', 28 | recordScreenshots: false, 29 | connectOptions: { 30 | username: process.env.SAUCE_USERNAME, 31 | accessKey: process.env.SAUCE_ACCESS_KEY, 32 | tunnelIdentifier: 'autoGeneratedTunnelID', 33 | }, 34 | }, 35 | }, karmaConfig)); 36 | }; 37 | -------------------------------------------------------------------------------- /karma.shared.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* This is the shared karma config */ 4 | var merge = require('lodash.merge'); 5 | var webpackConfig = require('./webpack.config'); 6 | 7 | var newWebpackConfig = merge({}, webpackConfig); 8 | // Remove the bits from the shared config 9 | // that we don't want for tests. 10 | delete newWebpackConfig.output; 11 | delete newWebpackConfig.entry; 12 | 13 | newWebpackConfig.plugins = []; 14 | 15 | // Expose the right kind of source map for test-loader.js 16 | newWebpackConfig.devtool = 'inline-source-map'; 17 | 18 | module.exports = { 19 | basePath: '', 20 | browsers: ['Firefox'], 21 | colors: true, 22 | frameworks: [ 23 | 'mocha', 24 | 'chai', 25 | 'sinon', 26 | ], 27 | files: [ 28 | 'tests/test-loader.js', 29 | ], 30 | preprocessors: { 31 | 'tests/test-loader.js': ['webpack', 'sourcemap'], 32 | }, 33 | reporters: ['mocha'], 34 | plugins: [ 35 | 'karma-sinon', 36 | 'karma-mocha', 37 | 'karma-mocha-reporter', 38 | 'karma-chai', 39 | 'karma-firefox-launcher', 40 | 'karma-sourcemap-loader', 41 | 'karma-webpack', 42 | ], 43 | singleRun: true, 44 | webpack: newWebpackConfig, 45 | webpackServer: { 46 | noInfo: true, 47 | quiet: true, 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /public/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | }, 6 | "rules": { 7 | "global-strict": 0, 8 | "strict": [2, "never"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /public/README.md: -------------------------------------------------------------------------------- 1 | # About the public dir 2 | 3 | All content under public will be served. 4 | 5 | ## What not to put here 6 | 7 | * Anything that shouldn't be made public. 8 | * Test files or unecessary cruft. 9 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-bold.eot -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-bold.ttf -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-bold.woff -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-bolditalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-bolditalic.eot -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-bolditalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-bolditalic.ttf -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-bolditalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-bolditalic.woff -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-italic.eot -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-italic.ttf -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-italic.woff -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-light.eot -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-light.ttf -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-light.woff -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-medium.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-medium.eot -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-medium.ttf -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-medium.woff -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-mediumitalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-mediumitalic.eot -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-mediumitalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-mediumitalic.ttf -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-mediumitalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-mediumitalic.woff -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-regular.eot -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-regular.ttf -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-regular.woff -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-thin.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-thin.eot -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-thin.ttf -------------------------------------------------------------------------------- /public/fonts/clearsans/clearsans-thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/clearsans/clearsans-thin.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-bold.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-bold.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-bold.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-bolditalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-bolditalic.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-bolditalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-bolditalic.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-bolditalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-bolditalic.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-book.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-book.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-book.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-book.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-book.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-book.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-bookitalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-bookitalic.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-bookitalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-bookitalic.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-bookitalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-bookitalic.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-eight.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-eight.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-eight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-eight.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-eight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-eight.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-eightitalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-eightitalic.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-eightitalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-eightitalic.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-eightitalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-eightitalic.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-extrabold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-extrabold.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-extrabold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-extrabold.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-extrabold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-extrabold.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-extrabolditalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-extrabolditalic.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-extrabolditalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-extrabolditalic.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-extrabolditalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-extrabolditalic.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-extralight.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-extralight.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-extralight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-extralight.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-extralight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-extralight.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-extralightitalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-extralightitalic.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-extralightitalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-extralightitalic.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-extralightitalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-extralightitalic.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-four.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-four.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-four.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-four.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-four.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-four.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-fouritalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-fouritalic.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-fouritalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-fouritalic.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-fouritalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-fouritalic.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-hair.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-hair.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-hair.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-hair.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-hair.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-hair.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-hairitalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-hairitalic.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-hairitalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-hairitalic.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-hairitalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-hairitalic.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-heavy.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-heavy.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-heavy.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-heavy.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-heavy.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-heavy.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-heavyitalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-heavyitalic.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-heavyitalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-heavyitalic.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-heavyitalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-heavyitalic.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-italic.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-italic.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-italic.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-light.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-light.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-light.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-lightitalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-lightitalic.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-lightitalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-lightitalic.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-lightitalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-lightitalic.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-medium.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-medium.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-medium.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-medium.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-mediumitalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-mediumitalic.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-mediumitalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-mediumitalic.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-mediumitalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-mediumitalic.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-regular.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-regular.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-regular.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-regularitalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-regularitalic.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-regularitalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-regularitalic.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-regularitalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-regularitalic.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-semibold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-semibold.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-semibold.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-semibold.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-semibolditalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-semibolditalic.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-semibolditalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-semibolditalic.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-semibolditalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-semibolditalic.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-thin.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-thin.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-thin.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-thin.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-thinitalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-thinitalic.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-thinitalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-thinitalic.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-thinitalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-thinitalic.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-two.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-two.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-two.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-two.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-two.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-two.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-twoitalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-twoitalic.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-twoitalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-twoitalic.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-twoitalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-twoitalic.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-ultra.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-ultra.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-ultra.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-ultra.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-ultra.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-ultra.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-ultraitalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-ultraitalic.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-ultraitalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-ultraitalic.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-ultraitalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-ultraitalic.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-ultralight.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-ultralight.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-ultralight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-ultralight.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-ultralight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-ultralight.woff -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-ultralightitalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-ultralightitalic.eot -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-ultralightitalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-ultralightitalic.ttf -------------------------------------------------------------------------------- /public/fonts/firasans/firasans-ultralightitalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/fonts/firasans/firasans-ultralightitalic.woff -------------------------------------------------------------------------------- /public/img/firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/img/firefox.png -------------------------------------------------------------------------------- /public/img/management/history.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/img/management/pay-methods.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/img/management/profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/img/management/subs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/img/spinnerlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/img/spinnerlight.png -------------------------------------------------------------------------------- /public/img/spinnerlight@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/img/spinnerlight@2x.png -------------------------------------------------------------------------------- /public/img/spinnerwhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/img/spinnerwhite.png -------------------------------------------------------------------------------- /public/img/spinnerwhite@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/img/spinnerwhite@2x.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Payments UI 10 | 11 | 12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /public/js/actions/api.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | import * as defaultSettings from 'settings'; 4 | 5 | 6 | export function fetch(request, 7 | {jquery=$, settings=defaultSettings, csrfToken} = {}) { 8 | request = Object.assign({ 9 | dataType: 'json', 10 | headers: {}, 11 | xhrFields: {}, 12 | }, request); 13 | 14 | request.url = settings.apiPrefix + request.url; 15 | 16 | if (settings.allowCORSRequests) { 17 | // This will send cookies on Access Control requests which is needed 18 | // for sessions. It really only applies to newer XHR2 transports which 19 | // have stricter security. We only need CORS for webpack hot 20 | // reloading development. 21 | request.xhrFields.withCredentials = true; 22 | } 23 | 24 | if (typeof csrfToken === 'undefined') { 25 | // This exists to stop someone from forgetting to pass in the stored CSRF 26 | // token. 27 | throw new Error( 28 | 'You must set a CSRF token value; ' + 29 | 'set it to false to fetch a resource without a token' 30 | ); 31 | } 32 | 33 | if (csrfToken) { 34 | request.headers['X-CSRFToken'] = csrfToken; 35 | } else { 36 | console.warn('making request to ' + request.url + 37 | ' but no CSRF token has been set'); 38 | } 39 | 40 | return jquery.ajax(request); 41 | } 42 | -------------------------------------------------------------------------------- /public/js/actions/braintree.js: -------------------------------------------------------------------------------- 1 | import braintree from 'braintree-web'; 2 | 3 | import * as errorCodes from 'constants/error-codes'; 4 | import * as notificationActions from './notifications'; 5 | 6 | 7 | export function tokenizeCreditCard({dispatch, braintreeToken, creditCard, 8 | callback, 9 | BraintreeClient=braintree.api.Client}) { 10 | 11 | if (typeof creditCard.number === 'undefined' || 12 | typeof creditCard.cvv === 'undefined' || 13 | typeof creditCard.expiration === 'undefined') { 14 | console.error('not a complete card object:', creditCard); 15 | throw new Error('Invalid card object'); 16 | } 17 | 18 | var client = new BraintreeClient({ 19 | clientToken: braintreeToken, 20 | }); 21 | 22 | client.tokenizeCard({ 23 | number: creditCard.number, 24 | expirationDate: creditCard.expiration, 25 | cvv: creditCard.cvv, 26 | }, (err, nonce) => { 27 | if (err) { 28 | console.error('Braintree tokenization error:', err); 29 | dispatch(notificationActions.showError( 30 | {errorCode: errorCodes.BRAINTREE_TOKENIZATION_ERROR})); 31 | } else { 32 | callback(nonce); 33 | } 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /public/js/actions/management.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from 'constants/action-types'; 2 | 3 | 4 | export function showMyAccount() { 5 | return { 6 | type: actionTypes.SHOW_MY_ACCOUNT, 7 | }; 8 | } 9 | 10 | export function showSignIn() { 11 | return { 12 | type: actionTypes.SHOW_SIGN_IN, 13 | }; 14 | } 15 | 16 | export function showSignOut() { 17 | return { 18 | type: actionTypes.SHOW_SIGN_OUT, 19 | }; 20 | } 21 | 22 | export function showPayMethods() { 23 | return { 24 | type: actionTypes.SHOW_PAY_METHODS, 25 | }; 26 | } 27 | 28 | export function showAddPayMethod() { 29 | return { 30 | type: actionTypes.SHOW_ADD_PAY_METHOD, 31 | }; 32 | } 33 | 34 | export function showDelPayMethod() { 35 | return { 36 | type: actionTypes.SHOW_DEL_PAY_METHOD, 37 | }; 38 | } 39 | 40 | export function showConfirmDelPayMethod(payMethodUri) { 41 | return { 42 | type: actionTypes.SHOW_CONFIRM_DEL_PAY_METHOD, 43 | payMethodUri: payMethodUri, 44 | }; 45 | } 46 | 47 | export function showHistory() { 48 | return { 49 | type: actionTypes.SHOW_HISTORY, 50 | }; 51 | } 52 | 53 | export function showSubscriptions() { 54 | return { 55 | type: actionTypes.SHOW_SUBS, 56 | }; 57 | } 58 | 59 | export function closeModal() { 60 | return { 61 | type: actionTypes.CLOSE_MODAL, 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /public/js/actions/notifications.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from 'constants/action-types'; 2 | import * as errorCodes from 'constants/error-codes'; 3 | 4 | import { gettext } from 'utils'; 5 | 6 | 7 | export function _addNotification({ text, errorCode, 8 | type='info', userDismissable, 9 | autoHide } = {}) { 10 | return { 11 | type: actionTypes.ADD_NOTIFICATION, 12 | data: { autoHide, text, type, userDismissable, errorCode }, 13 | }; 14 | } 15 | 16 | export function showInfo({text, ...opts} = {}) { 17 | opts.type = 'info'; 18 | 19 | if (!text) { 20 | throw new Error('An info notification requires text'); 21 | } else { 22 | opts.text = text; 23 | } 24 | 25 | // Default info type to being hidden on delay. 26 | if (typeof opts.autoHide === 'undefined') { 27 | opts.autoHide = true; 28 | } 29 | 30 | console.log('Showing info notification', opts); 31 | return _addNotification({...opts}); 32 | } 33 | 34 | export function showError({text, ...opts} = {}) { 35 | opts.type = 'error'; 36 | 37 | if (!opts.errorCode || typeof errorCodes[opts.errorCode] === 'undefined') { 38 | throw new Error('An error notification requires an errorCode ' + 39 | 'defined in constants/error-codes'); 40 | } 41 | 42 | if (!text) { 43 | opts.text = gettext('Internal error. Please try again later.'); 44 | } else { 45 | opts.text = text; 46 | } 47 | 48 | // Default to error notification being userDismissable. 49 | if (typeof opts.userDismissable === 'undefined') { 50 | opts.userDismissable = true; 51 | } 52 | 53 | console.log('Showing error notification', opts); 54 | return _addNotification({...opts}); 55 | } 56 | 57 | /* 58 | * The expectation is that this is called by the notificationList 59 | * automatically (after a timeout) or on a user click. It shouldn't 60 | * be necessary to call this directly. 61 | */ 62 | export function removeNotification(id, {delay} = {}) { 63 | var data = { 64 | type: actionTypes.REMOVE_NOTIFICATION, 65 | id: id, 66 | }; 67 | if (delay) { 68 | // Uses timeoutScheduler middleware. 69 | data.meta = {delay: delay}; 70 | } 71 | return data; 72 | } 73 | 74 | -------------------------------------------------------------------------------- /public/js/actions/processing.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from 'constants/action-types'; 2 | 3 | 4 | export function beginProcessing(processingId) { 5 | return processingAction(processingId, actionTypes.BEGIN_PROCESSING); 6 | } 7 | 8 | 9 | export function stopProcessing(processingId) { 10 | return processingAction(processingId, actionTypes.STOP_PROCESSING); 11 | } 12 | 13 | 14 | function processingAction(processingId, actionType) { 15 | if (!processingId) { 16 | throw new Error('processingId cannot be empty'); 17 | } 18 | return { 19 | type: actionType, 20 | processingId: processingId, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /public/js/apps/management/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | 4 | import { default as ManagementApp } from './app'; 5 | import dataStore from 'data-store'; 6 | 7 | 8 | React.render(( 9 | 10 | {() => } 11 | 12 | ), document.getElementById('placeholder')); 13 | -------------------------------------------------------------------------------- /public/js/apps/transaction/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | 4 | import tracking from 'tracking'; 5 | 6 | import { default as TransactionApp } from './app'; 7 | import dataStore from 'data-store'; 8 | 9 | 10 | tracking.init(); 11 | 12 | React.render(( 13 | 14 | {function() { 15 | return ; 16 | }} 17 | 18 | ), document.body); 19 | -------------------------------------------------------------------------------- /public/js/components/input-error.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import cx from 'classnames'; 3 | 4 | 5 | export default class InputError extends Component { 6 | 7 | static propTypes = { 8 | errorMessage: PropTypes.string.isRequired, 9 | errorModifier: PropTypes.oneOf(['center', 'right', 'left']), 10 | } 11 | 12 | render() { 13 | var { errorMessage, ...toolTipAttrs } = this.props; 14 | var errorClass = cx([ 15 | 'tooltip', 16 | this.props.errorModifier || 'left', 17 | ]); 18 | return ( 19 | {errorMessage} 21 | ); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /public/js/components/modal.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { gettext } from 'utils'; 3 | import cx from 'classnames'; 4 | 5 | 6 | export default class Modal extends Component { 7 | 8 | static propTypes = { 9 | children: PropTypes.object.isRequired, 10 | handleClose: PropTypes.func.isRequired, 11 | title: PropTypes.string, 12 | } 13 | 14 | onClose = (e) => { 15 | var targetClassName = e.target.getAttribute('class') || ''; 16 | var classes = targetClassName.split(' '); 17 | // Only deal with closing the window if the event 18 | // came from the backdrop or the close link. 19 | if (classes.length > 0 && 20 | (classes.indexOf('modal') > -1 || 21 | classes.indexOf('close') > -1)) { 22 | e.preventDefault(); 23 | e.stopPropagation(); 24 | this.props.handleClose(); 25 | } 26 | } 27 | 28 | render() { 29 | var classes = cx(['modal', {active: true}]); 30 | 31 | return ( 32 |
33 |
34 |
35 | {this.props.title ?

{this.props.title}

: null} 36 | 37 | {gettext('Close')} 38 | 39 |
40 |
41 | {this.props.children} 42 |
43 |
44 |
45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/js/components/notification-list.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react/addons'; 2 | import Notification from 'components/notification'; 3 | 4 | var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; 5 | 6 | export default class NotificationList extends Component { 7 | 8 | static propTypes = { 9 | TransitionGroup: PropTypes.node, 10 | // notifications is an Mapable array of 2 item arrays 11 | // e.g: [[key, notificationObj], [key, notificationObj], ...] 12 | notifications: PropTypes.array.isRequired, 13 | removeNotification: PropTypes.func.isRequired, 14 | } 15 | 16 | static defaultProps = { 17 | TransitionGroup: ReactCSSTransitionGroup, 18 | } 19 | 20 | render() { 21 | var CSSTransitionGroup = this.props.TransitionGroup; 22 | var items = this.props.notifications.map((item) => { 23 | var [key, val] = item; 24 | 25 | return ( 26 | 30 | ); 31 | }); 32 | 33 | return ( 34 | 38 | {items} 39 | 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/js/components/notification.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { gettext } from 'utils'; 3 | 4 | import cx from 'classnames'; 5 | 6 | 7 | export default class Notification extends Component { 8 | 9 | static propTypes = { 10 | autoHide: PropTypes.bool, 11 | key: PropTypes.string.isRequired, 12 | removeNotification: PropTypes.func.isRequired, 13 | text: PropTypes.string.isRequired, 14 | type: React.PropTypes.oneOf(['info', 'error']), 15 | userDismissable: React.PropTypes.bool, 16 | } 17 | 18 | static defaultProps = { 19 | autoHide: true, 20 | type: 'info', 21 | userDismissable: false, 22 | } 23 | 24 | componentDidMount() { 25 | if (this.props.userDismissable === false && 26 | this.props.autoHide === true) { 27 | this.props.removeNotification(this.props.key, {delay: 5000}); 28 | } 29 | } 30 | 31 | handleDismissClick = (e) => { 32 | e.preventDefault(); 33 | this.props.removeNotification(this.props.key); 34 | } 35 | 36 | render() { 37 | var classes = cx('notification', this.props.type, 38 | {autohide: this.props.autoHide}); 39 | return ( 40 |
  • 41 | {this.props.text} 42 | { this.props.userDismissable ? 43 | 46 | {gettext('Dismiss')} 47 | : null } 48 |
  • 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /public/js/components/pay-method-icon.jsx: -------------------------------------------------------------------------------- 1 | import cx from 'classnames'; 2 | import React, { Component, PropTypes } from 'react'; 3 | 4 | // Just a convenience mapping for payMethods from payMethod-validator 5 | // to shorted classes used in CSS. 6 | const payMethodTypeMap = { 7 | 'american-express': 'amex', 8 | 'diners-club': 'diners', 9 | 'master-card': 'mastercard', 10 | }; 11 | 12 | export default class PayMethodIcon extends Component { 13 | 14 | static propTypes = { 15 | payMethodType: PropTypes.oneOf([ 16 | 'amex', 17 | 'american-express', 18 | 'diners-club', 19 | 'discover', 20 | 'jcb', 21 | 'maestro', 22 | 'mastercard', 23 | 'master-card', 24 | 'visa', 25 | ]), 26 | } 27 | 28 | render() { 29 | // This is only displayed if a payMethodType is passed-in. 30 | var payMethodType = this.props.payMethodType; 31 | var payMethodClassName = cx([ 32 | 'pay-method-icon', 33 | 'pmtype-' + (payMethodTypeMap[payMethodType] || payMethodType), 34 | ]); 35 | return payMethodType ? : null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/js/components/pay-method-item.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import PayMethodIcon from 'components/pay-method-icon'; 3 | 4 | 5 | export default class PayMethodItem extends Component { 6 | 7 | static propTypes = { 8 | checked: PropTypes.bool.isRequired, 9 | inputType: PropTypes.string.isRequired, 10 | onChangeHandler: PropTypes.func.isRequired, 11 | payMethod: PropTypes.shape({ 12 | id: PropTypes.number, 13 | resource_uri: PropTypes.string, 14 | truncated_id: PropTypes.string, 15 | type_name: PropTypes.string, 16 | }), 17 | } 18 | 19 | static defaultProps = { 20 | inputType: 'radio', 21 | } 22 | 23 | render() { 24 | var payMethod = this.props.payMethod; 25 | var payMethodType = payMethod.type_name.toLowerCase(); 26 | var payMethodText = '●●●● ●●●● ●●●● ' + payMethod.truncated_id; 27 | var inputId = 'paymethod-' + payMethod.id; 28 | 29 | return ( 30 |
    31 | 32 | 40 | 41 |
    42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/js/components/pay-method-list.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import PayMethodItem from 'components/pay-method-item'; 3 | 4 | import cx from 'classnames'; 5 | 6 | 7 | export default class PayMethodList extends Component { 8 | 9 | static propTypes = { 10 | cssModifier: PropTypes.string, 11 | onPayMethodChange: PropTypes.func.isRequired, 12 | payMethods: PropTypes.arrayOf( 13 | PropTypes.shape({ 14 | id: PropTypes.number, 15 | resource_uri: PropTypes.string, 16 | truncated_id: PropTypes.string, 17 | type_name: PropTypes.string, 18 | }) 19 | ).isRequired, 20 | } 21 | 22 | render() { 23 | var payMethods = this.props.payMethods; 24 | var payMethodList = []; 25 | for (var i = 0; i < payMethods.length; i += 1) { 26 | var { checked, ...payMethod } = payMethods[i]; 27 | payMethodList.push(( 28 |
  • 29 | 35 |
  • 36 | )); 37 | } 38 | 39 | var classes = cx('pay-method-list', this.props.cssModifier); 40 | 41 | return ( 42 | 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/js/components/product-detail.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import * as products from 'products'; 3 | import { gettext } from 'utils'; 4 | 5 | export default class ProductDetail extends Component { 6 | 7 | static propTypes = { 8 | productId: PropTypes.string.isRequired, 9 | userDefinedAmount: PropTypes.string, 10 | } 11 | 12 | render() { 13 | 14 | var productData = products.get(this.props.productId); 15 | var recurrence = (productData.recurrence === 'monthly' ? 16 |
    {gettext('per month')}
    : ''); 17 | var price; 18 | 19 | // TODO: localize/format prices with currency symbol. 20 | if (productData.price && Object.keys(productData.price).length) { 21 | console.log('Showing configured price for product', productData.id); 22 | price = productData.price.en; 23 | } else { 24 | console.log('Showing user defined amount for product', 25 | productData.id); 26 | var decimalPrice = parseFloat(this.props.userDefinedAmount); 27 | price = '$' + parseFloat(Math.round(decimalPrice * 100) / 100).toFixed(2); 28 | } 29 | 30 | return ( 31 |
    32 |

    {productData.seller.name.en}

    33 |
    {productData.description.en}
    34 |
    {price}
    35 | {recurrence} 36 |
    37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/js/components/spinner.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { gettext } from 'utils'; 3 | 4 | 5 | export default class Spinner extends Component { 6 | 7 | static propTypes = { 8 | text: PropTypes.string.isRequired, 9 | } 10 | 11 | static defaultProps = { 12 | text: gettext('Loading'), 13 | } 14 | 15 | render() { 16 | return ( 17 |
    18 |
    19 | {this.props.text} 20 |
    21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /public/js/components/submit-button.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import { gettext } from 'utils'; 5 | 6 | 7 | export default class SubmitButton extends Component { 8 | 9 | static propTypes = { 10 | content: PropTypes.string, 11 | cssModifier: PropTypes.string, 12 | isDisabled: PropTypes.bool, 13 | showSpinner: PropTypes.bool, 14 | } 15 | 16 | static defaultProps = { 17 | cssModifier: null, 18 | content: gettext('Submit'), 19 | } 20 | 21 | render() { 22 | var { isDisabled, content, showSpinner, ...buttonAttrs } = this.props; 23 | 24 | var buttonClassNames = cx({ 25 | spinner: showSpinner, 26 | }, this.props.cssModifier); 27 | 28 | // If we're showing the spinner we want the 29 | // button to be automagically disabled. 30 | if (showSpinner) { 31 | isDisabled = true; 32 | } 33 | 34 | return ( 35 | 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/js/components/subscription-list.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import { gettext } from 'utils'; 4 | import Spinner from 'components/spinner'; 5 | import Subscription from 'components/subscription'; 6 | 7 | 8 | export default class SubscriptionList extends Component { 9 | 10 | static propTypes = { 11 | payMethods: PropTypes.array, 12 | subscriptions: PropTypes.array, 13 | } 14 | 15 | static defaultProps = { 16 | subscriptions: null, 17 | } 18 | 19 | render() { 20 | console.log('subscriptions:', this.props.subscriptions); 21 | 22 | if (this.props.subscriptions === null) { 23 | return ; 24 | } else { 25 | 26 | var subs = []; 27 | this.props.subscriptions.forEach((data) => { 28 | subs.push( 29 |
  • 30 | 32 |
  • 33 | ); 34 | }); 35 | 36 | if (subs.length) { 37 | return ( 38 | 41 | ); 42 | } else { 43 | return

    {gettext("You haven't subscribed to anything yet.")}

    ; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/js/components/subscription.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import PayMethodDropDown from 'components/pay-method-drop-down'; 4 | import { gettext } from 'utils'; 5 | import * as products from 'products'; 6 | 7 | 8 | export default class Subscription extends Component { 9 | 10 | static propTypes = { 11 | payMethods: PropTypes.array.isRequired, 12 | paymethod: PropTypes.string.isRequired, 13 | seller_product: PropTypes.shape({ 14 | public_id: PropTypes.string, 15 | }).isRequired, 16 | showNav: PropTypes.bool, 17 | showNextPayment: PropTypes.bool, 18 | } 19 | 20 | static defaultProps = { 21 | showNav: true, 22 | showNextPayment: true, 23 | } 24 | 25 | render() { 26 | var productData = products.get(this.props.seller_product.public_id); 27 | 28 | return ( 29 |
    30 |
    31 | 32 |
    33 |

    {productData.seller.name.en}

    34 |

    {productData.description.en}

    35 | 36 | {gettext('Monthly price')} {productData.price.en} 37 | {this.props.showNextPayment ? 38 | 39 | {gettext('Next payment')} PLACEHOLDER : null} 40 |
    41 |
    42 | {this.props.showNav ? 43 |
    44 | 46 | {this.props.payMethods.length ? 47 | : 52 |

    {gettext('No payment methods available')}

    53 | } 54 | {gettext('Cancel subscription')} 55 |
    : null} 56 |
    57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /public/js/constants/action-types.js: -------------------------------------------------------------------------------- 1 | export const ADD_NOTIFICATION = 'ADD_NOTIFICATION'; 2 | export const ADD_PAY_METHOD = 'ADD_PAY_METHOD'; 3 | export const APP_ERROR = 'APP_ERROR'; 4 | export const BEGIN_PROCESSING = 'BEGIN_PROCESSING'; 5 | export const CLOSE_MODAL = 'CLOSE_MODAL'; 6 | export const COMPLETE_TRANSACTION = 'COMPLETE_TRANSACTION'; 7 | export const CREDIT_CARD_SUBMISSION_ERRORS = 'CREDIT_CARD_SUBMISSION_ERRORS'; 8 | export const DEL_PAY_METHOD = 'DEL_PAY_METHOD'; 9 | export const GOT_BRAINTREE_TOKEN = 'GOT_BRAINTREE_TOKEN'; 10 | export const GOT_CSRF_TOKEN = 'GOT_CSRF_TOKEN'; 11 | export const GOT_PAY_METHODS = 'GOT_PAY_METHODS'; 12 | export const GOT_SUBS_BY_PAY_METHOD = 'GOT_SUBS_BY_PAY_METHOD'; 13 | export const GOT_USER_SUBS = 'GOT_USER_SUBS'; 14 | export const GOT_USER_TRANSACTIONS = 'GOT_USER_TRANSACTIONS'; 15 | export const LOADING_SUBS_BY_PAY_METHOD = 'LOADING_SUBS_BY_PAY_METHOD'; 16 | export const LOADING_USER_SUBS = 'LOADING_USER_SUBS'; 17 | export const LOADING_USER_TRANSACTIONS = 'LOADING_USER_TRANSACTIONS'; 18 | export const PAY_WITH_NEW_CARD = 'PAY_WITH_NEW_CARD'; 19 | export const REMOVE_NOTIFICATION = 'REMOVE_NOTIFICATION'; 20 | export const SHOW_ADD_PAY_METHOD = 'SHOW_ADD_PAY_METHOD'; 21 | export const SHOW_CONFIRM_DEL_PAY_METHOD = 'SHOW_CONFIRM_DEL_PAY_METHOD'; 22 | export const SHOW_DEL_PAY_METHOD = 'SHOW_DEL_PAY_METHOD'; 23 | export const SHOW_HISTORY = 'SHOW_HISTORY'; 24 | export const SHOW_MY_ACCOUNT = 'SHOW_MY_ACCOUNT'; 25 | export const SHOW_PAY_METHODS = 'SHOW_PAY_METHODS'; 26 | export const SHOW_SIGN_IN = 'SHOW_SIGN_IN'; 27 | export const SHOW_SIGN_OUT = 'SHOW_SIGN_OUT'; 28 | export const SHOW_SUBS = 'SHOW_SUBS'; 29 | export const STOP_PROCESSING = 'STOP_PROCESSING'; 30 | export const USER_SIGNED_IN = 'USER_SIGNED_IN'; 31 | export const USER_SIGNED_OUT = 'USER_SIGNED_OUT'; 32 | -------------------------------------------------------------------------------- /public/js/constants/error-codes.js: -------------------------------------------------------------------------------- 1 | export const ALREADY_SUBSCRIBED = 'ALREADY_SUBSCRIBED'; 2 | export const API_SIGN_IN_FAILURE = 'API_SIGN_IN_FAILURE'; 3 | export const API_SIGN_OUT_FAILURE = 'API_SIGN_OUT_FAILURE'; 4 | export const BRAINTREE_TOKENIZATION_ERROR = 'BRAINTREE_TOKENIZATION_ERROR'; 5 | export const BRAINTREE_TOKEN_GET_FAILED = 'BRAINTREE_TOKEN_GET_FAILED'; 6 | export const FXA_SIGN_IN_FAILURE = 'FXA_SIGN_IN_FAILURE'; 7 | export const FXA_TOKEN_SIGN_IN_FAILURE = 'FXA_TOKEN_SIGN_IN_FAILURE'; 8 | export const ONE_TIME_PAYMENT_FAILED = 'ONE_TIME_PAYMENT_FAILED'; 9 | export const PAY_METHOD_DELETION_FAILED = 'PAY_METHOD_DELETION_FAILED'; 10 | export const PAY_METHOD_GET_FAILED = 'PAY_METHOD_GET_FAILED'; 11 | export const PRODUCT_ID_INVALID = 'PRODUCT_ID_INVALID'; 12 | export const SUBS_BY_PAY_METHOD_GET_FAILED = 'SUBS_BY_PAY_METHOD_GET_FAILED'; 13 | export const SUBS_GET_FAILED = 'SUBS_GET_FAILED'; 14 | export const SUB_CREATION_FAILED = 'SUB_CREATION_FAILED'; 15 | export const SUB_UPDATE_FAILED = 'SUB_UPDATE_FAILED'; 16 | export const TRANSACTIONS_GET_FAILED = 'TRANSACTIONS_GET_FAILED'; 17 | -------------------------------------------------------------------------------- /public/js/data-store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | 4 | import rootReducer from 'reducers'; 5 | 6 | 7 | function logger({ getState }) { 8 | return next => action => { 9 | if (typeof action.type === 'undefined') { 10 | console.error('action.type is undefined.', 11 | 'Check that the action is defined in constants/action-types.js'); 12 | } 13 | console.info('redux: dispatching', action); 14 | const result = next(action); 15 | console.log('redux: state after', getState()); 16 | return result; 17 | }; 18 | } 19 | 20 | 21 | /** 22 | * Schedules actions with { meta: { delay: N } } to be delayed 23 | * by N milliseconds. 24 | * Makes `dispatch` return a function to cancel the timeout in this case. 25 | * http://rackt.github.io/redux/docs/advanced/Middleware.html#seven-examples 26 | */ 27 | const timeoutScheduler = store => next => action => { // eslint-disable-line 28 | if (!action.meta || !action.meta.delay) { 29 | return next(action); 30 | } 31 | 32 | console.log('Scheduling action with timeout', action); 33 | let timeoutId = setTimeout( 34 | () => { 35 | console.log('Running after timeout', action.meta.delay); 36 | next(action); 37 | }, 38 | action.meta.delay 39 | ); 40 | 41 | return function cancel() { 42 | clearTimeout(timeoutId); 43 | }; 44 | }; 45 | 46 | 47 | const createStoreWithMiddleware = applyMiddleware( 48 | timeoutScheduler, 49 | thunk, 50 | logger 51 | )(createStore); 52 | 53 | 54 | export function createReduxStore() { 55 | const store = createStoreWithMiddleware(rootReducer); 56 | 57 | if (module.hot) { 58 | // Enable Webpack hot module replacement for reducers 59 | // See: https://github.com/rackt/react-redux/releases/tag/v2.0.0 60 | module.hot.accept('reducers', () => { 61 | const nextRootReducer = require('reducers'); 62 | store.replaceReducer(nextRootReducer); 63 | }); 64 | } 65 | 66 | return store; 67 | } 68 | 69 | export default createReduxStore(); 70 | -------------------------------------------------------------------------------- /public/js/products.js: -------------------------------------------------------------------------------- 1 | const productData = { 2 | 'mozilla-concrete-brick': require('json!mozilla-concrete-brick'), 3 | 'mozilla-concrete-mortar': require('json!mozilla-concrete-mortar'), 4 | 'mozilla-foundation-donation': require('json!mozilla-foundation-donation'), 5 | 'mozilla-foundation-recurring-donation': 6 | require('json!mozilla-foundation-recurring-donation'), 7 | }; 8 | 9 | export default productData; 10 | 11 | 12 | export function get(id) { 13 | if (!productData[id]) { 14 | throw new Error('Invalid product: ' + id); 15 | } 16 | return productData[id]; 17 | } 18 | -------------------------------------------------------------------------------- /public/js/reducers/app.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from 'constants/action-types'; 2 | import { getId } from 'utils'; 3 | 4 | 5 | export const initialAppState = { 6 | csrfToken: null, 7 | // Notification is a mappable array e.g: 8 | // [[key, notification], [key, notification], ...] 9 | notifications: [], 10 | }; 11 | 12 | 13 | export default function app(state, action) { 14 | switch (action.type) { 15 | case actionTypes.GOT_CSRF_TOKEN: 16 | return Object.assign({}, state, { 17 | csrfToken: action.csrfToken, 18 | }); 19 | case actionTypes.ADD_NOTIFICATION: 20 | var newNotifications = state.notifications.slice(0); 21 | var notificationMap = new Map(newNotifications); 22 | var notification = notificationMap.set(getId(), action.data); 23 | newNotifications = [...notificationMap]; 24 | return Object.assign({}, state, { 25 | notifications: newNotifications, 26 | }); 27 | case actionTypes.REMOVE_NOTIFICATION: 28 | var newNotifications = state.notifications.slice(0); 29 | var notificationMap = new Map(newNotifications); 30 | var notification = notificationMap.get(action.id); 31 | if (notification) { 32 | console.log('Removing', notification, 'from notifications'); 33 | notificationMap.delete(action.id); 34 | newNotifications = [...notificationMap]; 35 | } else { 36 | console.warn("Can't remove non-existant notification id", action.id); 37 | } 38 | return Object.assign({}, state, { 39 | notifications: newNotifications, 40 | }); 41 | default: 42 | return state || initialAppState; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/js/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import { default as app } from './app'; 4 | import { default as management } from './management'; 5 | import { default as processing } from './processing'; 6 | import { default as transaction } from './transaction'; 7 | import { default as user } from './user'; 8 | 9 | 10 | const rootReducer = combineReducers({ 11 | app, 12 | management, 13 | processing, 14 | transaction, 15 | user, 16 | }); 17 | 18 | export default rootReducer; 19 | -------------------------------------------------------------------------------- /public/js/reducers/processing.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from 'constants/action-types'; 2 | 3 | // Map of things that are currently being processed. 4 | // Each key is a custom processing ID. When a value is `true` it means 5 | // the action is in process. 6 | // 7 | // Example: 8 | // `state['CardForm-id'] === true` would mean the CardForm is currently 9 | // being processed. 10 | // `!state['CardForm-id']` would mean the CardForm is not being processed. 11 | // 12 | export const initialProcessingState = {}; 13 | 14 | 15 | export default function app(state, action) { 16 | switch (action.type) { 17 | case actionTypes.BEGIN_PROCESSING: 18 | var currentlyInProcess = Object.assign({}, state); 19 | currentlyInProcess[action.processingId] = true; 20 | return currentlyInProcess; 21 | case actionTypes.STOP_PROCESSING: 22 | var currentlyInProcess = Object.assign({}, state); 23 | if (currentlyInProcess[action.processingId]) { 24 | // To prevent old entries from hanging around, 25 | // delete the flag entirely. 26 | delete currentlyInProcess[action.processingId]; 27 | } 28 | return currentlyInProcess; 29 | default: 30 | return state || initialProcessingState; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /public/js/reducers/transaction.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from 'constants/action-types'; 2 | 3 | 4 | export const initialTransState = { 5 | completed: false, 6 | availablePayMethods: [], 7 | cardSubmissionErrors: null, 8 | userEmail: undefined, 9 | }; 10 | 11 | 12 | export default function transaction(state, action) { 13 | 14 | if (action.type === actionTypes.COMPLETE_TRANSACTION) { 15 | return Object.assign({}, initialTransState, { 16 | completed: true, 17 | userEmail: action.userEmail, 18 | }); 19 | } 20 | 21 | if (action.type === actionTypes.PAY_WITH_NEW_CARD) { 22 | // What this does is tell Transaction that there are no available 23 | // payment methods for the transaction even though the user 24 | // itself may have some. This causes a 'pay with new card' form 25 | // to appear. 26 | return Object.assign({}, initialTransState, { 27 | availablePayMethods: [], 28 | }); 29 | } 30 | 31 | if (action.type === actionTypes.USER_SIGNED_IN) { 32 | // By default, assume the user wants to pay with their saved pay methods. 33 | return Object.assign({}, initialTransState, { 34 | availablePayMethods: action.user.payMethods, 35 | }); 36 | } 37 | 38 | if (action.type === actionTypes.CREDIT_CARD_SUBMISSION_ERRORS) { 39 | return Object.assign({}, initialTransState, { 40 | cardSubmissionErrors: action.apiErrorResult, 41 | }); 42 | } 43 | 44 | return state || initialTransState; 45 | } 46 | -------------------------------------------------------------------------------- /public/js/reducers/user.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from 'constants/action-types'; 2 | 3 | 4 | export const initialUserState = { 5 | signedIn: false, 6 | email: null, 7 | payMethods: [], 8 | braintreeToken: null, 9 | subscriptions: null, 10 | transactions: null, 11 | }; 12 | 13 | 14 | export default function user(state, action) { 15 | 16 | if (action.type === actionTypes.USER_SIGNED_OUT) { 17 | return Object.assign({}, initialUserState, { 18 | signedIn: false, 19 | }); 20 | } 21 | 22 | if (action.type === actionTypes.LOADING_USER_TRANSACTIONS) { 23 | return Object.assign({}, initialUserState, state, { 24 | transactions: initialUserState.transactions, 25 | }); 26 | } 27 | 28 | if (action.type === actionTypes.GOT_USER_TRANSACTIONS) { 29 | return Object.assign({}, initialUserState, state, { 30 | transactions: action.transactions, 31 | }); 32 | } 33 | 34 | if (action.type === actionTypes.USER_SIGNED_IN) { 35 | return Object.assign({}, initialUserState, { 36 | signedIn: true, 37 | email: action.user.email, 38 | payMethods: action.user.payMethods, 39 | }); 40 | } 41 | 42 | if (action.type === actionTypes.GOT_BRAINTREE_TOKEN) { 43 | return Object.assign({}, initialUserState, state, { 44 | braintreeToken: action.braintreeToken, 45 | }); 46 | } 47 | 48 | if (action.type === actionTypes.LOADING_USER_SUBS) { 49 | return Object.assign({}, initialUserState, state, { 50 | subscriptions: initialUserState.subscriptions, 51 | }); 52 | } 53 | 54 | if (action.type === actionTypes.GOT_USER_SUBS) { 55 | return Object.assign({}, initialUserState, state, { 56 | subscriptions: action.subscriptions, 57 | }); 58 | } 59 | 60 | if (action.type === actionTypes.GOT_PAY_METHODS) { 61 | return Object.assign({}, initialUserState, state, { 62 | payMethods: action.payMethods, 63 | }); 64 | } 65 | 66 | if (action.type === actionTypes.ADD_PAY_METHOD) { 67 | return Object.assign({}, initialUserState, state, { 68 | payMethods: action.payMethods, 69 | }); 70 | } 71 | 72 | if (action.type === actionTypes.DEL_PAY_METHOD) { 73 | return Object.assign({}, initialUserState, state, { 74 | payMethods: action.payMethods, 75 | }); 76 | } 77 | 78 | return state || initialUserState; 79 | } 80 | -------------------------------------------------------------------------------- /public/js/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | supportedLanguages: [ 3 | 'ca', 4 | 'cs', 5 | 'cy', 6 | 'da', 7 | 'de', 8 | 'dsb', 9 | 'en', 10 | 'es', 11 | 'es-AR', 12 | 'es-CL', 13 | 'et', 14 | 'eu', 15 | 'ff', 16 | 'fr', 17 | 'fy', 18 | 'he', 19 | 'hsb', 20 | 'hu', 21 | 'id', 22 | 'it', 23 | 'ja', 24 | 'ko', 25 | 'lt', 26 | 'nb-NO', 27 | 'nl', 28 | 'pa', 29 | 'pl', 30 | 'pt', 31 | 'pt-BR', 32 | 'rm', 33 | 'ru', 34 | 'sk', 35 | 'sl', 36 | 'sq', 37 | 'sr', 38 | 'sr-LATN', 39 | 'sv', 40 | 'sv-SE', 41 | 'tr', 42 | 'uk', 43 | 'zh-CN', 44 | 'zh-TW', 45 | ], 46 | tracking: { 47 | enabled: false, 48 | id: 'UA-35433268-60', 49 | }, 50 | 51 | // FxA client ID (configured per host). 52 | // This is used for user sign-in, not token sign in. 53 | // The payments-service API must be configured with each 54 | // corresponding secret. 55 | fxaClientId: undefined, 56 | fxaRedirectUri: undefined, 57 | fxaContentHost: 'https://stable.dev.lcip.org', 58 | fxaOauthHost: 'https://oauth-stable.dev.lcip.org/v1', 59 | apiPrefix: '/api', 60 | allowCORSRequests: false, 61 | 62 | // This is a map of custom settings that will be defined 63 | // based on host. 64 | hostSettings: { 65 | 'pay.dev.mozaws.net:8000': { 66 | fxaClientId: '90f432a069a26c77', 67 | fxaRedirectUri: 'http://pay.dev.mozaws.net:8000', 68 | }, 69 | // These are only used locally for docker. 70 | 'pay.dev:8000': { 71 | fxaClientId: '8d7c6c8549cc6deb', 72 | fxaRedirectUri: 'http://pay.dev:8000', 73 | }, 74 | 'pay.webpack:8080': { 75 | fxaClientId: 'a63657a4c78dd650', 76 | fxaRedirectUri: 'http://pay.webpack:8080', 77 | // Webpack needs to talk via CORS to the dev API. 78 | // See payments-service for how this is allowed. 79 | apiPrefix: 'http://pay.dev:8000/api', 80 | allowCORSRequests: true, 81 | }, 82 | }, 83 | }; 84 | 85 | 86 | if (typeof window !== 'undefined') { 87 | var host = window.location.host; 88 | var hostSettings = module.exports.hostSettings[host]; 89 | 90 | if (!hostSettings) { 91 | // For example, the unit tests are served by localhost which is unmapped. 92 | console.warn('no custom settings for host ' + host); 93 | } else { 94 | console.log('applying custom settings for host ' + host); 95 | for (var attr in hostSettings) { 96 | module.exports[attr] = hostSettings[attr]; 97 | } 98 | } 99 | 100 | } else { 101 | console.warn('probably not in a web browser; skipping host configuration'); 102 | } 103 | -------------------------------------------------------------------------------- /public/js/views/management/add-pay-method.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import CardForm from 'components/card-form'; 4 | 5 | import { default as tracking } from 'tracking'; 6 | import { gettext, setTitle } from 'utils'; 7 | 8 | 9 | export default class AddPayMethod extends Component { 10 | 11 | static propTypes = { 12 | addCreditCard: PropTypes.func.isRequired, 13 | braintreeToken: PropTypes.string.isRequired, 14 | cardSubmissionErrors: PropTypes.object, 15 | closeModal: PropTypes.func.isRequired, 16 | showPayMethods: PropTypes.func.isRequired, 17 | } 18 | 19 | componentDidMount() { 20 | setTitle(gettext('Add Payment Method')); 21 | tracking.setPage('/add-pay-method'); 22 | } 23 | 24 | handleCardSubmit(creditCard, processingId) { 25 | console.log('submitting credit card as new pay method'); 26 | this.props.addCreditCard({braintreeToken: this.props.braintreeToken, 27 | creditCard: creditCard, 28 | processingId: processingId}); 29 | } 30 | 31 | render() { 32 | return ( 33 |
    34 |

    {gettext('Add Payment Method')}

    35 | 42 | 43 | {gettext('Back')} 44 |
    45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/js/views/management/del-pay-method.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import PayMethodChoice from 'components/pay-method-choice'; 4 | 5 | import { default as tracking } from 'tracking'; 6 | import { gettext, setTitle } from 'utils'; 7 | 8 | 9 | export default class DelPayMethod extends Component { 10 | 11 | static propTypes = { 12 | closeModal: PropTypes.func.isRequired, 13 | payMethods: PropTypes.array.isRequired, 14 | showConfirmDelPayMethod: PropTypes.func.isRequired, 15 | showPayMethods: PropTypes.func.isRequired, 16 | } 17 | 18 | componentDidMount() { 19 | setTitle(gettext('Delete Payment Method')); 20 | tracking.setPage('/del-pay-method'); 21 | } 22 | 23 | handleSubmit = (payMethod) => { 24 | this.props.showConfirmDelPayMethod(payMethod); 25 | } 26 | 27 | render() { 28 | return ( 29 |
    30 |

    {gettext('Delete Payment Method')}

    31 |

    {gettext('Choose a payment method to delete. ' + 32 | "You'll confirm on the next step.")}

    33 |
    34 | 39 |
    40 | 41 | {gettext('Back')} 42 |
    43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/js/views/management/history.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import JsonTable from 'react-json-table'; 3 | import fecha from 'fecha'; 4 | 5 | import { configGetText, gettext, setTitle } from 'utils'; 6 | import * as products from 'products'; 7 | import Spinner from 'components/spinner'; 8 | 9 | 10 | export default class History extends Component { 11 | 12 | static propTypes = { 13 | getUserTransactions: PropTypes.func.isRequired, 14 | transactions: PropTypes.array.isRequired, 15 | }; 16 | 17 | componentDidMount() { 18 | setTitle(gettext('Transaction Receipts & History')); 19 | this.props.getUserTransactions(); 20 | } 21 | 22 | showReceipt(e, transactionId) { 23 | e.preventDefault(); 24 | // https://github.com/mozilla/payments-ui/issues/304 25 | console.log('TODO: show receipt for transaction', transactionId); 26 | } 27 | 28 | renderContent() { 29 | 30 | var columns = [ 31 | { 32 | key: 'created', 33 | label: gettext('Date'), 34 | cell: (item, columnKey) => ( 35 | fecha.format(fecha.parse(item[columnKey], 'YYYY-MM-DDThh:mm:ss.SSS'), 36 | 'MMM D, YYYY') 37 | ), 38 | }, 39 | { 40 | key: 'seller', 41 | label: gettext('Vendor & Product'), 42 | cell: (item) => ( 43 | configGetText( 44 | products.get( 45 | item.transaction.seller_product.public_id 46 | ).seller.name) 47 | ), 48 | }, 49 | { 50 | key: 'amount', 51 | label: gettext('Amount'), 52 | cell: (item) => ( 53 | // TODO: display currency with symbol (when possible) 54 | // https://github.com/mozilla/payments-ui/issues/308 55 | this.showReceipt(e, item.resource_pk)} 56 | href="#">{item.transaction.currency} {item.transaction.amount} 57 | ), 58 | }, 59 | ]; 60 | 61 | // TODO: this is currently showing all transactions but that might be 62 | // weird when it includes kinds other than subscription_charged_successfully 63 | 64 | if (this.props.transactions === null) { 65 | return ; 66 | } else { 67 | if (this.props.transactions.length) { 68 | return ; 69 | } else { 70 | return

    {gettext("You haven't purchased anything yet.")}

    ; 71 | } 72 | } 73 | } 74 | 75 | render() { 76 | return ( 77 |
    78 |

    {gettext('Transaction Receipts & History')}

    79 | {this.renderContent()} 80 |
    81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /public/js/views/management/my-account.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { gettext, setTitle } from 'utils'; 3 | 4 | 5 | export default class MyAccount extends Component { 6 | 7 | static propTypes = { 8 | user: PropTypes.object.isRequired, 9 | } 10 | 11 | componentDidMount() { 12 | setTitle(gettext('My Account')); 13 | } 14 | 15 | render() { 16 | return ( 17 |
    18 |

    {gettext('My Account')}

    19 | 20 |

    {gettext('Edit My Account')}

    21 |
    22 |

    {this.props.user.email}

    23 | 27 | {gettext('Change email address and password')} 28 | 29 |
    30 | 31 |

    {gettext('Delete Account')}

    32 |

    {gettext('When you delete your account, all of your ' + 33 | 'subscriptions will be cancelled. This action cannot ' + 34 | 'be undone. Are you sure you want to delete your account?')}

    35 |
    36 |
    37 | 38 |
    39 |
    40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/js/views/management/pay-methods.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import PayMethodList from 'components/pay-method-list'; 5 | 6 | import { gettext, isDisabled, setTitle } from 'utils'; 7 | 8 | 9 | export default class PayMethods extends Component { 10 | 11 | static propTypes = { 12 | getPayMethods: PropTypes.func.isRequired, 13 | payMethods: PropTypes.array.isRequired, 14 | showAddPayMethod: PropTypes.func.isRequired, 15 | showDelPayMethod: PropTypes.func.isRequired, 16 | }; 17 | 18 | componentDidMount() { 19 | setTitle(gettext('Payment Methods')); 20 | this.props.getPayMethods(); 21 | } 22 | 23 | handleAddPayMethod = e => { 24 | e.preventDefault(); 25 | this.props.showAddPayMethod(); 26 | } 27 | 28 | handleDelPayMethod = e => { 29 | e.preventDefault(); 30 | if (isDisabled(e.target)) { 31 | console.log('Delete link is disabled. no-op'); 32 | return; 33 | } 34 | this.props.showDelPayMethod(); 35 | } 36 | 37 | renderChild() { 38 | if (this.props.payMethods && this.props.payMethods.length) { 39 | return ( 40 | 41 | ); 42 | } 43 | return (

    44 | {gettext("You haven't added any credit cards yet")} 45 |

    ); 46 | } 47 | 48 | render() { 49 | 50 | var isDeleteDisabled = !this.props.payMethods || 51 | !this.props.payMethods.length; 52 | var deleteClasses = cx('delete', {disabled: isDeleteDisabled}); 53 | 54 | return ( 55 |
    56 |

    {gettext('Payment Methods')}

    57 |
    58 | {this.renderChild()} 59 | 69 |
    70 |
    71 | ); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /public/js/views/management/subscriptions.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import SubscriptionList from 'components/subscription-list'; 3 | 4 | import { gettext, setTitle } from 'utils'; 5 | 6 | 7 | export default class Subscriptions extends Component { 8 | 9 | static propTypes = { 10 | getUserSubscriptions: PropTypes.func.isRequired, 11 | payMethods: PropTypes.array.isRequired, 12 | userSubscriptions: PropTypes.array.isRequired, 13 | }; 14 | 15 | componentDidMount() { 16 | this.props.getUserSubscriptions(); 17 | setTitle(gettext('Subscriptions')); 18 | } 19 | 20 | render() { 21 | 22 | return ( 23 |
    24 |

    {gettext('Subscriptions')}

    25 | 29 |
    30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /public/js/views/shared/braintree-token.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import Spinner from 'components/spinner'; 4 | 5 | import { gettext } from 'utils'; 6 | import tracking from 'tracking'; 7 | 8 | export default class BrainTree extends Component { 9 | 10 | static propTypes = { 11 | getBraintreeToken: PropTypes.func.isRequired, 12 | }; 13 | 14 | componentDidMount() { 15 | tracking.setPage('/braintree-token'); 16 | this.props.getBraintreeToken(); 17 | } 18 | 19 | render() { 20 | return ; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /public/js/views/shared/sign-in.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import Spinner from 'components/spinner'; 4 | 5 | import { gettext } from 'utils'; 6 | import tracking from 'tracking'; 7 | 8 | 9 | export default class SignIn extends Component { 10 | 11 | static propTypes = { 12 | accessToken: PropTypes.string.isRequired, 13 | allowUserSignIn: PropTypes.bool, 14 | error: PropTypes.func.isRequired, 15 | tokenSignIn: PropTypes.func.isRequired, 16 | user: PropTypes.object.isRequired, 17 | userSignIn: PropTypes.func.isRequired, 18 | }; 19 | 20 | static defaultProps = { 21 | allowUserSignIn: true, 22 | } 23 | 24 | componentDidMount() { 25 | tracking.setPage('/sign-in'); 26 | if (!this.props.user.signedIn) { 27 | if (this.props.accessToken) { 28 | console.log('signing in with access token'); 29 | this.props.tokenSignIn(this.props.accessToken); 30 | } else if (this.props.allowUserSignIn) { 31 | console.log('prompting user to sign in'); 32 | this.props.userSignIn(); 33 | } else { 34 | return this.props.error( 35 | 'cannot sign in: no access token provided' 36 | ); 37 | } 38 | } 39 | } 40 | 41 | render() { 42 | return ; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /public/js/views/shared/sign-out.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import Spinner from 'components/spinner'; 4 | 5 | import { gettext } from 'utils'; 6 | import tracking from 'tracking'; 7 | 8 | 9 | export default class SignOut extends Component { 10 | 11 | static propTypes = { 12 | showSignIn: PropTypes.func.isRequired, 13 | user: PropTypes.object.isRequired, 14 | userSignOut: PropTypes.func.isRequired, 15 | }; 16 | 17 | componentDidMount() { 18 | tracking.setPage('/sign-out'); 19 | if (this.props.user.signedIn) { 20 | this.props.userSignOut(); 21 | } 22 | } 23 | 24 | handleShowSignIn = e => { 25 | e.preventDefault(); 26 | this.props.showSignIn(); 27 | } 28 | 29 | render() { 30 | if (this.props.user.signedIn) { 31 | return ; 32 | } else { 33 | return ( 34 |
    35 |

    36 | {gettext('Sign In')} 38 |

    39 |

    {gettext('Sign in to add and remove payment methods, ' + 40 | 'view receipts, and manage subscriptions.')}

    41 |
    42 | ); 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /public/js/views/transaction/product-pay-chooser.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import PayMethodChoice from 'components/pay-method-choice'; 4 | import ProductDetail from 'components/product-detail'; 5 | 6 | import * as products from 'products'; 7 | import { gettext } from 'utils'; 8 | import tracking from 'tracking'; 9 | 10 | 11 | export default class ProductPayChooser extends Component { 12 | 13 | static propTypes = { 14 | payMethods: PropTypes.array.isRequired, 15 | payWithNewCard: PropTypes.func.isRequired, 16 | processPayment: PropTypes.func.isRequired, 17 | productId: PropTypes.string.isRequired, 18 | userDefinedAmount: PropTypes.string, 19 | } 20 | 21 | componentDidMount() { 22 | tracking.setPage('/product-pay-chooser'); 23 | } 24 | 25 | handleSubmit = (payMethodUri, processingId) => { 26 | this.props.processPayment({productId: this.props.productId, 27 | userDefinedAmount: this.props.userDefinedAmount, 28 | payMethodUri: payMethodUri, 29 | processingId: processingId}); 30 | } 31 | 32 | render() { 33 | var product = products.get(this.props.productId); 34 | var submitPrompt; 35 | if (product.seller.kind === 'donations') { 36 | submitPrompt = gettext('Donate now'); 37 | } else { 38 | // TODO: also handle non-recurring, non-donations here. 39 | submitPrompt = gettext('Subscribe'); 40 | } 41 | 42 | return ( 43 | 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /public/js/views/transaction/product-pay.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import CardForm from 'components/card-form'; 4 | import ProductDetail from 'components/product-detail'; 5 | 6 | import * as products from 'products'; 7 | import { default as tracking } from 'tracking'; 8 | import { gettext } from 'utils'; 9 | 10 | 11 | export default class ProductPay extends Component { 12 | 13 | static propTypes = { 14 | braintreeToken: PropTypes.string.isRequired, 15 | cardSubmissionErrors: PropTypes.object, 16 | processPayment: PropTypes.func.isRequired, 17 | productId: PropTypes.string.isRequired, 18 | userDefinedAmount: PropTypes.string, 19 | } 20 | 21 | static defaultProps = { 22 | cardSubmissionErrors: null, 23 | } 24 | 25 | componentDidMount() { 26 | tracking.setPage('/product-pay'); 27 | } 28 | 29 | handleCardSubmit = (creditCard, processingId, {email} = {}) => { 30 | console.log('submitting credit card to sign up for subscription', 31 | this.props.productId); 32 | var data = { 33 | processingId: processingId, 34 | productId: this.props.productId, 35 | creditCard: creditCard, 36 | braintreeToken: this.props.braintreeToken, 37 | userDefinedAmount: this.props.userDefinedAmount, 38 | }; 39 | if (email) { 40 | console.log('Card submission included an email address'); 41 | data.email = email; 42 | } 43 | this.props.processPayment(data); 44 | } 45 | 46 | render() { 47 | var product = products.get(this.props.productId); 48 | var submitPrompt; 49 | if (product.seller.kind === 'donations') { 50 | submitPrompt = gettext('Donate now'); 51 | var emailFieldRequired = product.user_identification === 'email'; 52 | } else { 53 | // TODO: also handle non-recurring, non-donations here. 54 | submitPrompt = gettext('Subscribe'); 55 | } 56 | 57 | return ( 58 |
    59 | 63 | 71 |
    72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /public/management.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Management 10 | 11 | 12 |
    13 | Mozilla 14 |
    15 |
    16 |
    17 |
    18 |
    19 |
    20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/media/README.md: -------------------------------------------------------------------------------- 1 | Only used for tabzilla. 2 | -------------------------------------------------------------------------------- /public/media/img/tabzilla-static-high-res.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/media/img/tabzilla-static-high-res.png -------------------------------------------------------------------------------- /public/media/img/tabzilla-static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/public/media/img/tabzilla-static.png -------------------------------------------------------------------------------- /public/scss/_base.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | height: 100%; 6 | } 7 | 8 | main { 9 | padding: 20px; 10 | width: 100%; 11 | } 12 | 13 | hr { 14 | border: 1px solid $heading-border; 15 | border-bottom: none; 16 | border-right: none; 17 | border-left: none; 18 | margin: 2em 0; 19 | } 20 | -------------------------------------------------------------------------------- /public/scss/_buttons.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | display: inline-block; 3 | text-decoration: none; 4 | } 5 | 6 | .button, 7 | a.button, 8 | button { 9 | @include font(); 10 | background: $button-background-color; 11 | border: 0; 12 | border-radius: $small-border-radius; 13 | color: $message-text-color; 14 | cursor: pointer; 15 | font-size: $medium-font; 16 | margin: 10px 0; 17 | padding: 0.5em 1em; 18 | transition-duration: $short-transition; 19 | transition-property: background-color; 20 | text-align: center; 21 | width: 100%; 22 | line-height: 1.5em; 23 | 24 | &:active, 25 | &:hover, 26 | &:focus { 27 | background: $button-background-hover-color; 28 | } 29 | 30 | &:focus { 31 | @include focus(); 32 | } 33 | 34 | &:disabled, 35 | &.disabled { 36 | background: $button-background-disabled-color; 37 | cursor: not-allowed; 38 | } 39 | 40 | &.warning { 41 | background: $awooga-red; 42 | } 43 | } 44 | 45 | button::-moz-focus-inner { 46 | border: 0; 47 | padding: 0; 48 | } 49 | -------------------------------------------------------------------------------- /public/scss/_complete-payment.scss: -------------------------------------------------------------------------------- 1 | .complete { 2 | text-align: center; 3 | 4 | .accepted { 5 | @include header-font(); 6 | font-size: $large-font; 7 | font-weight: 200; 8 | padding: 20px 0; 9 | } 10 | 11 | .receipt { 12 | padding-bottom: 1em; 13 | 14 | span { 15 | display: block; 16 | } 17 | } 18 | 19 | button { 20 | max-width: calc(100% - 40px); 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /public/scss/_global.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | -------------------------------------------------------------------------------- /public/scss/_json-table.scss: -------------------------------------------------------------------------------- 1 | .jsonTable { 2 | width: 100%; 3 | 4 | td, th { 5 | text-align: left; 6 | padding: 0.35em; 7 | } 8 | 9 | thead { 10 | border: 1px solid $ship-grey; 11 | } 12 | 13 | th { 14 | font-weight: normal; 15 | background-color: $lightish-grey; 16 | color: $moody-grey; 17 | 18 | select { 19 | background-color: $lightish-grey; 20 | border-radius: 2px; 21 | border: 1px solid $ship-grey; 22 | color: $moody-grey; 23 | padding: 0.3em; 24 | } 25 | } 26 | 27 | tr { 28 | border-left: 1px solid $ship-grey; 29 | border-right: 1px solid $ship-grey; 30 | 31 | &.jsonEven { 32 | background-color: $management-grey; 33 | } 34 | 35 | &:last-child { 36 | border-bottom: 1px solid $ship-grey; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/scss/_mgmt-nav-sprite.scss: -------------------------------------------------------------------------------- 1 | .nav:after { 2 | background: url(../img/mgmt-nav-sprite.svg) no-repeat; 3 | } 4 | 5 | .history:after { 6 | background-position: 0 0; 7 | width: 21.84px; 8 | height: 28px; 9 | } 10 | 11 | .pay-methods:after { 12 | background-position: 0 33.333333333333336%; 13 | width: 28px; 14 | height: 28px; 15 | } 16 | 17 | .profile:after { 18 | background-position: 0 66.66666666666667%; 19 | width: 28px; 20 | height: 28px; 21 | } 22 | 23 | .subs:after { 24 | background-position: 0 100%; 25 | width: 24.29px; 26 | height: 28px; 27 | } 28 | 29 | -------------------------------------------------------------------------------- /public/scss/_modal.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | 3 | &.active { 4 | opacity: 1; 5 | } 6 | 7 | background: rgba(0, 0, 0, 0.5); 8 | bottom: 0; 9 | height: 100%; 10 | left: 0; 11 | opacity: 0; 12 | position: fixed; 13 | right: 0; 14 | top: 0; 15 | // Ensure transform on .inner doesn't 16 | // render on half a pixel thus causing fuzziness. 17 | transform-style: preserve-3d; 18 | z-index: 20; 19 | 20 | .inner { 21 | background: #fff; 22 | border-radius: 3px; 23 | border: 1px solid #C3CFD8; 24 | box-shadow: 0 3px 7px rgba(0, 0, 0, 0.5); 25 | height: 468px; 26 | margin: 0 auto; 27 | padding: 20px; 28 | position: relative; 29 | top: 50%; 30 | transform: translateY(-50%); 31 | width: 318px; 32 | } 33 | 34 | .close { 35 | color: #666; 36 | font-size: $large-font; 37 | line-height: 1ex; 38 | padding: 10px; 39 | position: absolute; 40 | right: -5px; 41 | text-decoration: none; 42 | top: -5px; 43 | z-index: 30; 44 | 45 | &:before { 46 | content: '×'; 47 | } 48 | } 49 | 50 | header { 51 | text-align: center; 52 | 53 | h2 { 54 | margin: 1em; 55 | font-weight: 400; 56 | } 57 | } 58 | 59 | form { 60 | padding-top: 0; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /public/scss/_notification.scss: -------------------------------------------------------------------------------- 1 | .notification-list { 2 | color: #fff; 3 | left: 200px; 4 | margin: 0; 5 | padding: 0; 6 | position: fixed; 7 | right: 0; 8 | text-align: center; 9 | top: 0; 10 | z-index: 30; 11 | 12 | .transaction & { 13 | position: static; 14 | left: 20px; 15 | right: 20px; 16 | margin-bottom: 20px; 17 | } 18 | } 19 | 20 | 21 | 22 | .notification { 23 | list-style-type: none; 24 | margin: 0; 25 | padding: 0.5em; 26 | position: relative; 27 | 28 | .transaction & { 29 | border-radius: 3px; 30 | } 31 | 32 | &.error { 33 | background: transparentize($awooga-red, 0.1); 34 | } 35 | 36 | &.info { 37 | background: transparentize($light-blue, 0.1); 38 | } 39 | 40 | .dismiss:after { 41 | color: #fff; 42 | content: '×'; 43 | display: block; 44 | height: 2em; 45 | position: absolute; 46 | right: 10px; 47 | top: 50%; 48 | transform: translateY(-50%); 49 | width: 2em; 50 | } 51 | } 52 | 53 | .notification-enter { 54 | opacity: 0.01; 55 | 56 | &.notification-enter-active { 57 | opacity: 1; 58 | transition: opacity .5s ease-in; 59 | } 60 | } 61 | 62 | .notification-leave { 63 | opacity: 1; 64 | 65 | &.notification-leave-active { 66 | opacity: 0.01; 67 | transition: opacity .5s ease-in; 68 | } 69 | } 70 | 71 | .notification-appear { 72 | opacity: 0.01; 73 | transition: opacity .5s ease-in; 74 | 75 | &.notification-appear-active { 76 | opacity: 1; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /public/scss/_pay-method-drop-down.scss: -------------------------------------------------------------------------------- 1 | .proxy-select { 2 | 3 | background: #fff; 4 | border: 1px solid #ccc; 5 | border-radius: 4px; 6 | font-size: $medium-font; 7 | margin: 1em 0; 8 | padding: 0.5em 1.35em 0.5em 0.5em; 9 | position: relative; 10 | width: 100%; 11 | max-width: 300px; 12 | 13 | &.active { 14 | border-color: $default-focus-color; 15 | box-shadow: 0 0 1px 0 $default-focus-color; 16 | } 17 | 18 | .has-pay-method { 19 | padding-left: 50px; 20 | } 21 | 22 | .pay-method-icon { 23 | left: 8px; 24 | } 25 | 26 | &:after { 27 | font-size: 12px; 28 | color: #666; 29 | content: '\25BC'; 30 | display: block; 31 | right: 0.5em; 32 | top: 50%; 33 | transform: translateY(-50%); 34 | position: absolute; 35 | } 36 | 37 | select { 38 | -moz-appearance: none; 39 | -webkit-appearance: none; 40 | appearance: none; 41 | opacity: 0; 42 | position: absolute; 43 | top: 0; 44 | right: 0; 45 | left: 0; 46 | bottom: 0; 47 | width: 100%; 48 | height: 100%; 49 | z-index: 10; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /public/scss/_pay-method-item.scss: -------------------------------------------------------------------------------- 1 | .pay-method-item { 2 | position: relative; 3 | 4 | input[type=hidden] + label { 5 | padding: 1em 0 1em 70px; 6 | text-align: left; 7 | } 8 | 9 | input[type=radio] { 10 | opacity: 0; 11 | position: absolute; 12 | 13 | &:checked + label, 14 | &:hover + label, 15 | &:focus + label { 16 | cursor: pointer; 17 | border: 1px solid $radio-selected-border; 18 | border-radius: 5px; 19 | background: $radio-selected-background; 20 | transition: all $medium-transition; 21 | } 22 | } 23 | 24 | & label { 25 | border: 1px solid transparent; 26 | display: block; 27 | font-size: $medium-font; 28 | font-weight: 700; 29 | padding: 1em 15px 1em 60px; 30 | position: relative; 31 | text-align: right; 32 | width: 100%; 33 | } 34 | 35 | .pay-method-icon { 36 | left: 15px; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/scss/_pay-method-list.scss: -------------------------------------------------------------------------------- 1 | .pay-method-list { 2 | margin: 0; 3 | padding: 0; 4 | 5 | li { 6 | list-style-type: none; 7 | margin: 10px 0; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /public/scss/_pay-method-sprite.scss: -------------------------------------------------------------------------------- 1 | .pay-method-icon { 2 | background: url(../img/pay-method-sprite.svg) no-repeat; 3 | } 4 | 5 | .pmtype-amex { 6 | background-position: 0 0; 7 | width: 40px; 8 | height: 25.05px; 9 | } 10 | 11 | .pmtype-diners { 12 | background-position: 0 16.57317695053544%; 13 | width: 40px; 14 | height: 25.12px; 15 | } 16 | 17 | .pmtype-discover { 18 | background-position: 0 33.26722538545199%; 19 | width: 40px; 20 | height: 25.69px; 21 | } 22 | 23 | .pmtype-jcb { 24 | background-position: 0 49.71953085160632%; 25 | width: 40px; 26 | height: 25.12px; 27 | } 28 | 29 | .pmtype-maestro { 30 | background-position: 0 66.29270780214176%; 31 | width: 40px; 32 | height: 25.12px; 33 | } 34 | 35 | .pmtype-mastercard { 36 | background-position: 0 82.8658847526772%; 37 | width: 40px; 38 | height: 25.12px; 39 | } 40 | 41 | .pmtype-visa { 42 | background-position: 0 99.43906170321264%; 43 | width: 40px; 44 | height: 25.12px; 45 | } 46 | 47 | -------------------------------------------------------------------------------- /public/scss/_pay-methods.scss: -------------------------------------------------------------------------------- 1 | .no-results { 2 | padding: 2em 0; 3 | } 4 | 5 | .button.add-pay-method { 6 | margin-right: 1em; 7 | } 8 | -------------------------------------------------------------------------------- /public/scss/_product.scss: -------------------------------------------------------------------------------- 1 | .product { 2 | display: block; 3 | text-align: center; 4 | @include header-font(); 5 | 6 | .title { 7 | font-size: $medium-font; 8 | font-weight: 400; 9 | padding-bottom: 0.35em; 10 | margin: 0; 11 | } 12 | 13 | .price { 14 | font-size: $very-large-font; 15 | line-height: 1em; 16 | font-weight: 200; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /public/scss/_signed-out.scss: -------------------------------------------------------------------------------- 1 | .management .signed-out { 2 | width: 100%; 3 | margin-top: 26%; 4 | 5 | p { 6 | width: 220px; 7 | margin: 0 auto 1em auto; 8 | } 9 | 10 | .button.quiet { 11 | background-color: transparent; 12 | width: 100%; 13 | margin-bottom: 0; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /public/scss/_spinner.scss: -------------------------------------------------------------------------------- 1 | 2 | //used to spin loader elements 3 | @keyframes spin { 4 | from { 5 | transform: rotate(0deg); 6 | } 7 | to { 8 | transform: rotate(360deg); 9 | } 10 | } 11 | 12 | .spinner-cont { 13 | margin: 0 auto; 14 | max-width: 100%; 15 | width: 300px; 16 | .text { 17 | display: block; 18 | margin: 0 auto; 19 | padding: 10px; 20 | text-align: center; 21 | } 22 | } 23 | 24 | .spinner:after { 25 | margin: 130px auto 0; 26 | content: ''; 27 | animation: 0.9s spin infinite linear; 28 | display: block; 29 | overflow: hidden; 30 | text-indent: 100%; 31 | font-size: 0; 32 | white-space: nowrap; 33 | @include hidpi-background-image('spinnerlight', 36px 36px); 34 | height: 36px; 35 | width: 36px; 36 | } 37 | 38 | button.spinner { 39 | position: relative; 40 | text-indent: -999em; 41 | direction: ltr; 42 | 43 | &:after { 44 | @include hidpi-background-image('spinnerwhite', 30px 30px); 45 | content: ''; 46 | display: block; 47 | height: 30px; 48 | left: 0; 49 | margin: -15px auto 0; 50 | position: absolute; 51 | right: 0; 52 | top: 50%; 53 | width: 30px; 54 | z-index: 10; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /public/scss/_subscriptions.scss: -------------------------------------------------------------------------------- 1 | .subscription-list { 2 | margin: 1em 0; 3 | padding: 0; 4 | 5 | li { 6 | list-style-type: none; 7 | padding: 2em 0 0; 8 | margin: 2em 0 0; 9 | border-top: 1px solid $heading-border; 10 | 11 | &:first-child { 12 | border-top: none; 13 | } 14 | } 15 | 16 | &.compact { 17 | margin-bottom: 2em; 18 | 19 | li { 20 | border-top: none; 21 | margin: 1em 0 0; 22 | padding: 1em 0 0; 23 | } 24 | 25 | .product { 26 | max-width: 100%; 27 | 28 | .description { 29 | display: inline; 30 | } 31 | 32 | .price { 33 | border-top: none; 34 | display: inline; 35 | 36 | &:before { 37 | content: ' – '; 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | 45 | 46 | 47 | .subscription { 48 | overflow: hidden; 49 | 50 | .proxy-select { 51 | margin-top: 0; 52 | } 53 | 54 | .product { 55 | max-width: 50%; 56 | 57 | img { 58 | display: block; 59 | float: left; 60 | margin-right: 20px; 61 | width: 50px; 62 | } 63 | 64 | .meta { 65 | float: left; 66 | width: calc(100% - 70px); 67 | } 68 | 69 | .seller { 70 | font-weight: normal; 71 | margin: 0 0 0.1em 0; 72 | overflow: hidden; 73 | padding: 0; 74 | text-overflow: ellipsis; 75 | white-space: nowrap; 76 | } 77 | 78 | .description { 79 | margin: 0 0 1em; 80 | text-align: left; 81 | } 82 | 83 | .next-payment, 84 | .price { 85 | border-top: 1px solid $heading-border; 86 | display: block; 87 | padding: 0.5em 0; 88 | } 89 | } 90 | 91 | .actions, 92 | .cancel, 93 | .change-pay-account { 94 | float: right; 95 | } 96 | } 97 | 98 | 99 | -------------------------------------------------------------------------------- /public/scss/_tooltip.scss: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | opacity: 1; 3 | transition: 0.1s opacity $short-transition; 4 | display: block; 5 | color: #fff; 6 | background: $error-background-color; 7 | padding: 0.5em 0.75em 0.7em; 8 | z-index: 10; 9 | position: absolute; 10 | white-space: nowrap; 11 | top: -2.1em; 12 | 13 | &:before { 14 | content: ''; 15 | width: 0; 16 | height: 0; 17 | // Dashed fixes anti-aliasing in ff. 18 | border-left: 1em dashed transparent; 19 | border-right: 1em dashed transparent; 20 | border-top: 1em solid $error-background-color; 21 | border-bottom: 0; 22 | top: 100%; 23 | left: 0.5em; 24 | position: absolute; 25 | z-index: 9; 26 | } 27 | 28 | &.right { 29 | right: 0; 30 | 31 | &:before { 32 | left: inherit; 33 | right: 0.5em; 34 | text-align: right; 35 | } 36 | } 37 | 38 | &.center { 39 | left: 50%; 40 | transform: translateX(-50%); 41 | &:before { 42 | left: calc(50% - 1em); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/scss/_transaction.scss: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 30px; 3 | } 4 | -------------------------------------------------------------------------------- /public/scss/_typography.scss: -------------------------------------------------------------------------------- 1 | // $local-name, $path, $family, $font-weight, $font-style="normal" 2 | 3 | // Fira Sans 4 | @include font-face('Fira Sans Regular', 'firasans/firasans-light', 'Fira Sans', 200); 5 | @include font-face('Fira Sans Regular Italic', 'firasans/firasans-lightitalic', 'Fira Sans', 200, 'italic'); 6 | 7 | @include font-face('Fira Sans Regular', 'firasans/firasans-regular', 'Fira Sans', 400); 8 | @include font-face('Fira Sans Regular Italic', 'firasans/firasans-regularitalic', 'Fira Sans', 400, 'italic'); 9 | 10 | @include font-face('Fira Sans Medium', 'firasans/firasans-medium', 'Fira Sans', 500); 11 | @include font-face('Fira Sans Medium Italic', 'firasans/firasans-mediumitalic', 'Fira Sans', 500, 'italic'); 12 | 13 | @include font-face('Fira Sans Bold', 'firasans/firasans-bold', 'Fira Sans', 700); 14 | @include font-face('Fira Sans Bold Italic', 'firasans/firasans-bolditalic', 'Fira Sans', 700, 'italic'); 15 | 16 | // Clear Sans 17 | @include font-face('Clear Sans Regular', 'clearsans/clearsans-regular', 'Clear Sans', 400); 18 | @include font-face('Clear Sans Italic', 'clearsans/clearsans-italic', 'Clear Sans', 400, 'italic'); 19 | 20 | @include font-face('Clear Sans Medium', 'clearsans/clearsans-medium', 'Clear Sans', 500); 21 | @include font-face('Clear Sans Medium Italic', 'clearsans/clearsans-mediumitalic', 'Clear Sans', 500, 'italic'); 22 | 23 | @include font-face('Clear Sans Bold', 'clearsans/clearsans-bold', 'Clear Sans', 700); 24 | @include font-face('Clear Sans Bold Italic', 'clearsans/clearsans-bolditalic', 'Clear Sans', 700, 'italic'); 25 | 26 | h1,h2,h3,h4,h5,h6 { 27 | @include header-font(); 28 | line-height: 1.35em; 29 | margin: .75em 0; 30 | } 31 | 32 | body { 33 | @include font(); 34 | color: $text-color; 35 | font-size: $base-font; 36 | } 37 | 38 | h1,h2,h3,h4,h5,h6 { 39 | @include header-font(); 40 | } 41 | 42 | a { 43 | text-decoration: none; 44 | color: $the-real-blue; 45 | } 46 | 47 | h1 { font-size: 150%; } 48 | h2 { font-size: 140%; } 49 | h3 { font-size: 130%; } 50 | h4 { font-size: 120%; } 51 | h5 { font-size: 110%; } 52 | 53 | .warn { 54 | color: $awooga-red; 55 | } 56 | -------------------------------------------------------------------------------- /public/scss/_utils.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Hide only visually, but have it available for screen readers: 3 | * http://snook.ca/archives/html_and_css/hiding-content-for-accessibility 4 | */ 5 | 6 | .vh, 7 | .visuallyhidden { 8 | @include visuallyhidden(); 9 | } 10 | -------------------------------------------------------------------------------- /public/scss/common.scss: -------------------------------------------------------------------------------- 1 | @import 'lib/normalize'; 2 | @import 'inc/vars'; 3 | @import 'inc/mixins'; 4 | @import '_base'; 5 | @import '_global'; 6 | @import '_buttons'; 7 | @import '_forms'; 8 | @import '_notification'; 9 | @import '_pay-methods'; 10 | @import '_pay-method-item'; 11 | @import '_pay-method-list'; 12 | @import '_pay-method-drop-down'; 13 | @import '_pay-method-sprite'; 14 | @import '_spinner'; 15 | @import '_tooltip'; 16 | @import '_typography'; 17 | @import '_utils'; 18 | 19 | -------------------------------------------------------------------------------- /public/scss/config/dev/_settings.scss: -------------------------------------------------------------------------------- 1 | $base-path: '/'; 2 | -------------------------------------------------------------------------------- /public/scss/config/local/_settings.scss: -------------------------------------------------------------------------------- 1 | $base-path: '../'; 2 | -------------------------------------------------------------------------------- /public/scss/email.scss: -------------------------------------------------------------------------------- 1 | @import 'inc/vars'; 2 | @import 'inc/mixins'; 3 | 4 | @import '_typography'; 5 | @import '_utils'; 6 | @import '_buttons'; 7 | @import '_email'; 8 | -------------------------------------------------------------------------------- /public/scss/inc/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin font() { 2 | font-family: $default-font; 3 | } 4 | 5 | @mixin header-font() { 6 | font-family: $header-font; 7 | } 8 | 9 | @mixin respond-to($breakpoint) { 10 | @if map-has-key($breakpoints, $breakpoint) { 11 | @media #{map-get($breakpoints, $breakpoint)} { 12 | @content; 13 | } 14 | } 15 | @else { 16 | @warn 'Unfortunately, no value could be retrieved from `#{$breakpoint}`. ' 17 | + 'Please make sure it is defined in `$breakpoints` map.'; 18 | } 19 | } 20 | 21 | @mixin placeholder() { 22 | color: $input-placeholder-color !important; 23 | } 24 | 25 | @mixin center-text() { 26 | text-align: center !important; 27 | } 28 | 29 | @mixin focus() { 30 | border-color: $default-focus-color; 31 | box-shadow:0 0 5px 0 $default-focus-color; 32 | outline:none; 33 | } 34 | 35 | @mixin font-face($local-name, $path, $family, $font-weight, $font-style:'normal') { 36 | $filepath: "#{$font-path}#{$path}"; 37 | @font-face { 38 | font-family: "#{$family}"; 39 | src: url("#{$filepath}.eot"); 40 | src: local($local-name), 41 | url("#{$filepath}.woff") format('woff'), 42 | url("#{$filepath}.ttf") format('truetype'); 43 | font-weight: unquote($font-weight); 44 | font-style: unquote($font-style); 45 | } 46 | } 47 | 48 | // Image management 49 | @mixin hidpi-background-image($filename, $background-size: 'mixed', $extension: png) { 50 | background-image: url("#{$image-path}#{$filename}.#{$extension}"); 51 | @if ($background-size != 'mixed') { 52 | background-size: $background-size; 53 | } 54 | @media (min--moz-device-pixel-ratio: 1.3), 55 | (-o-min-device-pixel-ratio: 2.6/2), 56 | (-webkit-min-device-pixel-ratio: 1.3), 57 | (min-device-pixel-ratio: 1.3), 58 | (min-resolution: 1.3dppx) { 59 | background-image: url("#{$image-path}#{$filename}@2x.#{$extension}"); 60 | } 61 | } 62 | 63 | @mixin visuallyhidden() { 64 | position: absolute; 65 | overflow: hidden; 66 | clip: rect(0 0 0 0); 67 | height: 1px; width: 1px; 68 | margin: -1px; padding: 0; border: 0; 69 | } 70 | -------------------------------------------------------------------------------- /public/scss/inc/vars.scss: -------------------------------------------------------------------------------- 1 | @import '_settings.scss'; 2 | 3 | $image-path: '#{$base-path}img/'; 4 | $font-path: '#{$base-path}fonts/'; 5 | $svg-path: '#{$base-path}svg/'; 6 | 7 | // Font Family Vars 8 | $default-font: "Clear Sans", Helvetica, Arial, sans-serif; 9 | $header-font: "Fira Sans", Helvetica, Arial, sans-serif; 10 | 11 | // Colour Vars 12 | $baby-blue: #E0F1FA; 13 | $light-blue: #0096dc; 14 | $uniform-blue: #0080D8; 15 | $the-real-blue: #0095dd; 16 | $management-grey: #F2F2F2; 17 | $subtle-grey: #F5F5F5; 18 | $stormy-grey: #BDBFBD; 19 | $moody-grey: #8A8A8A; 20 | $awooga-red: #d63920; 21 | 22 | $active-orange: #FF9500; 23 | $active-grey: #343F48; 24 | $ship-grey: #c1c1c1; 25 | $prefs-grey: #424F5A; 26 | $hover-grey: #5E6972; 27 | $dark-grey: #333; 28 | $lightish-grey: #EBEBEB; 29 | $mozlogo: #A5B1B6; 30 | 31 | $heading-border: #C8C8C8; 32 | 33 | $button-background-color: $the-real-blue; 34 | $button-background-disabled-color: #8a9ba8; 35 | $button-background-hover-color: #08c; 36 | $default-focus-color: $light-blue; 37 | 38 | $error-background-color: #d63920; 39 | $error-text-color: #d63920; 40 | 41 | $input-border-color: #424f59; 42 | $input-text-color: #424f59; 43 | $input-placeholder-color: #8a9ba8; 44 | $input-row-border-color: #8a9ba8; 45 | $input-row-focus-border-color: $the-real-blue; 46 | $input-row-hover-border-color: #424f59; 47 | $input-row-box-shadow-color: #f2f2f2; 48 | $input-left-right-padding: 20px; 49 | 50 | $radio-selected-background : $baby-blue; 51 | $radio-selected-border: $uniform-blue; 52 | 53 | $message-text-color: #fff; 54 | $mobile-html-background-color: #fff; 55 | 56 | $text-color: #424f59; 57 | 58 | // Font-Size Variables 59 | $very-large-font: 42px; 60 | $extra-large-font: 36px; 61 | $large-font: 24px; 62 | $medium-font: 18px; 63 | $base-font: 14px; 64 | $small-font: 12px; 65 | $media-adjustment: 2px; 66 | 67 | 68 | // Animation Variables 69 | $short-transition: 150ms; 70 | $medium-transition: $short-transition * 3; 71 | $long-transition: $short-transition * 7; 72 | 73 | // Breakpoints. 74 | $breakpoints: ( 75 | small: '(max-width: 520px), (orientation:landscape) and (min-width:481px) and (max-height: 480px)', 76 | big: '(min-width: 521px)' 77 | ); 78 | 79 | // Border-Radius Variables 80 | $small-border-radius: 4px; 81 | $big-border-radius: 4px; 82 | -------------------------------------------------------------------------------- /public/scss/lib/tabzilla.scss: -------------------------------------------------------------------------------- 1 | #tabzilla { 2 | float: right; 3 | position: relative; 4 | } 5 | #tabzilla a { 6 | text-indent: 120%; 7 | white-space: nowrap; 8 | overflow: hidden; 9 | background-image: url('../media/img/tabzilla-static.png'); 10 | background-repeat: no-repeat; 11 | display: block; 12 | height: 37px; 13 | position: relative; 14 | width: 147px; 15 | z-index: 2; 16 | } 17 | @media (-webkit-min-device-pixel-ratio: 1.5), (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (min-resolution: 1.5dppx) { 18 | #tabzilla a { 19 | background-image: url('../media/img/tabzilla-static-high-res.png'); 20 | -webkit-background-size: 147px 37px; 21 | background-size: 147px 37px; 22 | } 23 | } 24 | #tabzilla:before { 25 | background-color: transparent; 26 | content: ''; 27 | display: block; 28 | height: 26px; 29 | left: 28px; 30 | position: absolute; 31 | top: 0; 32 | width: 88px; 33 | z-index: 1; 34 | } 35 | -------------------------------------------------------------------------------- /public/scss/management.scss: -------------------------------------------------------------------------------- 1 | @import 'inc/vars'; 2 | @import 'inc/mixins'; 3 | 4 | @import '_modal'; 5 | @import '_subscriptions'; 6 | @import '_mgmt-nav-sprite.scss'; 7 | @import '_json-table'; 8 | @import '_signed-out'; 9 | @import 'lib/tabzilla'; 10 | @import '_management'; 11 | -------------------------------------------------------------------------------- /public/scss/transaction.scss: -------------------------------------------------------------------------------- 1 | @import 'inc/vars'; 2 | @import 'inc/mixins'; 3 | 4 | @import '_product'; 5 | @import '_complete-payment'; 6 | @import '_transaction'; 7 | -------------------------------------------------------------------------------- /public/svg/diners.svg: -------------------------------------------------------------------------------- 1 | diners -------------------------------------------------------------------------------- /public/svg/discover.svg: -------------------------------------------------------------------------------- 1 | discover -------------------------------------------------------------------------------- /public/svg/history.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svg/jcb.svg: -------------------------------------------------------------------------------- 1 | Slice 1 -------------------------------------------------------------------------------- /public/svg/pay-methods.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svg/profile.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svg/subs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svg/visa.svg: -------------------------------------------------------------------------------- 1 | Slice 1 -------------------------------------------------------------------------------- /styleguide/jsx/card-form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CardForm } from 'components/card-form'; 3 | 4 | React.render(, document.getElementById('view')); 5 | -------------------------------------------------------------------------------- /styleguide/jsx/modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Modal from 'components/modal'; 3 | import { CardForm } from 'components/card-form'; 4 | 5 | React.render( 6 | 7 | , document.getElementById('modal')); 8 | -------------------------------------------------------------------------------- /styleguide/jsx/spinner.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Spinner from 'components/spinner'; 3 | 4 | React.render(, document.getElementById('view')); 5 | -------------------------------------------------------------------------------- /styleguide/pages/buttons.md: -------------------------------------------------------------------------------- 1 | # Buttons 2 | 3 | Here's some examples of the buttons styles used in the payments-ui. 4 | 5 | ```iframe 6 | title: Standard Buttons 7 | renderer: nunjucks 8 | template: buttons.html 9 | ``` 10 | 11 | These are the spinner buttons. 12 | 13 | ```iframe 14 | title: buttons with spinner 15 | renderer: nunjucks 16 | template: buttons-spinner.html 17 | ``` 18 | -------------------------------------------------------------------------------- /styleguide/pages/card-form.md: -------------------------------------------------------------------------------- 1 | # Card Form 2 | 3 | This is the card form component. 4 | 5 | ```iframe 6 | sourcecodeSelector: '#view' 7 | dynamicSource: true 8 | title: Card Form 9 | renderer: nunjucks 10 | template: jsx.html 11 | bundle: card-form 12 | ``` 13 | -------------------------------------------------------------------------------- /styleguide/pages/modal.md: -------------------------------------------------------------------------------- 1 | # Modal Dialogue 2 | 3 | This is the modal dialogue component. 4 | 5 | ```iframe 6 | sourcecodeSelector: '#modal' 7 | dynamicSource: true 8 | title: Modal Dialogue 9 | renderer: nunjucks 10 | template: jsx.html 11 | bundle: modal 12 | ``` 13 | -------------------------------------------------------------------------------- /styleguide/pages/spinner.md: -------------------------------------------------------------------------------- 1 | # Spinner 2 | 3 | This is the spinner component. 4 | 5 | ```iframe 6 | sourcecodeSelector: '#view' 7 | dynamicSource: true 8 | title: Spinner 9 | renderer: nunjucks 10 | template: jsx.html 11 | bundle: spinner 12 | ``` 13 | -------------------------------------------------------------------------------- /styleguide/pages/typography.md: -------------------------------------------------------------------------------- 1 | # Typography 2 | 3 | This is an example of what the fonts look like using some basic markup. 4 | 5 | ```iframe 6 | renderer: nunjucks 7 | template: typography.html 8 | ``` 9 | -------------------------------------------------------------------------------- /styleguide/templates/base-styleguide.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{ title }} 11 | 12 | 13 |
    14 | 16 |
    17 |
    18 |
    19 | {% block page %}{% endblock %} 20 |
    21 |
    22 | {% block script %} 23 | {% endblock %} 24 | 25 | 26 | -------------------------------------------------------------------------------- /styleguide/templates/buttons-spinner.html: -------------------------------------------------------------------------------- 1 | {% extends 'base-styleguide.html' %} 2 | 3 | {% block page %} 4 |

    Buttons With Spinners

    5 | 6 | 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /styleguide/templates/buttons.html: -------------------------------------------------------------------------------- 1 | {% extends 'base-styleguide.html' %} 2 | 3 | {% block page %} 4 |

    Buttons

    5 | 6 | 7 | 8 |

    Link Buttons

    9 | Button Link 10 | Button Link (disabled) 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /styleguide/templates/jsx.html: -------------------------------------------------------------------------------- 1 | {% extends 'base-styleguide.html' %} 2 | 3 | {% block script %} 4 | 5 | 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /styleguide/templates/typography.html: -------------------------------------------------------------------------------- 1 | {% extends 'base-styleguide.html' %} 2 | 3 | {% block page %} 4 |

    Heading Level 1

    5 |

    Heading Level 2

    6 |

    Heading Level 3

    7 |

    Heading Level 4

    8 |
    Heading Level 5
    9 |
    Heading Level 6
    10 | 11 |

    The quick brown fox jumps over the lazy dog

    12 |

    The quick brown fox jumps over the lazy dog (emphasis)

    13 |

    The quick brown fox jumps over the lazy dog (strong)

    14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /tasks/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | }, 5 | "rules": { 6 | "eol-last": 0, 7 | "comma-dangle": 0, 8 | "strict": [2, "never"], 9 | "max-len": 0, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tasks/abideCompile.js: -------------------------------------------------------------------------------- 1 | // TODO when we know what format we want. 2 | -------------------------------------------------------------------------------- /tasks/abideCreate.js: -------------------------------------------------------------------------------- 1 | var settings = require('../public/js/settings'); 2 | 3 | module.exports = { 4 | default: { // Target name. 5 | options: { 6 | template: '../payments-l10n/locale/templates/LC_MESSAGES/payments-ui.pot', 7 | languages: settings.supportedLanguages, 8 | localeDir: 'locale', 9 | } 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /tasks/abideExtract.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | js: { 3 | src: [ 4 | 'public/dist/*.js', 5 | ], 6 | dest: '../payments-l10n/locale/templates/LC_MESSAGES/payments-ui.pot', 7 | options: { 8 | language: 'JavaScript', 9 | } 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /tasks/abideMerge.js: -------------------------------------------------------------------------------- 1 | // TODO: when we know what we're doing. 2 | -------------------------------------------------------------------------------- /tasks/clean.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | deps: [ 3 | 'public/lib', 4 | 'public/scss/lib', 5 | 'public/fonts', 6 | ], 7 | dist: [ 8 | 'public/dist' 9 | ] 10 | }; 11 | -------------------------------------------------------------------------------- /tasks/cog.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | styleguide: { 3 | src: 'styleguide', 4 | options: { 5 | sourcecodeSelector: '#view', 6 | templateGlobals: { 7 | projectName: 'Payments UI Styleguide', 8 | appMedia: '../static', 9 | }, 10 | templateConfig: { 11 | templatePaths: ['templates'], 12 | }, 13 | copy: [{ 14 | src: '../public/', 15 | target: 'static/public', 16 | }, { 17 | src: 'jsx-bundles/', 18 | target: 'static/jsx', 19 | }], 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /tasks/concurrent.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: { 3 | logConcurrentOutput: true 4 | }, 5 | styleguide: { 6 | tasks: ['watch:sass', 'watch:styleguide'], 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /tasks/copy.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | nodedeps: { 3 | files: [{ 4 | cwd: 'node_modules/normalize.css/', 5 | expand: true, 6 | src: 'normalize.css', 7 | dest: 'public/scss/lib/', 8 | ext: '.scss', 9 | }, { 10 | cwd: 'node_modules/connect-fonts-clearsans/fonts/default/', 11 | expand: true, 12 | src: '*', 13 | dest: 'public/fonts/clearsans/', 14 | }, { 15 | cwd: 'node_modules/connect-fonts-firasans/fonts/default/', 16 | expand: true, 17 | src: '*', 18 | dest: 'public/fonts/firasans/', 19 | }, { 20 | cwd: 'node_modules/mozilla-tabzilla/', 21 | expand: true, 22 | src: ['**/*.png'], 23 | dest: 'public/', 24 | }, { 25 | cwd: 'node_modules/mozilla-tabzilla/css/', 26 | expand: true, 27 | ext: '.scss', 28 | src: ['**/*.css'], 29 | dest: 'public/scss/lib/', 30 | }], 31 | 32 | }, 33 | 34 | }; 35 | -------------------------------------------------------------------------------- /tasks/csslint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lax: { 3 | options: { 4 | csslintrc: '.csslintrc' 5 | }, 6 | src: ['public/dist/main.css'] 7 | }, 8 | strict: { 9 | options: { 10 | 'adjoining-classes': false, 11 | 'box-sizing': false, 12 | 'bulletproof-font-face': false, 13 | }, 14 | src: ['public/dist/main.css'] 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /tasks/devserver.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: { 3 | base: 'styleguide/build', 4 | type: 'http', 5 | port: 4001, 6 | async: false, 7 | }, 8 | server: {} 9 | }; 10 | -------------------------------------------------------------------------------- /tasks/eslint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | js: { 3 | src: [ 4 | 'public/js/**/*.js*', 5 | '!public/js/bundle.js', 6 | 'tests/**/*.js*', 7 | 'Gruntfile.js', 8 | 'tasks/*.js', 9 | ], 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /tasks/gh-pages.js: -------------------------------------------------------------------------------- 1 | // get a formatted commit message to review changes from the commit log 2 | // github will turn some of these into clickable links 3 | function getDeployMessage() { 4 | var ret = '\n\n'; 5 | if (process.env.TRAVIS !== 'true') { 6 | ret += 'missing env vars for travis-ci'; 7 | return ret; 8 | } 9 | ret += 'branch: ' + process.env.TRAVIS_BRANCH + '\n'; 10 | ret += 'SHA: ' + process.env.TRAVIS_COMMIT + '\n'; 11 | ret += 'range SHA: ' + process.env.TRAVIS_COMMIT_RANGE + '\n'; 12 | ret += 'build id: ' + process.env.TRAVIS_BUILD_ID + '\n'; 13 | ret += 'build number: ' + process.env.TRAVIS_BUILD_NUMBER + '\n'; 14 | return ret; 15 | } 16 | 17 | module.exports = { 18 | styleguide: { 19 | options: { 20 | base: 'styleguide/build', 21 | message: 'Updating docs', 22 | repo: 'git@github.com:mozilla/payments-ui.git' 23 | }, 24 | src: ['**'], 25 | }, 26 | // This is a special branch that contains the built files. 27 | docker: { 28 | options: { 29 | // silent option prevents decrypted credentials leaking into 30 | // travis logs. See https://github.com/tschaub/grunt-gh-pages#optionssilent 31 | silent: true, 32 | branch: 'docker-hub-build', 33 | clone: '.grunt/grunt-gh-pages/docker-hub-build/', 34 | user: { 35 | name: process.env.GH_USER, 36 | email: process.env.GH_EMAIL, 37 | }, 38 | repo: 'https://' + process.env.GH_TOKEN + '@github.com/mozilla/payments-ui.git', 39 | message: 'publish docker build branch (auto)' + getDeployMessage(), 40 | }, 41 | src: ['public/**/*', 'Dockerfile'], 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /tasks/karma.js: -------------------------------------------------------------------------------- 1 | var grunt = require('grunt'); 2 | 3 | module.exports = { 4 | options: { 5 | logLevel: grunt.option('log-level') || 'INFO', 6 | }, 7 | dev: { 8 | configFile: 'karma.conf.js', 9 | singleRun: false, 10 | autoWatch: true 11 | }, 12 | run: { 13 | configFile: 'karma.conf.js', 14 | }, 15 | sauce: { 16 | configFile: 'karma.conf.sauce.js', 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /tasks/sass.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | var env_ = 'local'; 4 | 5 | if (process.env.DEV) { 6 | env_ = 'dev'; 7 | } 8 | 9 | 10 | module.exports = { 11 | options: { 12 | sourceMap: true, 13 | includePaths: [ 14 | path.join('public/scss/config/', env_), 15 | ] 16 | }, 17 | dev: { 18 | options: { 19 | outputStyle: 'expanded' 20 | }, 21 | files: { 22 | // Common CSS shared between transaction and management app. 23 | 'public/dist/common.css': 'public/scss/common.scss', 24 | // Individual app CSS 25 | 'public/dist/transaction.css': 'public/scss/transaction.scss', 26 | 'public/dist/management.css': 'public/scss/management.scss', 27 | }, 28 | }, 29 | min: { 30 | options: { 31 | outputStyle: 'compressed' 32 | }, 33 | files: { 34 | 'public/dist/common.min.css': 'public/scss/common.scss', 35 | 'public/dist/transaction.min.css': 'public/scss/transaction.scss', 36 | 'public/dist/management.min.css': 'public/scss/management.scss', 37 | }, 38 | }, 39 | email: { 40 | options: { 41 | outputStyle: 'expanded' 42 | }, 43 | files: { 44 | 'public/dist/email.css': 'public/scss/email.scss', 45 | }, 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /tasks/svg_sprite.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | paymethods: { 3 | expand: true, 4 | cwd: 'node_modules/payment-icons/min/flat', 5 | src: ['*.svg'], 6 | dest: 'public', 7 | options: { 8 | shape: { 9 | dimension: { 10 | maxWidth: 40, 11 | maxHeight: 40 12 | }, 13 | dest: 'svg' 14 | }, 15 | mode: { 16 | css: { 17 | layout: 'vertical', 18 | common: 'pay-method-icon', 19 | dimensions: true, 20 | padding: 10, 21 | prefix: '.pmtype-%s', 22 | dest: 'scss', 23 | sprite: '../img/pay-method-sprite.svg', 24 | bust: false, 25 | render: { 26 | scss: { 27 | dest: '_pay-method-sprite.scss' 28 | } 29 | } 30 | } 31 | } 32 | } 33 | }, 34 | management: { 35 | expand: true, 36 | cwd: 'public/img/management', 37 | src: ['*.svg'], 38 | dest: 'public', 39 | options: { 40 | shape: { 41 | dimension: { 42 | maxWidth: 28, 43 | maxHeight: 28 44 | }, 45 | dest: 'svg' 46 | }, 47 | mode: { 48 | css: { 49 | layout: 'vertical', 50 | common: 'nav:after', 51 | dimensions: true, 52 | padding: 20, 53 | prefix: '.%s:after', 54 | dest: 'scss', 55 | sprite: '../img/mgmt-nav-sprite.svg', 56 | bust: false, 57 | render: { 58 | scss: { 59 | dest: '_mgmt-nav-sprite.scss' 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | }; 68 | -------------------------------------------------------------------------------- /tasks/watch.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sass: { 3 | files: 'public/scss/**/*.scss', 4 | tasks: ['sass:dev'], 5 | options: { 6 | debounceDelay: 250 7 | } 8 | }, 9 | sassemail: { 10 | files: 'public/scss/**/*.scss', 11 | tasks: ['sass:email'], 12 | options: { 13 | debounceDelay: 250 14 | } 15 | }, 16 | styleguide: { 17 | files: [ 18 | 'public/dist/**/*', 19 | 'public/img/**/*', 20 | 'public/svg/**/*', 21 | 'styleguide/**/*', 22 | '!styleguide/build/**/*', 23 | ], 24 | tasks: ['cog'], 25 | options: { 26 | debounceDelay: 250 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /tasks/webpack-dev-server.js: -------------------------------------------------------------------------------- 1 | var merge = require('lodash.merge'); 2 | var webpack = require('webpack'); 3 | var webpackConfig = require('../webpack.config'); 4 | 5 | // Update webpack config with hot module loading config. 6 | var newWebpackConfig = merge({}, webpackConfig); 7 | newWebpackConfig.devtool = 'eval'; 8 | newWebpackConfig.plugins.push(new webpack.NoErrorsPlugin()); 9 | newWebpackConfig.module.loaders[0].loaders.unshift('react-hot'); 10 | 11 | 12 | module.exports = { 13 | options: { 14 | host: 'localhost', 15 | contentBase: 'public/', 16 | historyApiFallback: true, 17 | hot: true, 18 | inline: true, 19 | publicPath: '/dist/', 20 | }, 21 | start: { 22 | webpack: newWebpackConfig, 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /tasks/webpack.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | var webpackConfig = require('../webpack.config.js'); 5 | 6 | 7 | module.exports = { 8 | options: webpackConfig, 9 | dev: { 10 | // Default to all the options in webpackConfig 11 | }, 12 | prod: { 13 | output: { 14 | path: path.join(__dirname, '../public/dist/'), 15 | filename: '[name].bundle.min.js', 16 | }, 17 | plugins: [ 18 | new webpack.optimize.UglifyJsPlugin({minimize: true}) 19 | ], 20 | }, 21 | styleguide: { 22 | entry: { 23 | // Explicit entries for the styleguide. 24 | 'modal': './styleguide/jsx/modal', 25 | 'card-form': './styleguide/jsx/card-form', 26 | 'spinner': './styleguide/jsx/spinner', 27 | }, 28 | output: { 29 | path: path.join(__dirname, '../styleguide/jsx-bundles/'), 30 | filename: '[name].bundle.js', 31 | chunkFilename: '[id].chunk.js', 32 | }, 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true, 4 | "node": true, 5 | }, 6 | "rules": { 7 | "strict": [2, "never"], 8 | }, 9 | "globals": { 10 | "assert": false, 11 | "sinon": false, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/payments-ui/5cd67c44637259b457d051d633f5e70311a838f5/tests/.gitkeep -------------------------------------------------------------------------------- /tests/actions/test.api.js: -------------------------------------------------------------------------------- 1 | import * as api from 'actions/api'; 2 | 3 | 4 | describe('api', () => { 5 | describe('fetch', () => { 6 | 7 | var fakeSettings; 8 | var fakeJquery; 9 | 10 | beforeEach(() => { 11 | fakeSettings = {apiPrefix: 'http://not-a-real-api.com/api'}; 12 | fakeJquery = {ajax: sinon.stub()}; 13 | }); 14 | 15 | function fetch(request={url: '/any/path/'}, opt=null) { 16 | if (!opt) { 17 | opt = { 18 | settings: fakeSettings, 19 | jquery: fakeJquery, 20 | csrfToken: false, 21 | }; 22 | } 23 | return api.fetch(request, opt); 24 | } 25 | 26 | it('prefixes URLs with the API host', () => { 27 | var request = {url: '/some/service'}; 28 | fetch(request); 29 | 30 | assert.equal(fakeJquery.ajax.firstCall.args[0].url, 31 | fakeSettings.apiPrefix + request.url); 32 | }); 33 | 34 | it('does not send credentials when CORS is not allowed', () => { 35 | fakeSettings.allowCORSRequests = false; 36 | fetch(); 37 | assert.equal(fakeJquery.ajax.firstCall.args[0].xhrFields.withCredentials, 38 | undefined); 39 | }); 40 | 41 | it('sends credentials when CORS is allowed', () => { 42 | fakeSettings.allowCORSRequests = true; 43 | fetch(); 44 | assert.equal(fakeJquery.ajax.firstCall.args[0].xhrFields.withCredentials, 45 | true); 46 | }); 47 | 48 | it('requires a CSRF token', () => { 49 | assert.throws( 50 | () => fetch({url: '/some/service'}, {csrfToken: undefined}), 51 | /You must set a CSRF token/ 52 | ); 53 | }); 54 | 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/actions/test.braintree.js: -------------------------------------------------------------------------------- 1 | import * as braintreeActions from 'actions/braintree'; 2 | import * as errorCodes from 'constants/error-codes'; 3 | 4 | import * as helpers from '../helpers'; 5 | 6 | 7 | const fakeCreditCard = { 8 | number: '411111111111', 9 | cvv: '222', 10 | expiration: '06/16', 11 | }; 12 | 13 | 14 | describe('braintreeActions', function() { 15 | var dispatchSpy; 16 | 17 | beforeEach(function() { 18 | dispatchSpy = sinon.spy(); 19 | }); 20 | 21 | function tokenizeCreditCard({dispatch=dispatchSpy, 22 | braintreeToken='braintree-token', 23 | creditCard=fakeCreditCard, 24 | BraintreeClient=helpers.FakeBraintreeClient, 25 | callback=() => {}} = {}) { 26 | braintreeActions.tokenizeCreditCard({dispatch: dispatch, 27 | braintreeToken: braintreeToken, 28 | creditCard: creditCard, 29 | callback: callback, 30 | BraintreeClient: BraintreeClient}); 31 | } 32 | 33 | describe('tokenizeCreditCard', function() { 34 | 35 | it('should tokenize a credit card', function() { 36 | var tokenizeSpy = sinon.spy(); 37 | class FakeClient { 38 | tokenizeCard = tokenizeSpy; 39 | } 40 | 41 | tokenizeCreditCard({BraintreeClient: FakeClient}); 42 | 43 | var request = tokenizeSpy.firstCall.args[0]; 44 | assert.equal(request.number, fakeCreditCard.number); 45 | assert.equal(request.expirationDate, fakeCreditCard.expiration); 46 | assert.equal(request.cvv, fakeCreditCard.cvv); 47 | }); 48 | 49 | it('should call back with pay method nonce', function() { 50 | var callbackSpy = sinon.spy(); 51 | tokenizeCreditCard({callback: callbackSpy}); 52 | assert.equal(callbackSpy.firstCall.args[0], 'some-nonce'); 53 | }); 54 | 55 | it('should throw with a bad card', function() { 56 | 57 | // Bad card with missing cvv 58 | // and expiration. 59 | var fakeBadCard = { 60 | number: '411111111111', 61 | }; 62 | 63 | assert.throws(() => { 64 | tokenizeCreditCard({creditCard: fakeBadCard}); 65 | }, Error, /Invalid card object/); 66 | }); 67 | 68 | it('should dispatch an error on tokenization failure', function() { 69 | tokenizeCreditCard({BraintreeClient: helpers.FakeBraintreeClientError}); 70 | var action = dispatchSpy.firstCall.args[0]; 71 | helpers.assertNotificationErrorCode( 72 | action, errorCodes.BRAINTREE_TOKENIZATION_ERROR); 73 | }); 74 | 75 | }); 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /tests/actions/test.notification-actions.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from 'constants/action-types'; 2 | import * as errorCodes from 'constants/error-codes'; 3 | import * as notificationActions from 'actions/notifications'; 4 | 5 | import * as helpers from '../helpers'; 6 | 7 | 8 | describe('notificationActions', function() { 9 | 10 | function removeNotification(id) { 11 | return notificationActions.removeNotification(id); 12 | } 13 | 14 | it('should dispatch ADD_NOTIFICATION', function() { 15 | var action = notificationActions.showError({ 16 | errorCode: errorCodes.PRODUCT_ID_INVALID, 17 | text: 'whatever', 18 | }); 19 | helpers.assertNotificationErrorCode( 20 | action, errorCodes.PRODUCT_ID_INVALID); 21 | }); 22 | 23 | it('should default to autoHide for info notification', function() { 24 | var action = notificationActions.showInfo({text: 'whatever'}); 25 | assert.equal(action.data.autoHide, true); 26 | }); 27 | 28 | it('should default to being dismissable for error notification', function() { 29 | var action = notificationActions.showError({ 30 | errorCode: errorCodes.PRODUCT_ID_INVALID, 31 | }); 32 | assert.equal(action.data.userDismissable, true); 33 | }); 34 | 35 | it('should throw on missing text in info notification', function() { 36 | assert.throws(() => { 37 | notificationActions.showInfo(); 38 | }, Error, /requires text/); 39 | }); 40 | 41 | it('should throw on missing errorCode in error', function() { 42 | assert.throws(() => { 43 | notificationActions.showError(); 44 | }, Error, /requires an errorCode/); 45 | }); 46 | 47 | it('sets default error message', function() { 48 | var action = notificationActions.showError({ 49 | errorCode: errorCodes.PRODUCT_ID_INVALID, 50 | }); 51 | assert.include(action.data.text, 'Internal error'); 52 | }); 53 | 54 | it('should dispatch user REMOVE_NOTIFICATION', function() { 55 | var action = removeNotification('awooga'); 56 | assert.deepEqual(action, { 57 | id: 'awooga', 58 | type: actionTypes.REMOVE_NOTIFICATION, 59 | }); 60 | }); 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /tests/actions/test.processing-actions.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from 'constants/action-types'; 2 | import { beginProcessing, stopProcessing } from 'actions/processing'; 3 | 4 | 5 | describe('Processing Actions', function() { 6 | 7 | it('should dispatch a begin processing action', function() { 8 | assert.deepEqual(beginProcessing('proc-id'), { 9 | type: actionTypes.BEGIN_PROCESSING, 10 | processingId: 'proc-id', 11 | }); 12 | }); 13 | 14 | it('should dispatch a stop processing action', function() { 15 | assert.deepEqual(stopProcessing('proc-id'), { 16 | type: actionTypes.STOP_PROCESSING, 17 | processingId: 'proc-id', 18 | }); 19 | }); 20 | 21 | it('should throw an error for empty processing IDs', function() { 22 | assert.throws(() => beginProcessing(null), Error, 23 | /processingId cannot be empty/); 24 | }); 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /tests/apps/test.management-app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react/lib/ReactTestUtils'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import { createReduxStore } from 'data-store'; 6 | 7 | import * as actionTypes from 'constants/action-types'; 8 | import * as mgmtActions from 'actions/management'; 9 | import ManagementApp from 'apps/management/app'; 10 | 11 | import * as helpers from '../helpers'; 12 | 13 | 14 | describe('management/app', function() { 15 | var fakeWindow; 16 | var FakeManagement; 17 | var FakeManageCards; 18 | var FakeSignIn; 19 | var store; 20 | 21 | beforeEach(function() { 22 | fakeWindow = { 23 | location: { 24 | href: '/', 25 | }, 26 | }; 27 | FakeManagement = helpers.stubComponent(); 28 | FakeManageCards = helpers.stubComponent(); 29 | FakeSignIn = helpers.stubComponent(); 30 | store = createReduxStore(); 31 | }); 32 | 33 | function mountView({ window = fakeWindow } = {}) { 34 | 35 | var container = TestUtils.renderIntoDocument( 36 | 37 | {() => } 43 | 44 | ); 45 | return TestUtils.findRenderedComponentWithType( 46 | container, ManagementApp 47 | ); 48 | } 49 | 50 | it('should sign-in with access token', function() { 51 | var token = 'some-fxa-token'; 52 | 53 | var view = mountView({ 54 | window: { 55 | location: { 56 | href: '/?access_token=' + token, 57 | }, 58 | }, 59 | }); 60 | 61 | var mgmt = TestUtils.findRenderedComponentWithType( 62 | view, FakeManagement 63 | ); 64 | // The second child should be the sign-in component. 65 | // Make sure it gets the access token. 66 | var signIn = mgmt.props.children[1]; 67 | assert.equal(signIn.props.accessToken, token); 68 | 69 | }); 70 | 71 | it('should pass in card submission errors', function() { 72 | store.dispatch({ 73 | type: actionTypes.GOT_BRAINTREE_TOKEN, 74 | braintreeToken: 'some-token', 75 | }); 76 | store.dispatch(mgmtActions.showAddPayMethod()); 77 | var apiError = {error_response: 'some error'}; 78 | store.dispatch({ 79 | type: actionTypes.CREDIT_CARD_SUBMISSION_ERRORS, 80 | apiErrorResult: apiError, 81 | }); 82 | var view = mountView(); 83 | 84 | var mgmt = TestUtils.findRenderedComponentWithType( 85 | view, FakeManagement 86 | ); 87 | var addPayMethod = mgmt.props.children[1]; 88 | assert.equal(addPayMethod.props.cardSubmissionErrors, apiError); 89 | 90 | }); 91 | 92 | }); 93 | -------------------------------------------------------------------------------- /tests/components/test.modal.jsx: -------------------------------------------------------------------------------- 1 | import React, { findDOMNode } from 'react'; 2 | import TestUtils from 'react/lib/ReactTestUtils'; 3 | 4 | import Modal from 'components/modal'; 5 | 6 | import * as helpers from '../helpers'; 7 | 8 | 9 | describe('Modal', function() { 10 | 11 | beforeEach(function() { 12 | this.closeFunc = sinon.stub(); 13 | this.Modal = TestUtils.renderIntoDocument( 14 | 15 |
    16 |

    Just some noddy content

    17 | Click me 18 |
    19 |
    20 | ); 21 | this.eventStub = { 22 | preventDefault: sinon.stub(), 23 | stopPropagation: sinon.stub(), 24 | }; 25 | }); 26 | 27 | it('should have a title', function() { 28 | var title = helpers.findByTag(this.Modal, 'h2'); 29 | assert.equal(findDOMNode(title).firstChild.nodeValue, 'whatever'); 30 | }); 31 | 32 | it('should have child content', function() { 33 | var content = helpers.findByClass(this.Modal, 'm-content'); 34 | assert.equal(findDOMNode(content).nodeName.toLowerCase(), 'div'); 35 | }); 36 | 37 | it('should have child paragraph', function() { 38 | var text = helpers.findByClass(this.Modal, 'm-text'); 39 | assert.equal(findDOMNode(text).firstChild.nodeValue, 40 | 'Just some noddy content'); 41 | }); 42 | 43 | it('should call close func when clicking the modal background', function() { 44 | var modal = helpers.findByClass(this.Modal, 'modal'); 45 | TestUtils.Simulate.click(modal, this.eventStub); 46 | assert.ok(this.eventStub.preventDefault.called); 47 | assert.ok(this.eventStub.stopPropagation.called); 48 | assert.ok(this.closeFunc.called); 49 | }); 50 | 51 | it('should call close func when clicking the close link', function() { 52 | var close = helpers.findByClass(this.Modal, 'close'); 53 | TestUtils.Simulate.click(findDOMNode(close), this.eventStub); 54 | assert.ok(this.eventStub.preventDefault.called); 55 | assert.ok(this.eventStub.stopPropagation.called); 56 | assert.ok(this.closeFunc.called); 57 | }); 58 | 59 | it('should not call close func when clicking other content.', function() { 60 | var otherLink = helpers.findByClass(this.Modal, 'other-link'); 61 | TestUtils.Simulate.click(findDOMNode(otherLink), this.eventStub); 62 | assert.notOk(this.eventStub.preventDefault.called); 63 | assert.notOk(this.eventStub.stopPropagation.called); 64 | assert.notOk(this.closeFunc.called); 65 | }); 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /tests/components/test.notification-list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react/lib/ReactTestUtils'; 3 | 4 | import NotificationList from 'components/notification-list'; 5 | 6 | import * as helpers from '../helpers'; 7 | 8 | 9 | describe('NotificationList', function() { 10 | 11 | beforeEach(function() { 12 | this.removeNotification = sinon.stub(); 13 | var MockCSSTransitionGroup = helpers.stubComponent('ul'); 14 | var notifications = [ 15 | ['foo', { 16 | type: 'error', 17 | text: 'error text', 18 | userDismissable: true, 19 | autoHide: false, 20 | }, 21 | ], ['bar', { 22 | type: 'info', 23 | text: 'info text', 24 | userDismissable: false, 25 | autoHide: true, 26 | }, 27 | ], 28 | ]; 29 | this.NotificationList = TestUtils.renderIntoDocument( 30 | 35 | ); 36 | console.log(React.findDOMNode(this.NotificationList)); 37 | }); 38 | 39 | it('should have two notifications', function() { 40 | var lis = helpers.findAllByTag(this.NotificationList, 'li'); 41 | assert.equal(lis.length, 2); 42 | }); 43 | 44 | it('should have an error notification', function() { 45 | assert.ok(helpers.findByClass(this.NotificationList, 'error')); 46 | }); 47 | 48 | it('should have an info notification', function() { 49 | assert.ok(helpers.findByClass(this.NotificationList, 'info')); 50 | }); 51 | 52 | }); 53 | 54 | -------------------------------------------------------------------------------- /tests/components/test.notification.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react/lib/ReactTestUtils'; 3 | 4 | import Notification from 'components/notification'; 5 | import { getId } from 'utils'; 6 | 7 | import * as helpers from '../helpers'; 8 | 9 | 10 | describe('Notification', function() { 11 | 12 | beforeEach(function() { 13 | this.removeNotification = sinon.stub(); 14 | this.getNotification = ({ autoHide=false, userDismissable=false, 15 | type='info', text='hai' } = {}) => { 16 | return TestUtils.renderIntoDocument( 17 | 25 | ); 26 | }; 27 | }); 28 | 29 | it('should have text', function() { 30 | var notification = this.getNotification({text: 'whatever'}); 31 | assert.equal( 32 | helpers.findByClass(notification, 'text') 33 | .getDOMNode().textContent, 'whatever'); 34 | }); 35 | 36 | it('should have error class for type: error', function() { 37 | var notification = this.getNotification({type: 'error'}); 38 | assert.ok(helpers.findByClass(notification, 'error')); 39 | }); 40 | 41 | it('should have info class for type: info', function() { 42 | var notification = this.getNotification({type: 'info'}); 43 | assert.ok(helpers.findByClass(notification, 'info')); 44 | }); 45 | 46 | it('should have a dismiss link when userDismissable is true', function() { 47 | var notification = this.getNotification({userDismissable: true}); 48 | assert.equal( 49 | helpers.findByTag(notification, 'a') 50 | .getDOMNode().getAttribute('class'), 'dismiss'); 51 | }); 52 | 53 | it('should call removeNotification with delay', function() { 54 | var notification = this.getNotification({autoHide: true}); 55 | assert.equal(this.removeNotification.firstCall.args[0], 56 | notification.props.key); 57 | assert.equal(this.removeNotification.firstCall.args[1].delay, 5000); 58 | }); 59 | 60 | it('should call removeNotificationClick on click', function() { 61 | var notification = this.getNotification({userDismissable: true}); 62 | var link = helpers.findByTag(notification, 'a').getDOMNode(); 63 | var prevDefault = sinon.stub(); 64 | TestUtils.Simulate.click(link, {preventDefault: prevDefault}); 65 | assert.ok(this.removeNotification.called); 66 | assert.ok(prevDefault.called); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /tests/components/test.pay-method-icon.jsx: -------------------------------------------------------------------------------- 1 | import React, { findDOMNode } from 'react'; 2 | import TestUtils from 'react/lib/ReactTestUtils'; 3 | 4 | import PayMethodIcon from 'components/pay-method-icon'; 5 | 6 | describe('Pay Method Icon', function() { 7 | 8 | var payMethodTypes = [ 9 | 'amex', 10 | 'discover', 11 | 'jcb', 12 | 'maestro', 13 | 'mastercard', 14 | 'visa', 15 | ]; 16 | 17 | function testPayMethod(payMethodType) { 18 | return function() { 19 | var PayMethodIcon_ = TestUtils.renderIntoDocument( 20 | 21 | ); 22 | var payMethodIconNode = findDOMNode(PayMethodIcon_); 23 | assert.include( 24 | payMethodIconNode.getAttribute('class'), 'pmtype-' + payMethodType); 25 | }; 26 | } 27 | 28 | payMethodTypes.forEach(function(payMethod) { 29 | it('sets payMethod class to: ' + payMethod, testPayMethod(payMethod)); 30 | }); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /tests/components/test.spinner.jsx: -------------------------------------------------------------------------------- 1 | import React, { findDOMNode } from 'react'; 2 | import TestUtils from 'react/lib/ReactTestUtils'; 3 | 4 | import * as helpers from '../helpers'; 5 | 6 | import Spinner from 'components/spinner'; 7 | 8 | 9 | describe('Spinner', function() { 10 | 11 | it('Uses loading as the default text', function() { 12 | var Spinner_ = TestUtils.renderIntoDocument(); 13 | var textNode = helpers.findByClass(Spinner_, 'text'); 14 | assert.equal(findDOMNode(textNode).firstChild.nodeValue, 'Loading'); 15 | }); 16 | 17 | it('Uses custom text as supplied', function() { 18 | var Spinner_ = TestUtils.renderIntoDocument(); 19 | var textNode = helpers.findByClass(Spinner_, 'text'); 20 | assert.equal(findDOMNode(textNode).firstChild.nodeValue, 'whatever'); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /tests/components/test.submit-button.js: -------------------------------------------------------------------------------- 1 | import React, { findDOMNode } from 'react'; 2 | import TestUtils from 'react/lib/ReactTestUtils'; 3 | 4 | import SubmitButton from 'components/submit-button'; 5 | 6 | 7 | describe('SubmitButton', function() { 8 | 9 | it('uses submit as the default text', function() { 10 | var Button = TestUtils.renderIntoDocument(); 11 | assert.equal(findDOMNode(Button).firstChild.nodeValue, 'Submit'); 12 | }); 13 | 14 | it('uses custom text as supplied', function() { 15 | var Button = TestUtils.renderIntoDocument( 16 | ); 17 | assert.equal(findDOMNode(Button).firstChild.nodeValue, 'whatever'); 18 | }); 19 | 20 | it('gets a css modifier class', function() { 21 | var Button = TestUtils.renderIntoDocument( 22 | ); 23 | assert.include(findDOMNode(Button).className, 'some-modifier'); 24 | }); 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /tests/reducers/test.app-reducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from 'constants/action-types'; 2 | import appReducer from 'reducers/app'; 3 | import { initialAppState } from 'reducers/app'; 4 | 5 | 6 | describe('App Reducer', function() { 7 | 8 | it('should store CSRF token', function() { 9 | var app = appReducer(initialAppState, { 10 | type: actionTypes.GOT_CSRF_TOKEN, 11 | csrfToken: 'some-csrf-token', 12 | }); 13 | assert.deepEqual(app, Object.assign({}, initialAppState, { 14 | csrfToken: 'some-csrf-token', 15 | })); 16 | }); 17 | 18 | it('should add a notification', function() { 19 | var app = appReducer(initialAppState, { 20 | type: actionTypes.ADD_NOTIFICATION, 21 | data: {text: 'foo', type: 'info'}, 22 | }); 23 | assert.equal(app.notifications.length, 1); 24 | assert.equal(app.notifications[0].length, 2); 25 | assert.deepEqual(app.notifications[0][1], 26 | {text: 'foo', type: 'info'}); 27 | }); 28 | 29 | it('should remove a notification', function() { 30 | var app = appReducer(initialAppState, { 31 | type: actionTypes.ADD_NOTIFICATION, 32 | data: {text: 'foo', type: 'info'}, 33 | }); 34 | var id = app.notifications[0][0]; 35 | assert.equal(app.notifications.length, 1); 36 | var app2 = appReducer(app, { 37 | type: actionTypes.REMOVE_NOTIFICATION, 38 | id: id, 39 | }); 40 | assert.equal(app2.notifications.length, 0); 41 | }); 42 | 43 | it('should not blow up removing a non-existant notification', function() { 44 | assert.doesNotThrow(() => { 45 | appReducer(initialAppState, { 46 | type: actionTypes.REMOVE_NOTIFICATION, 47 | id: 'blah', 48 | }); 49 | }); 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /tests/reducers/test.management-reducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from 'constants/action-types'; 2 | import { default as management, initialMgmtState } from 'reducers/management'; 3 | 4 | 5 | describe('management reducer', () => { 6 | 7 | it('sets a default view on sign-in', () => { 8 | assert.deepEqual( 9 | management(null, {type: actionTypes.USER_SIGNED_IN}), 10 | Object.assign({}, initialMgmtState, { 11 | tab: 'profile', 12 | view: actionTypes.SHOW_MY_ACCOUNT, 13 | }) 14 | ); 15 | }); 16 | 17 | it('preserves view state on sign-in', () => { 18 | var existingState = Object.assign({}, initialMgmtState, { 19 | tab: 'pay-methods', 20 | view: actionTypes.SHOW_PAY_METHODS, 21 | }); 22 | assert.deepEqual( 23 | management(existingState, {type: actionTypes.USER_SIGNED_IN}), 24 | existingState 25 | ); 26 | }); 27 | 28 | it('resets view state when showing sign-in', () => { 29 | var existingState = Object.assign({}, initialMgmtState, { 30 | tab: 'pay-methods', 31 | view: actionTypes.SHOW_PAY_METHODS, 32 | }); 33 | assert.deepEqual( 34 | management(existingState, {type: actionTypes.SHOW_SIGN_IN}), 35 | initialMgmtState 36 | ); 37 | }); 38 | 39 | it('stores card submission errors', () => { 40 | var apiError = {error_response: 'invalid monkey name'}; 41 | var state = Object.assign({}, initialMgmtState, { 42 | type: actionTypes.CREDIT_CARD_SUBMISSION_ERRORS, 43 | cardSubmissionErrors: apiError, 44 | }); 45 | assert.deepEqual(state.cardSubmissionErrors, apiError); 46 | }); 47 | 48 | it('preserves tab on error', () => { 49 | var existingState = Object.assign({}, initialMgmtState, { 50 | tab: 'pay-methods', 51 | }); 52 | assert.strictEqual( 53 | management(existingState, 54 | {type: actionTypes.CREDIT_CARD_SUBMISSION_ERRORS}).tab, 55 | existingState.tab); 56 | }); 57 | 58 | }); 59 | -------------------------------------------------------------------------------- /tests/reducers/test.processing-reducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from 'constants/action-types'; 2 | import processingReducer from 'reducers/processing'; 3 | import { initialProcessingState } from 'reducers/processing'; 4 | 5 | 6 | describe('Processing Reducer', function() { 7 | 8 | it('should initialize state', function() { 9 | assert.deepEqual(processingReducer(undefined, {}), 10 | initialProcessingState); 11 | }); 12 | 13 | it('should store true when a component begins processing', function() { 14 | var procId = 'some-id'; 15 | var state = processingReducer(initialProcessingState, { 16 | type: actionTypes.BEGIN_PROCESSING, 17 | processingId: procId, 18 | }); 19 | assert.equal(state[procId], true); 20 | }); 21 | 22 | it('should clear state when a component stops processing', function() { 23 | var procId = 'some-id'; 24 | var state = processingReducer(initialProcessingState, { 25 | type: actionTypes.BEGIN_PROCESSING, 26 | processingId: procId, 27 | }); 28 | state = processingReducer(state, { 29 | type: actionTypes.STOP_PROCESSING, 30 | processingId: procId, 31 | }); 32 | // This should now be deleted. 33 | assert.equal(state[procId], undefined); 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /tests/reducers/test.trans-reducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from 'constants/action-types'; 2 | import * as transactionActions from 'actions/transaction'; 3 | import transactionReducer from 'reducers/transaction'; 4 | import { initialTransState } from 'reducers/transaction'; 5 | 6 | 7 | describe('Transaction Reducer', function() { 8 | 9 | it('should initialize a transaction', function() { 10 | var trans = transactionReducer(undefined, {}); 11 | assert.deepEqual(trans, initialTransState); 12 | }); 13 | 14 | it('should handle completed transactions', function() { 15 | var trans = transactionReducer(undefined, transactionActions.complete()); 16 | assert.deepEqual(trans, Object.assign({}, initialTransState, { 17 | completed: true, 18 | })); 19 | }); 20 | 21 | it('should store an optional email on completion', function() { 22 | var email = 'someone@somewhere.org'; 23 | var trans = transactionReducer( 24 | undefined, 25 | transactionActions.complete({userEmail: email}) 26 | ); 27 | assert.deepEqual(trans, Object.assign({}, initialTransState, { 28 | completed: true, 29 | userEmail: email, 30 | })); 31 | }); 32 | 33 | it('should set available methods to user saved pay methods', function() { 34 | var payMethods = [{type: 'Visa'}]; 35 | 36 | var trans = transactionReducer(undefined, { 37 | type: actionTypes.USER_SIGNED_IN, 38 | user: { 39 | payMethods: payMethods, 40 | }, 41 | }); 42 | 43 | assert.deepEqual(trans, Object.assign({}, initialTransState, { 44 | availablePayMethods: payMethods, 45 | })); 46 | }); 47 | 48 | }); 49 | -------------------------------------------------------------------------------- /tests/test-loader.js: -------------------------------------------------------------------------------- 1 | // Webpack tests entry point. Bundles all the test files 2 | // into a single file. 3 | // See: https://github.com/webpack/karma-webpack#alternative-usage 4 | import 'babel-core/polyfill'; 5 | 6 | var logLevels = ['log', 'warn', 'info', 'error', 'debug']; 7 | logLevels.forEach(function(level) { 8 | console[level] = function() {}; 9 | }); 10 | 11 | var context = require.context('.', true, /.*?test\..*?.jsx?$/); 12 | context.keys().forEach(context); 13 | -------------------------------------------------------------------------------- /tests/test.gh-pages-config.js: -------------------------------------------------------------------------------- 1 | import ghPagesConfig from '../tasks/gh-pages'; 2 | 3 | describe('grunt-gh-pages config', function() { 4 | it('Docker builds should be silent=true', function() { 5 | assert.equal(ghPagesConfig.docker.options.silent, true); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/test.product-data.js: -------------------------------------------------------------------------------- 1 | import products from 'products'; 2 | 3 | 4 | describe('products', function() { 5 | 6 | it('should have concrete brick data', function() { 7 | assert.equal(products['mozilla-concrete-brick'].id, 8 | 'mozilla-concrete-brick'); 9 | }); 10 | 11 | it('should have concrete mortar data', function() { 12 | assert.equal(products['mozilla-concrete-mortar'].id, 13 | 'mozilla-concrete-mortar'); 14 | }); 15 | 16 | }); 17 | -------------------------------------------------------------------------------- /tests/test.tracking.js: -------------------------------------------------------------------------------- 1 | import { Tracking } from 'tracking'; 2 | 3 | 4 | describe('Tracking uninitialized', function() { 5 | 6 | it('should throw when calling setPage', function() { 7 | assert.throws(function() { 8 | var t = new Tracking({enabled: true}); 9 | t.setPage('whatevar'); 10 | }, /Must call init\(\) first/); 11 | }); 12 | 13 | it('should throw when calling sendEvent', function() { 14 | assert.throws(function() { 15 | var t = new Tracking({enabled: true}); 16 | t.sendEvent({ 17 | category: 'cat', 18 | action: 'some-action', 19 | }); 20 | }, /Must call init\(\) first/); 21 | }); 22 | 23 | }); 24 | 25 | 26 | describe('Tracking functions', function() { 27 | 28 | beforeEach(function() { 29 | this.t = new Tracking({ 30 | id: 'whatever', 31 | enabled: true, 32 | }); 33 | this.t.initialized = true; 34 | window.ga = sinon.stub(); 35 | }); 36 | 37 | it('should throw if page not set', function() { 38 | assert.throws(() => { 39 | this.t.setPage(); 40 | }, Error, /page is required/); 41 | }); 42 | 43 | it('should call ga', function() { 44 | this.t.setPage('whatever'); 45 | assert.ok(window.ga.called); 46 | }); 47 | 48 | it('should throw if category not set', function() { 49 | assert.throws(() => { 50 | this.t.sendEvent({}); 51 | }, Error, /opts\.category is required/); 52 | }); 53 | 54 | it('should throw if action not set', function() { 55 | assert.throws(() => { 56 | this.t.sendEvent({ 57 | category: 'whatever', 58 | }); 59 | }, Error, /opts\.action is required/); 60 | }); 61 | 62 | it('should call _ga', function() { 63 | this.t.sendEvent({ 64 | category: 'whatever', 65 | action: 'some-action', 66 | }); 67 | assert.ok(window.ga.called); 68 | }); 69 | 70 | }); 71 | -------------------------------------------------------------------------------- /tests/views/test.braintree-token.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react/lib/ReactTestUtils'; 3 | 4 | import BraintreeToken from 'views/shared/braintree-token'; 5 | 6 | 7 | describe('BraintreeToken', function() { 8 | 9 | var getBraintreeTokenSpy = sinon.spy(); 10 | 11 | function mountView() { 12 | return TestUtils.renderIntoDocument( 13 | 14 | ); 15 | } 16 | 17 | it('should sign in on mount', function() { 18 | mountView(); 19 | assert.ok(getBraintreeTokenSpy.called); 20 | }); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /tests/views/test.history.jsx: -------------------------------------------------------------------------------- 1 | import JsonTable from 'react-json-table'; 2 | import React from 'react'; 3 | import TestUtils from 'react/lib/ReactTestUtils'; 4 | 5 | import Spinner from 'components/spinner'; 6 | import History from 'views/management/history'; 7 | 8 | import { transactionData } from '../actions/test.transaction'; 9 | 10 | 11 | describe('History', function() { 12 | var fakeTransactionGetter; 13 | 14 | beforeEach(function() { 15 | fakeTransactionGetter = sinon.stub(); 16 | }); 17 | 18 | function mountView({transactions=null} = {}) { 19 | return TestUtils.renderIntoDocument( 20 | 24 | ); 25 | } 26 | 27 | it('should show a spinner when loading transactions', function() { 28 | var view = mountView({transactions: null}); 29 | var spinner = TestUtils.findRenderedComponentWithType( 30 | view, Spinner 31 | ); 32 | assert.ok(spinner); 33 | }); 34 | 35 | it('should load transactions on mount', function() { 36 | mountView(); 37 | assert.equal(fakeTransactionGetter.called, true); 38 | }); 39 | 40 | it('should show transactions', function() { 41 | var data = transactionData(); 42 | var view = mountView({transactions: data}); 43 | var table = TestUtils.findRenderedComponentWithType( 44 | view, JsonTable 45 | ); 46 | assert.equal(table.props.rows, data); 47 | }); 48 | 49 | it('should format transaction dates', function() { 50 | var data = transactionData(); 51 | var view = mountView({transactions: data}); 52 | var table = TestUtils.findRenderedComponentWithType( 53 | view, JsonTable 54 | ); 55 | 56 | var cell; 57 | // Use the JsonTable configuration to test formatting of date values. 58 | table.props.columns.forEach(config => { 59 | if (config.key === 'created') { 60 | // Simulate how JsonTable formats a row. 61 | cell = config.cell({created: '2015-08-07T14:53:23.966'}, 'created'); 62 | } 63 | }); 64 | 65 | assert.equal(cell, 'Aug 7, 2015'); 66 | }); 67 | 68 | }); 69 | -------------------------------------------------------------------------------- /tests/views/test.management.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react/lib/ReactTestUtils'; 3 | 4 | import { initialUserState } from 'reducers/user'; 5 | import Management from 'views/management'; 6 | 7 | import * as helpers from '../helpers'; 8 | 9 | 10 | describe('Management', function() { 11 | var fakeSubscriptionGetter; 12 | var fakeShowSignOut; 13 | var fakeUser; 14 | 15 | beforeEach(function() { 16 | fakeSubscriptionGetter = sinon.stub(); 17 | fakeShowSignOut = sinon.stub(); 18 | fakeUser = Object.assign({}, initialUserState, { 19 | signedIn: true, 20 | email: 'f@f.com', 21 | }); 22 | }); 23 | 24 | function mountView() { 25 | return TestUtils.renderIntoDocument( 26 | 31 | ); 32 | } 33 | 34 | it('should sign out on button click', function() { 35 | var view = mountView(); 36 | var button = helpers.findById(view, 'show-sign-out'); 37 | 38 | TestUtils.Simulate.click(button); 39 | assert.equal(fakeShowSignOut.called, true); 40 | }); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /tests/views/test.pay-methods.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react/lib/ReactTestUtils'; 3 | 4 | import PayMethods from 'views/management/pay-methods'; 5 | 6 | import * as helpers from '../helpers'; 7 | 8 | 9 | describe('Pay Methods', function() { 10 | 11 | beforeEach(function() { 12 | const fakeCards = [ 13 | { 14 | id: 1, 15 | resource_uri: 'something', 16 | truncated_id: '4321', 17 | type_name: 'visa', 18 | }, { 19 | id: 2, 20 | resource_uri: 'whatever', 21 | truncated_id: '1234', 22 | type_name: 'amex', 23 | }, 24 | ]; 25 | 26 | this.getPayMethodsStub = sinon.stub(); 27 | this.PayMethods = TestUtils.renderIntoDocument( 28 | 31 | ); 32 | }); 33 | 34 | it('should call getPayMethods stub', function() { 35 | assert.equal(this.getPayMethodsStub.called, true); 36 | }); 37 | 38 | it('should have a card list', function() { 39 | var content = helpers.findByClass(this.PayMethods, 'pay-method-list'); 40 | assert.ok(TestUtils.isDOMComponent(content)); 41 | }); 42 | 43 | it('should show no-results message', function() { 44 | var noPayMethods = TestUtils.renderIntoDocument( 45 | 48 | ); 49 | var content = helpers.findByClass(noPayMethods, 'no-results'); 50 | assert.ok(TestUtils.isDOMComponent(content)); 51 | }); 52 | 53 | it('should have disabled class on delete link if no cards', function() { 54 | var noPayMethods = TestUtils.renderIntoDocument( 55 | 58 | ); 59 | var delButton = helpers.findByClass(noPayMethods, 'delete').getDOMNode(); 60 | assert.ok(delButton.className.split(' ').indexOf('disabled') > -1); 61 | }); 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /tests/views/test.product-pay.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react/lib/ReactTestUtils'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import { createReduxStore } from 'data-store'; 6 | import CardForm from 'components/card-form'; 7 | import ProductPay from 'views/transaction/product-pay'; 8 | 9 | 10 | describe('ProductPay', function() { 11 | 12 | var fakePaymentProcessor; 13 | var defaultProductId = 'mozilla-concrete-brick'; 14 | var braintreeToken = 'some-braintree-token'; 15 | var store; 16 | var userDefinedAmount = '10.00'; 17 | 18 | beforeEach(function() { 19 | fakePaymentProcessor = sinon.spy(); 20 | store = createReduxStore(); 21 | }); 22 | 23 | function mountView({productId=defaultProductId} = {}) { 24 | var container = TestUtils.renderIntoDocument( 25 | 26 | {() => { 27 | return ( 28 | 35 | ); 36 | }} 37 | 38 | ); 39 | 40 | return TestUtils.findRenderedComponentWithType( 41 | container, ProductPay 42 | ); 43 | } 44 | 45 | it('should handle payment processing', function() { 46 | var View = mountView(); 47 | var card = TestUtils.findRenderedComponentWithType( 48 | View, CardForm 49 | ); 50 | 51 | var cardData = {number: '411111111111'}; 52 | card.props.handleCardSubmit(cardData); 53 | 54 | assert.equal(fakePaymentProcessor.called, true); 55 | var args = fakePaymentProcessor.firstCall.args; 56 | assert.equal(args[0].userDefinedAmount, userDefinedAmount); 57 | assert.equal(args[0].productId, defaultProductId); 58 | assert.equal(args[0].creditCard, cardData); 59 | assert.equal(args[0].braintreeToken, braintreeToken); 60 | }); 61 | 62 | it('should prompt for donation', function() { 63 | var View = mountView({productId: 'mozilla-foundation-donation'}); 64 | var card = TestUtils.findRenderedComponentWithType( 65 | View, CardForm 66 | ); 67 | assert.equal(card.props.submitPrompt, 'Donate now'); 68 | }); 69 | 70 | it('should prompt for recurring donation', function() { 71 | var View = mountView({productId: 'mozilla-foundation-recurring-donation'}); 72 | var card = TestUtils.findRenderedComponentWithType( 73 | View, CardForm 74 | ); 75 | assert.equal(card.props.submitPrompt, 'Donate now'); 76 | }); 77 | 78 | }); 79 | -------------------------------------------------------------------------------- /tests/views/test.sign-in.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react/lib/ReactTestUtils'; 3 | 4 | import { initialUserState } from 'reducers/user'; 5 | import SignIn from 'views/shared/sign-in'; 6 | 7 | 8 | describe('SignIn', function() { 9 | 10 | var fakeAccessToken = 'some-oauth-access-token'; 11 | var fakeError; 12 | var fakeTokenSignIn; 13 | var fakeUser; 14 | var fakeUserSignIn; 15 | 16 | beforeEach(function() { 17 | fakeError = sinon.spy(); 18 | fakeTokenSignIn = sinon.spy(); 19 | fakeUser = Object.assign({}, initialUserState); 20 | fakeUserSignIn = sinon.spy(); 21 | }); 22 | 23 | function mountView({accessToken=fakeAccessToken, 24 | user=fakeUser, allowUserSignIn} = {}) { 25 | return TestUtils.renderIntoDocument( 26 | 34 | ); 35 | } 36 | 37 | it('should sign in on mount', function() { 38 | mountView(); 39 | assert.equal(fakeTokenSignIn.firstCall.args[0], fakeAccessToken); 40 | }); 41 | 42 | it('should do a token sign-in with access token', function() { 43 | var token = 'some-fxa-token'; 44 | mountView({accessToken: token}); 45 | assert.equal(fakeTokenSignIn.firstCall.args[0], token); 46 | }); 47 | 48 | it('should do a user sign-in without access token', function() { 49 | mountView({accessToken: null}); 50 | assert.equal(fakeUserSignIn.called, true); 51 | }); 52 | 53 | it('should not sign in when user is already signed in', function() { 54 | fakeUser.signedIn = true; 55 | mountView(); 56 | assert.equal(fakeUserSignIn.called, false); 57 | assert.equal(fakeTokenSignIn.called, false); 58 | }); 59 | 60 | it('should error when user sign-in not allowed', function() { 61 | mountView({allowUserSignIn: false, accessToken: null}); 62 | assert.equal(fakeUserSignIn.called, false); 63 | assert.equal(fakeTokenSignIn.called, false); 64 | assert.equal(fakeError.called, true); 65 | }); 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /tests/views/test.sign-out.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react/lib/ReactTestUtils'; 3 | 4 | import { initialUserState } from 'reducers/user'; 5 | import SignOut from 'views/shared/sign-out'; 6 | 7 | 8 | describe('SignOut', function() { 9 | 10 | var fakeShowSignIn; 11 | var fakeUser; 12 | var fakeUserSignOut; 13 | 14 | beforeEach(function() { 15 | fakeShowSignIn = sinon.spy(); 16 | fakeUserSignOut = sinon.spy(); 17 | fakeUser = Object.assign({}, initialUserState, { 18 | signedIn: true, 19 | }); 20 | }); 21 | 22 | function mountView() { 23 | return TestUtils.renderIntoDocument( 24 | 29 | ); 30 | } 31 | 32 | it('should sign out on mount', function() { 33 | mountView(); 34 | assert.equal(fakeUserSignOut.called, true); 35 | }); 36 | 37 | it('should not do sign-out when already signed out', function() { 38 | fakeUser.signedIn = false; 39 | mountView(); 40 | assert.equal(fakeUserSignOut.called, false); 41 | }); 42 | 43 | it('should sign back in on button click', function() { 44 | fakeUser.signedIn = false; 45 | var view = mountView(); 46 | var button = TestUtils.findRenderedDOMComponentWithTag( 47 | view, 'a'); 48 | TestUtils.Simulate.click(button); 49 | assert.equal(fakeShowSignIn.called, true); 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /tests/views/test.subscriptions.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react/lib/ReactTestUtils'; 3 | 4 | import Subscriptions from 'views/management/subscriptions'; 5 | 6 | 7 | describe('Subscriptions', function() { 8 | 9 | var getUserSubsSpy = sinon.stub(); 10 | 11 | function mountView() { 12 | return TestUtils.renderIntoDocument( 13 | 14 | ); 15 | } 16 | 17 | it('should call getUserSubscriptions on mount', function() { 18 | mountView(); 19 | assert.equal(getUserSubsSpy.called, true); 20 | }); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var webpack = require('webpack'); 5 | 6 | module.exports = { 7 | devtool: 'source-map', 8 | entry: { 9 | 'management': './public/js/apps/management/main.jsx', 10 | 'transaction': './public/js/apps/transaction/main.jsx', 11 | }, 12 | failOnError: true, 13 | output: { 14 | path: path.join(__dirname, 'public/dist/'), 15 | filename: '[name].bundle.js', 16 | publicPath: '/dist/', 17 | sourceMapFilename: '[file].map', 18 | }, 19 | module: { 20 | loaders: [ 21 | { 22 | exclude: /(node_modules|bower_components)/, 23 | test: /\.jsx?$/, 24 | // es7.objectRestSpread to enable ES7 rest spread operators 25 | // eg: let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; 26 | loaders: ['babel?optional[]=es7.objectRestSpread&optional[]=es7.classProperties&stage=2'], 27 | }, 28 | ], 29 | }, 30 | plugins: [ 31 | new webpack.optimize.CommonsChunkPlugin('commons.js', 32 | ['management', 'payment']), 33 | ], 34 | resolve: { 35 | // you can now require('file') instead of require('file.json') 36 | extensions: ['', '.js', '.jsx', '.json'], 37 | modulesDirectories: [ 38 | 'public/js/', 39 | 'node_modules', 40 | 'node_modules/mozilla-payments-config/json/products/', 41 | ], 42 | }, 43 | stats: { 44 | // Configure the console output 45 | colors: true, 46 | modules: true, 47 | reasons: true, 48 | }, 49 | watch: true, 50 | }; 51 | --------------------------------------------------------------------------------