FE-603: init with config (#5)

This commit is contained in:
Alexandra Usacheva 2018-05-18 19:37:57 +03:00 committed by GitHub
parent 24d58cc71b
commit b534080593
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 764 additions and 88 deletions

View File

@ -44,7 +44,8 @@ module.exports = {
}),
new CopyWebpackPlugin(
[
{from: './src/app/assets/icons', to: './assets/icons'}
{from: './src/app/assets/icons', to: './assets/icons'},
{from: './src/appConfig.json', to: '..'}
],
{debug: 'warning'}
)

218
package-lock.json generated
View File

@ -43,6 +43,12 @@
"integrity": "sha512-NOLEgsT6UiDTjnWG5Hd2Mg25LRyz/oe8ql3wbjzgSFeRzRROhPmtlsvIrei4B46UjERF0td9SZ1ZXPLOdcrBHg==",
"dev": true
},
"@types/query-string": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@types/query-string/-/query-string-5.1.0.tgz",
"integrity": "sha512-9/sJK+T04pNq7uwReR0CLxqXj1dhxiTapZ1tIxA0trEsT6FRS0bz09YMcMb7tsVBTm4RJ0NEBYGsAjoEmqoFXg==",
"dev": true
},
"@types/ramda": {
"version": "0.25.24",
"resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.25.24.tgz",
@ -87,6 +93,75 @@
"@types/react": "*"
}
},
"@types/react-redux": {
"version": "5.0.20",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-5.0.20.tgz",
"integrity": "sha512-WeiE+bcA/6JaPIFpOJ75rvtM2/+Yu41k0YMgIXLnjRbyL55vG7B22HzFrVkyoIQNbCZFBz+pWdRDWRmNG4USBw==",
"dev": true,
"requires": {
"@types/react": "*",
"redux": "^3.6.0"
},
"dependencies": {
"redux": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
"integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
"dev": true,
"requires": {
"lodash": "^4.2.1",
"lodash-es": "^4.2.1",
"loose-envify": "^1.1.0",
"symbol-observable": "^1.0.3"
}
},
"symbol-observable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
"dev": true
}
}
},
"@types/redux": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@types/redux/-/redux-3.6.0.tgz",
"integrity": "sha1-8evh5UEVGAcuT9/KXHbhbnTBOZo=",
"dev": true,
"requires": {
"redux": "*"
}
},
"@types/redux-form": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/@types/redux-form/-/redux-form-7.2.5.tgz",
"integrity": "sha512-R4FHhJyB6P5zxbvtVp4C9qH3MYobxXwBMqf8bTL4j57w8qiDjMV+NiH1MEn/8n8zX0x5djRueqxeL0pVUyWQog==",
"dev": true,
"requires": {
"@types/react": "*",
"redux": "^3.6.0"
},
"dependencies": {
"redux": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
"integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
"dev": true,
"requires": {
"lodash": "^4.2.1",
"lodash-es": "^4.2.1",
"loose-envify": "^1.1.0",
"symbol-observable": "^1.0.3"
}
},
"symbol-observable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
"dev": true
}
}
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@ -2651,8 +2726,7 @@
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"dev": true
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
},
"decompress-response": {
"version": "3.3.0",
@ -2666,8 +2740,7 @@
"deep-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
"integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
"dev": true
"integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU="
},
"deep-extend": {
"version": "0.4.2",
@ -3136,6 +3209,11 @@
"is-symbol": "^1.0.1"
}
},
"es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -5391,6 +5469,11 @@
"integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
"dev": true
},
"hoist-non-react-statics": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz",
"integrity": "sha512-6Bl6XsDT1ntE0lHbIhr4Kp2PGcleGZ66qu5Jqk8lc0Xc/IeG6gVLmwUGs/K0Us+L8VWoKgj0uWdPMataOsm31w=="
},
"home-or-tmp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
@ -5828,7 +5911,6 @@
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"dev": true,
"requires": {
"loose-envify": "^1.0.0"
}
@ -6123,8 +6205,7 @@
"is-promise": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
"integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
"dev": true
"integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o="
},
"is-property": {
"version": "1.0.2",
@ -6499,23 +6580,6 @@
"invert-kv": "^1.0.0"
}
},
"libphonenumber-js": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.1.11.tgz",
"integrity": "sha512-ofIEkWyZaPvZLVP3QyOtEAY/Ie630VuV+rLIgLQ6OSV1wQtzjhG3mXabivbHPZlqV2lWEo2C8qnluBc8fdbDig==",
"requires": {
"minimist": "^1.2.0",
"semver-compare": "^1.0.0",
"xml2js": "^0.4.17"
},
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
}
}
},
"listr": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/listr/-/listr-0.13.0.tgz",
@ -6795,8 +6859,7 @@
"lodash": {
"version": "4.17.5",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz",
"integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==",
"dev": true
"integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw=="
},
"lodash-es": {
"version": "4.17.8",
@ -7769,6 +7832,18 @@
"prepend-http": "^1.0.0",
"query-string": "^4.1.0",
"sort-keys": "^1.0.0"
},
"dependencies": {
"query-string": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",
"integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=",
"dev": true,
"requires": {
"object-assign": "^4.1.0",
"strict-uri-encode": "^1.0.0"
}
}
}
},
"npm-run-path": {
@ -9041,13 +9116,19 @@
"dev": true
},
"query-string": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",
"integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=",
"dev": true,
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-6.1.0.tgz",
"integrity": "sha512-pNB/Gr8SA8ff8KpUFM36o/WFAlthgaThka5bV19AD9PNTH20Pwq5Zxodif2YyHwrctp6SkL4GqlOot0qR/wGaw==",
"requires": {
"object-assign": "^4.1.0",
"strict-uri-encode": "^1.0.0"
"decode-uri-component": "^0.2.0",
"strict-uri-encode": "^2.0.0"
},
"dependencies": {
"strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
"integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY="
}
}
},
"querystring": {
@ -9201,6 +9282,19 @@
"prop-types": "^15.6.0"
}
},
"react-redux": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.7.tgz",
"integrity": "sha512-5VI8EV5hdgNgyjfmWzBbdrqUkrVRKlyTKk1sGH3jzM2M2Mhj/seQgPXaz6gVAj2lz/nz688AdTqMO18Lr24Zhg==",
"requires": {
"hoist-non-react-statics": "^2.5.0",
"invariant": "^2.0.0",
"lodash": "^4.17.5",
"lodash-es": "^4.17.5",
"loose-envify": "^1.1.0",
"prop-types": "^15.6.0"
}
},
"read-chunk": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-2.1.0.tgz",
@ -9334,6 +9428,44 @@
}
}
},
"redux": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
"integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
"requires": {
"lodash": "^4.2.1",
"lodash-es": "^4.2.1",
"loose-envify": "^1.1.0",
"symbol-observable": "^1.0.3"
},
"dependencies": {
"symbol-observable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
}
}
},
"redux-devtools-extension": {
"version": "2.13.2",
"resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.2.tgz",
"integrity": "sha1-4Pmo6N/KfBe+kscSSVijuU6ykR0="
},
"redux-form": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/redux-form/-/redux-form-7.3.0.tgz",
"integrity": "sha512-WcZRsRsVG25l8Cih3bEeeoZFxSIvoHqTpBRe5Ifl1ob7xvEpYLXyYYHAFER1DpTfMZPgTPHZ4UkR4ILFP3hzkw==",
"requires": {
"deep-equal": "^1.0.1",
"es6-error": "^4.1.1",
"hoist-non-react-statics": "^2.5.0",
"invariant": "^2.2.3",
"is-promise": "^2.1.0",
"lodash": "^4.17.5",
"lodash-es": "^4.17.5",
"prop-types": "^15.6.1"
}
},
"redux-saga": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-0.16.0.tgz",
@ -9850,7 +9982,8 @@
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
},
"schema-utils": {
"version": "0.4.5",
@ -9910,11 +10043,6 @@
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
"dev": true
},
"semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w="
},
"send": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
@ -12008,20 +12136,6 @@
"safe-buffer": "~5.1.0"
}
},
"xml2js": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
"requires": {
"sax": ">=0.6.0",
"xmlbuilder": "~9.0.1"
}
},
"xmlbuilder": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
"integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0="
},
"xtend": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",

