feat(payments): fix #1280, consume configuration in payments frontend

This also changes the config naming on the frontend to match the server configuration.
This commit is contained in:
Ian Bicking 2019-06-06 16:54:42 -05:00
Родитель f737d834ec
Коммит 1c54e449a0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 1DA122EDDE590E8B
9 изменённых файлов: 157 добавлений и 52 удалений

Просмотреть файл

@ -1,5 +1,5 @@
import React, { useEffect, ReactNode } from 'react';
import config from '../../src/lib/config';
import { config } from '../../src/lib/config';
import { StripeProvider } from 'react-stripe-elements';
import { MockLoader } from './MockLoader';
@ -20,7 +20,7 @@ export const MockApp = ({
}, []);
return (
<StripeProvider apiKey={config.STRIPE_API_KEY}>
<StripeProvider apiKey={config.stripe.apiKey}>
<MockLoader>
{children}
</MockLoader>
@ -28,4 +28,4 @@ export const MockApp = ({
);
};
export default MockApp;
export default MockApp;

Просмотреть файл

@ -10,7 +10,7 @@
<meta name=robots content=noindex,nofollow>
<meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=2,user-scalable=yes">
<meta name=apple-itunes-app content="app-id=989804926, affiliate-data=ct=smartbanner-fxa">
<meta name="fxa-content-server/config" content="__SERVER_CONFIG__">
<meta name="fxa-config" content="__SERVER_CONFIG__">
<meta name="fxa-feature-flags" content="__FEATURE_FLAGS__">
<script src="https://js.stripe.com/v3/"></script>

Просмотреть файл

@ -97,6 +97,14 @@ const conf = convict({
format: 'String',
},
servers: {
auth: {
url: {
default: 'http://127.0.0.1:9000',
doc: 'The url of the fxa-auth-server instance',
env: 'AUTH_SERVER_URL',
format: 'url',
}
},
content: {
url: {
default: 'http://127.0.0.1:3030',
@ -142,6 +150,14 @@ const conf = convict({
format: 'url'
}
},
stripe: {
apiKey: {
default: 'pk_test_FL2cOisOukoCQUZsrochvTlk00ff4IakfE',
doc: 'API key for Stripe',
env: 'STRIP_API_KEY',
format: String,
}
},
});
// handle configuration files. you can specify a CSV list of configuration

Просмотреть файл

@ -26,10 +26,24 @@ module.exports = () => {
// Each of these config values (e.g., 'servers.content') will be exposed as the given
// variable to the client/browser (via fxa-content-server/config)
const CLIENT_CONFIG_MAP = {
contentUrl: 'servers.content',
oAuthUrl: 'servers.oauth',
profileUrl: 'servers.profile',
const CLIENT_CONFIG = {
servers: {
auth: {
url: config.get('servers.auth.url'),
},
content: {
url: config.get('servers.content.url'),
},
oauth: {
url: config.get('servers.oauth.url'),
},
profile: {
url: config.get('servers.profile.url'),
},
},
stripe: {
apiKey: config.get('stripe.apiKey'),
},
};
// This is a list of all the paths that should resolve to index.html:
@ -88,18 +102,11 @@ module.exports = () => {
return result;
}
function getClientConfig() {
// See also packages/fxa-content-server/server/lib/routes/get-index.js
const clientConfig = {};
for (const exportVariable in CLIENT_CONFIG_MAP) {
clientConfig[exportVariable] = config.get(CLIENT_CONFIG_MAP[exportVariable]);
}
return clientConfig;
}
const STATIC_DIRECTORY =
path.join(__dirname, '..', '..', config.get('staticResources.directory'));
const STATIC_INDEX_HTML = fs.readFileSync(path.join(STATIC_DIRECTORY, 'index.html'), {encoding: 'UTF-8'});
const proxyUrl = config.get('proxyStaticResourcesFrom');
if (proxyUrl) {
logger.info('static.proxying', { url: proxyUrl });
@ -115,13 +122,12 @@ module.exports = () => {
return proxyResData;
}
const body = proxyResData.toString('utf8');
return injectHtmlConfig(body, getClientConfig(), {});
return injectHtmlConfig(body, CLIENT_CONFIG, {});
}
}));
} else {
logger.info('static.directory', { directory: STATIC_DIRECTORY });
const STATIC_INDEX_HTML = fs.readFileSync(path.join(STATIC_DIRECTORY, 'index.html'), {encoding: 'UTF-8'});
const renderedStaticHtml = injectHtmlConfig(STATIC_INDEX_HTML, getClientConfig(), {});
const renderedStaticHtml = injectHtmlConfig(STATIC_INDEX_HTML, CLIENT_CONFIG, {});
for (const route of INDEX_ROUTES) {
// FIXME: should set ETag, Not-Modified:
app.get(route, (req, res) => {

Просмотреть файл

@ -35,7 +35,7 @@ export const App = ({
}: AppProps) => {
// Note: every Route below should also be listed in INDEX_ROUTES in server/lib/server.js
return (
<StripeProvider apiKey={config.STRIPE_API_KEY}>
<StripeProvider apiKey={config.stripe.apiKey}>
<Provider store={store}>
<LoadingOverlay />
<Router>

Просмотреть файл

@ -2,11 +2,13 @@ import React from 'react';
import { render } from 'react-dom';
import { createAppStore, actions } from './store';
import config from './lib/config';
import { config, readConfigFromMeta } from './lib/config';
import './index.scss';
import App from './App';
async function init() {
readConfigFromMeta();
const store = createAppStore();
const queryParams = parseParams(window.location.search);
@ -20,11 +22,11 @@ async function init() {
actions.fetchToken(accessToken),
actions.fetchProfile(accessToken),
].map(store.dispatch);
render(
<App {...{ accessToken, config, store, queryParams }} />,
document.getElementById('root')
);
);
}
}
@ -58,7 +60,7 @@ async function getVerifiedAccessToken({
try {
const result = await fetch(
`${config.OAUTH_API_ROOT}/verify`,
`${config.servers.oauth.url}/v1/verify`,
{
body: JSON.stringify({ token: accessToken }),
headers: { 'Content-Type': 'application/json' },
@ -76,7 +78,7 @@ async function getVerifiedAccessToken({
if (! accessToken) {
// TODO: bounce through a login redirect to get back here with a token
window.location.href = `${config.CONTENT_SERVER_ROOT}/settings`;
window.location.href = `${config.servers.content.url}/settings`;
return accessToken;
}

Просмотреть файл

@ -1,8 +0,0 @@
// TODO: make this dynamic based on host and/or config injected by server
export default {
AUTH_API_ROOT: 'http://127.0.0.1:9000/v1',
CONTENT_SERVER_ROOT: 'http://127.0.0.1:3030',
OAUTH_API_ROOT: 'http://127.0.0.1:9010/v1',
PROFILE_API_ROOT: 'http://127.0.0.1:1111/v1',
STRIPE_API_KEY: 'pk_test_FL2cOisOukoCQUZsrochvTlk00ff4IakfE',
};

Просмотреть файл

@ -0,0 +1,89 @@
// This configuration is a subset of the configuration declared in server/config/index.js
// Which config is copied over is defined in server/lib/server.js
export interface Config {
featureFlags: {[key: string]: any}
servers: {
auth: {
url: string
}
content: {
url: string
}
oauth: {
url: string
}
profile: {
url: string
}
}
stripe: {
apiKey: string
}
lang: string
}
export const config: Config = {
featureFlags: {},
servers: {
auth: {
url: '',
},
content: {
url: '',
},
oauth: {
url: '',
},
profile: {
url: '',
},
},
stripe: {
apiKey: '',
},
lang: '',
};
function decodeConfig(content: string|null) {
if (!content) {
throw new Error('Configuration is empty');
}
const decoded = decodeURIComponent(content);
try {
return JSON.parse(decoded);
} catch (e) {
throw new Error(`Invalid configuration ${JSON.stringify(content)}: ${decoded}`);
}
}
export function readConfigFromMeta() {
const configEl = document.head.querySelector('meta[name="fxa-config"]');
if (!configEl) {
throw new Error('<meta name="fxa-config"> is missing');
}
updateConfig(decodeConfig(configEl.getAttribute('content')));
const featureEl = document.head.querySelector('meta[name="fxa-feature-flags"]');
if (!featureEl) {
throw new Error('<meta name="fxa-feature-flags"> is missing');
}
updateConfig({featureFlags: decodeConfig(featureEl.getAttribute('content'))});
updateConfig({lang: document.documentElement.lang});
}
function merge(obj: any, data: any) {
for (const [key, value] of Object.entries(data)) {
if (value === null || typeof value !== "object") {
obj[key] = value;
} else {
if (!obj[key]) {
obj[key] = {};
}
merge(obj[key], value);
}
}
return obj;
}
export function updateConfig(newData: any) {
merge(config, newData);
}

Просмотреть файл

@ -4,7 +4,7 @@ import ReduxThunk from 'redux-thunk';
import { createPromise as promiseMiddleware } from 'redux-promise-middleware';
import typeToReducer from 'type-to-reducer';
import config from '../lib/config';
import { config } from '../lib/config';
import {
apiGet,
@ -34,7 +34,7 @@ export const defaultState: State = {
profile: fetchDefault({}),
updatePayment: fetchDefault(false),
subscriptions: fetchDefault([]),
token: fetchDefault({}),
token: fetchDefault({}),
}
};
@ -76,30 +76,30 @@ export const actions: ActionCreators = {
...createActions(
{
fetchProfile: accessToken =>
apiGet(accessToken, `${config.PROFILE_API_ROOT}/profile`),
apiGet(accessToken, `${config.servers.profile.url}/v1/profile`),
fetchPlans: accessToken =>
apiGet(accessToken, `${config.AUTH_API_ROOT}/oauth/subscriptions/plans`),
apiGet(accessToken, `${config.servers.auth.url}/v1/oauth/subscriptions/plans`),
fetchSubscriptions: accessToken =>
apiGet(accessToken, `${config.AUTH_API_ROOT}/oauth/subscriptions/active`),
apiGet(accessToken, `${config.servers.auth.url}/v1/oauth/subscriptions/active`),
fetchToken: accessToken =>
apiPost(accessToken, `${config.OAUTH_API_ROOT}/introspect`, { token: accessToken }),
apiPost(accessToken, `${config.servers.oauth.url}/v1/introspect`, { token: accessToken }),
fetchCustomer: accessToken =>
apiGet(accessToken, `${config.AUTH_API_ROOT}/oauth/subscriptions/customer`),
apiGet(accessToken, `${config.servers.auth.url}/v1/oauth/subscriptions/customer`),
createSubscription: (accessToken, params) =>
apiPost(
accessToken,
`${config.AUTH_API_ROOT}/oauth/subscriptions/active`,
`${config.servers.auth.url}/v1/oauth/subscriptions/active`,
params
),
cancelSubscription: (accessToken, subscriptionId) =>
apiDelete(
accessToken,
`${config.AUTH_API_ROOT}/oauth/subscriptions/active/${subscriptionId}`
`${config.servers.auth.url}/v1/oauth/subscriptions/active/${subscriptionId}`
),
updatePayment: (accessToken, { paymentToken }) =>
apiPost(
accessToken,
`${config.AUTH_API_ROOT}/oauth/subscriptions/updatePayment`,
`${config.servers.auth.url}/v1/oauth/subscriptions/updatePayment`,
{ paymentToken }
),
},
@ -115,16 +115,16 @@ export const actions: ActionCreators = {
async (dispatch: Function, getState: Function) => {
await Promise.all([
dispatch(actions.fetchCustomer(accessToken)),
dispatch(actions.fetchSubscriptions(accessToken))
dispatch(actions.fetchSubscriptions(accessToken))
])
},
fetchPlansAndSubscriptions: (accessToken: string) =>
async (dispatch: Function, getState: Function) => {
await Promise.all([
dispatch(actions.fetchPlans(accessToken)),
dispatch(actions.fetchCustomer(accessToken)),
dispatch(actions.fetchSubscriptions(accessToken))
dispatch(actions.fetchSubscriptions(accessToken))
])
},
@ -134,12 +134,12 @@ export const actions: ActionCreators = {
await dispatch(actions.fetchCustomerAndSubscriptions(accessToken));
},
cancelSubscriptionAndRefresh: (accessToken: string, subscriptionId: object) =>
cancelSubscriptionAndRefresh: (accessToken: string, subscriptionId: object) =>
async (dispatch: Function, getState: Function) => {
await dispatch(actions.cancelSubscription(accessToken, subscriptionId));
await dispatch(actions.fetchCustomerAndSubscriptions(accessToken));
},
updatePaymentAndRefresh: (accessToken: string, params: object) =>
async (dispatch: Function, getState: Function) => {
await dispatch(actions.updatePayment(accessToken, params));
@ -156,9 +156,9 @@ export const reducers = {
{
[actions.fetchProfile.toString()]:
fetchReducer('profile'),
[actions.fetchPlans.toString()]:
[actions.fetchPlans.toString()]:
fetchReducer('plans'),
[actions.fetchSubscriptions.toString()]:
[actions.fetchSubscriptions.toString()]:
fetchReducer('subscriptions'),
[actions.fetchToken.toString()]:
fetchReducer('token'),
@ -183,7 +183,7 @@ export const reducers = {
),
};
export const selectorsFromState =
export const selectorsFromState =
(...names: Array<string>) =>
(state: State) =>
mapToObject(names, (name: string) => selectors[name](state));