Unverified Commit e1785af7 authored by Hanwen Cheng's avatar Hanwen Cheng Committed by GitHub
Browse files

refactor: migrating to typescript (#521)

* init typescript settings

* change let to const

* use typescript strict

* update lint config for typescript

* update lint config for typescript

* upgrade to react-navigation v4 and use typed navigator

* add required dependency for react navigation

* update with eslint and App.tsx

* following merging

* refactor buttons

* refactor

* stash

* add types for networks specs

* fix add legacy account error

* update typescript configs

* refactor identity and account stores

* hoc.js to typescript

* refactor main landing page

* complete identity utils and account store

* renaming files

* refactor components and payloads component

* refactor all the components and scanner store

* fix typescript compiler errors

* rename alls screens

* refactor screens

* refactor all the screens

* fix typescript compiler errors

* fixed compiler and lint error

* reorder files and use path alias

* reoder imports

* ignore jest caches

* integrate typescript with jest

* integrate e2e test

* update travis config and readme

* remove logs and comments

* delete redundant line

* fix scanner name

* multi signing fix

* add default catch

* use await to avoid async problem and remove one warning with source.uri

* use path alias of e2e and utils

* imporve sign button display in small screen

* hide path derivation option for ethereum account

* fix path display error

* fix ethereum delete problem

* imporve navigation

* upgrade react navigation to resolve bug

* add react hooks lint

* fix missing icon bug
parent e5f959a9
Pipeline #81282 failed with stages
in 2 minutes and 17 seconds
const commonRules = {
"no-bitwise": "off",
"comma-dangle": ["error", "never"],
"object-curly-spacing": ["error", "always"],
"quotes": ["error", "single", { "avoidEscape": true }],
"no-unused-vars": ["error", { "args": "none" }],
"react-native/no-inline-styles": "off",
"sort-keys": ["error", "asc", {"caseSensitive": true, "natural": false, "minKeys": 2}],
"import/order": ["error", {
"newlines-between": "always"
}]
};
module.exports = { module.exports = {
extends: ["@react-native-community", "plugin:prettier/recommended", "plugin:import/errors", "plugin:import/warnings"], extends: [
"@react-native-community",
"plugin:prettier/recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript"
],
globals: { inTest: "writable" },
overrides: [ overrides: [
{ {
"files": ["e2e/*.spec.js", "e2e/init.js", "e2e/e2eUtils.js"], files: ["e2e/*.spec.js", "e2e/init.js", "e2e/e2eUtils.js"],
"rules": { rules: {
"no-undef": "off" "no-undef": "off"
} }
},
{
files: ["**/*.ts", "**/*.tsx"],
env: { "browser": true, "es6": true, "node": true },
extends: [
"@react-native-community",
"plugin:prettier/recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: { "jsx": true },
ecmaVersion: 2018,
sourceType: "module",
project: "./tsconfig.json"
},
plugins: ["@typescript-eslint", "react-hooks"],
rules: {
...commonRules,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/semi": ["error"],
"@typescript-eslint/no-use-before-define": ["error", { "variables": false }], // enable defining variables after react component;
"@typescript-eslint/no-non-null-assertion": 0,
'@typescript-eslint/camelcase': 0,
"no-void": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"semi": "off"
}
} }
], ],
parserOptions: { parserOptions: {
...@@ -16,18 +69,19 @@ module.exports = { ...@@ -16,18 +69,19 @@ module.exports = {
}, },
}, },
settings: { settings: {
"import/resolver": {
"node": {
"extensions": [".js", ".jsx", ".ts", ".tsx"]
},
"typescript": {
"alwaysTryTypes": true // always try to resolve types under `<roo/>@types` directory even it doesn't contain any source code, like `@types/unist`
},
},
"import/ignore": "react-navigation",
react: { react: {
"pragma": "React", // Pragma to use, default to "React" "pragma": "React", // Pragma to use, default to "React"
"version": "16.9.0", // React version, default to the latest React stable release "version": "16.9.0", // React version, default to the latest React stable release
}, },
}, },
rules: { rules: commonRules
"no-bitwise": "off",
"comma-dangle": ["error", "never"],
"object-curly-spacing": ["error", "always"],
"quotes": ["error", "single", { "avoidEscape": true }],
"no-unused-vars": ["error", { "args": "none" }],
"react-native/no-inline-styles": "off",
"sort-keys": ["error", "asc", {"caseSensitive": true, "natural": false, "minKeys": 2}]
}
}; };
[ignore]
; We fork some components by platform
.*/*[.]android.js
; Ignore "BUCK" generated dirs
<PROJECT_ROOT>/\.buckd/
; Ignore unexpected extra "@providesModule"
.*/node_modules/.*/node_modules/fbjs/.*
; Ignore duplicate module providers
; For RN Apps installed via npm, "Libraries" folder is inside
; "node_modules/react-native" but in the source repo it is in the root
node_modules/react-native/Libraries/react-native/React.js
; Ignore polyfills
node_modules/react-native/Libraries/polyfills/.*
; These should not be required directly
; require from fbjs/lib instead: require('fbjs/lib/warning')
node_modules/warning/.*
; Flow doesn't support platforms
.*/Libraries/Utilities/HMRLoadingView.js
[untyped]
.*/node_modules/@react-native-community/cli/.*/.*
[include]
[libs]
node_modules/react-native/Libraries/react-native/react-native-interface.js
node_modules/react-native/flow/
[options]
emoji=true
esproposal.optional_chaining=enable
esproposal.nullish_coalescing=enable
module.file_ext=.js
module.file_ext=.json
module.file_ext=.ios.js
module.system=haste
module.system.haste.use_name_reducers=true
# get basename
module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1'
module.system.haste.paths.blacklist=.*/__tests__/.*
module.system.haste.paths.blacklist=.*/__mocks__/.*
module.system.haste.paths.whitelist=<PROJECT_ROOT>/node_modules/react-native/Libraries/.*
module.system.haste.paths.whitelist=<PROJECT_ROOT>/node_modules/react-native/RNTester/.*
module.system.haste.paths.whitelist=<PROJECT_ROOT>/node_modules/react-native/IntegrationTests/.*
module.system.haste.paths.blacklist=<PROJECT_ROOT>/node_modules/react-native/Libraries/react-native/react-native-implementation.js
module.system.haste.paths.blacklist=<PROJECT_ROOT>/node_modules/react-native/Libraries/Animated/src/polyfills/.*
munge_underscores=true
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FlowFixMeProps
suppress_type=$FlowFixMeState
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
[lints]
sketchy-null-number=warn
sketchy-null-mixed=warn
sketchy-number=warn
untyped-type-import=warn
nonstrict-import=warn
deprecated-type=warn
unsafe-getters-setters=warn
inexact-spread=warn
unnecessary-invariant=warn
signature-verification-failure=warn
deprecated-utility=error
[strict]
deprecated-type
nonstrict-import
sketchy-null
unclear-type
unsafe-getters-setters
untyped-import
untyped-type-import
[version]
^0.109.0
yarn-error.log yarn-error.log
package-lock.json package-lock.json
# typescript output directory
dist/
# jest cache
.jest/cache
# NDK # NDK
NDK/ NDK/
rust/.cargo rust/.cargo
......
{ {
"printWidth": 80, "printWidth": 80,
"parser": "flow", "parser": "typescript",
"singleQuote": true, "singleQuote": true,
"useTabs": true, "useTabs": true,
"tabWidth": 2 "tabWidth": 2
......
--- ---
language: node_js language: node_js
node_js: 8 node_js: 10
cache: cache:
directories: directories:
......
...@@ -35,7 +35,7 @@ Parity Signer was built to be used offline. The mobile device used to run the ap ...@@ -35,7 +35,7 @@ Parity Signer was built to be used offline. The mobile device used to run the ap
## Build it ## Build it
### Requirements ### Requirements
- `node.js` (tested on `v8.4.0`) - `node.js` ( `>=10`)
- `yarn` (tested on `1.6.0`) - `yarn` (tested on `1.6.0`)
- `rustup` (tested on `rustup 1.16.0`) - `rustup` (tested on `rustup 1.16.0`)
- `rustc` (tested on `rustc 1.32.0 (9fda7c223 2019-01-16)`) - `rustc` (tested on `rustc 1.32.0 (9fda7c223 2019-01-16)`)
......
...@@ -169,6 +169,7 @@ android { ...@@ -169,6 +169,7 @@ android {
} }
dependencies { dependencies {
implementation project(':react-native-safe-area-context')
implementation project(':react-native-camera') implementation project(':react-native-camera')
implementation project(':react-native-secure-storage') implementation project(':react-native-secure-storage')
implementation project(':react-native-gesture-handler') implementation project(':react-native-gesture-handler')
......
...@@ -3,6 +3,7 @@ package io.parity.signer; ...@@ -3,6 +3,7 @@ package io.parity.signer;
import android.app.Application; import android.app.Application;
import com.facebook.react.ReactApplication; import com.facebook.react.ReactApplication;
import com.th3rdwave.safeareacontext.SafeAreaContextPackage;
import com.reactlibrary.RNSecureStoragePackage; import com.reactlibrary.RNSecureStoragePackage;
import org.reactnative.camera.RNCameraPackage; import org.reactnative.camera.RNCameraPackage;
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage; import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
...@@ -30,6 +31,7 @@ public class MainApplication extends Application implements ReactApplication { ...@@ -30,6 +31,7 @@ public class MainApplication extends Application implements ReactApplication {
protected List<ReactPackage> getPackages() { protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList( return Arrays.<ReactPackage>asList(
new MainReactPackage(), new MainReactPackage(),
new SafeAreaContextPackage(),
new RNSecureStoragePackage(), new RNSecureStoragePackage(),
new RNCameraPackage(), new RNCameraPackage(),
new SvgPackage(), new SvgPackage(),
......
rootProject.name = 'Parity Signer' rootProject.name = 'Parity Signer'
include ':react-native-safe-area-context'
project(':react-native-safe-area-context').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-safe-area-context/android')
include ':react-native-secure-storage' include ':react-native-secure-storage'
project(':react-native-secure-storage').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-secure-storage/android') project(':react-native-secure-storage').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-secure-storage/android')
include ':react-native-gesture-handler' include ':react-native-gesture-handler'
......
...@@ -16,6 +16,23 @@ module.exports = { ...@@ -16,6 +16,23 @@ module.exports = {
vm: 'vm-browserify' vm: 'vm-browserify'
} }
} }
],
[
'module-resolver',
{
alias: {
components: './src/components',
constants: './src/constants',
e2e: './e2e',
res: './res',
screens: './src/screens',
stores: './src/stores',
styles: './src/styles',
types: './src/types',
utils: './src/utils'
},
root: ['.']
}
] ]
], ],
presets: ['module:metro-react-native-babel-preset'] presets: ['module:metro-react-native-babel-preset']
......
{ {
"setupFilesAfterEnv": ["./init.js"], "setupFilesAfterEnv": ["./init.ts"],
"testEnvironment": "node", "testEnvironment": "node",
"reporters": ["detox/runners/jest/streamlineReporter"], "reporters": ["detox/runners/jest/streamlineReporter"],
"verbose": true "verbose": true
......
...@@ -14,36 +14,43 @@ ...@@ -14,36 +14,43 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
'use strict'; import { expect, element, by } from 'detox';
import testIDs from './testIDs'; import testIDs from './testIDs';
const { IdentityPin } = testIDs; const { IdentityPin } = testIDs;
export const testTap = async buttonId => await element(by.id(buttonId)).tap(); export const testTap = async (buttonId: string): Promise<Detox.Actions<any>> =>
await element(by.id(buttonId)).tap();
export const testVisible = async componentId => export const testVisible = async (componentId: string): Promise<void> =>
await expect(element(by.id(componentId))).toBeVisible(); await expect(element(by.id(componentId))).toBeVisible();
export const testExist = async componentId => export const testExist = async (componentId: string): Promise<void> =>
await expect(element(by.id(componentId))).toExist(); await expect(element(by.id(componentId))).toExist();
export const testNotExist = async componentId => export const testNotExist = async (componentId: string): Promise<void> =>
await expect(element(by.id(componentId))).toNotExist(); await expect(element(by.id(componentId))).toNotExist();
export const testNotVisible = async componentId => export const testNotVisible = async (componentId: string): Promise<void> =>
await expect(element(by.id(componentId))).toBeNotVisible(); await expect(element(by.id(componentId))).toBeNotVisible();
export const tapBack = async () => export const tapBack = async (): Promise<Detox.Actions<any>> =>
await element(by.id(testIDs.Header.headerBackButton)) await element(by.id(testIDs.Header.headerBackButton))
.atIndex(0) .atIndex(0)
.tap(); .tap();
export const testInput = async (inputId, inputText) => { export const testInput = async (
inputId: string,
inputText: string
): Promise<void> => {
await element(by.id(inputId)).typeText(inputText); await element(by.id(inputId)).typeText(inputText);
await element(by.id(inputId)).tapReturnKey(); await element(by.id(inputId)).tapReturnKey();
}; };
export const testInputWithDone = async (inputId, inputText) => { export const testInputWithDone = async (
inputId: string,
inputText: string
): Promise<void> => {
await element(by.id(inputId)).typeText(inputText); await element(by.id(inputId)).typeText(inputText);
if (device.getPlatform() === 'ios') { if (device.getPlatform() === 'ios') {
await element(by.label('Done')) await element(by.label('Done'))
...@@ -54,7 +61,10 @@ export const testInputWithDone = async (inputId, inputText) => { ...@@ -54,7 +61,10 @@ export const testInputWithDone = async (inputId, inputText) => {
} }
}; };
export const testScrollAndTap = async (buttonId, screenId) => { export const testScrollAndTap = async (
buttonId: string,
screenId: string
): Promise<void> => {
await waitFor(element(by.id(buttonId))) await waitFor(element(by.id(buttonId)))
.toBeVisible() .toBeVisible()
.whileElement(by.id(screenId)) .whileElement(by.id(screenId))
...@@ -62,6 +72,6 @@ export const testScrollAndTap = async (buttonId, screenId) => { ...@@ -62,6 +72,6 @@ export const testScrollAndTap = async (buttonId, screenId) => {
await testTap(buttonId); await testTap(buttonId);
}; };
export const testUnlockPin = async pinCode => { export const testUnlockPin = async (pinCode: string): Promise<void> => {
await testInputWithDone(IdentityPin.unlockPinInput, pinCode); await testInputWithDone(IdentityPin.unlockPinInput, pinCode);
}; };
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
'use strict'; import { by, device, element } from 'detox';
import testIDs from './testIDs'; import testIDs from './testIDs';
import { import {
...@@ -54,7 +54,7 @@ const defaultPath = '//default', ...@@ -54,7 +54,7 @@ const defaultPath = '//default',
const mockSeedPhrase = const mockSeedPhrase =
'split cradle example drum veteran swear cruel pizza guilt surface mansion film grant benefit educate marble cargo ignore bind include advance grunt exile grow'; 'split cradle example drum veteran swear cruel pizza guilt surface mansion film grant benefit educate marble cargo ignore bind include advance grunt exile grow';
const testSetUpDefaultPath = async () => { const testSetUpDefaultPath = async (): Promise<void> => {
await testInput(IdentityPin.setPin, pinCode); await testInput(IdentityPin.setPin, pinCode);
await testInputWithDone(IdentityPin.confirmPin, pinCode); await testInputWithDone(IdentityPin.confirmPin, pinCode);
await testVisible(AccountNetworkChooser.chooserScreen); await testVisible(AccountNetworkChooser.chooserScreen);
......
...@@ -14,12 +14,11 @@ ...@@ -14,12 +14,11 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
'use strict'; import { init, cleanup } from 'detox';
import adapter from 'detox/runners/jest/adapter';
import specReporter from 'detox/runners/jest/specReporter';
const detox = require('detox'); import { detox as config } from '../package.json';
const config = require('../package.json').detox;
const adapter = require('detox/runners/jest/adapter');
const specReporter = require('detox/runners/jest/specReporter');
// Set the default timeout // Set the default timeout
jest.setTimeout(120000); jest.setTimeout(120000);
...@@ -30,7 +29,7 @@ jasmine.getEnv().addReporter(adapter); ...@@ -30,7 +29,7 @@ jasmine.getEnv().addReporter(adapter);
jasmine.getEnv().addReporter(specReporter); jasmine.getEnv().addReporter(specReporter);
beforeAll(async () => { beforeAll(async () => {
await detox.init(config, { launchApp: false }); await init(config, { launchApp: false });
}); });
beforeEach(async () => { beforeEach(async () => {
...@@ -39,5 +38,5 @@ beforeEach(async () => { ...@@ -39,5 +38,5 @@ beforeEach(async () => {
afterAll(async () => { afterAll(async () => {
await adapter.afterAll(); await adapter.afterAll();
await detox.cleanup(); await cleanup();
}); });
...@@ -14,23 +14,30 @@ ...@@ -14,23 +14,30 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
'use strict'; import {
SUBSTRATE_NETWORK_LIST,
SubstrateNetworkKeys
} from 'constants/networkSpecs';
import { TxRequestData } from 'types/scannerTypes';
import { NETWORK_LIST, SubstrateNetworkKeys } from '../src/constants'; export const signingTestIdentityPath = `//${
SUBSTRATE_NETWORK_LIST[SubstrateNetworkKeys.KUSAMA].pathId
export const signingTestIdentityPath = `//${NETWORK_LIST[SubstrateNetworkKeys.KUSAMA].pathID}//default`; }//default`;
const setRemarkExtrinsicKusama = const setRemarkExtrinsicKusama =
'47900000100005301021e169bcc4cdb062f1c85f971be770b6aea1bd32ac1bf7877aa54ccd73309014a20180000010c11111145030000fe030000b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafeaf2df518f7017e0442a1d01b0f175e0fa5d427470014c1c3ab2131e6250072a70ec'; '47900000100005301021e169bcc4cdb062f1c85f971be770b6aea1bd32ac1bf7877aa54ccd73309014a20180000010c11111145030000fe030000b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafeaf2df518f7017e0442a1d01b0f175e0fa5d427470014c1c3ab2131e6250072a70ec';
export const createMockSignRequest = () => ({ export const createMockSignRequest = (): TxRequestData => ({
bounds: { bounds: {
bounds: [
{ x: '50', y: '50' },
{ x: '100', y: '100' }
],
height: 1440, height: 1440,
origin: [],
width: 1920 width: 1920
}, },
data: '', data: '',
rawData: setRemarkExtrinsicKusama, rawData: setRemarkExtrinsicKusama,
target: 319, target: 319,
type: 'QR_CODE' type: 'qr'
}); });
...@@ -14,8 +14,6 @@ ...@@ -14,8 +14,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
'use strict';
const testIDs = { const testIDs = {
AccountListScreen: { AccountListScreen: {
accountList: 'accountList' accountList: 'accountList'
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
*/ */
import { AppRegistry, YellowBox } from 'react-native'; import { AppRegistry, YellowBox } from 'react-native';
import App from './src/App'; import App from './src/App';
YellowBox.ignoreWarnings([ YellowBox.ignoreWarnings([
'Warning: isMounted(...) is deprecated', 'Warning: isMounted(...) is deprecated',
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
*/ */
import { AppRegistry, YellowBox } from 'react-native'; import { AppRegistry, YellowBox } from 'react-native';
import App from './src/App'; import App from './src/App';
YellowBox.ignoreWarnings([ YellowBox.ignoreWarnings([
'Warning: isMounted(...) is deprecated', 'Warning: isMounted(...) is deprecated',
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@