View File

@ -26,6 +26,9 @@
"@types/react": "~16.3.11",
"@types/react-addons-css-transition-group": "~15.0.4",
"@types/react-dom": "~16.0.5",
"@types/react-redux": "~5.0.14",
"@types/redux": "~3.6.0",
"@types/redux-form": "~7.2.5",
"cache-loader": "~1.2.2",
"copy-webpack-plugin": "~4.5.1",
"css-loader": "~0.28.11",
@ -59,6 +62,10 @@
"react": "~16.3.2",
"react-addons-css-transition-group": "~15.6.2",
"react-dom": "~16.3.2",
"react-redux": "~5.0.5",
"redux": "~3.7.2",
"redux-devtools-extension": "~2.13.2",
"redux-form": "~7.3.0",
"redux-saga": "~0.16.0"
}
}

View File

@ -0,0 +1,6 @@
import { Action } from 'redux';
export interface AbstractAction<P = null, M = null> extends Action {
payload?: P;
meta?: M;
}

View File

@ -0,0 +1,11 @@
import { AppConfig } from 'app/backend';
import { AbstractAction, TypeKeys } from 'app/actions';
export interface Config {
appConfig: AppConfig;
}
export interface AppConfigReceived extends AbstractAction<Config> {
type: TypeKeys.APP_CONFIG_RECEIVED;
payload: Config;
}

