diff --git a/package.json b/package.json index b640547f..72d432e3 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,9 @@ "@types/classnames": "~2.2.3", "@types/react": "~16.0.1", "@types/react-dom": "~16.0.1", + "@types/react-redux": "~5.0.13", "@types/react-transition-group": "~1.1.0", + "@types/url-parse": "~1.1.0", "autoprefixer": "~6.7.7", "awesome-typescript-loader": "~3.2.3", "babel-core": "~6.24.1", @@ -71,6 +73,7 @@ "node-sass": "~4.5.1", "react-addons-test-utils": "~15.5.1", "react-test-renderer": "~15.5.4", + "redux-devtools-extension": "~2.13.2", "sass-loader": "~6.0.3", "sinon": "~2.2.0", "sinon-chai": "~2.10.0", diff --git a/src/app/actions/index.ts b/src/app/actions/index.ts new file mode 100644 index 00000000..57dde4a9 --- /dev/null +++ b/src/app/actions/index.ts @@ -0,0 +1,2 @@ +export * from './result-action'; +export * from './type-keys'; diff --git a/src/app/actions/result-action/close.ts b/src/app/actions/result-action/close.ts new file mode 100644 index 00000000..e4f1ca61 --- /dev/null +++ b/src/app/actions/result-action/close.ts @@ -0,0 +1,13 @@ +import { TypeKeys } from '../type-keys'; +import { Dispatch } from 'redux'; +import { Result } from '../../state'; +import { ResultAction } from './result-action'; + +export function close(): Dispatch { + return (dispatch: Dispatch) => { + dispatch({ + type: TypeKeys.SET_RESULT, + payload: Result.close + } as ResultAction); + }; +} diff --git a/src/app/actions/result-action/index.ts b/src/app/actions/result-action/index.ts new file mode 100644 index 00000000..cd6e5d71 --- /dev/null +++ b/src/app/actions/result-action/index.ts @@ -0,0 +1,2 @@ +export * from './result-action'; +export * from './close'; diff --git a/src/app/actions/result-action/result-action.ts b/src/app/actions/result-action/result-action.ts new file mode 100644 index 00000000..76c81fcc --- /dev/null +++ b/src/app/actions/result-action/result-action.ts @@ -0,0 +1,8 @@ +import { Action } from 'redux'; +import { TypeKeys } from '../type-keys'; +import { Result } from '../../state'; + +export interface ResultAction extends Action { + type: TypeKeys.SET_RESULT, + payload: Result +} diff --git a/src/app/actions/type-keys.ts b/src/app/actions/type-keys.ts new file mode 100644 index 00000000..38a6a25c --- /dev/null +++ b/src/app/actions/type-keys.ts @@ -0,0 +1,3 @@ +export enum TypeKeys { + SET_RESULT = 'SET_RESULT' +} diff --git a/src/app/components/container/close/close.tsx b/src/app/components/container/close/close.tsx index a2000005..13e44b1d 100644 --- a/src/app/components/container/close/close.tsx +++ b/src/app/components/container/close/close.tsx @@ -1,14 +1,24 @@ import * as React from 'react'; +import { bindActionCreators, Dispatch } from 'redux'; +import { connect } from 'react-redux'; import * as styles from './close.scss'; -import {Icon} from '../../index'; +import { Icon } from '../../index'; +import { close } from '../../../actions/result-action'; +import { ResultAction } from '../../../actions/result-action/result-action'; -export class Close extends React.Component { - - render() { - return ( -
- -
- ); - } +interface CloseProps { + close: () => Dispatch; } + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + close: bindActionCreators(close, dispatch) +}); + +const CloseDef: React.SFC = (props) => + ( +
+ +
+ ); + +export const Close = connect(null, mapDispatchToProps)(CloseDef); diff --git a/src/app/components/container/container.scss b/src/app/components/container/container.scss index 7ee7def9..472f6b5f 100644 --- a/src/app/components/container/container.scss +++ b/src/app/components/container/container.scss @@ -28,16 +28,20 @@ } } -.overlay { +.overlayContainer { position: absolute; height: 100%; min-height: 100%; width: 100%; +} + +.overlay { + height: 100%; + width: 100%; @include responsive(sm) { background: rgba(0, 0, 0, .7); transform: translateZ(-1000px); - animation: fadein .5s; } } @@ -74,10 +78,25 @@ } } +.appearOverlay { + @include responsive(sm) { + animation: fadein .5s; + } +} + +.enterOverlay { + background: green; +} + +.leaveOverlay { + @include responsive(sm) { + animation: fadeout .5s; + } +} + .appearContainer { @include responsive(sm) { - //animation: popup .75s; - animation: popout 1s; + animation: popup .75s; } } @@ -87,7 +106,7 @@ .leaveContainer { @include responsive(sm) { - animation: rotatein 0s; + animation: popout 1s; } } diff --git a/src/app/components/container/container.scss.d.ts b/src/app/components/container/container.scss.d.ts index 4508f9d7..9a719bbb 100644 --- a/src/app/components/container/container.scss.d.ts +++ b/src/app/components/container/container.scss.d.ts @@ -1,20 +1,24 @@ export const mainContainer: string; +export const overlayContainer: string; export const overlay: string; -export const fadein: string; export const container: string; export const form_container: string; +export const appearOverlay: string; +export const fadein: string; +export const enterOverlay: string; +export const leaveOverlay: string; +export const fadeout: string; export const appearContainer: string; -export const popout: string; +export const popup: string; export const enterContainer: string; export const leaveContainer: string; -export const rotatein: string; +export const popout: string; export const appearFormContainer: string; +export const rotatein: string; export const enterFormContainer: string; export const leaveFormContainer: string; export const rotateout: string; -export const fadeout: string; export const slidedown: string; export const slideup: string; export const growth: string; -export const popup: string; export const shake: string; diff --git a/src/app/components/container/container.tsx b/src/app/components/container/container.tsx index 8ff337f9..26553967 100644 --- a/src/app/components/container/container.tsx +++ b/src/app/components/container/container.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import * as TransitionGroup from 'react-transition-group'; import * as styles from './container.scss'; -import {Header, Info, Footer, Form, ThreeDSContainer, ContainerLoader, Close} from '../index'; +import { Header, Info, Footer, Form, ThreeDSContainer, ContainerLoader, Close } from '../index'; export class Container extends React.Component { static getView(): string { @@ -21,7 +21,23 @@ export class Container extends React.Component { const CSSTransitionGroup = TransitionGroup.CSSTransitionGroup; return (
-
+ +
+ {Container.getView() === 'loading' ? : false} {Container.getView() !== 'loading' ?
- + {Container.getView() === 'default' ? { + return Promise.all([ + this.resolveInitConfig(transport), + this.loadAppConfig() + ]).then((configs) => { + return { + origin: this.getOrigin(), + initConfig: configs[0], + appConfig: configs[1] + }; + }); + } + + private static loadAppConfig(): Promise { + return fetch('../appConfig.json', { + headers: { + 'Content-Type': 'application/json' + }, + method: 'GET' + }).then((response) => response.json()); + } + + private static resolveInitConfig(transport: Transport): Promise { + return new Promise((resolve) => { + this.isUriContext() + ? resolve(UriSerializer.deserialize(location.search)) + : transport.on(PossibleEvents.init, (config) => resolve(config)); + }).then((config: InitConfig) => { + config.integrationType = this.calcIntegrationType(); + return config; + }); + } + + private static calcIntegrationType(): IntegrationType { + return IntegrationType.invoice; // TODO implement here + } + + private static isUriContext(): boolean { + return !!location.search; + } + + private static getOrigin(): string { + const currentScript: any = document.currentScript || this.getCurrentScript(); + const url = URL(currentScript.src); + return url.origin; + } + + private static getCurrentScript(): HTMLElement { + const scripts = document.getElementsByTagName('script'); + return scripts[scripts.length - 1]; + } +} diff --git a/src/app/config/config.ts b/src/app/config/config.ts new file mode 100644 index 00000000..3d60b62f --- /dev/null +++ b/src/app/config/config.ts @@ -0,0 +1,8 @@ +import { AppConfig } from './app-config'; +import { InitConfig } from './init-config'; + +export class Config { + origin: string; + initConfig: InitConfig; + appConfig: AppConfig; +} diff --git a/src/app/config/customer-init-config.ts b/src/app/config/customer-init-config.ts new file mode 100644 index 00000000..dcf45f54 --- /dev/null +++ b/src/app/config/customer-init-config.ts @@ -0,0 +1,12 @@ +import { InitConfig } from './init-config'; +import { IntegrationType } from './integration-type'; + +export class CustomerInitConfig extends InitConfig { + customerID: string; + customerAccessToken: string; + + constructor() { + super(); + this.integrationType = IntegrationType.customer; + } +} diff --git a/src/app/config/hold-expiration.ts b/src/app/config/hold-expiration.ts new file mode 100644 index 00000000..50b3db4e --- /dev/null +++ b/src/app/config/hold-expiration.ts @@ -0,0 +1,4 @@ +export enum HoldExpiration { + cancel = 'cancel', + capture = 'capture' +} diff --git a/src/app/config/index.ts b/src/app/config/index.ts new file mode 100644 index 00000000..5a08bed4 --- /dev/null +++ b/src/app/config/index.ts @@ -0,0 +1,9 @@ +export * from './app-config'; +export * from './config'; +export * from './config-resolver'; +export * from './customer-init-config'; +export * from './hold-expiration'; +export * from './init-config'; +export * from './integration-type'; +export * from './invoice-init-config'; +export * from './invoice-template-init-config'; diff --git a/src/app/config/init-config.ts b/src/app/config/init-config.ts new file mode 100644 index 00000000..b4fa5889 --- /dev/null +++ b/src/app/config/init-config.ts @@ -0,0 +1,13 @@ +import { HoldExpiration } from './hold-expiration'; +import { IntegrationType } from './integration-type'; + +export class InitConfig { + integrationType: IntegrationType; + terminals: boolean = false; + paymentFlowHold: boolean = false; + holdExpiration: HoldExpiration = HoldExpiration.cancel; + locale: string = 'auto'; + name?: string; + description?: string; + email?: string; +} diff --git a/src/app/config/integration-type.ts b/src/app/config/integration-type.ts new file mode 100644 index 00000000..22aab31e --- /dev/null +++ b/src/app/config/integration-type.ts @@ -0,0 +1,5 @@ +export enum IntegrationType { + invoice = 'invoice', + invoiceTemplate = 'invoiceTemplate', + customer = 'customer' +} diff --git a/src/app/config/invoice-init-config.ts b/src/app/config/invoice-init-config.ts new file mode 100644 index 00000000..e4f332d3 --- /dev/null +++ b/src/app/config/invoice-init-config.ts @@ -0,0 +1,12 @@ +import { InitConfig } from './init-config'; +import { IntegrationType } from './integration-type'; + +export class InvoiceInitConfig extends InitConfig { + invoiceID: string; + invoiceAccessToken: string; + + constructor() { + super(); + this.integrationType = IntegrationType.invoice; + } +} diff --git a/src/app/config/invoice-template-init-config.ts b/src/app/config/invoice-template-init-config.ts new file mode 100644 index 00000000..00d2abe8 --- /dev/null +++ b/src/app/config/invoice-template-init-config.ts @@ -0,0 +1,12 @@ +import { InitConfig } from './init-config'; +import { IntegrationType } from './integration-type'; + +export class InvoiceTemplateInitConfig extends InitConfig { + invoiceTemplateID: string; + invoiceTemplateAccessToken: string; + + constructor() { + super(); + this.integrationType = IntegrationType.invoiceTemplate; + } +} diff --git a/src/app/configure-store.ts b/src/app/configure-store.ts new file mode 100644 index 00000000..0d5074cb --- /dev/null +++ b/src/app/configure-store.ts @@ -0,0 +1,10 @@ +import { applyMiddleware, combineReducers, createStore, Store } from 'redux'; +import { composeWithDevTools } from 'redux-devtools-extension'; +import thunk from 'redux-thunk'; +import { resultReducer } from './reducers/result-reducer'; + +export function configureStore(initialState: any): Store { + return createStore(combineReducers({ + result: resultReducer + }), initialState, composeWithDevTools(applyMiddleware(thunk))); +} diff --git a/src/app/finalize-app.ts b/src/app/finalize-app.ts new file mode 100644 index 00000000..a276802d --- /dev/null +++ b/src/app/finalize-app.ts @@ -0,0 +1,41 @@ +import * as ReactDOM from 'react-dom'; +import { Transport, PossibleEvents } from '../communication-ts'; +import { Result } from './state'; + +class AppFinalizer { + + private actionTimeout = 300; + + constructor(private transport: Transport, private checkoutEl: HTMLElement) { } + + close() { + ReactDOM.unmountComponentAtNode(this.checkoutEl); + setTimeout(() => { + this.transport.emit(PossibleEvents.close); + this.transport.destroy(); + }, this.actionTimeout); + } + + done(redirectUrl?: string, popupMode?: boolean) { + ReactDOM.unmountComponentAtNode(this.checkoutEl); + setTimeout(() => { + this.transport.emit(PossibleEvents.done); + this.transport.destroy(); + if (popupMode) { + redirectUrl ? location.replace(redirectUrl) : window.close(); + } + }, this.actionTimeout); + } +} + +export function finalize(result: Result, transport: Transport, checkoutEl: HTMLElement, redirectUrl?: string, popupMode?: boolean) { + const finalizer = new AppFinalizer(transport, checkoutEl); + switch (result) { + case Result.close: + finalizer.close(); + break; + case Result.done: + finalizer.done(); + break; + } +} diff --git a/src/app/index.tsx b/src/app/index.tsx index 86422e74..26e422e0 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -1,10 +1,36 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import { Container } from './components'; +import { Provider } from 'react-redux'; import './styles/main.scss'; import './styles/forms.scss'; +import { ConfigResolver } from './config/config-resolver'; +import { Child } from '../communication-ts/child'; +import { Container } from './components'; +import { configureStore } from './configure-store'; +import { finalize } from './finalize-app'; -ReactDOM.render( - , - document.getElementById('app') -); +Child.resolve() + .then((transport) => + Promise.all([ + transport, + ConfigResolver.resolve(transport) + ])) + .then((res) => { + const app = document.getElementById('app'); + const store = configureStore({}); + store.subscribe(() => { + const state = store.getState(); + if (state.result) { + finalize(state.result, res[0], app); + } + }); + ReactDOM.render( + + + , + app + ); + }) + .catch((error) => { + throw new Error(error); // TODO need to implement + }); diff --git a/src/app/reducers/result-reducer.ts b/src/app/reducers/result-reducer.ts new file mode 100644 index 00000000..a8569d4b --- /dev/null +++ b/src/app/reducers/result-reducer.ts @@ -0,0 +1,13 @@ +import { State } from '../state/state'; +import { TypeKeys } from '../actions/type-keys'; +import { ResultAction } from '../actions/result-action/result-action'; + +export function resultReducer(s: State = null, action: ResultAction) { + switch (action.type) { + case TypeKeys.SET_RESULT: { + return action.payload; + } + default: + return s; + } +} diff --git a/src/app/state/index.ts b/src/app/state/index.ts new file mode 100644 index 00000000..8cb5f4c8 --- /dev/null +++ b/src/app/state/index.ts @@ -0,0 +1,2 @@ +export * from './result'; +export * from './state'; diff --git a/src/app/state/result.ts b/src/app/state/result.ts new file mode 100644 index 00000000..3c2416df --- /dev/null +++ b/src/app/state/result.ts @@ -0,0 +1,4 @@ +export enum Result { + close = 'close', + done = 'done' +} diff --git a/src/app/state/state.ts b/src/app/state/state.ts new file mode 100644 index 00000000..449e9da4 --- /dev/null +++ b/src/app/state/state.ts @@ -0,0 +1,5 @@ +import { Result } from '.'; + +export type State = { + readonly result: Result; +} diff --git a/src/app/utils/apply-mixins.ts b/src/app/utils/apply-mixins.ts new file mode 100644 index 00000000..6b8bfbf4 --- /dev/null +++ b/src/app/utils/apply-mixins.ts @@ -0,0 +1,7 @@ +export function applyMixins(derivedCtor: any, baseCtors: any[]) { + baseCtors.forEach(baseCtor => { + Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => { + derivedCtor.prototype[name] = baseCtor.prototype[name]; + }); + }); +} diff --git a/src/communication-ts/child.ts b/src/communication-ts/child.ts new file mode 100644 index 00000000..0e0f9eff --- /dev/null +++ b/src/communication-ts/child.ts @@ -0,0 +1,42 @@ +import { + RealTransport, + Transport, + ContextResolver, + StubTransport, + TransportInfo +} from '.'; + +export class Child { + + static resolve(): Promise { + return new Promise((resolve) => { + if (ContextResolver.isAvailable() && window.opener) { + const target = window.opener; + const context = ContextResolver.get(); + return resolve(new RealTransport(target, context.parentOrigin, window)); + } else if (!this.inIframe() && !window.opener) { + return resolve(new StubTransport()); + } else { + const shake = (e: MessageEvent) => { + if (e && e.data === TransportInfo.parentHandshakeMessageName) { + const target = e.source; + target.postMessage(TransportInfo.childHandshakeMessageName, e.origin); + ContextResolver.set({ + parentOrigin: e.origin + }); + return resolve(new RealTransport(target, e.origin, window)); + } + }; + window.addEventListener('message', shake, false); + } + }); + } + + private static inIframe(): boolean { + try { + return window.self !== window.top; + } catch (e) { + return true; + } + } +} diff --git a/src/communication-ts/context-resolver.ts b/src/communication-ts/context-resolver.ts new file mode 100644 index 00000000..2c442951 --- /dev/null +++ b/src/communication-ts/context-resolver.ts @@ -0,0 +1,25 @@ +export class ContextResolver { + + static set(context: any) { + try { + sessionStorage.setItem(this.key, JSON.stringify(context)); + /* tslint:disable:no-empty */ + } catch (e) {} + } + + static get(): any { + try { + return JSON.parse(sessionStorage.getItem(this.key)); + /* tslint:disable:no-empty */ + } catch (e) {} + } + + static isAvailable(): boolean { + try { + return !!JSON.parse(sessionStorage.getItem(this.key)); + /* tslint:disable:no-empty */ + } catch (e) {} + } + + private static key = 'checkout-context'; +} diff --git a/src/communication-ts/index.ts b/src/communication-ts/index.ts new file mode 100644 index 00000000..a3bbf150 --- /dev/null +++ b/src/communication-ts/index.ts @@ -0,0 +1,8 @@ +export * from './child'; +export * from './context-resolver'; +export * from './transport'; +export * from './real-transport'; +export * from './stub-transport'; +export * from './transport-info'; +export * from './possible-events'; +export * from './transport-message'; diff --git a/src/communication-ts/possible-events.ts b/src/communication-ts/possible-events.ts new file mode 100644 index 00000000..256a3839 --- /dev/null +++ b/src/communication-ts/possible-events.ts @@ -0,0 +1,5 @@ +export enum PossibleEvents { + init = 'init-payform', + done = 'payment-done', + close = 'close' +} diff --git a/src/communication-ts/real-transport.ts b/src/communication-ts/real-transport.ts new file mode 100644 index 00000000..bdcfbc3b --- /dev/null +++ b/src/communication-ts/real-transport.ts @@ -0,0 +1,44 @@ +import { Transport, TransportInfo, PossibleEvents, TransportMessage } from '.'; + +export class RealTransport implements Transport { + + private target: Window; + private origin: string; + private events: any = {}; + + constructor(target: Window, origin: string, source: Window) { + this.target = target; + this.origin = origin; + source.addEventListener('message', this.listener.bind(this), false); + } + + emit(name: PossibleEvents, data?: any) { + const serialized = JSON.stringify({ + data, + name, + transport: TransportInfo.transportName + } as TransportMessage); + this.target.postMessage(serialized, this.origin); + } + + on(eventName: PossibleEvents, callback: (data: any) => any) { + this.events[eventName] = callback; + } + + destroy() { + window.removeEventListener('message', this.listener.bind(this), false); + } + + private listener(e: MessageEvent) { + let parsed: TransportMessage; + try { + parsed = JSON.parse(e.data); + /* tslint:disable:no-empty */ + } catch (e) {} + if (parsed && (parsed.name in this.events)) { + if (parsed.transport === TransportInfo.transportName) { + this.events[parsed.name].call(this, parsed.data); + } + } + } +} diff --git a/src/communication-ts/stub-transport.ts b/src/communication-ts/stub-transport.ts new file mode 100644 index 00000000..13d0a8b8 --- /dev/null +++ b/src/communication-ts/stub-transport.ts @@ -0,0 +1,17 @@ +import { Transport, PossibleEvents } from '.'; + +export class StubTransport implements Transport { + + emit(name: PossibleEvents, data: any) { + console.info('transport stub emit: ', name, data); + } + + on(eventName: PossibleEvents, callback: (data: any) => any) { + callback({}); + console.info('transport stub on: ', eventName, callback); + } + + destroy() { + console.info('transport stub destroy'); + } +} diff --git a/src/communication-ts/transport-info.ts b/src/communication-ts/transport-info.ts new file mode 100644 index 00000000..686fe6f3 --- /dev/null +++ b/src/communication-ts/transport-info.ts @@ -0,0 +1,5 @@ +export enum TransportInfo { + transportName = 'rbkmoney-checkout', + parentHandshakeMessageName = 'rbkmoney-checkout-handshake', + childHandshakeMessageName = 'rbkmoney-payframe-handshake' +} diff --git a/src/communication-ts/transport-message.ts b/src/communication-ts/transport-message.ts new file mode 100644 index 00000000..a62a830d --- /dev/null +++ b/src/communication-ts/transport-message.ts @@ -0,0 +1,7 @@ +import { TransportInfo } from '.'; + +export class TransportMessage { + data: any; + name: string; + transport: TransportInfo; +} diff --git a/src/communication-ts/transport.ts b/src/communication-ts/transport.ts new file mode 100644 index 00000000..d8293b20 --- /dev/null +++ b/src/communication-ts/transport.ts @@ -0,0 +1,9 @@ +import { PossibleEvents } from '.'; + +export interface Transport { + emit(name: PossibleEvents, data?: any): void; + + on(eventName: PossibleEvents, callback: (data: any) => any): void; + + destroy(): void; +} diff --git a/src/utils/uri-serializer.ts b/src/utils/uri-serializer.ts new file mode 100644 index 00000000..f5a4d4ab --- /dev/null +++ b/src/utils/uri-serializer.ts @@ -0,0 +1,46 @@ +export class UriSerializer { + static serialize(params: any): string { + let urlParams = ''; + for (const prop in params) { + if (params.hasOwnProperty(prop)) { + const value = params[prop]; + if ((typeof value === 'function') || (value === undefined) || (value === null)) { + continue; + } + if (urlParams !== '') { + urlParams += '&'; + } + urlParams += `${prop}=${encodeURIComponent(value)}`; + } + } + return urlParams; + } + + static 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] = value; + } + } + } + return result; + } +} diff --git a/tsconfig.json b/tsconfig.json index d7396dd2..17e2987b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,8 @@ "target": "es5", "jsx": "react", "lib": ["es2015", "es2017", "dom"], - "baseUrl": "." + "baseUrl": ".", + "experimentalDecorators": true }, "include": [ "./src/**/*" diff --git a/tslint.json b/tslint.json index 63698ae7..001ec12f 100644 --- a/tslint.json +++ b/tslint.json @@ -5,6 +5,9 @@ "member-access": [true, "no-public"], "trailing-comma": false, "ordered-imports": false, - "no-console": [true, "log"] + "no-console": [true, "log"], + "max-line-length": false, + "object-literal-sort-keys": false, + "interface-name": false } }