зеркало из https://github.com/mozilla/fxa.git
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:
Родитель
f737d834ec
Коммит
1c54e449a0
|
@ -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));
|
||||
|
|
Загрузка…
Ссылка в новой задаче