View File

@ -0,0 +1 @@
export * from './app-config-received';

5
src/app/actions/index.ts Normal file
View File

@ -0,0 +1,5 @@
export * from './type-keys';
export * from './abstract-action';
export * from './initialize-actions';
export * from './config-actions';
export * from './model-actions';

View File

@ -0,0 +1 @@
export * from './initialize-actions';

View File

@ -0,0 +1,21 @@
import { AbstractAction, TypeKeys } from 'app/actions';
import { InitConfig } from 'app/config';
import { LogicError } from 'app/backend';
export interface InitializeAppRequested extends AbstractAction<InitConfig> {
type: TypeKeys.INITIALIZE_APP_REQUESTED;
payload: InitConfig;
}
export interface InitializeAppCompleted extends AbstractAction {
type: TypeKeys.INITIALIZE_APP_COMPLETED;
}
export interface InitializeAppFailed extends AbstractAction<LogicError> {
type: TypeKeys.INITIALIZE_APP_FAILED;
payload: LogicError;
}
export const initializeApp = () => ({
type: TypeKeys.INITIALIZE_APP_REQUESTED
});

View File

@ -0,0 +1 @@
export * from './initialize-action';

View File

@ -0,0 +1,7 @@
import { AbstractAction, TypeKeys } from 'app/actions';
import { ModelState } from 'app/state';
export interface InitializeModelCompleted extends AbstractAction<ModelState> {
type: TypeKeys.INITIALIZE_MODEL_COMPLETED;
payload: ModelState;
}

View File

@ -0,0 +1,7 @@
export enum TypeKeys {
INITIALIZE_APP_REQUESTED = 'INITIALIZE_APP_REQUESTED',
INITIALIZE_APP_COMPLETED = 'INITIALIZE_APP_COMPLETED',
INITIALIZE_APP_FAILED = 'INITIALIZE_APP_FAILED',
APP_CONFIG_RECEIVED = 'APP_CONFIG_RECEIVED',
INITIALIZE_MODEL_COMPLETED = 'INITIALIZE_MODEL_COMPLETED'
}

View File

@ -0,0 +1,3 @@
export class AppConfig {
wapiEndpoint: string;
}

View File

@ -0,0 +1,36 @@
function s4(): string {
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
}
function guid(): string {
return `${s4()}${s4()}-${s4()}${s4()}`;
}
export class FetchWapiParams {
endpoint: string;
accessToken: string;
method?: 'GET' | 'POST' | 'PUT';
body?: any;
}
export function fetchWapi<T>(param: FetchWapiParams): Promise<T> {
return new Promise((resolve, reject) => {
fetch(param.endpoint, {
method: param.method || 'GET',
headers: {
'Content-Type': 'application/json;charset=utf-8',
'Authorization': `Bearer ${param.accessToken}`,
'X-Request-ID': guid()
},
body: param.body ? JSON.stringify(param.body) : undefined
}).then((res) =>
res.status >= 200 && res.status <= 300
? resolve(res.json())
: res.json()
.then((ex) => reject(ex))
.catch(() => reject({
message: `${res.status}: ${res.statusText}`
}))
).catch((ex) => reject({message: `${ex}`}));
});
}

View File

@ -0,0 +1,11 @@
import { AppConfig } from './app-config';
import { getNocacheValue } from 'app/utils/get-nocache-value';
export const getAppConfig = (): Promise<AppConfig> => (
fetch(`../appConfig.json?nocache=${getNocacheValue()}`, {
headers: {
'Content-Type': 'application/json'
},
method: 'GET'
}).then((response) => response.json())
);

View File

@ -0,0 +1,7 @@
import { Identity, fetchWapi } from 'app/backend';
export const getIdentityByID = (wapiEndpoint: string, accessToken: string, identityID: string): Promise<Identity> =>
fetchWapi({
endpoint: `${wapiEndpoint}/identities/${identityID}`,
accessToken
});

5
src/app/backend/index.ts Normal file
View File

@ -0,0 +1,5 @@
export * from './model';
export * from './fetch-wapi';
export * from './app-config';
export * from './get-app-config';
export * from './get-identity';

View File

@ -0,0 +1,11 @@
export class Identity {
id: string;
name: string;
createdAt: string;
provider: string;
class: string;
level: string;
effectiveChallenge: string;
isBlocked: boolean;
metadata: {};
}

View File

@ -0,0 +1,2 @@
export * from './logic-error';
export * from './identity';

View File

@ -0,0 +1,4 @@
export class LogicError {
code: string;
message: string;
}

View File

@ -0,0 +1,23 @@
@import "../../../styles/animations";
.loader {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
@include responsive(sm) {
position: relative;
height: 100%;
width: 100%;
top: 0;
left: 0;
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
justify-content: center;
transform: translate(0, 0);
animation: growth .75s;
}
}

View File

@ -0,0 +1,11 @@
export const loader: string;
export const growth: string;
export const fadein: string;
export const fadeout: string;
export const slidedown: string;
export const slideup: string;
export const popup: string;
export const popout: string;
export const rotatein: string;
export const rotateout: string;
export const shake: string;

View File

@ -0,0 +1,11 @@
import * as React from 'react';
import * as styles from './app-loader.scss';
import { Loader } from '../../ui/loader';
const AppLoaderDef: React.SFC = () => (
<div className={styles.loader}>
<Loader/>
</div>
);
export const AppLoader = AppLoaderDef;

View File

@ -0,0 +1 @@
export * from './app-loader';

View File

@ -0,0 +1,6 @@
import { InitializeAppState } from 'app/state';
export interface AppProps {
initializeApp: InitializeAppState;
initApp: () => any;
}

View File

@ -0,0 +1,40 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { layout } from './app.scss';
import { Overlay } from './overlay';
import { ModalContainer } from './modal-container';
import { initializeApp } from 'app/actions';
import { State } from 'app/state';
import { AppProps } from './app-props';
import { AppLoader } from './app-loader';
class AppDef extends React.Component<AppProps> {
componentWillMount() {
this.props.initApp();
}
render() {
const {initialized, error} = this.props.initializeApp;
return (
<div className={layout}>
<Overlay/>
{error ? <div>{error.message}</div> : false}
{!initialized && !error ? <AppLoader/> : false}
{initialized ? <ModalContainer/> : false}
</div>
);
}
}
const mapStateToProps = (state: State) => ({
initializeApp: state.initializeApp
});
const mapDispatchToProps = (dispatch: Dispatch<State>) => ({
initApp: bindActionCreators(initializeApp, dispatch),
});
export const App = connect(mapStateToProps, mapDispatchToProps)(AppDef);

View File

@ -1 +1 @@
export * from './layout';
export * from './app';

View File

@ -1,12 +0,0 @@
import * as React from 'react';
import { layout } from './layout.scss';
import { Overlay } from './overlay';
import { ModalContainer } from './modal-container';
export const App: React.SFC = () => (
<div className={layout}>
<Overlay/>
<ModalContainer/>
</div>
);

View File

@ -5,7 +5,7 @@ import { header, text, _center } from './mobile-header.scss';
export const MobileHeader: React.SFC = () => (
<header className={header}>
<div className={cx(text, {[_center]: true})}>
[props.initConfig.name]
Процедура идентификации
</div>
</header>
);

View File

@ -42,10 +42,6 @@
padding: 30px;
box-sizing: border-box;
background-image: linear-gradient(90deg, $deep-blue 0%, $blue 39%, $lightest-blue 100%);
footer {
display: none;
}
}
&.with_shadow {

View File

@ -1,13 +1,19 @@
import * as React from 'react';
import { connect } from 'react-redux';
import * as cx from 'classnames';
import * as ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import { appear, leave, overlay, img, bg1, bg2, bg3, bg4, bg5, bg6, bg7, bg8 } from './overlay.scss';
import { State } from 'app/state';
const backgrounds: ReadonlyArray<string> = [bg1, bg2, bg3, bg4, bg5, bg6, bg7, bg8];
const getRandom = (): number => Math.floor(Math.random() * 7);
export const Overlay: React.SFC = () => (
interface OverlayDefProps {
inFrame: boolean;
}
const OverlayDef: React.SFC<OverlayDefProps> = (props) => (
<ReactCSSTransitionGroup
transitionName={{enter: null, appear, leave}}
transitionEnter={false}
@ -15,8 +21,14 @@ export const Overlay: React.SFC = () => (
transitionAppearTimeout={500}
transitionLeaveTimeout={500}>
<div key='overlay' className={cx(overlay, {
[img]: true,
[backgrounds[getRandom()]]: true
[img]: !props.inFrame,
[backgrounds[getRandom()]]: !props.inFrame
})} />
</ReactCSSTransitionGroup>
);
const mapStateToProps = (state: State) => ({
inFrame: state.config.inFrame
});
export const Overlay = connect(mapStateToProps)(OverlayDef);

View File

@ -0,0 +1 @@
export * from './resolve-config';

View File

@ -0,0 +1,23 @@
import { PossibleEvents, Transport } from '../../../communication';
import { Config } from '../config';
import { getOrigin } from '../../../get-origin';
import { UserConfig } from './user-config';
import { resolveInitConfig } from './resolve-init-config';
import { isInFrame } from '../../../is-in-iframe';
import { deserialize } from 'app/utils/uri-serializer';
const isUriContext = !!location.search;
const resolveUserConfig = (transport: Transport): Promise<UserConfig> => {
return new Promise((resolve) =>
isUriContext
? resolve(deserialize(location.search))
: transport.on(PossibleEvents.init, (config) => resolve(config)));
};
export const resolveConfig = (transport: Transport): Promise<Config> =>
resolveUserConfig(transport).then((userConfig) => ({
origin: getOrigin(),
inFrame: isInFrame(),
initConfig: resolveInitConfig(userConfig)
}));

View File

@ -0,0 +1,9 @@
import { InitConfig } from 'app/config';
import { UserConfig } from './user-config';
export const resolveInitConfig = (userConfig: UserConfig): InitConfig => {
return {
...new InitConfig(),
...userConfig
};
};

View File

@ -0,0 +1 @@
export type UserConfig = any;

9
src/app/config/config.ts Normal file
View File

@ -0,0 +1,9 @@
import { AppConfig } from 'app/backend';
import { InitConfig } from './init-config';
export class Config {
origin: string;
inFrame: boolean;
initConfig: InitConfig;
appConfig?: AppConfig;
}

View File

@ -0,0 +1,6 @@
import { InitConfig } from 'app/config/init-config';
import { StartIdentityChallengeParams } from '../../initializer/model';
export class IdentityChallengeInitConfig extends InitConfig {
params: StartIdentityChallengeParams;
}

4
src/app/config/index.ts Normal file
View File

@ -0,0 +1,4 @@
export * from './config';
export * from './config-resolver';
export * from './init-config';
export * from './identity-challenge-init-config';

View File

@ -0,0 +1,6 @@
import { ActionType } from '../../communication/model';
export class InitConfig {
token: string;
type: ActionType;
}

View File

@ -0,0 +1,19 @@
import { applyMiddleware, combineReducers, createStore, Store } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import { reducer as formReducer } from 'redux-form';
import createSagaMiddleware from 'redux-saga';
import { State } from './state';
import rootSaga from 'app/sagas/root-saga';
import { configReducer, initializeAppReducer, modelReducer } from 'app/reducers';
export function configureStore(initState: any): Store<State> {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(combineReducers({
initializeApp: initializeAppReducer,
config: configReducer,
model: modelReducer,
form: formReducer
}), initState, composeWithDevTools(applyMiddleware(sagaMiddleware)));
sagaMiddleware.run(rootSaga);
return store;
}

View File

@ -4,13 +4,21 @@ import * as ReactDOM from 'react-dom';
import './styles/main.scss';
import { Child } from '../communication';
import { App } from './components/app';
import { resolveConfig } from './config';
import { configureStore } from 'app/configure-store';
import { Provider } from 'react-redux';
const app = document.getElementById('app');
Child.resolve()
.then(() => {
ReactDOM.render(
<App/>,
app
);
.then((transport) => {
resolveConfig(transport).then((config) => {
const store = configureStore({config});
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
app
);
});
});

4
src/app/log-messages.ts Normal file
View File

@ -0,0 +1,4 @@
export const logPrefix = '[RbkmoneyWallet]';
export const sadnessMessage = 'Param will not be applied.';
export const getMessageInvalidValue = (fieldName: string, value: string, reason: string): string =>
`${logPrefix} Invalid value of param '${fieldName}':'${value}'. ${reason} ${sadnessMessage}`;

View File

@ -0,0 +1,16 @@
import { ConfigState } from 'app/state';
import { AppConfigReceived, TypeKeys } from 'app/actions';
type ConfigReducerAction = AppConfigReceived;
export const configReducer = (s: ConfigState = null, action: ConfigReducerAction) => {
switch (action.type) {
case TypeKeys.APP_CONFIG_RECEIVED:
return {
...s,
...action.payload
};
}
return s;
};

View File

@ -0,0 +1,3 @@
export * from './initialize-app-reducer';
export * from './config-reducer';
export * from './model-reducer';

View File

@ -0,0 +1,27 @@
import { InitializeAppCompleted, InitializeAppFailed, InitializeAppRequested, TypeKeys } from 'app/actions';
import { InitializeAppState } from 'app/state';
type InitializeAppAction =
InitializeAppFailed |
InitializeAppRequested |
InitializeAppCompleted;
const initState = {
initialized: false
};
export const initializeAppReducer = (s: InitializeAppState = initState, action: InitializeAppAction) => {
switch (action.type) {
case TypeKeys.INITIALIZE_APP_COMPLETED:
return {
...s,
initialized: true
};
case TypeKeys.INITIALIZE_APP_FAILED:
return {
...s,
error: action.payload
};
}
return s;
};

View File

@ -0,0 +1,17 @@
import { ModelState } from 'app/state';
import { TypeKeys } from 'app/actions';
import { InitializeModelCompleted } from 'app/actions/model-actions/initialize-action';
type ModelReducerAction =
InitializeModelCompleted;
export function modelReducer(s: ModelState = null, action: ModelReducerAction): ModelState {
switch (action.type) {
case TypeKeys.INITIALIZE_MODEL_COMPLETED:
return {
...s,
...action.payload
};
}
return s;
}

View File

@ -0,0 +1 @@
export * from './initialize-app';

View File

@ -0,0 +1,39 @@
import { call, CallEffect, ForkEffect, put, PutEffect, select, SelectEffect, takeLatest } from 'redux-saga/effects';
import { loadAppConfig } from './load-app-config';
import {
InitializeAppCompleted,
InitializeAppFailed,
TypeKeys
} from 'app/actions';
import { State } from 'app/state';
import { initializeModel } from './initialize-model';
type InitializeAppPutEffect =
InitializeAppCompleted |
InitializeAppFailed;
export type InitializeAppEffect =
CallEffect |
PutEffect<InitializeAppPutEffect> |
SelectEffect;
export function* initializeApp(): Iterator<InitializeAppEffect> {
try {
yield call(loadAppConfig);
const config = yield select((state: State) => state.config);
const {appConfig: {endpoint}, initConfig} = config;
yield call(initializeModel, endpoint, initConfig);
yield put({
type: TypeKeys.INITIALIZE_APP_COMPLETED
} as InitializeAppCompleted);
} catch (error) {
yield put({
type: TypeKeys.INITIALIZE_APP_FAILED,
payload: error
} as InitializeAppFailed);
}
}
export function* watchInitializeApp(): Iterator<ForkEffect> {
yield takeLatest(TypeKeys.INITIALIZE_APP_REQUESTED, initializeApp);
}

View File

@ -0,0 +1,31 @@
import { call, CallEffect, put, PutEffect } from 'redux-saga/effects';
import { InitConfig, IdentityChallengeInitConfig } from 'src/app/config/index';
import { ActionType } from '../../../communication/model';
import { getIdentityByID, Identity } from 'app/backend/index';
import { InitializeModelCompleted, TypeKeys } from 'app/actions';
import { ModelState } from 'app/state';
export function* resolveIdentity(endpoint: string, config: IdentityChallengeInitConfig): Iterator<CallEffect | Identity> {
const token = config.token;
const id = config.params.identityID;
return yield call(getIdentityByID, endpoint, token, id);
}
interface ResolvedActionType {
identity?: Identity;
}
export function* resolveActionType(endpoint: string, config: InitConfig): Iterator<CallEffect | ResolvedActionType | ModelState> {
switch (config.type) {
case ActionType.userIdentity:
const identity = yield call(resolveIdentity, endpoint, config);
return {identity};
}
}
export type InitializeEffect = CallEffect | PutEffect<InitializeModelCompleted>;
export function* initializeModel(endpoint: string, config: InitConfig): Iterator<InitializeEffect> {
const modelChunk = yield call(resolveActionType, endpoint, config);
yield put({type: TypeKeys.INITIALIZE_MODEL_COMPLETED, payload: modelChunk} as InitializeModelCompleted);
}

View File

@ -0,0 +1,15 @@
import { call, CallEffect, put, PutEffect } from 'redux-saga/effects';
import { AppConfigReceived, TypeKeys } from 'app/actions';
import { getAppConfig } from 'app/backend';
type LoadConfigEffect = CallEffect | PutEffect<AppConfigReceived>;
export function* loadAppConfig(): IterableIterator<LoadConfigEffect> {
const appConfig = yield call(getAppConfig);
yield put({
type: TypeKeys.APP_CONFIG_RECEIVED,
payload: {
appConfig
}
} as AppConfigReceived);
}

View File

@ -0,0 +1,8 @@
import { all } from 'redux-saga/effects';
import { watchInitializeApp } from './initialize-app';
export default function* rootSaga(): any {
yield all([
watchInitializeApp()
]);
}

View File

@ -0,0 +1,9 @@
import { InitConfig } from 'app/config';
import { AppConfig } from 'app/backend';
export class ConfigState {
origin: string;
inFrame: boolean;
initConfig: InitConfig;
appConfig?: AppConfig;
}

4
src/app/state/index.ts Normal file
View File

@ -0,0 +1,4 @@
export * from './state';
export * from './initialize-app-state';
export * from './config-state';
export * from './model-state';

View File

@ -0,0 +1,8 @@
import { LogicError } from 'app/backend';
type Error = LogicError;
export interface InitializeAppState {
initialized: boolean;
error?: Error;
}

View File

@ -0,0 +1,5 @@
import { Identity } from 'app/backend';
export class ModelState {
identity?: Identity;
}

11
src/app/state/state.ts Normal file
View File

@ -0,0 +1,11 @@
import {
InitializeAppState,
ConfigState,
ModelState
} from '.';
export interface State {
readonly initializeApp: InitializeAppState;
readonly config: ConfigState;
readonly model: ModelState;
}

View File

@ -0,0 +1 @@
export const getNocacheValue = () => new Date().getTime();

View File

@ -0,0 +1,35 @@
const parseStringOrObject = (value: any) => {
try {
return JSON.parse(value);
} catch (e) {
return value;
}
};
export const deserialize = (url: string): any => {
const split = (typeof url === 'string' && url !== '') && url.split('?');
if (!split) {
return {};
}
const params = split.length > 1 ? split[1] : split[0];
const result = JSON.parse(`{"${decodeURI(params).replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g, '":"')}"}`);
for (const prop in result) {
if (result.hasOwnProperty(prop)) {
const value = decodeURIComponent(result[prop]);
if (value === 'true') {
result[prop] = true;
} else if (value === 'false') {
result[prop] = false;
} else if (value === 'undefined') {
result[prop] = undefined;
} else if (value === 'null') {
result[prop] = null;
} else if (value !== '' && !isNaN(value as any)) {
result[prop] = parseFloat(value);
} else {
result[prop] = parseStringOrObject(value);
}
}
}
return result;
};

3
src/appConfig.json Normal file
View File

@ -0,0 +1,3 @@
{
"wapiEndpoint": "<wapi endpoint>"
}

View File

@ -9,7 +9,7 @@ export class Child {
const target = window.opener;
const context = ContextResolver.get();
return resolve(new RealTransport(target, context.parentOrigin, window));
} else if (isInFrame() && !window.opener) {
} else if (!isInFrame() && !window.opener) {
return resolve(new StubTransport());
} else {
const shake = (e: MessageEvent) => {

View File

@ -1,6 +1,6 @@
import { RealTransport } from './real-transport';
import { Transport } from './transport';
import { TransportInfo } from './model/transport-info';
import { TransportInfo } from './model';
export class Parent {

View File

@ -1,4 +1,4 @@
import { PossibleEvents } from './model/index';
import { PossibleEvents } from './model';
export interface Transport {
emit(name: PossibleEvents, data?: any): void;

View File

@ -18,7 +18,7 @@ export class IframeInitializer extends Initializer {
const parent = new Parent(target, this.origin);
return new Promise((resolve) => {
return parent.sendHandshake().then((transport) => {
transport.emit(PossibleEvents.init, {data});
transport.emit(PossibleEvents.init, data);
resolve(transport);
});
});

View File

@ -1,15 +1,19 @@
import { Parent, Transport } from '../communication';
import { Initializer } from './initializer';
import { InitializerData } from '../communication/model';
import isObject from 'lodash-es/isObject';
const serialize = (params: any): string => {
let urlParams = '';
for (const prop in params) {
if (params.hasOwnProperty(prop)) {
const value = params[prop];
let value = params[prop];
if ((typeof value === 'function') || (value === undefined) || (value === null)) {
continue;
}
if (isObject(value)) {
value = JSON.stringify(value);
}
if (urlParams !== '') {
urlParams += '&';
}

View File

@ -1,7 +1,7 @@
export const isInFrame = (): boolean => {
try {
return window.self === window.top;
return window.self !== window.top;
} catch (e) {
return false;
return true;
}
};

View File

@ -17,7 +17,7 @@
"no-var-keyword": true,
"no-parameter-reassignment": true,
"typedef": [true, "call-signature"],
"typedef": false,
"readonly-keyword": false,
"readonly-array": true,
@ -28,7 +28,7 @@
"no-this": false,
"no-class": false,
"no-mixed-interface": true,
"no-mixed-interface": false,
"no-expression-statement": false,
"no-if-statement": false
}