diff --git a/.babelrc b/.babelrc index 0c7d7f0e4b..67c859dcde 100644 --- a/.babelrc +++ b/.babelrc @@ -11,4 +11,4 @@ "extensions": ["ts"] }] ] -} \ No newline at end of file +} diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 37151989c6..77c04c0096 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -1,30 +1,40 @@ --- name: Bug Report about: Create a report to help us improve +title: '' +labels: bug +assignees: '' --- -**Describe the bug** +### Describe the bug + A clear and concise description of what the bug is. -**To Reproduce** +### To Reproduce + Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error -**Expected behavior** +### Expected behavior + A clear and concise description of what you expected to happen. -**Screenshots** +### Screenshots + If applicable, add screenshots to help explain your problem. -**Smartphone (please complete the following information):** +### Smartphone (please complete the following information): + - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] -**Additional context** +### Additional context + Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature-implementation.md b/.github/ISSUE_TEMPLATE/feature-implementation.md index 363238887d..3fa6be71cd 100644 --- a/.github/ISSUE_TEMPLATE/feature-implementation.md +++ b/.github/ISSUE_TEMPLATE/feature-implementation.md @@ -1,16 +1,24 @@ --- name: Feature Implementation about: Implementation Scope and Description +title: '' +labels: feature +assignees: '' --- -**What is the scope of the feature** +### Scope of the feature + A clear and concise description of what the feature will address -**Enumerate the tasks that are needed to implement the feature** +### Tasks + +*Enumerate the tasks that are needed to implement the feature* + - [ ] subtask - [ ] subtask - [ ] subtask -**Additional important information** +### Additional important information + Add any additonal important information which is connected to the feature diff --git a/.github/ISSUE_TEMPLATE/general.md b/.github/ISSUE_TEMPLATE/general.md index 8ec0259ff1..c1ae4d0921 100644 --- a/.github/ISSUE_TEMPLATE/general.md +++ b/.github/ISSUE_TEMPLATE/general.md @@ -1,6 +1,9 @@ --- name: General about: What needs to be done +title: '' +labels: '' +assignees: '' --- diff --git a/README.md b/README.md index ead062f711..16e0d03e58 100644 --- a/README.md +++ b/README.md @@ -71,4 +71,4 @@ Documentation ------------- Additional documentation can be found at our [wiki](https://github.com/jolocom/smartwallet-app/wiki). -Copyright (C) 2014-2018 JOLOCOM GmbH +Copyright (C) 2014-2019 JOLOCOM GmbH diff --git a/android/app/build.gradle b/android/app/build.gradle index 912551ffb3..20209ceb00 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -102,8 +102,8 @@ android { applicationId "com.jolocomwallet" minSdkVersion 16 targetSdkVersion 26 - versionCode 16 - versionName "1.5.0" + versionCode 17 + versionName "1.6.0" ndk { abiFilters "armeabi-v7a", "x86" } @@ -150,6 +150,7 @@ android { } dependencies { + compile project(':react-native-version-number') compile project(':react-native-languages') compile project(':react-native-randombytes') compile project(':react-native-vector-icons') diff --git a/android/app/src/main/java/com/jolocomwallet/MainApplication.java b/android/app/src/main/java/com/jolocomwallet/MainApplication.java index 3981abdd3b..eb500b6548 100644 --- a/android/app/src/main/java/com/jolocomwallet/MainApplication.java +++ b/android/app/src/main/java/com/jolocomwallet/MainApplication.java @@ -3,6 +3,7 @@ import android.app.Application; import com.facebook.react.ReactApplication; +import com.apsl.versionnumber.RNVersionNumberPackage; import com.reactcommunity.rnlanguages.RNLanguagesPackage; import com.bitgo.randombytes.RandomBytesPackage; import com.oblador.vectoricons.VectorIconsPackage; @@ -39,6 +40,7 @@ public boolean getUseDeveloperSupport() { protected List getPackages() { return Arrays.asList( new MainReactPackage(), + new RNVersionNumberPackage(), new RNLanguagesPackage(), new RandomBytesPackage(), new VectorIconsPackage(), diff --git a/android/settings.gradle b/android/settings.gradle index 145b8e5807..c368ecfef2 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,4 +1,6 @@ rootProject.name = 'jolocomwallet' +include ':react-native-version-number' +project(':react-native-version-number').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-version-number/android') include ':react-native-languages' project(':react-native-languages').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-languages/android') include ':react-native-randombytes' diff --git a/bin/generateTerms.ts b/bin/generateTerms.ts new file mode 100644 index 0000000000..0ce372074b --- /dev/null +++ b/bin/generateTerms.ts @@ -0,0 +1,11 @@ +import strings from '../src/locales/strings' +import { writeFileSync } from 'fs' + +const en = {} + +Object.keys(strings) + .sort() + .forEach(key => (en[strings[key]] = strings[key])) + +const path = __dirname + '/../src/locales/en.json' +writeFileSync(path, JSON.stringify(en, null, 2), { flag: 'w' }) diff --git a/ios/smartwallet.xcodeproj/project.pbxproj b/ios/smartwallet.xcodeproj/project.pbxproj index 8e0349b8c4..74a761cfcc 100644 --- a/ios/smartwallet.xcodeproj/project.pbxproj +++ b/ios/smartwallet.xcodeproj/project.pbxproj @@ -5,7 +5,6 @@ }; objectVersion = 46; objects = { - /* Begin PBXBuildFile section */ 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; }; @@ -25,6 +24,7 @@ 23238968F2BA48C4A9BB4AC4 /* Foundation.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 640439B9112A41768C633964 /* Foundation.ttf */; }; 28B670DA172F46598B3D5E4B /* SimpleLineIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 853CE29A4ED949C79BF46AF4 /* SimpleLineIcons.ttf */; }; 296C5B93DD624867B8F0966B /* MaterialIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E247948545D24BE58E811111 /* MaterialIcons.ttf */; }; + 2C4C73F9D1194547BBD589E3 /* libRNVersionNumber.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64B87D43A313485EB3520460 /* libRNVersionNumber.a */; }; 4BF187ACD94048C79892D747 /* Ionicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3D9BCA202C9047859CCE2A1B /* Ionicons.ttf */; }; 4D64A0CFA79C40748565CDF5 /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9393C6FEDE02408C97050CBA /* libRNVectorIcons.a */; }; 54CAEABDB21B40C19D55C5CE /* Feather.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 2AFA3F2DBECD4F52A9425B23 /* Feather.ttf */; }; @@ -327,6 +327,20 @@ remoteGlobalIDString = 358F4ED71D1E81A9004DF814; remoteInfo = RCTBlob; }; + B4B543C12298234E002944F4 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 10DF72DE13E94336B6D1DA7E /* RNVersionNumber.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RNVersionNumber; + }; + B4B543C32298234E002944F4 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 10DF72DE13E94336B6D1DA7E /* RNVersionNumber.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 2858ECD01F8B91B400610575; + remoteInfo = "RNVersionNumber-tvOS"; + }; DC46523220A1D5D2000515C9 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = E4B7FA621F764080A23F67F9 /* RNCamera.xcodeproj */; @@ -449,6 +463,7 @@ 00E356EE1AD99517003FC87E /* smartwalletTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = smartwalletTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* smartwalletTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = smartwalletTests.m; sourceTree = ""; }; + 10DF72DE13E94336B6D1DA7E /* RNVersionNumber.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNVersionNumber.xcodeproj; path = "../node_modules/react-native-version-number/ios/RNVersionNumber.xcodeproj"; sourceTree = ""; }; 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = "../node_modules/react-native/Libraries/Settings/RCTSettings.xcodeproj"; sourceTree = ""; }; 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = "../node_modules/react-native/Libraries/WebSocket/RCTWebSocket.xcodeproj"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* smartwallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = smartwallet.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -474,6 +489,7 @@ 5680E6CFCB924E788BB7FC2C /* RNVectorIcons.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNVectorIcons.xcodeproj; path = "../node_modules/react-native-vector-icons/RNVectorIcons.xcodeproj"; sourceTree = ""; }; 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = ""; }; 640439B9112A41768C633964 /* Foundation.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Foundation.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Foundation.ttf"; sourceTree = ""; }; + 64B87D43A313485EB3520460 /* libRNVersionNumber.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNVersionNumber.a; sourceTree = ""; }; 77F057F262F14D45AA1DBBE8 /* libRNFetchBlob.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNFetchBlob.a; sourceTree = ""; }; 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; }; 7F6B1D8837CC457889388E57 /* ReactNativePermissions.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = ReactNativePermissions.xcodeproj; path = "../node_modules/react-native-permissions/ios/ReactNativePermissions.xcodeproj"; sourceTree = ""; }; @@ -540,6 +556,7 @@ 4D64A0CFA79C40748565CDF5 /* libRNVectorIcons.a in Frameworks */, F47B6A88E4754685952B54A9 /* libReactNativePermissions.a in Frameworks */, 13739399034E4271864C3DDB /* libRNRandomBytes.a in Frameworks */, + 2C4C73F9D1194547BBD589E3 /* libRNVersionNumber.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -733,6 +750,7 @@ 5680E6CFCB924E788BB7FC2C /* RNVectorIcons.xcodeproj */, 7F6B1D8837CC457889388E57 /* ReactNativePermissions.xcodeproj */, CAF102D56DB146639EF6CC66 /* RNRandomBytes.xcodeproj */, + 10DF72DE13E94336B6D1DA7E /* RNVersionNumber.xcodeproj */, ); name = Libraries; sourceTree = ""; @@ -783,6 +801,15 @@ name = Products; sourceTree = ""; }; + B4B543BD2298234E002944F4 /* Products */ = { + isa = PBXGroup; + children = ( + B4B543C22298234E002944F4 /* libRNVersionNumber.a */, + B4B543C42298234E002944F4 /* libRNVersionNumber-tvOS.a */, + ); + name = Products; + sourceTree = ""; + }; DC4651FF20A1D5D1000515C9 /* Recovered References */ = { isa = PBXGroup; children = ( @@ -793,6 +820,7 @@ 9393C6FEDE02408C97050CBA /* libRNVectorIcons.a */, 2F64871E059A476B8EAF29A5 /* libReactNativePermissions.a */, ABE391BA79A149EE8D5553B3 /* libRNRandomBytes.a */, + 64B87D43A313485EB3520460 /* libRNVersionNumber.a */, ); name = "Recovered References"; sourceTree = ""; @@ -1049,6 +1077,10 @@ ProductGroup = DC46520A20A1D5D2000515C9 /* Products */; ProjectRef = 5680E6CFCB924E788BB7FC2C /* RNVectorIcons.xcodeproj */; }, + { + ProductGroup = B4B543BD2298234E002944F4 /* Products */; + ProjectRef = 10DF72DE13E94336B6D1DA7E /* RNVersionNumber.xcodeproj */; + }, { ProductGroup = DCBEC41A20CABE240099FD03 /* Products */; ProjectRef = DCBEC41920CABE240099FD03 /* SplashScreen.xcodeproj */; @@ -1333,6 +1365,20 @@ remoteRef = ADBDB9261DFEBF0700ED6528 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + B4B543C22298234E002944F4 /* libRNVersionNumber.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRNVersionNumber.a; + remoteRef = B4B543C12298234E002944F4 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + B4B543C42298234E002944F4 /* libRNVersionNumber-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libRNVersionNumber-tvOS.a"; + remoteRef = B4B543C32298234E002944F4 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; DC46523320A1D5D2000515C9 /* libRNCamera.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -1562,6 +1608,7 @@ "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", "$(SRCROOT)/../node_modules/react-native-permissions/ios/**", "$(SRCROOT)/../node_modules/react-native-randombytes", + "$(SRCROOT)/../node_modules/react-native-version-number/ios", ); INFOPLIST_FILE = smartwalletTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; @@ -1569,6 +1616,7 @@ LIBRARY_SEARCH_PATHS = ( "$(inherited)", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); OTHER_LDFLAGS = ( "-ObjC", @@ -1599,6 +1647,7 @@ "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", "$(SRCROOT)/../node_modules/react-native-permissions/ios/**", "$(SRCROOT)/../node_modules/react-native-randombytes", + "$(SRCROOT)/../node_modules/react-native-version-number/ios", ); INFOPLIST_FILE = smartwalletTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; @@ -1606,6 +1655,7 @@ LIBRARY_SEARCH_PATHS = ( "$(inherited)", "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", ); OTHER_LDFLAGS = ( "-ObjC", @@ -1637,6 +1687,7 @@ "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", "$(SRCROOT)/../node_modules/react-native-permissions/ios/**", "$(SRCROOT)/../node_modules/react-native-randombytes", + "$(SRCROOT)/../node_modules/react-native-version-number/ios", ); "HEADER_SEARCH_PATHS[arch=*]" = "$(SRCROOT)/../node_modules/react-native-splash-screen/ios"; INFOPLIST_FILE = smartwallet/Info.plist; @@ -1672,6 +1723,7 @@ "$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager", "$(SRCROOT)/../node_modules/react-native-permissions/ios/**", "$(SRCROOT)/../node_modules/react-native-randombytes", + "$(SRCROOT)/../node_modules/react-native-version-number/ios", ); "HEADER_SEARCH_PATHS[arch=*]" = "$(SRCROOT)/../node_modules/react-native-splash-screen/ios"; INFOPLIST_FILE = smartwallet/Info.plist; diff --git a/ios/smartwallet/Info.plist b/ios/smartwallet/Info.plist index e7eec4d37e..2335fc1117 100644 --- a/ios/smartwallet/Info.plist +++ b/ios/smartwallet/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.5.0 + 1.6.0 CFBundleSignature ???? CFBundleURLTypes @@ -36,7 +36,7 @@ CFBundleVersion 1 LSApplicationCategoryType - + LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/package.json b/package.json index e14346b912..4d026fc34b 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,21 @@ { "name": "jolocomwallet", - "version": "1.5.0", + "version": "1.6.0", "private": true, "devDependencies": { + "@svgr/cli": "^4.2.0", "@types/enzyme": "^3.1.9", "@types/i18n-js": "^3.0.1", "@types/jest": "^22.0.0", "@types/node": "^10.0.6", + "@types/ramda": "^0.26.8", "@types/react": "^16.0.40", "@types/react-native": "^0.55.7", "@types/react-native-material-ui": "^1.31.1", "@types/react-native-snap-carousel": "^3.6.0", "@types/react-native-sqlite-storage": "^3.3.0", "@types/react-native-vector-icons": "^4.6.0", - "@types/react-navigation": "^1.2.3", + "@types/react-navigation": "^1.5.15", "@types/react-redux": "^5.0.15", "@types/react-test-renderer": "^16.0.0", "@typescript-eslint/eslint-plugin": "^1.5.0", @@ -34,11 +36,11 @@ "eslint-plugin-prettier": "^3.0.1", "eslint-plugin-react": "^7.12.4", "haul": "1.0.0-beta.14", - "immutable": "^3.8.2", "jest": "^22.4.4", "material-colors": "^1.2.5", "mockdate": "^2.0.2", "prettier": "^1.15.3", + "react-devtools": "^3.6.1", "react-dom": "^16.3.0", "react-native-typescript-transformer": "^1.2.3", "react-test-renderer": "16.2.0", @@ -65,7 +67,10 @@ "build:ios": "haul bundle --entry-file='index.ts' --bundle-output='./ios/smartwallet/main.jsbundle' --minify=false --dev=false --platform='ios' --assets-dest='./ios'", "build:android": "haul bundle --entry-file='index.ts' --minify=true --dev=false --platform='android'", "test": "node node_modules/jest/bin/jest.js", - "format": "eslint --fix --ext .ts --ext .tsx ." + "format": "eslint --fix --ext .ts --ext .tsx .", + "icon": "svgr --native -d src/resources/svg/", + "devtools": "react-devtools &", + "terms": "ts-node -O '{\"module\": \"commonjs\"}' bin/generateTerms.ts" }, "jest": { "setupFiles": [ @@ -113,12 +118,14 @@ "crypto-js": "^3.1.9-1", "ethereumjs-wallet": "^0.6.0", "i18n-js": "^3.1.0", - "jolocom-lib": "^2.4.3", + "jolocom-lib": "^3.0.0", "material-colors": "^1.2.5", "object.assign": "^4.1.0", + "ramda": "^0.26.1", "react": "16.2.0", "react-native": "^0.55.3", "react-native-camera": "^1.1.1", + "react-native-crypto": "^2.1.2", "react-native-fetch-blob": "^0.10.8", "react-native-indicator": "^0.7.0", "react-native-keychain": "^3.0.0-rc.3", @@ -133,18 +140,17 @@ "react-native-svg": "^6.3.1", "react-native-svg-uri": "^1.2.3", "react-native-vector-icons": "^4.6.0", - "react-navigation": "^1.5.8", + "react-native-version-number": "^0.3.6", + "react-navigation": "1.6", "react-navigation-redux-helpers": "^1.0.3", "react-redux": "^5.0.7", "redux": "^3.7.2", - "redux-thunk": "^2.2.0", + "redux-thunk": "^2.3.0", + "sjcl": "^1.0.7", "sqlite3": "^4.0.0", "squel": "^5.12.1", "wif": "^2.0.6" }, - "resolutions": { - "jolocom-lib/cred-types-jolocom-core": "0.0.10" - }, "rnpm": { "assets": [ "./src/assets/fonts" diff --git a/src/NavigatorContainer.tsx b/src/NavigatorContainer.tsx index e38888983b..20ba5e6f72 100644 --- a/src/NavigatorContainer.tsx +++ b/src/NavigatorContainer.tsx @@ -6,32 +6,25 @@ import { } from 'react-navigation' import { connect } from 'react-redux' import { BackHandler, Linking, StatusBar } from 'react-native' -import { AnyAction } from 'redux' -import { Routes } from 'src/routes' import { RootState } from 'src/reducers/' import { navigationActions, accountActions, genericActions } from 'src/actions/' -import { BottomActionBar } from './ui/generic/' import { routeList } from './routeList' import { LoadingSpinner } from 'src/ui/generic/loadingSpinner' +import { ThunkDispatch } from './store' +import { handleDeepLink } from './actions/navigation' +import { toggleLoading } from './actions/account' +import { Routes } from './routes' +import { withErrorHandling, withLoading } from './actions/modifiers' +import { showErrorScreen } from './actions/generic' const { createReduxBoundAddListener, } = require('react-navigation-redux-helpers') -interface ConnectProps { - navigation: RootState['navigation'] - openScanner: () => void - goBack: () => void - handleDeepLink: (url: string) => void - checkIfAccountExists: () => void - initApp: () => Promise -} - -interface OwnProps { - dispatch: (action: AnyAction) => void -} - -interface Props extends ConnectProps, OwnProps { +interface Props + extends ReturnType, + ReturnType { + dispatch: ThunkDispatch deepLinkLoading: boolean } @@ -85,20 +78,27 @@ export class NavigatorContainer extends React.Component { render() { const { routes, index } = this.props.navigation const currentRoute = routes[index].routeName - return [ - , - , - this.props.deepLinkLoading && , - currentRoute === routeList.Home && ( - - ), + const darkBackgroundPages = [ + routeList.Landing, + routeList.SeedPhrase, + routeList.Exception, + routeList.Loading, + routeList.Entropy, ] + const isDarkBackground = darkBackgroundPages.includes(currentRoute) + return ( + + + + {this.props.deepLinkLoading && } + + ) } } @@ -107,16 +107,25 @@ const mapStateToProps = (state: RootState) => ({ deepLinkLoading: state.sso.deepLinkLoading, }) -const mapDispatchToProps = (dispatch: Function) => ({ - goBack: () => dispatch(navigationActions.goBack()), - handleDeepLink: (url: string) => - dispatch(navigationActions.handleDeepLink(url)), +const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ + goBack: () => navigationActions.goBack, + handleDeepLink: async (url: string) => + dispatch( + withLoading(toggleLoading)( + withErrorHandling(showErrorScreen)(handleDeepLink(url)), + ), + ), openScanner: () => dispatch( navigationActions.navigate({ routeName: routeList.QRCodeScanner }), ), - checkIfAccountExists: () => dispatch(accountActions.checkIdentityExists()), - initApp: async () => await dispatch(genericActions.initApp()), + checkIfAccountExists: () => + dispatch( + withLoading(toggleLoading)( + withErrorHandling(showErrorScreen)(accountActions.checkIdentityExists), + ), + ), + initApp: () => dispatch(genericActions.initApp), }) export const Navigator = connect( diff --git a/src/actions/account/index.ts b/src/actions/account/index.ts index 38a96299ed..dfb15ce0b1 100644 --- a/src/actions/account/index.ts +++ b/src/actions/account/index.ts @@ -1,6 +1,4 @@ -import { AnyAction, Dispatch } from 'redux' -import { genericActions, navigationActions } from 'src/actions/' -import { BackendMiddleware } from 'src/backendMiddleware' +import { navigationActions } from 'src/actions/' import { routeList } from 'src/routeList' import { DecoratedClaims, CategorizedClaims } from 'src/reducers/account' import { SignedCredential } from 'jolocom-lib/js/credentials/signedCredential/signedCredential' @@ -11,7 +9,11 @@ import { } from '../../lib/util' import { cancelReceiving } from '../sso' import { JolocomLib } from 'jolocom-lib' -import { AppError, ErrorCode } from 'src/lib/errors' +import { ThunkAction } from 'src/store' +import { groupBy, zipWith, mergeRight, omit, uniq, map } from 'ramda' +import { compose } from 'redux' +import { CredentialMetadataSummary } from '../../lib/storage/storage' +import { IdentitySummary } from '../sso/types' export const setDid = (did: string) => ({ type: 'DID_SET', @@ -28,124 +30,122 @@ export const resetSelected = () => ({ }) export const handleClaimInput = (fieldValue: string, fieldName: string) => ({ - type: 'HANLDE_CLAIM_INPUT', + type: 'HANDLE_CLAIM_INPUT', fieldName, fieldValue, }) -export const toggleClaimsLoading = (value: boolean) => ({ - type: 'TOGGLE_CLAIMS_LOADING', - value, -}) - -export const checkIdentityExists = () => async ( - dispatch: Dispatch, - getState: Function, - backendMiddleware: BackendMiddleware, +export const checkIdentityExists: ThunkAction = async ( + dispatch, + getState, + backendMiddleware, ) => { - try { - const { keyChainLib, storageLib, encryptionLib } = backendMiddleware - const encryptedEntropy = await storageLib.get.encryptedSeed() - if (!encryptedEntropy) { - dispatch(toggleLoading(false)) - dispatch( - navigationActions.navigatorReset({ routeName: routeList.Landing }), - ) + const { keyChainLib, storageLib, encryptionLib } = backendMiddleware + const encryptedEntropy = await storageLib.get.encryptedSeed().catch(err => { + // TODO Fix this + if (err.message.indexOf('no such table') === 0) { return } - const password = await keyChainLib.getPassword() - const decryptedSeed = encryptionLib.decryptWithPass({ - cipher: encryptedEntropy, - pass: password, - }) - // TODO: rework the seed param on lib, currently cleartext seed is being passed around. Bad. - const userVault = new JolocomLib.KeyProvider( - Buffer.from(decryptedSeed, 'hex'), - password, + }) + + if (!encryptedEntropy) { + return dispatch( + navigationActions.navigatorReset({ routeName: routeList.Landing }), ) - await backendMiddleware.setIdentityWallet(userVault, password) - const identityWallet = backendMiddleware.identityWallet - dispatch(setDid(identityWallet.identity.did)) + } - dispatch(toggleLoading(false)) - dispatch(navigationActions.navigatorReset({ routeName: routeList.Home })) - } catch (err) { - if (err.message.indexOf('no such table') === 0) { - return - } - dispatch(genericActions.showErrorScreen(new AppError(ErrorCode.WalletInitFailed, err))) + const password = await keyChainLib.getPassword() + + const decryptedSeed = encryptionLib.decryptWithPass({ + cipher: encryptedEntropy, + pass: password, + }) + + if (!decryptedSeed) { + throw new Error('could not decrypt seed') } + + // TODO: rework the seed param on lib, currently cleartext seed is being passed around. Bad. + const userVault = JolocomLib.KeyProvider.fromSeed( + Buffer.from(decryptedSeed, 'hex'), + password, + ) + + await backendMiddleware.setIdentityWallet(userVault, password) + const identityWallet = backendMiddleware.identityWallet + dispatch(setDid(identityWallet.identity.did)) + + return dispatch( + navigationActions.navigatorReset({ routeName: routeList.Home }), + ) } -export const openClaimDetails = (claim: DecoratedClaims) => ( - dispatch: Dispatch, -) => { +export const openClaimDetails = ( + claim: DecoratedClaims, +): ThunkAction => dispatch => { dispatch(setSelected(claim)) - dispatch( + return dispatch( navigationActions.navigate({ routeName: routeList.ClaimDetails, }), ) } -export const saveClaim = () => async ( - dispatch: Dispatch, - getState: Function, - backendMiddleware: BackendMiddleware, +export const saveClaim: ThunkAction = async ( + dispatch, + getState, + backendMiddleware, ) => { const { identityWallet, storageLib, keyChainLib } = backendMiddleware - try { - const did = getState().account.did.get('did') - const claimsItem = getState().account.claims.toJS().selected - const password = await keyChainLib.getPassword() - - const verifiableCredential = await identityWallet.create.signedCredential( - { - metadata: getClaimMetadataByCredentialType(claimsItem.credentialType), - claim: claimsItem.claimData, - subject: did, - }, - password, - ) + const did = getState().account.did.did + const claimsItem = getState().account.claims.selected + const password = await keyChainLib.getPassword() + + const verifiableCredential = await identityWallet.create.signedCredential( + { + metadata: getClaimMetadataByCredentialType(claimsItem.credentialType), + // the library acts directly on the object passed in, so a copy should be made first + claim: { ...claimsItem.claimData }, + subject: did, + }, + password, + ) - if (claimsItem.id) { - await storageLib.delete.verifiableCredential(claimsItem.id) - } + if (claimsItem.id) { + await storageLib.delete.verifiableCredential(claimsItem.id) + } - await storageLib.store.verifiableCredential(verifiableCredential) - await setClaimsForDid() + await storageLib.store.verifiableCredential(verifiableCredential) - dispatch( - navigationActions.navigatorReset({ - routeName: routeList.Home, - }), - ) - } catch (err) { - dispatch(genericActions.showErrorScreen(new AppError(ErrorCode.SaveClaimFailed, err))) - } + await dispatch(setClaimsForDid) + + return dispatch( + navigationActions.navigatorReset({ + routeName: routeList.Home, + }), + ) } // TODO Currently only rendering / adding one -export const saveExternalCredentials = () => async ( - dispatch: Dispatch, - getState: Function, - backendMiddleware: BackendMiddleware, +export const saveExternalCredentials: ThunkAction = async ( + dispatch, + getState, + backendMiddleware, ) => { const { storageLib } = backendMiddleware - const externalCredentials = getState().account.claims.toJS().pendingExternal - const cred: SignedCredential = externalCredentials[0] + const externalCredentials = getState().account.claims.pendingExternal - if (cred.id) { - await storageLib.delete.verifiableCredential(cred.id) + if (!externalCredentials.offer.length) { + return dispatch(cancelReceiving) } - try { - await storageLib.store.verifiableCredential(externalCredentials[0]) - dispatch(cancelReceiving()) - } catch (err) { - dispatch(genericActions.showErrorScreen(new AppError(ErrorCode.SaveExternalCredentialFailed, err))) - } + const cred: SignedCredential = externalCredentials.offer[0].credential + + await storageLib.delete.verifiableCredential(cred.id) + await storageLib.store.verifiableCredential(cred) + + return dispatch(cancelReceiving) } export const toggleLoading = (value: boolean) => ({ @@ -153,58 +153,80 @@ export const toggleLoading = (value: boolean) => ({ value, }) -export const setClaimsForDid = () => async ( - dispatch: Dispatch, - getState: Function, - backendMiddleware: BackendMiddleware, +export const setClaimsForDid: ThunkAction = async ( + dispatch, + getState, + backendMiddleware, ) => { - dispatch(toggleClaimsLoading(true)) - const storageLib = backendMiddleware.storageLib + const { storageLib } = backendMiddleware const verifiableCredentials: SignedCredential[] = await storageLib.get.verifiableCredential() + + const metadata = await Promise.all( + verifiableCredentials.map(el => storageLib.get.credentialMetadata(el)), + ) + + const issuers = uniq(verifiableCredentials.map(cred => cred.issuer)) + + const issuerMetadata = await Promise.all( + issuers.map(storageLib.get.publicProfile), + ) + const claims = prepareClaimsForState( verifiableCredentials, + metadata, + issuerMetadata, ) as CategorizedClaims - dispatch({ + return dispatch({ type: 'SET_CLAIMS_FOR_DID', claims, }) - - dispatch(toggleClaimsLoading(false)) } -const prepareClaimsForState = (credentials: SignedCredential[]) => { - const categorizedClaims = {} - const decoratedCredentials = convertToDecoratedClaim(credentials) +export const prepareClaimsForState = ( + credentials: SignedCredential[], + credentialMetadata: Array, + issuerMetadata: Array, +) => + compose( + groupBy(getCredentialUiCategory), + zipWith(mergeRight, credentialMetadata), + map(addIssuerInfo(issuerMetadata)), + map(convertToDecoratedClaim), + )(credentials) + +export const addIssuerInfo = ( + issuerProfiles: Array<{ did: string } | IdentitySummary> | [], +) => (claim: DecoratedClaims) => { + if (!issuerProfiles || !issuerProfiles.length) { + return claim + } - decoratedCredentials.forEach(decoratedCred => { - const uiCategory = getCredentialUiCategory(decoratedCred.credentialType) + const issuer = issuerProfiles.find(el => el.did === claim.issuer.did) - try { - categorizedClaims[uiCategory].push(decoratedCred) - } catch (err) { - categorizedClaims[uiCategory] = [decoratedCred] - } - }) - - return categorizedClaims + return issuer + ? { + ...claim, + issuer, + } + : claim } -// TODO Util, make subject mandatory -export const convertToDecoratedClaim = ( - vCreds: SignedCredential[], -): DecoratedClaims[] => - vCreds.map(vCred => { - const claimData = { ...vCred.claim } - delete claimData.id - - return { - credentialType: getUiCredentialTypeByType(vCred.type), - claimData, - id: vCred.id, - issuer: vCred.issuer, - subject: vCred.claim.id || 'Not found', - expires: vCred.expires || undefined, - } - }) +/** @TODO Util, make subject mandatory (in lib) */ +export const convertToDecoratedClaim = ({ + claim, + type, + issuer, + id, + expires, +}: SignedCredential): DecoratedClaims => ({ + credentialType: getUiCredentialTypeByType(type), + issuer: { + did: issuer, + }, + claimData: omit(['id'], claim), + id, + subject: claim.id || 'Not found', + expires: expires || undefined, +}) diff --git a/src/actions/documents/index.ts b/src/actions/documents/index.ts new file mode 100644 index 0000000000..fc2c35bda2 --- /dev/null +++ b/src/actions/documents/index.ts @@ -0,0 +1,27 @@ +import { navigationActions } from '..' +import { routeList } from 'src/routeList' +import { DecoratedClaims } from 'src/reducers/account' +import { ThunkAction } from 'src/store' + +export const SET_DOC_DETAIL = 'SET_SELECTED_DOCUMENT_DETAIL' +export const CLEAR_DOC_DETAIL = 'CLEAR_SELECTED_DOCUMENT_DETAIL' + +export const setSelectedDocument = (document: DecoratedClaims) => ({ + type: SET_DOC_DETAIL, + value: document, +}) + +export const clearSelectedDocument = () => ({ + type: CLEAR_DOC_DETAIL, +}) + +export const openDocumentDetails = ( + document: DecoratedClaims, +): ThunkAction => async dispatch => { + dispatch(setSelectedDocument(document)) + dispatch( + navigationActions.navigate({ + routeName: routeList.DocumentDetails, + }), + ) +} diff --git a/src/actions/generic/index.ts b/src/actions/generic/index.ts index 348d79e09c..c726277425 100644 --- a/src/actions/generic/index.ts +++ b/src/actions/generic/index.ts @@ -1,28 +1,57 @@ import { navigationActions } from 'src/actions/' -import { AnyAction, Dispatch } from 'redux' import { routeList } from 'src/routeList' -import { BackendMiddleware } from '../../backendMiddleware' import SplashScreen from 'react-native-splash-screen' +import I18n from 'src/locales/i18n' +import { ThunkAction } from 'src/store' +import { AppError, ErrorCode } from 'src/lib/errors' -export const showErrorScreen = (error: Error, returnTo = routeList.Home) => ( - dispatch: Dispatch, -) => - dispatch( - navigationActions.navigate({ - routeName: routeList.Exception, - params: { returnTo, error }, - }), - ) +export const showErrorScreen = (error: AppError) => + navigationActions.navigate({ + routeName: routeList.Exception, + params: { + returnTo: error.navigateTo || routeList.Home, + error, + }, + }) -export const initApp = () => async ( - dispatch: Dispatch, - getState: Function, - backendMiddleware: BackendMiddleware, +export const initApp: ThunkAction = async ( + dispatch, + getState, + backendMiddleware, ) => { try { await backendMiddleware.initStorage() + const storedSettings = await backendMiddleware.storageLib.get.settingsObject() + + // locale setup + if (storedSettings.locale) I18n.locale = storedSettings.locale + else storedSettings.locale = I18n.locale + SplashScreen.hide() + return dispatch(loadSettings(storedSettings)) } catch (e) { - dispatch(showErrorScreen(e, routeList.Landing)) + return dispatch( + showErrorScreen( + new AppError(ErrorCode.WalletInitFailed, e, routeList.Landing), + ), + ) } } + +export const loadSettings = (settings: { [key: string]: any }) => ({ + type: 'LOAD_SETTINGS', + value: settings, +}) + +export const setLocale = (locale: string): ThunkAction => async ( + dispatch, + getState, + backendMiddleware, +) => { + await backendMiddleware.storageLib.store.setting('locale', locale) + I18n.locale = locale + return dispatch({ + type: 'SET_LOCALE', + value: locale, + }) +} diff --git a/src/actions/modifiers.ts b/src/actions/modifiers.ts new file mode 100644 index 0000000000..adb0f4feba --- /dev/null +++ b/src/actions/modifiers.ts @@ -0,0 +1,25 @@ +import { ActionCreator } from 'redux' +import { AnyAction, ThunkAction } from 'src/store' +import { AppError } from '../lib/errors' + +export const withLoading = (loadingAction: ActionCreator) => ( + wrappedAction: ThunkAction, +): ThunkAction => async dispatch => { + try { + dispatch(loadingAction(true)) + return await dispatch(wrappedAction) + } finally { + dispatch(loadingAction(false)) + } +} + +export const withErrorHandling = ( + errorHandler: ActionCreator, + errorModifier: (error: AppError) => AppError = (error: AppError) => error, +) => (wrappedAction: ThunkAction): ThunkAction => async dispatch => { + try { + return await dispatch(wrappedAction) + } catch (error) { + return dispatch(errorHandler(errorModifier(error))) + } +} diff --git a/src/actions/navigation/index.ts b/src/actions/navigation/index.ts index 98556a69c2..74655e1304 100644 --- a/src/actions/navigation/index.ts +++ b/src/actions/navigation/index.ts @@ -1,20 +1,25 @@ import { NavigationActions, NavigationNavigateActionPayload, + NavigationResetAction, } from 'react-navigation' -import { AnyAction, Dispatch } from 'redux' -import { ssoActions } from 'src/actions/' -import { toggleLoading } from '../account' -import { BackendMiddleware } from 'src/backendMiddleware' -import { setDeepLinkLoading, toggleDeepLinkFlag } from '../sso' +import { setDeepLinkLoading } from 'src/actions/sso' import { routeList } from 'src/routeList' +import { JolocomLib } from 'jolocom-lib' +import { interactionHandlers } from '../../lib/storage/interactionTokens' +import { showErrorScreen } from '../generic' +import { AppError, ErrorCode } from '../../lib/errors' +import { withErrorHandling, withLoading } from 'src/actions/modifiers' +import { ThunkAction } from '../../store' export const navigate = (options: NavigationNavigateActionPayload) => NavigationActions.navigate(options) -export const goBack = () => NavigationActions.back() +export const goBack = NavigationActions.back() -export const navigatorReset = (newScreen: NavigationNavigateActionPayload) => +export const navigatorReset = ( + newScreen: NavigationNavigateActionPayload, +): NavigationResetAction => NavigationActions.reset({ index: 0, actions: [navigate(newScreen)], @@ -25,12 +30,12 @@ export const navigatorReset = (newScreen: NavigationNavigateActionPayload) => * It then matches the route name and dispatches a corresponding action * @param url - a deep link string with the following schema: appName://routeName/params */ -export const handleDeepLink = (url: string) => async ( - dispatch: Dispatch, - getState: Function, - backendMiddleware: BackendMiddleware, +export const handleDeepLink = (url: string): ThunkAction => ( + dispatch, + getState, + backendMiddleware, ) => { - dispatch(toggleLoading(true)) + // TODO Fix const route: string = url.replace(/.*?:\/\//g, '') const params: string = (route.match(/\/([^\/]+)\/?$/) as string[])[1] || '' const routeName = route.split('/')[0] @@ -42,13 +47,30 @@ export const handleDeepLink = (url: string) => async ( ) { // The identityWallet is initialised before the deep link is handled. if (!backendMiddleware.identityWallet) { - dispatch(toggleLoading(false)) - dispatch(navigatorReset({ routeName: routeList.Landing })) - return + return dispatch(navigatorReset({ routeName: routeList.Landing })) } - dispatch(setDeepLinkLoading(true)) - dispatch(toggleDeepLinkFlag(true)) - dispatch(ssoActions.parseJWT(params)) + const interactionToken = JolocomLib.parse.interactionToken.fromJWT(params) + const handler = interactionHandlers[interactionToken.interactionType] + + if (handler) { + return dispatch( + withLoading(setDeepLinkLoading)( + withErrorHandling(showErrorScreen)(handler(interactionToken, true)), + ), + ) + } + + /** @TODO Use error code */ + return dispatch( + showErrorScreen( + new AppError(ErrorCode.Unknown, new Error('No handler found')), + ), + ) } + + /** @TODO Better return */ + return navigate({ + routeName: routeList.Home, + }) } diff --git a/src/actions/registration/index.ts b/src/actions/registration/index.ts index 6ee83ecbf8..14262948f4 100644 --- a/src/actions/registration/index.ts +++ b/src/actions/registration/index.ts @@ -1,102 +1,97 @@ -import { AnyAction, Dispatch } from 'redux' -import { navigationActions, genericActions } from 'src/actions/' -import { BackendMiddleware } from 'src/backendMiddleware' +import { navigationActions } from 'src/actions/' import { routeList } from 'src/routeList' import * as loading from 'src/actions/registration/loadingStages' import { setDid } from 'src/actions/account' import { JolocomLib } from 'jolocom-lib' -import { SoftwareKeyProvider } from 'jolocom-lib/js/vaultedKeyProvider/softwareProvider' -const bip39 = require('bip39') import { generateSecureRandomBytes } from 'src/lib/util' -import { AppError, ErrorCode } from 'src/lib/errors' +import { ThunkAction } from '../../store' +import { navigatorReset } from '../navigation' + +const bip39 = require('bip39') export const setLoadingMsg = (loadingMsg: string) => ({ type: 'SET_LOADING_MSG', value: loadingMsg, }) -export const startRegistration = () => async ( - dispatch: Dispatch, - getState: Function, - backendMiddleware: BackendMiddleware, -) => { - try { - const randomPassword = await generateSecureRandomBytes(32) - const entropy = await generateSecureRandomBytes(16) - const encodedEntropy = entropy.toString('hex') - await backendMiddleware.keyChainLib.savePassword( - randomPassword.toString('base64'), - ) - dispatch( - navigationActions.navigatorReset({ - routeName: routeList.Loading, - }), - ) - dispatch(setLoadingMsg(loading.loadingStages[0])) - return dispatch(createIdentity(encodedEntropy)) - } catch (err) { - return dispatch(genericActions.showErrorScreen(err, routeList.Landing)) - } +export const submitEntropy = ( + encodedEntropy: string, +): ThunkAction => dispatch => { + dispatch( + navigationActions.navigatorReset({ + routeName: routeList.Loading, + }), + ) + + dispatch(setLoadingMsg(loading.loadingStages[0])) + return dispatch(createIdentity(encodedEntropy)) } -export const finishRegistration = () => (dispatch: Dispatch) => { - dispatch(navigationActions.navigatorReset({ routeName: routeList.Home })) +export const startRegistration: ThunkAction = async ( + dispatch, + getState, + backendMiddleware, +) => { + const randomPassword = await generateSecureRandomBytes(32) + + await backendMiddleware.keyChainLib.savePassword( + randomPassword.toString('base64'), + ) + + return dispatch( + navigationActions.navigatorReset({ + routeName: routeList.Entropy, + }), + ) } -export const createIdentity = (encodedEntropy: string) => async ( - dispatch: Dispatch, - getState: Function, - backendMiddleware: BackendMiddleware, +export const finishRegistration = navigatorReset({ routeName: routeList.Home }) + +export const createIdentity = (encodedEntropy: string): ThunkAction => async ( + dispatch, + getState, + backendMiddleware, ) => { const { encryptionLib, keyChainLib, storageLib, registry } = backendMiddleware - try { - const password = await keyChainLib.getPassword() - const encEntropy = encryptionLib.encryptWithPass({ - data: encodedEntropy, - pass: password, - }) - const entropyData = { encryptedEntropy: encEntropy, timestamp: Date.now() } - await storageLib.store.encryptedSeed(entropyData) - const userVault = new SoftwareKeyProvider( - Buffer.from(encodedEntropy, 'hex'), - password, - ) - - dispatch(setLoadingMsg(loading.loadingStages[1])) - - await JolocomLib.util.fuelKeyWithEther( - userVault.getPublicKey({ - encryptionPass: password, - derivationPath: JolocomLib.KeyTypes.ethereumKey, - }), - ) - - dispatch(setLoadingMsg(loading.loadingStages[2])) - const identityWallet = await registry.create(userVault, password) - - const personaData = { - did: identityWallet.identity.did, - controllingKeyPath: JolocomLib.KeyTypes.jolocomIdentityKey, - } - - await storageLib.store.persona(personaData) - dispatch(setDid(identityWallet.identity.did)) - dispatch(setLoadingMsg(loading.loadingStages[3])) - await backendMiddleware.setIdentityWallet(userVault, password) - - return dispatch( - navigationActions.navigatorReset({ - routeName: routeList.SeedPhrase, - params: { mnemonic: bip39.entropyToMnemonic(encodedEntropy) }, - }), - ) - } catch (error) { - return dispatch( - genericActions.showErrorScreen( - new AppError(ErrorCode.RegistrationFailed, error), - routeList.Landing, - ), - ) + const password = await keyChainLib.getPassword() + const encEntropy = encryptionLib.encryptWithPass({ + data: encodedEntropy, + pass: password, + }) + const entropyData = { encryptedEntropy: encEntropy, timestamp: Date.now() } + await storageLib.store.encryptedSeed(entropyData) + const userVault = JolocomLib.KeyProvider.fromSeed( + Buffer.from(encodedEntropy, 'hex'), + password, + ) + + dispatch(setLoadingMsg(loading.loadingStages[1])) + + await JolocomLib.util.fuelKeyWithEther( + userVault.getPublicKey({ + encryptionPass: password, + derivationPath: JolocomLib.KeyTypes.ethereumKey, + }), + ) + + dispatch(setLoadingMsg(loading.loadingStages[2])) + const identityWallet = await registry.create(userVault, password) + + const personaData = { + did: identityWallet.identity.did, + controllingKeyPath: JolocomLib.KeyTypes.jolocomIdentityKey, } + + await storageLib.store.persona(personaData) + dispatch(setDid(identityWallet.identity.did)) + dispatch(setLoadingMsg(loading.loadingStages[3])) + await backendMiddleware.setIdentityWallet(userVault, password) + + return dispatch( + navigationActions.navigatorReset({ + routeName: routeList.SeedPhrase, + params: { mnemonic: bip39.entropyToMnemonic(encodedEntropy) }, + }), + ) } diff --git a/src/actions/registration/loadingStages.ts b/src/actions/registration/loadingStages.ts index 866eb399cc..d71f23dc38 100644 --- a/src/actions/registration/loadingStages.ts +++ b/src/actions/registration/loadingStages.ts @@ -1,8 +1,9 @@ import I18n from 'src/locales/i18n' +import strings from '../../locales/strings' export const loadingStages = [ - I18n.t('Encrypting and storing data locally'), - I18n.t('Fueling with ether'), - I18n.t('Registering decentralized identity'), - I18n.t('Preparing launch'), + I18n.t(strings.ENCRYPTING_AND_STORING_DATA_LOCALLY), + I18n.t(strings.FUELING_WITH_ETHER), + I18n.t(strings.REGISTERING_DECENTRALIZED_IDENTITY), + I18n.t(strings.PREPARING_LAUNCH), ] diff --git a/src/actions/sso/authenticationRequest.ts b/src/actions/sso/authenticationRequest.ts index 6581fc64c3..b9041a0b4e 100644 --- a/src/actions/sso/authenticationRequest.ts +++ b/src/actions/sso/authenticationRequest.ts @@ -1,15 +1,14 @@ import { JSONWebToken } from 'jolocom-lib/js/interactionTokens/JSONWebToken' -import { Dispatch, AnyAction } from 'redux' -import { BackendMiddleware } from 'src/backendMiddleware' -import { navigationActions, ssoActions } from 'src/actions' -import { showErrorScreen } from 'src/actions/generic' +import { navigationActions } from 'src/actions' import { Authentication } from 'jolocom-lib/js/interactionTokens/authentication' import { StateAuthenticationRequestSummary } from 'src/reducers/sso' import { routeList } from 'src/routeList' import { cancelSSO, clearInteractionRequest } from '.' import { Linking } from 'react-native' import { JolocomLib } from 'jolocom-lib' -import { AppError, ErrorCode } from 'src/lib/errors' +import { ThunkAction } from '../../store' +import { AppError } from '../../lib/errors' +import ErrorCode from '../../lib/errorCodes' export const setAuthenticationRequest = ( request: StateAuthenticationRequestSummary, @@ -20,42 +19,31 @@ export const setAuthenticationRequest = ( export const consumeAuthenticationRequest = ( authenticationRequest: JSONWebToken, -) => async ( - dispatch: Dispatch, - getState: Function, - backendMiddleware: BackendMiddleware, -) => { + isDeepLinkInteraction: boolean = false, +): ThunkAction => async (dispatch, getState, backendMiddleware) => { const { identityWallet } = backendMiddleware - try { - await identityWallet.validateJWT(authenticationRequest) - const authenticationDetails: StateAuthenticationRequestSummary = { - requester: authenticationRequest.issuer, - callbackURL: authenticationRequest.interactionToken.callbackURL, - description: authenticationRequest.interactionToken.description, - requestJWT: authenticationRequest.encode(), - } - dispatch(setAuthenticationRequest(authenticationDetails)) - dispatch( - navigationActions.navigatorReset({ - routeName: routeList.AuthenticationConsent, - }), - ) - } catch (err) { - dispatch( - showErrorScreen(new AppError(ErrorCode.AuthenticationRequestFailed, err)), - ) - } finally { - dispatch(ssoActions.setDeepLinkLoading(false)) + await identityWallet.validateJWT(authenticationRequest) + const authenticationDetails: StateAuthenticationRequestSummary = { + requester: authenticationRequest.issuer, + callbackURL: authenticationRequest.interactionToken.callbackURL, + description: authenticationRequest.interactionToken.description, + requestJWT: authenticationRequest.encode(), } + dispatch(setAuthenticationRequest(authenticationDetails)) + return dispatch( + navigationActions.navigatorReset({ + routeName: routeList.AuthenticationConsent, + params: { + isDeepLinkInteraction, + }, + }), + ) } -export const sendAuthenticationResponse = () => async ( - dispatch: Dispatch, - getState: Function, - backendMiddleware: BackendMiddleware, -) => { +export const sendAuthenticationResponse = ( + isDeepLinkInteraction: boolean, +): ThunkAction => async (dispatch, getState, backendMiddleware) => { const { identityWallet } = backendMiddleware - const { isDeepLinkInteraction } = getState().sso const { callbackURL, @@ -75,22 +63,21 @@ export const sendAuthenticationResponse = () => async ( ) if (isDeepLinkInteraction) { - return Linking.openURL(`${callbackURL}/${response.encode()}`).then(() => - dispatch(cancelSSO()), - ) - } else { - return fetch(callbackURL, { - method: 'POST', - body: JSON.stringify({ token: response.encode() }), - headers: { 'Content-Type': 'application/json' }, - }).then(() => dispatch(cancelSSO())) + const callback = `${callbackURL}/${response.encode()}` + if (!(await Linking.canOpenURL(callback))) { + throw new AppError(ErrorCode.DeepLinkUrlNotFound) + } + return Linking.openURL(callback).then(() => dispatch(cancelSSO)) } - } catch (err) { - dispatch(clearInteractionRequest()) - dispatch( - showErrorScreen( - new AppError(ErrorCode.AuthenticationResponseFailed, err), - ), - ) + + await fetch(callbackURL, { + method: 'POST', + body: JSON.stringify({ token: response.encode() }), + headers: { 'Content-Type': 'application/json' }, + }) + + return dispatch(cancelSSO) + } finally { + dispatch(clearInteractionRequest) } } diff --git a/src/actions/sso/credentialOfferRequest.ts b/src/actions/sso/credentialOfferRequest.ts new file mode 100644 index 0000000000..0408b03084 --- /dev/null +++ b/src/actions/sso/credentialOfferRequest.ts @@ -0,0 +1,95 @@ +import { JSONWebToken } from 'jolocom-lib/js/interactionTokens/JSONWebToken' +import { AppError, ErrorCode } from '../../lib/errors' +import { showErrorScreen } from '../generic' +import { CredentialOfferRequest } from 'jolocom-lib/js/interactionTokens/credentialOfferRequest' +import { receiveExternalCredential } from './index' +import { all, compose, isEmpty, isNil, map, mergeRight, omit, either } from 'ramda' +import { httpAgent } from '../../lib/http' +import { JolocomLib } from 'jolocom-lib' +import { CredentialsReceive } from 'jolocom-lib/js/interactionTokens/credentialsReceive' +import { ThunkAction } from 'src/store' +import { keyIdToDid } from 'jolocom-lib/js/utils/helper' +import { withErrorHandling, withLoading } from '../modifiers' +import { toggleLoading } from '../account' + +export const consumeCredentialOfferRequest = ( + credOfferRequest: JSONWebToken, + isDeepLinkInteraction: boolean = false, +): ThunkAction => async ( + dispatch, + getState, + { keyChainLib, identityWallet, registry }, +) => { + await identityWallet.validateJWT(credOfferRequest, undefined, registry) + const { interactionToken } = credOfferRequest + const { callbackURL } = interactionToken + + if (!areRequirementsEmpty(interactionToken)) { + throw new Error('Input requests are not yet supported on the wallet') + } + + const { did: offerorDid, publicProfile } = await registry.resolve( + keyIdToDid(credOfferRequest.issuer), + ) + + const parsedProfile = publicProfile + ? omit(['id', 'did'], publicProfile.toJSON().claim) + : {} + + const offerorInfo = mergeRight( + { did: offerorDid }, + { publicProfile: parsedProfile }, + ) + const selectedCredentialTypes = interactionToken.offeredTypes.map(type => ({ + type, + })) + + const selectedMetadata = interactionToken.offeredTypes.map(type => ({ + issuer: { + did: keyIdToDid(credOfferRequest.issuer), + }, + type, + renderInfo: interactionToken.getRenderInfoForType(type) || {}, + metadata: interactionToken.getMetadataForType(type) || {}, + })) + + const password = await keyChainLib.getPassword() + + const credOfferResponse = await identityWallet.create.interactionTokens.response.offer( + { callbackURL, selectedCredentials: selectedCredentialTypes }, + password, + credOfferRequest, + ) + + const res = await httpAgent.postRequest<{ token: string }>( + callbackURL, + { 'Content-Type': 'application/json' }, + { token: credOfferResponse.encode() }, + ) + + const credentialReceive = JolocomLib.parse.interactionToken.fromJWT< + CredentialsReceive + >(res.token) + + return dispatch( + withLoading(toggleLoading)( + withErrorHandling( + showErrorScreen, + err => new AppError(ErrorCode.CredentialsReceiveFailed, err), + )( + receiveExternalCredential( + credentialReceive, + offerorInfo, + isDeepLinkInteraction, + selectedMetadata, + ), + ), + ), + ) +} + +const areRequirementsEmpty = (interactionToken: CredentialOfferRequest) => + compose( + all(either(isNil, isEmpty)), + map(interactionToken.getRequestedInputForType.bind(interactionToken)), + )(interactionToken.offeredTypes) diff --git a/src/actions/sso/index.ts b/src/actions/sso/index.ts index 0840ce125c..7729779294 100644 --- a/src/actions/sso/index.ts +++ b/src/actions/sso/index.ts @@ -1,29 +1,25 @@ -import { Dispatch, AnyAction } from 'redux' import { Linking } from 'react-native' import { JolocomLib } from 'jolocom-lib' import { StateCredentialRequestSummary, StateVerificationSummary, } from 'src/reducers/sso' -import { BackendMiddleware } from 'src/backendMiddleware' -import { navigationActions, accountActions } from 'src/actions' +import { navigationActions } from 'src/actions' import { routeList } from 'src/routeList' import { SignedCredential } from 'jolocom-lib/js/credentials/signedCredential/signedCredential' -import { showErrorScreen } from 'src/actions/generic' import { getUiCredentialTypeByType } from 'src/lib/util' -import { InteractionType } from 'jolocom-lib/js/interactionTokens/types' -import { resetSelected } from '../account' -import { CredentialOffer } from 'jolocom-lib/js/interactionTokens/credentialOffer' -import { PaymentRequest } from 'jolocom-lib/js/interactionTokens/paymentRequest' +import { convertToDecoratedClaim, resetSelected } from '../account' import { JSONWebToken } from 'jolocom-lib/js/interactionTokens/JSONWebToken' import { CredentialsReceive } from 'jolocom-lib/js/interactionTokens/credentialsReceive' import { CredentialRequest } from 'jolocom-lib/js/interactionTokens/credentialRequest' -import { getIssuerPublicKey } from 'jolocom-lib/js/utils/helper' -import { SoftwareKeyProvider } from 'jolocom-lib/js/vaultedKeyProvider/softwareProvider' -import { consumePaymentRequest } from './paymentRequest' -import { Authentication } from 'jolocom-lib/js/interactionTokens/authentication' -import { consumeAuthenticationRequest } from './authenticationRequest' -import { AppError, ErrorCode } from 'src/lib/errors' +import { ThunkAction } from '../../store' +import { CredentialMetadataSummary } from '../../lib/storage/storage' +import { mergeRight, omit } from 'ramda' +import { keyIdToDid } from 'jolocom-lib/js/utils/helper' +import { DecoratedClaims } from '../../reducers/account' +import { IdentitySummary } from './types' +import { AppError } from '../../lib/errors' +import ErrorCode from '../../lib/errorCodes' export const setCredentialRequest = ( request: StateCredentialRequestSummary, @@ -32,13 +28,19 @@ export const setCredentialRequest = ( value: request, }) -export const clearInteractionRequest = () => ({ +export const clearInteractionRequest = { type: 'CLEAR_INTERACTION_REQUEST', -}) +} -export const setReceivingCredential = (external: SignedCredential[]) => ({ +export const setReceivingCredential = ( + requester: IdentitySummary, + external: Array<{ + decoratedClaim: DecoratedClaims + credential: SignedCredential + }>, +) => ({ type: 'SET_EXTERNAL', - external, + value: { offeror: requester, offer: external }, }) export const setDeepLinkLoading = (value: boolean) => ({ @@ -46,135 +48,69 @@ export const setDeepLinkLoading = (value: boolean) => ({ value, }) -export const parseJWT = (encodedJwt: string) => async ( - dispatch: Dispatch, -) => { - dispatch(accountActions.toggleLoading(true)) - try { - const returnedDecodedJwt = await JolocomLib.parse.interactionToken.fromJWT( - encodedJwt, - ) - - switch (returnedDecodedJwt.interactionType) { - case InteractionType.CredentialRequest: - return dispatch( - consumeCredentialRequest(returnedDecodedJwt as JSONWebToken< - CredentialRequest - >), - ) - case InteractionType.CredentialOffer: - return dispatch( - consumeCredentialOfferRequest(returnedDecodedJwt as JSONWebToken< - CredentialOffer - >), - ) - case InteractionType.CredentialsReceive: - return dispatch( - receiveExternalCredential(returnedDecodedJwt as JSONWebToken< - CredentialsReceive - >), - ) - case InteractionType.PaymentRequest: - return dispatch( - consumePaymentRequest(returnedDecodedJwt as JSONWebToken< - PaymentRequest - >), - ) - case InteractionType.Authentication: - return dispatch( - consumeAuthenticationRequest(returnedDecodedJwt as JSONWebToken< - Authentication - >), - ) - default: - return new Error('Unknown interaction type when parsing JWT') - } - } catch (err) { - dispatch(accountActions.toggleLoading(false)) - dispatch(setDeepLinkLoading(false)) - dispatch(showErrorScreen(new AppError(ErrorCode.ParseJWTFailed, err))) +export const receiveExternalCredential = ( + credReceive: JSONWebToken, + offeror: IdentitySummary, + isDeepLinkInteraction: boolean, + credentialOfferMetadata?: CredentialMetadataSummary[], +): ThunkAction => async (dispatch, getState, backendMiddleware) => { + const { identityWallet, registry, storageLib } = backendMiddleware + + await identityWallet.validateJWT(credReceive, undefined, registry) + const providedCredentials = credReceive.interactionToken.signedCredentials + + const validationResults = await JolocomLib.util.validateDigestables( + providedCredentials, + ) + + // TODO Error Code + if (validationResults.includes(false)) { + throw new Error('Invalid credentials received') } -} - -export const consumeCredentialOfferRequest = ( - credOfferRequest: JSONWebToken, -) => async ( - dispatch: Dispatch, - getState: Function, - backendMiddleware: BackendMiddleware, -) => { - const { keyChainLib, identityWallet, registry } = backendMiddleware - try { - await identityWallet.validateJWT(credOfferRequest, undefined, registry) - - const password = await keyChainLib.getPassword() - const credOfferResponse = await identityWallet.create.interactionTokens.response.offer( - { - callbackURL: credOfferRequest.interactionToken.callbackURL, - instant: credOfferRequest.interactionToken.instant, - requestedInput: {}, - }, - password, - credOfferRequest, + if (credentialOfferMetadata) { + await Promise.all( + credentialOfferMetadata.map(storageLib.store.credentialMetadata), ) + } - const res = await fetch(credOfferRequest.interactionToken.callbackURL, { - method: 'POST', - body: JSON.stringify({ token: credOfferResponse.encode() }), - headers: { 'Content-Type': 'application/json' }, - }).then(body => body.json()) - - dispatch(parseJWT(res.token)) - } catch (err) { - dispatch(accountActions.toggleLoading(false)) - dispatch(setDeepLinkLoading(false)) - dispatch( - showErrorScreen(new AppError(ErrorCode.CredentialOfferFailed, err)), - ) + if (offeror) { + await storageLib.store.issuerProfile(offeror) } -} -export const receiveExternalCredential = ( - credReceive: JSONWebToken, -) => async ( - dispatch: Dispatch, - getState: Function, - backendMiddleware: BackendMiddleware, -) => { - const { identityWallet, registry } = backendMiddleware + // TODO change convertToDecoratedClaim to (metadata) => (cred): decoratedClaim + // the types of the cred metadata arrays where it is use differ too much to do it simply right now + const asDecoratedCredentials = providedCredentials.map(cred => { + const md = credentialOfferMetadata + ? credentialOfferMetadata.filter(mds => cred.type.includes(mds.type)) + : undefined - try { - await identityWallet.validateJWT(credReceive, undefined, registry) - const providedCredentials = credReceive.interactionToken.signedCredentials - - const results = await Promise.all( - providedCredentials.map(async vcred => { - const remoteIdentity = await registry.resolve(vcred.issuer) - return SoftwareKeyProvider.verifyDigestable( - getIssuerPublicKey(vcred.signer.keyId, remoteIdentity.didDocument), - vcred, - ) - }), - ) + const renderInfo = md && md.length ? md[0].renderInfo : undefined - if (!results.every(el => el === true)) { - throw new Error('Signature validation failed') + return { + ...convertToDecoratedClaim(cred), + renderInfo, } - - dispatch(setReceivingCredential(providedCredentials)) - dispatch( - navigationActions.navigatorReset({ - routeName: routeList.CredentialDialog, - }), - ) - } catch (error) { - dispatch( - showErrorScreen(new AppError(ErrorCode.CredentialsReceiveFailed, error)), - ) - } finally { - dispatch(accountActions.toggleLoading(false)) - } + }) + + dispatch( + setReceivingCredential( + offeror, + providedCredentials.map((cred, i) => ({ + credential: cred, + decoratedClaim: asDecoratedCredentials[i], + })), + ), + ) + + return dispatch( + navigationActions.navigatorReset({ + routeName: routeList.CredentialDialog, + params: { + isDeepLinkInteraction, + }, + }), + ) } interface AttributeSummary { @@ -188,98 +124,107 @@ interface AttributeSummary { export const consumeCredentialRequest = ( decodedCredentialRequest: JSONWebToken, -) => async ( - dispatch: Dispatch, - getState: Function, - backendMiddleware: BackendMiddleware, -) => { + isDeepLinkInteraction: boolean, +): ThunkAction => async (dispatch, getState, backendMiddleware) => { const { storageLib, identityWallet, registry } = backendMiddleware - const { did } = getState().account.did.toJS() + const { did } = getState().account.did - try { - await identityWallet.validateJWT( - decodedCredentialRequest, - undefined, - registry, - ) - const requestedTypes = - decodedCredentialRequest.interactionToken.requestedCredentialTypes - const attributesForType = await Promise.all( - requestedTypes.map(storageLib.get.attributesByType), - ) + await identityWallet.validateJWT( + decodedCredentialRequest, + undefined, + registry, + ) - const populatedWithCredentials = await Promise.all( - attributesForType.map(async entry => { - if (entry.results.length) { - return Promise.all( - entry.results.map(async result => ({ - type: getUiCredentialTypeByType(entry.type), - values: result.values, - verifications: await storageLib.get.verifiableCredential({ - id: result.verification, - }), - })), - ) - } - - return [ - { - type: getUiCredentialTypeByType(entry.type), - values: [], - verifications: [], - }, - ] - }), - ) + const { did: issuerDid, publicProfile } = await registry.resolve( + keyIdToDid(decodedCredentialRequest.issuer), + ) + + const parsedProfile = publicProfile + ? omit(['id', 'did'], publicProfile.toJSON().claim) + : {} - const abbreviated = populatedWithCredentials.map(attribute => - attribute.map(entry => ({ - ...entry, - verifications: entry.verifications.map((vCred: SignedCredential) => ({ - id: vCred.id, - issuer: vCred.issuer, - selfSigned: vCred.signer.did === did, - expires: vCred.expires, - })), + const issuerInfo = mergeRight( + { did: issuerDid }, + { publicProfile: parsedProfile }, + ) + + const { + requestedCredentialTypes: requestedTypes, + } = decodedCredentialRequest.interactionToken + + const attributesForType = await Promise.all( + requestedTypes.map(storageLib.get.attributesByType), + ) + + const populatedWithCredentials = await Promise.all( + attributesForType.map(async entry => { + if (entry.results.length) { + return Promise.all( + entry.results.map(async result => ({ + type: getUiCredentialTypeByType(entry.type), + values: result.values, + verifications: await storageLib.get.verifiableCredential({ + id: result.verification, + }), + })), + ) + } + + return [ + { + type: getUiCredentialTypeByType(entry.type), + values: [], + verifications: [], + }, + ] + }), + ) + + const abbreviated = populatedWithCredentials.map(attribute => + attribute.map(entry => ({ + ...entry, + verifications: entry.verifications.map((vCred: SignedCredential) => ({ + id: vCred.id, + issuer: { + did: vCred.issuer, + }, + selfSigned: vCred.signer.did === did, + expires: vCred.expires, })), - ) - const flattened = abbreviated.reduce((acc, val) => acc.concat(val)) - - // TODO requester shouldn't be optional - const summary = { - callbackURL: decodedCredentialRequest.interactionToken.callbackURL, - requester: decodedCredentialRequest.issuer, - availableCredentials: flattened, - requestJWT: decodedCredentialRequest.encode(), - } + })), + ) - dispatch(setCredentialRequest(summary)) - dispatch(navigationActions.navigatorReset({ routeName: routeList.Consent })) - dispatch(setDeepLinkLoading(false)) - } catch (error) { - dispatch( - showErrorScreen(new AppError(ErrorCode.CredentialRequestFailed, error)), - ) - } finally { - dispatch(accountActions.toggleLoading(false)) + const flattened = abbreviated.reduce((acc, val) => acc.concat(val)) + + // TODO requester shouldn't be optional + const summary = { + callbackURL: decodedCredentialRequest.interactionToken.callbackURL, + requester: issuerInfo, + availableCredentials: flattened, + requestJWT: decodedCredentialRequest.encode(), } + + dispatch(setCredentialRequest(summary)) + return dispatch( + navigationActions.navigatorReset({ + routeName: routeList.Consent, + params: { isDeepLinkInteraction }, + }), + ) } export const sendCredentialResponse = ( selectedCredentials: StateVerificationSummary[], -) => async ( - dispatch: Dispatch, - getState: Function, - backendMiddleware: BackendMiddleware, -) => { + isDeepLinkInteraction: boolean = false, +): ThunkAction => async (dispatch, getState, backendMiddleware) => { const { storageLib, keyChainLib, identityWallet } = backendMiddleware const { activeCredentialRequest: { callbackURL, requestJWT }, - isDeepLinkInteraction, } = getState().sso try { const password = await keyChainLib.getPassword() + const request = JolocomLib.parse.interactionToken.fromJWT(requestJWT) const credentials = await Promise.all( selectedCredentials.map( @@ -290,7 +235,6 @@ export const sendCredentialResponse = ( const jsonCredentials = credentials.map(cred => cred.toJSON()) - const request = JolocomLib.parse.interactionToken.fromJWT(requestJWT) const response = await identityWallet.create.interactionTokens.response.share( { callbackURL, @@ -301,37 +245,35 @@ export const sendCredentialResponse = ( ) if (isDeepLinkInteraction) { - return Linking.openURL(`${callbackURL}/${response.encode()}`).then(() => - dispatch(cancelSSO()), - ) + const callback = `${callbackURL}${response.encode()}` + if (!(await Linking.canOpenURL(callback))) { + throw new AppError(ErrorCode.DeepLinkUrlNotFound) + } + + return Linking.openURL(callback).then(() => dispatch(cancelSSO)) } else { return fetch(callbackURL, { method: 'POST', body: JSON.stringify({ token: response.encode() }), headers: { 'Content-Type': 'application/json' }, - }).then(() => dispatch(cancelSSO())) + }) } - } catch (error) { - dispatch(clearInteractionRequest()) - dispatch(accountActions.toggleLoading(false)) - dispatch( - showErrorScreen(new AppError(ErrorCode.CredentialResponseFailed, error)), - ) + } finally { + dispatch(cancelSSO) + dispatch(clearInteractionRequest) } } -export const cancelSSO = () => (dispatch: Dispatch) => { - dispatch(clearInteractionRequest()) - dispatch(accountActions.toggleLoading(false)) - dispatch(navigationActions.navigatorReset({ routeName: routeList.Home })) +export const cancelSSO: ThunkAction = dispatch => { + dispatch(clearInteractionRequest) + return dispatch( + navigationActions.navigatorReset({ routeName: routeList.Home }), + ) } -export const cancelReceiving = () => (dispatch: Dispatch) => { +export const cancelReceiving: ThunkAction = dispatch => { dispatch(resetSelected()) - dispatch(navigationActions.navigatorReset({ routeName: routeList.Home })) + return dispatch( + navigationActions.navigatorReset({ routeName: routeList.Home }), + ) } - -export const toggleDeepLinkFlag = (value: boolean) => ({ - type: 'SET_DEEP_LINK_FLAG', - value, -}) diff --git a/src/actions/sso/paymentRequest.ts b/src/actions/sso/paymentRequest.ts index a29f699682..158b22448c 100644 --- a/src/actions/sso/paymentRequest.ts +++ b/src/actions/sso/paymentRequest.ts @@ -1,16 +1,17 @@ import { JSONWebToken } from 'jolocom-lib/js/interactionTokens/JSONWebToken' -import { Dispatch, AnyAction } from 'redux' -import { BackendMiddleware } from 'src/backendMiddleware' -import { navigationActions, ssoActions } from 'src/actions' +import { navigationActions } from 'src/actions' import { routeList } from 'src/routeList' import { PaymentRequest } from 'jolocom-lib/js/interactionTokens/paymentRequest' import { StatePaymentRequestSummary } from 'src/reducers/sso' -import { showErrorScreen } from 'src/actions/generic' import { JolocomLib } from 'jolocom-lib' import { Linking } from 'react-native' import { cancelSSO, clearInteractionRequest } from 'src/actions/sso' import { JolocomRegistry } from 'jolocom-lib/js/registries/jolocomRegistry' -import { AppError, ErrorCode } from 'src/lib/errors' +import { ThunkDispatch } from '../../store' +import { RootState } from '../../reducers' +import { BackendMiddleware } from '../../backendMiddleware' +import { AppError } from '../../lib/errors' +import ErrorCode from '../../lib/errorCodes' export const setPaymentRequest = (request: StatePaymentRequestSummary) => ({ type: 'SET_PAYMENT_REQUEST', @@ -19,52 +20,49 @@ export const setPaymentRequest = (request: StatePaymentRequestSummary) => ({ export const consumePaymentRequest = ( paymentRequest: JSONWebToken, + isDeepLinkInteraction: boolean = false, ) => async ( - dispatch: Dispatch, - getState: Function, + dispatch: ThunkDispatch, + getState: () => RootState, backendMiddleware: BackendMiddleware, ) => { const { identityWallet, registry } = backendMiddleware - try { - await identityWallet.validateJWT( - paymentRequest, - undefined, - registry as JolocomRegistry, - ) + await identityWallet.validateJWT( + paymentRequest, + undefined, + registry as JolocomRegistry, + ) - const paymentDetails: StatePaymentRequestSummary = { - receiver: { - did: paymentRequest.issuer, - address: paymentRequest.interactionToken.transactionOptions - .to as string, - }, - callbackURL: paymentRequest.interactionToken.callbackURL, - amount: paymentRequest.interactionToken.transactionOptions.value, - description: paymentRequest.interactionToken.description, - paymentRequest: paymentRequest.encode(), - } - dispatch(setPaymentRequest(paymentDetails)) - dispatch( - navigationActions.navigatorReset({ routeName: routeList.PaymentConsent }), - ) - } catch (err) { - dispatch(showErrorScreen(new AppError(ErrorCode.PaymentRequestFailed, err))) - } finally { - dispatch(ssoActions.setDeepLinkLoading(false)) + const paymentDetails: StatePaymentRequestSummary = { + receiver: { + did: paymentRequest.issuer, + address: paymentRequest.interactionToken.transactionOptions.to as string, + }, + callbackURL: paymentRequest.interactionToken.callbackURL, + amount: paymentRequest.interactionToken.transactionOptions.value, + description: paymentRequest.interactionToken.description, + paymentRequest: paymentRequest.encode(), } + dispatch(setPaymentRequest(paymentDetails)) + return dispatch( + navigationActions.navigatorReset({ + routeName: routeList.PaymentConsent, + params: { isDeepLinkInteraction }, + }), + ) } -export const sendPaymentResponse = () => async ( - dispatch: Dispatch, - getState: Function, +export const sendPaymentResponse = (isDeepLinkInteraction: boolean) => async ( + dispatch: ThunkDispatch, + getState: () => RootState, backendMiddleware: BackendMiddleware, ) => { const { identityWallet } = backendMiddleware const { activePaymentRequest: { callbackURL, paymentRequest }, - isDeepLinkInteraction, } = getState().sso + // add loading screen here try { const password = await backendMiddleware.keyChainLib.getPassword() @@ -82,20 +80,20 @@ export const sendPaymentResponse = () => async ( ) if (isDeepLinkInteraction) { - return Linking.openURL(`${callbackURL}/${response.encode()}`).then(() => - dispatch(cancelSSO()), - ) + const callback = `${callbackURL}/${response.encode()}` + if (!(await Linking.canOpenURL(callback))) { + throw new AppError(ErrorCode.DeepLinkUrlNotFound) + } + + return Linking.openURL(callback).then(() => dispatch(cancelSSO)) } else { return fetch(callbackURL, { method: 'POST', body: JSON.stringify({ token: response.encode() }), headers: { 'Content-Type': 'application/json' }, - }).then(() => dispatch(cancelSSO())) + }).then(() => dispatch(cancelSSO)) } - } catch (err) { - dispatch(clearInteractionRequest()) - dispatch( - showErrorScreen(new AppError(ErrorCode.PaymentResponseFailed, err)), - ) + } finally { + dispatch(clearInteractionRequest) } } diff --git a/src/actions/sso/types.ts b/src/actions/sso/types.ts new file mode 100644 index 0000000000..3e7f47743d --- /dev/null +++ b/src/actions/sso/types.ts @@ -0,0 +1,15 @@ +import { PublicProfileClaimMetadata } from 'cred-types-jolocom-core/types' + +/** + * @dev Simply using all claims required by the public profile + */ + +type IssuerPublicProfileSummary = PublicProfileClaimMetadata['claimInterface'] + +/** + * @dev An identity summary is composed of a DID + all public info (currently public profile) + */ +export interface IdentitySummary { + did: string + publicProfile?: IssuerPublicProfileSummary +} diff --git a/src/lib/entropyGenerator.ts b/src/lib/entropyGenerator.ts new file mode 100644 index 0000000000..f8055acd32 --- /dev/null +++ b/src/lib/entropyGenerator.ts @@ -0,0 +1,26 @@ +const sjcl = require('sjcl') + +export interface EntropyGeneratorInterface { + addFromDelta: (d: number) => void + getProgress: () => number + generateRandomString: (wordCount: number) => string +} + +export class EntropyGenerator implements EntropyGenerator { + private generator = new sjcl.prng(10) + + addFromDelta(d: number): void { + this.generator.addEntropy(d, 1, 'user') + } + + getProgress(): number { + return this.generator.getProgress() + } + + generateRandomString(wordCount: number): string { + // returns an array of length wordCount filled with random 4 byte words. + const intArray = new Int32Array(this.generator.randomWords(wordCount)) + const buf = Buffer.from(intArray.buffer) + return buf.toString('hex') + } +} diff --git a/src/lib/errorCodes.ts b/src/lib/errorCodes.ts new file mode 100644 index 0000000000..461d57ffa8 --- /dev/null +++ b/src/lib/errorCodes.ts @@ -0,0 +1,31 @@ +enum ErrorCode { + Unknown = 'Unknown', + + // actions/account/index + WalletInitFailed = 'WalletInit', + SaveClaimFailed = 'SaveClaim', + SaveExternalCredentialFailed = 'SaveExtCred', + + // actions/sso + DeepLinkUrlNotFound = 'DeepLinkUrlNotFound', + + // actions/sso/authenticationRequest + AuthenticationRequestFailed = 'AuthRequest', + AuthenticationResponseFailed = 'AuthResponse', + + // actions/sso/paymentRequest + PaymentRequestFailed = 'PayRequest', + PaymentResponseFailed = 'PayResponse', + + // actions/sso/index + CredentialOfferFailed = 'CredOffer', + CredentialsReceiveFailed = 'CredsReceive', + CredentialRequestFailed = 'CredRequest', + CredentialResponseFailed = 'CredResponse', + ParseJWTFailed = 'ParseJWT', + + // actions/registration + RegistrationFailed = 'Registration', +} + +export default ErrorCode diff --git a/src/lib/errors.ts b/src/lib/errors.ts index f431135213..e777422f52 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -1,62 +1,23 @@ -export const enum ErrorCode { - Unknown = 'Unknown', - - // actions/account/index - WalletInitFailed = 'WalletInit', - SaveClaimFailed = 'SaveClaim', - SaveExternalCredentialFailed = 'SaveExtCred', - - // actions/sso/authenticationRequest - AuthenticationRequestFailed = 'AuthRequest', - AuthenticationResponseFailed = 'AuthResponse', - - // actions/sso/paymentRequest - PaymentRequestFailed = 'PayRequest', - PaymentResponseFailed = 'PayResponse', - - // actions/sso/index - CredentialOfferFailed = 'CredOffer', - CredentialsReceiveFailed = 'CredsReceive', - CredentialRequestFailed = 'CredRequest', - CredentialResponseFailed = 'CredResponse', - ParseJWTFailed = 'ParseJWT', - - // actions/registration - RegistrationFailed = 'Registration', -} - -// NOTE: these strings are localized, remember to update locale files -const errorMessages: { [key in ErrorCode]: string } = { - [ErrorCode.Unknown]: 'Unknown Error', - - [ErrorCode.WalletInitFailed]: 'Unable to initialize wallet', - [ErrorCode.SaveClaimFailed]: 'Could not save claim', - [ErrorCode.SaveExternalCredentialFailed]: - 'Could not save external credential', - - [ErrorCode.AuthenticationRequestFailed]: 'Authentication request failed', - [ErrorCode.AuthenticationResponseFailed]: 'Authentication response failed', - [ErrorCode.PaymentRequestFailed]: 'Payment request failed', - [ErrorCode.PaymentResponseFailed]: 'Payment response failed', - - [ErrorCode.CredentialOfferFailed]: 'Credential offer failed', - [ErrorCode.CredentialsReceiveFailed]: 'Could not receive credentials', - [ErrorCode.CredentialRequestFailed]: 'Credential request failed', - [ErrorCode.CredentialResponseFailed]: 'Credential response failed', - [ErrorCode.ParseJWTFailed]: 'Could not parse JSONWebToken', - - [ErrorCode.RegistrationFailed]: 'Registration failed', -} +import { routeList } from '../routeList' +import strings from '../locales/strings' +import ErrorCode from './errorCodes' export class AppError extends Error { - code: ErrorCode - origError: any - - constructor(code = ErrorCode.Unknown, origError?: any) { - super(errorMessages[code] || errorMessages[ErrorCode.Unknown]) - this.code = code + // private code: ErrorCode + public origError: any + navigateTo: routeList + + public constructor( + code = ErrorCode.Unknown, + origError?: any, + navigateTo: routeList = routeList.Home, + ) { + super(strings[code] || strings[ErrorCode.Unknown]) + // this.code = code this.origError = origError + this.navigateTo = navigateTo } } -export const errorTitleMessages = ['Damn.', 'Oh no.', 'Uh oh.'] +export const errorTitleMessages = [strings.DAMN, strings.OH_NO, strings.UH_OH] +export { ErrorCode } diff --git a/src/lib/filter.ts b/src/lib/filter.ts new file mode 100644 index 0000000000..6b12c1db54 --- /dev/null +++ b/src/lib/filter.ts @@ -0,0 +1,40 @@ +export type Ordering = (t1: T, t2: T) => number +export type Filter = (t: T) => boolean +type Transformation = (list: T[]) => T[] + +/** + * @description - Builds one {@link Transformation} from an array {@link Ordering} or {@link Filter} functions + * @param ts - List of functions used to order or filter the credential list + * @returns - Function combining all order / filter operations passed in + */ + +export const buildTransform = (ts: Array | Ordering>) => + ts + .map>(t => + isFilter(t) ? filterToTransformation(t) : orderingToTransformation(t), + ) + .reduce>( + (acc, curr) => (list: T[]) => curr(acc(list)), + identityTransformation, + ) +/** + * @dev func.length checks for the number of arguments in function signature + * @param func - The function to test + */ + +const isFilter = (func: Filter | Ordering): func is Filter => + func.length === 1 + +// left commented as a converse example of isFilter +// const isOrdering = (func: Filter | Ordering): func is Ordering => +// func.length === 2 + +const filterToTransformation = (filter: Filter): Transformation => ( + list: T[], +): T[] => list.filter(filter) + +const orderingToTransformation = ( + ordering: Ordering, +): Transformation => (list: T[]): T[] => list.sort(ordering) + +const identityTransformation = (list: T[]) => list diff --git a/src/lib/filterCreds.ts b/src/lib/filterCreds.ts new file mode 100644 index 0000000000..72b773afa2 --- /dev/null +++ b/src/lib/filterCreds.ts @@ -0,0 +1,32 @@ +import { buildTransform, Filter, Ordering } from './filter' +import { SignedCredential } from 'jolocom-lib/js/credentials/signedCredential/signedCredential' +import { complement } from 'ramda' + +const expiredFilter: Filter = cred => + cred.expires.valueOf() < new Date().valueOf() + +const validFilter: Filter = complement(expiredFilter) + +const issuerFilter = (issuerDid: string): Filter => cred => + cred.issuer === issuerDid + +const typeFilter = (typ: string): Filter => cred => + cred.type.includes(typ) + +const mostRecentOrder: Ordering = (c1, c2) => + c2.issued.valueOf() - c1.issued.valueOf() + +/** + * @dev Some basic predefined filters, if required they can be removed + * and the filtering / ordering construction functions can be exposed instead + */ + +export const filters = { + filterByExpired: buildTransform([expiredFilter]), + filterByValid: buildTransform([validFilter]), + filterByIssuer: (did: string) => buildTransform([issuerFilter(did)]), + filterByType: (typ: string) => buildTransform([typeFilter(typ)]), + orderByRecent: buildTransform([mostRecentOrder]), + documentFilter: (documentTypes: string[]) => + buildTransform(documentTypes.map(typeFilter)), +} diff --git a/src/lib/filterDecoratedClaims.ts b/src/lib/filterDecoratedClaims.ts new file mode 100644 index 0000000000..becf4f2567 --- /dev/null +++ b/src/lib/filterDecoratedClaims.ts @@ -0,0 +1,28 @@ +import { buildTransform, Filter } from './filter' +import { DecoratedClaims } from 'src/reducers/account' +import { complement } from 'ramda' + +const expiredFilter: Filter = cred => + cred.expires ? cred.expires.valueOf() < new Date().valueOf() : true + +const validFilter: Filter = complement(expiredFilter) + +const issuerFilter = (issuerDid: string): Filter => cred => + cred.issuer.did === issuerDid + +const typeFilter = (typ: string): Filter => cred => + cred.credentialType.includes(typ) + +/** + * @dev Some basic predefined filters, if required they can be removed + * and the filtering / ordering construction functions can be exposed instead + */ + +export const filters = { + filterByExpired: buildTransform([expiredFilter]), + filterByValid: buildTransform([validFilter]), + filterByIssuer: (did: string) => buildTransform([issuerFilter(did)]), + filterByType: (typ: string) => buildTransform([typeFilter(typ)]), + documentFilter: (documentTypes: string[]) => + buildTransform(documentTypes.map(typeFilter)), +} diff --git a/src/lib/storage/OfferMetadataAgent.ts b/src/lib/storage/OfferMetadataAgent.ts new file mode 100644 index 0000000000..3be05c2ac1 --- /dev/null +++ b/src/lib/storage/OfferMetadataAgent.ts @@ -0,0 +1 @@ +export class OfferMetadataAgent {} diff --git a/src/lib/storage/entities/cacheEntity.ts b/src/lib/storage/entities/cacheEntity.ts new file mode 100644 index 0000000000..ba338c378a --- /dev/null +++ b/src/lib/storage/entities/cacheEntity.ts @@ -0,0 +1,12 @@ +import { Entity, Column, PrimaryColumn } from 'typeorm/browser' +import { Expose } from 'class-transformer' + +@Entity('cache') +@Expose() +export class CacheEntity { + @PrimaryColumn() + key!: string + + @Column({ nullable: false, type: 'simple-json' }) + value!: any +} diff --git a/src/lib/storage/entities/index.ts b/src/lib/storage/entities/index.ts index 6b5a606b3c..5c4abccef3 100644 --- a/src/lib/storage/entities/index.ts +++ b/src/lib/storage/entities/index.ts @@ -1,21 +1,27 @@ import { + SettingEntity, CredentialEntity, MasterKeyEntity, PersonaEntity, SignatureEntity, VerifiableCredentialEntity, + CacheEntity, } from '.' +export { SettingEntity } from './settingEntity' export { CredentialEntity } from './credentialEntity' export { MasterKeyEntity } from './masterKeyEntity' export { PersonaEntity } from './personaEntity' export { SignatureEntity } from './signatureEntity' export { VerifiableCredentialEntity } from './verifiableCredentialEntity' +export { CacheEntity } from './cacheEntity' export const entityList = [ + SettingEntity, CredentialEntity, MasterKeyEntity, PersonaEntity, SignatureEntity, VerifiableCredentialEntity, + CacheEntity, ] diff --git a/src/lib/storage/entities/settingEntity.ts b/src/lib/storage/entities/settingEntity.ts new file mode 100644 index 0000000000..66a2ac4479 --- /dev/null +++ b/src/lib/storage/entities/settingEntity.ts @@ -0,0 +1,10 @@ +import { PrimaryColumn, Entity, Column } from 'typeorm/browser' + +@Entity('settings') +export class SettingEntity { + @PrimaryColumn() + key!: string + + @Column('simple-json') + value: any +} diff --git a/src/lib/storage/interactionTokens.ts b/src/lib/storage/interactionTokens.ts new file mode 100644 index 0000000000..070b3b9575 --- /dev/null +++ b/src/lib/storage/interactionTokens.ts @@ -0,0 +1,37 @@ +import { JSONWebToken } from 'jolocom-lib/js/interactionTokens/JSONWebToken' +import { InteractionType } from 'jolocom-lib/js/interactionTokens/types' +import { consumeCredentialRequest } from '../../actions/sso' +import { consumeAuthenticationRequest } from '../../actions/sso/authenticationRequest' +import { consumeCredentialOfferRequest } from '../../actions/sso/credentialOfferRequest' +import { Authentication } from 'jolocom-lib/js/interactionTokens/authentication' +import { CredentialOfferRequest } from 'jolocom-lib/js/interactionTokens/credentialOfferRequest' +import { CredentialRequest } from 'jolocom-lib/js/interactionTokens/credentialRequest' +import { PaymentRequest } from 'jolocom-lib/js/interactionTokens/paymentRequest' +import { consumePaymentRequest } from '../../actions/sso/paymentRequest' +/** + * @param Metadata should not need to be passed to credential receive because it comes from cred Offer + * Furthermore, this only needs to be defined for requests + */ + +export const interactionHandlers = { + [InteractionType.Authentication]: >( + interactionToken: T, + isDeepLinkInteraction: boolean, + ) => consumeAuthenticationRequest(interactionToken, isDeepLinkInteraction), + [InteractionType.CredentialRequest]: < + T extends JSONWebToken + >( + interactionToken: T, + isDeepLinkInteraction: boolean, + ) => consumeCredentialRequest(interactionToken, isDeepLinkInteraction), + [InteractionType.CredentialOfferRequest]: < + T extends JSONWebToken + >( + interactionToken: T, + isDeepLinkInteraction: boolean, + ) => consumeCredentialOfferRequest(interactionToken, isDeepLinkInteraction), + [InteractionType.PaymentRequest]: >( + interactionToken: T, + isDeepLinkInteraction: boolean, + ) => consumePaymentRequest(interactionToken, isDeepLinkInteraction), +} diff --git a/src/lib/storage/storage.ts b/src/lib/storage/storage.ts index 431fb7c6ec..e87407fb99 100644 --- a/src/lib/storage/storage.ts +++ b/src/lib/storage/storage.ts @@ -5,13 +5,20 @@ import { } from 'typeorm/browser' import { plainToClass } from 'class-transformer' import { + SettingEntity, PersonaEntity, MasterKeyEntity, VerifiableCredentialEntity, SignatureEntity, CredentialEntity, + CacheEntity, } from 'src/lib/storage/entities' import { SignedCredential } from 'jolocom-lib/js/credentials/signedCredential/signedCredential' +import { + CredentialOfferMetadata, + CredentialOfferRenderInfo, +} from 'jolocom-lib/js/interactionTokens/interactionTokens.types' +import { IdentitySummary } from '../../actions/sso/types' interface PersonaAttributes { did: string @@ -34,21 +41,41 @@ export class Storage { private config: ConnectionOptions public store = { - verifiableCredential: this.storeVClaim.bind(this), + setting: this.saveSetting.bind(this), persona: this.storePersonaFromJSON.bind(this), + verifiableCredential: this.storeVClaim.bind(this), encryptedSeed: this.storeEncryptedSeed.bind(this), + credentialMetadata: (metadata: CredentialMetadataSummary) => + this.createConnectionIfNeeded().then(() => + storeCredentialMetadata(this.connection)(metadata), + ), + issuerProfile: (issuer: IdentitySummary) => + this.createConnectionIfNeeded().then(() => + storeIssuerProfile(this.connection)(issuer), + ), } public get = { + settingsObject: this.getSettingsObject.bind(this), + setting: this.getSetting.bind(this), persona: this.getPersonas.bind(this), verifiableCredential: this.getVCredential.bind(this), attributesByType: this.getAttributesByType.bind(this), vCredentialsByAttributeValue: this.getVCredentialsForAttribute.bind(this), encryptedSeed: this.getEncryptedSeed.bind(this), + credentialMetadata: (credential: SignedCredential) => + this.createConnectionIfNeeded().then(() => + getMetadataForCredential(this.connection)(credential), + ), + publicProfile: (did: string) => + this.createConnectionIfNeeded().then(() => + getPublicProfile(this.connection)(did), + ), } public delete = { verifiableCredential: this.deleteVCred.bind(this), + // credentialMetadata: this.deleteCredentialMetadata.bind(this) } public initConnection = this.createConnectionIfNeeded.bind(this) @@ -63,6 +90,31 @@ export class Storage { } } + private async getSettingsObject(): Promise<{ [key: string]: any }> { + await this.createConnectionIfNeeded() + const settingsList = await this.connection.manager.find(SettingEntity) + const settings = {} + settingsList.forEach(setting => { + settings[setting.key] = setting.value + }) + return settings + } + + private async getSetting(key: string): Promise { + await this.createConnectionIfNeeded() + const setting = await this.connection.manager.findOne(SettingEntity, { + key, + }) + if (setting) return setting.value + } + + private async saveSetting(key: string, value: any): Promise { + await this.createConnectionIfNeeded() + const repo = this.connection.getRepository(SettingEntity) + const setting = repo.create({ key, value }) + await repo.save(setting) + } + // TODO: refactor needed on multiple personas private async getPersonas(query?: object): Promise { await this.createConnectionIfNeeded() @@ -226,3 +278,61 @@ export class Storage { .execute() } } + +export interface CredentialMetadata { + type: string + renderInfo: CredentialOfferRenderInfo + metadata: CredentialOfferMetadata +} + +export interface CredentialMetadataSummary extends CredentialMetadata { + issuer: IdentitySummary +} + +const storeCredentialMetadata = (connection: Connection) => ( + credentialMetadata: CredentialMetadataSummary, +) => { + const { issuer, type: credentialType } = credentialMetadata + + const cacheEntry = plainToClass(CacheEntity, { + key: buildMetadataKey(issuer.did, credentialType), + value: { ...credentialMetadata, issuer: credentialMetadata.issuer.did }, + }) + + return connection.manager.save(cacheEntry) +} + +const getMetadataForCredential = (connection: Connection) => async ({ + issuer, + type: credentialType, +}: SignedCredential) => { + const entryKey = buildMetadataKey(issuer, credentialType) + const [entry] = await connection.manager.findByIds(CacheEntity, [entryKey]) + return (entry && entry.value) || {} +} + +const buildMetadataKey = ( + issuer: string, + credentialType: string | string[], +): string => { + if (typeof credentialType === 'string') { + return `${issuer}${credentialType}` + } + + return `${issuer}${credentialType[credentialType.length - 1]}` +} +const storeIssuerProfile = (connection: Connection) => ( + issuer: IdentitySummary, +) => { + const cacheEntry = plainToClass(CacheEntity, { + key: issuer.did, + value: issuer, + }) + + return connection.manager.save(cacheEntry) +} + +const getPublicProfile = (connection: Connection) => async (did: string) => { + const [issuerProfile] = await connection.manager.findByIds(CacheEntity, [did]) + return (issuerProfile && issuerProfile.value) || { did } +} diff --git a/src/lib/util.ts b/src/lib/util.ts index 2932943d9e..3c1a984a45 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -7,6 +7,8 @@ import { import { BaseMetadata } from 'cred-types-jolocom-core' import { NativeModules } from 'react-native' +import { DecoratedClaims } from '../reducers/account' +import { equals } from 'ramda' // this comes from 'react-native-randombytes' const { RNRandomBytes } = NativeModules @@ -30,15 +32,16 @@ export const getClaimMetadataByCredentialType = ( export const getUiCredentialTypeByType = (type: string[]): string => uiCredentialTypeByType[type[1]] || prepareLabel(type[1]) -export const getCredentialUiCategory = (type: string): string => { +export const getCredentialUiCategory = ({ + credentialType, +}: DecoratedClaims): string => { const uiCategories = Object.keys(uiCategoryByCredentialType) - const category = uiCategories.find(uiCategory => { - const categoryDefinition = uiCategoryByCredentialType[uiCategory] - return categoryDefinition.some(entry => entry === type) - }) - - return category || Categories.Other + return ( + uiCategories.find(uiCategory => + uiCategoryByCredentialType[uiCategory].some(equals(credentialType)), + ) || Categories.Other + ) } export const prepareLabel = (label: string): string => { diff --git a/src/locales/de.js b/src/locales/de.json similarity index 51% rename from src/locales/de.js rename to src/locales/de.json index ad9f350834..835e8a5e95 100644 --- a/src/locales/de.js +++ b/src/locales/de.json @@ -1,58 +1,95 @@ -export default { - "Encrypting and storing data locally": "Daten lokal verschlüsseln und speichern", - "Fueling with ether": "Mit Ether aufladen", - "Registering decentralized identity": "Dezentrale Identität registrieren", - "Preparing launch": "Start vorbereiten", - "All claims": "Alle Claims", - "My identity": "Meine Identität", - "Documents": "Dokumente", - "Receiving new credential": "Neue Claims erhalten", - "Share claims": "Claims teilen", - "There was an error with your request": "Bei Ihrer Anfrage ist ein Fehler aufgetreten", - "Oops!": "Oops!", - "Try again": "Versuchen Sie es nochmal", +{ + "add": "hinzufügen", "Add claim": "Weiter", - "Name of issuer": "Name des Ausstellers", - "Document details/claims": "Dokumentdetails/Claims", + "Address Line1": "Anschrift", + "Address Line2": "Anschrift (optional)", + "All claims": "Alle Claims", + "Authentication request failed": "Authentifizierungsanfrage fehlgeschlagen", + "Authentication response failed": "Authentifizierungsantwort fehlgeschlagen", + "Authorization request": "Genehmigungsanfrage", + "Authorize": "Zulassen", + "Cancel": "Stornieren", + "City": "Stadt", "Coming Soon": "Demnächst", - "You can scan the qr code now!": "Sie können den QR-Code jetzt scannen!", - "Cancel": "Cancel", - "Your Jolocom Wallet": "Ihre Jolocom-Geldbörse", - "Take back control of your digital self and protect your private data against unfair usage": "Übernehmen Sie die Kontrolle über Ihr digitales Ich und schützen Sie Ihre privaten Daten gegen unlautere Nutzung", - "It's easy": "Es ist einfach", + "Confirm": "Bestätigen", + "Confirm payment": "Bezahlung Bestätigen", + "Contact": "Kontakt\n", + "Continue": "Fortsetzen", + "Could not parse JSONWebToken": "JSONWebToken konnte nicht verarbeitet werden", + "Could not receive credentials": "Claim konnte nicht empfangen werden", + "Could not save claim": "Claim konnte nicht gespeichert werden", + "Could not save external credential": "Externer Claim konnte nicht gespeichert werden", + "Country": "Land\n", + "Credential offer failed": "Claimangebot fehlgeschlagen", + "Credential request failed": "Claimanfrage fehlgeschlagen", + "Credential response failed": "Claimantwort fehlgeschlagen", + "Damn": "Mist", + "Deny": "Nein Danke", + "Document details/claims": "Dokumentdetails / Claims", + "Documents": "Dokumente", + "Email": "Email", + "Encrypting and storing data locally": "Daten lokal verschlüsseln und speichern", + "Enhanced privacy": "Erhöhte Privatssphäre", + "expired": "Abgelaufen", + "Family Name": "Nachname", + "For": "Für", + "For security purposes, we need some randomness": "Aus Sicherheitsgründen brauchen wir zufällig erzeugte Daten", "Forget about long forms and registrations": "Vergessen Sie lange Formulare und Anmeldungen", - "Instantly access services without using your social media profiles": "Sofortiger Zugriff auf Dienste ohne Verwendung Ihrer Social-Media-Profile", - "Enhanced privacy": "Erhöhter Privatssphäre", - "Share only the information a service really needs": "Teilen Sie nur die Informationen, die ein Dienst wirklich benötigt", - "Protect your digital self against fraud": "Schützen Sie Ihr digitales Ich gegen Betrug", - "Greater control": "Mehr Kontrolle", - "Keep all your data with you in one place, available at any time": "Bewahren Sie alle Ihre Daten an einem Ort auf, jederzeit verfügbar", - "Track where you sign in to services": "Verfolgen Sie Ihr Anmeldeverhalten", + "Fueling with ether": "Mit Ether aufladen", "Get started": "Loslegen", - "Continue": "Fortsetzen", - "Write these words down on an analog and secure place": "Schreiben Sie diese Wörter an einem analogen und sicheren Ort auf", - "Without these words, you cannot access your wallet again": "Ohne diese Wörter können Sie nicht mehr auf Ihr SmartWallet zugreifen", - "Yes, I wrote it down": "Erledigt", "Give us a few moments": "Nur einen Moment bitte", - "to set up your identity": "Ihre Identität wird erstellt", - "+ add": "+ adden", + "Given Name": "Vorname", + "Go back": "Zurück", + "Greater control": "Mehr Kontrolle", + "Instantly access services without using your social media profiles": "Sofortiger Zugang zu Diensten ohne Verwendung Ihrer Social-Media-Profile", + "It's easy": "Es ist einfach", + "Keep all your data with you in one place, available at any time": "Bewahren Sie alle Ihre Daten an einem Ort auf, jederzeit verfügbar", + "Language": "Sprache", + "Login records": "Login Einträge", + "Mobile Phone": "Mobilnummer", + "My Identity": "Meine Identität", + "Name": "Name", + "Name of issuer": "Name des Ausstellers", + "No documents to see here": "Sie haben noch keine Dokumente", "No local claims": "Keine lokalen Claims", - "Self-signed": "Selbst-signiert", - "Deny": "Nein Danke", - "This service is asking you to share the following claims": "Dieser Dienst bittet Sie, die folgenden Claims zu teilen", + "Oh no": "Oh Nein", + "Other": "Andere", + "Payment method": "Bezahlmethode", + "Payment request failed": "Zahlungsanfrage fehlgeschlagen", + "Payment response failed": "Zahlungsantwort fehlgeschlagen", "Personal": "Persönlich", - "Name": "Name", - "Email": "Email", - "Mobile Phone": "Mobilnummer", - "Given Name": "Vorname", - "Family Name": "Nachname", - "Telephone": "Telefon", - "Contact": "Kontakt", + "Please tap the screen and draw on it randomly": "Tippen Sie auf den Bildschirm und zeichnen Sie drauf los", "Postal Address": "Adresse", - "Address Line1": "Anschrift", - "Address Line2": "Anschrift (optional)", "Postal Code": "Postleitzahl", - "City": "Stadt", - "Country": "Land", - "Your Jolocom Wallet": "Deine Jolocom Wallet" -} + "Preparing launch": "Start vorbereiten", + "Protect your digital self against fraud": "Schützen Sie Ihr digitales Ich gegen Betrug", + "Receiving new credential": "Neue Claims erhalten", + "Registering decentralized identity": "Dezentrale Identität registrieren", + "Registration failed": "Registrierung fehlgeschlagen", + "Self-signed": "Selbst-signiert", + "Settings": "Einstellungen", + "Share claims": "Claims teilen", + "Share only the information a service really needs": "Teilen Sie nur die Informationen, die ein Dienst wirklich benötigt", + "Take back control of your digital self and protect your private data against unfair usage": "Übernehmen Sie die Kontrolle über Ihr digitales Ich und schützen Sie Ihre privaten Daten gegen unlautere Nutzung", + "Telephone": "Telefon", + "There was an error with your request": "Bei Ihrer Anfrage ist ein Fehler aufgetreten", + "This service is asking you to share the following claims": "Dieser Dienst bittet Sie, die folgenden Claims zu teilen", + "To": "An", + "to set up your identity": "Ihre Identität wird erstellt", + "Track where you sign in to services": "Verfolgen Sie Ihr Anmeldeverhalten", + "Try again": "Versuchen Sie es nochmal", + "Uh oh": "Hmmm", + "Unable to initialize wallet": "Wallet konnte nicht initialisiert werden", + "Unknown Error": "Unbekannter Fehler", + "version": "Version", + "with your SmartWallet?": "mit ihrer SmartWallet?", + "Without these words, you cannot access your wallet again": "Ohne diese Wörter können Sie nicht mehr auf Ihr SmartWallet zugreifen", + "Would you like to": "Würden Sie gern", + "Write these words down on an analog and secure place": "Schreiben Sie diese Wörter an einem analogen und sicheren Ort auf", + "Yes, I wrote it down": "Ja, ich habe es aufgeschrieben", + "You can scan the qr code now!": "Sie können den QR-Code jetzt scannen!", + "You haven't logged in to any services yet": "Sie haben sich bisher bei keinem Service eingeloggt", + "Your Jolocom SmartWallet": "Ihre Jolocom SmartWallet", + "Your Jolocom Wallet": "Ihre Jolocom Wallet", + "Your preferences": "Ihre Einstellungen" +} \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 0000000000..69db52a6a1 --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,93 @@ +{ + "add": "add", + "Address Line1": "Address Line1", + "Address Line2": "Address Line2", + "Add claim": "Add claim", + "All claims": "All claims", + "Authorization request": "Authorization request", + "Authorize": "Authorize", + "Authentication request failed": "Authentication request failed", + "Authentication response failed": "Authentication response failed", + "Cancel": "Cancel", + "City": "City", + "Coming Soon": "Coming Soon", + "Confirm": "Confirm", + "Confirm payment": "Confirm payment", + "Contact": "Contact", + "Continue": "Continue", + "Country": "Country", + "Credential offer failed": "Credential offer failed", + "Credential request failed": "Credential request failed", + "Credential response failed": "Credential response failed", + "Could not receive credentials": "Could not receive credentials", + "Damn": "Damn", + "Deny": "Deny", + "Documents": "Documents", + "Document details/claims": "Document details/claims", + "Could not find receiving application": "Could not find receiving application", + "Email": "Email", + "Encrypting and storing data locally": "Encrypting and storing data locally", + "Enhanced privacy": "Enhanced privacy", + "expired": "expired", + "Family Name": "Family Name", + "For": "For", + "Forget about long forms and registrations": "Forget about long forms and registrations", + "For security purposes, we need some randomness": "For security purposes, we need some randomness", + "Fueling with ether": "Fueling with ether", + "Get started": "Get started", + "Given Name": "Given Name", + "Give us a few moments": "Give us a few moments", + "Go back": "Go back", + "Greater control": "Greater control", + "Instantly access services without using your social media profiles": "Instantly access services without using your social media profiles", + "It's easy": "It's easy", + "Keep all your data with you in one place, available at any time": "Keep all your data with you in one place, available at any time", + "Language": "Language", + "Login records": "Login records", + "Mobile Phone": "Mobile Phone", + "My Identity": "My Identity", + "Name": "Name", + "Name of issuer": "Name of issuer", + "No documents to see here": "No documents to see here", + "No local claims": "No local claims", + "Oh no": "Oh no", + "Other": "Other", + "Personal": "Personal", + "Please tap the screen and draw on it randomly": "Please tap the screen and draw on it randomly", + "Postal Address": "Postal Address", + "Postal Code": "Postal Code", + "Preparing launch": "Preparing launch", + "Protect your digital self against fraud": "Protect your digital self against fraud", + "Could not parse JSONWebToken": "Could not parse JSONWebToken", + "Payment request failed": "Payment request failed", + "Payment response failed": "Payment response failed", + "Receiving new credential": "Receiving new credential", + "Registering decentralized identity": "Registering decentralized identity", + "Registration failed": "Registration failed", + "Self-signed": "Self-signed", + "Settings": "Settings", + "Share claims": "Share claims", + "Share only the information a service really needs": "Share only the information a service really needs", + "Could not save claim": "Could not save claim", + "Could not save external credential": "Could not save external credential", + "Take back control of your digital self and protect your private data against unfair usage": "Take back control of your digital self and protect your private data against unfair usage", + "Telephone": "Telephone", + "There was an error with your request": "There was an error with your request", + "This service is asking you to share the following claims": "This service is asking you to share the following claims", + "To": "To", + "to set up your identity": "to set up your identity", + "Track where you sign in to services": "Track where you sign in to services", + "Uh oh": "Uh oh", + "Unknown Error": "Unknown Error", + "version": "version", + "Without these words, you cannot access your wallet again": "Without these words, you cannot access your wallet again", + "with your SmartWallet?": "with your SmartWallet?", + "Would you like to": "Would you like to", + "Write these words down on an analog and secure place": "Write these words down on an analog and secure place", + "Unable to initialize wallet": "Unable to initialize wallet", + "Yes, I wrote it down": "Yes, I wrote it down", + "Your Jolocom Wallet": "Your Jolocom Wallet", + "Your preferences": "Your preferences", + "You can scan the qr code now!": "You can scan the qr code now!", + "You haven't logged in to any services yet": "You haven't logged in to any services yet" +} \ No newline at end of file diff --git a/src/locales/i18n.ts b/src/locales/i18n.ts index 20aad9560a..460aff1e1e 100644 --- a/src/locales/i18n.ts +++ b/src/locales/i18n.ts @@ -1,8 +1,8 @@ const RNLanguages = require('react-native-languages') import I18n from 'i18n-js' -const de = require('./de').default -const nl = require('./nl').default +const de = require('./de.json') +const nl = require('./nl.json') I18n.locale = RNLanguages.language.split('-')[0] I18n.defaultLocale = 'en' @@ -13,8 +13,10 @@ I18n.translations = { nl, } +export const locales = ['en', 'de', 'nl'] + export const getI18nImage = (fileName: string): File => { - const locale = Object.keys(I18n.translations).includes(I18n.locale) + const locale = locales.includes(I18n.locale) ? I18n.locale : I18n.defaultLocale diff --git a/src/locales/nl.js b/src/locales/nl.json similarity index 61% rename from src/locales/nl.js rename to src/locales/nl.json index 8ddfc81558..4dce687655 100644 --- a/src/locales/nl.js +++ b/src/locales/nl.json @@ -1,58 +1,93 @@ -export default { - "Encrypting and storing data locally": "Data lokaal versleutelen en opslaan", - "Fueling with ether": "Ether tanken", - "Registering decentralized identity": "Gedecentraliseerde identiteit registreren", - "Preparing launch": "De lancering voorbereiden", - "All claims": "Alle claims", - "My identity": "Mijn identiteit", - "Documents": "Documenten", - "Receiving new credential": "Nieuwe inloggegevens ontvangen", - "Share claims": "Deel claims", - "There was an error with your request": "Er is een fout opgetreden bij uw aanvraag", - "Oops!": "Oeps!", - "Try again": "Probeer opnieuw", +{ + "add": "voeg toe", "Add claim": "Claim toevoegen", - "Name of issuer": "Naam van uitgevende instantie", - "Document details/claims": "Documentdetails/claims", - "Coming Soon": "Binnenkort", - "You can scan the qr code now!": "U kunt de QR-code nu scannen!", + "Address Line1": "Adres regel 1", + "Address Line2": "Adres regel 2", + "All claims": "Alle claims", + "Authentication request failed": "Authentication request failed", + "Authentication response failed": "Authentication response failed", + "Authorization request": "Authorization request", + "Authorize": "Authorize", "Cancel": "Cancel", - "Your Jolocom SmartWallet": "Uw Jolocom SmartWallet", - "Take back control of your digital self and protect your private data against unfair usage": "Neem de controle over uw digitale zelf over en bescherm uw privégegevens tegen oneerlijk gebruik", - "It's easy": "Simpel", - "Forget about long forms and registrations": "Zonder lange formulieren en registraties", - "Instantly access services without using your social media profiles": "Direct toegang tot services, niet via uw sociale media-profielen", - "Enhanced privacy": "Verhoogde privacy", - "Share only the information a service really needs": "Deel alleen de informatie die een service echt nodig heeft", - "Protect your digital self against fraud": "Bescherm uw digitale identiteit tegen fraude", - "Greater control": "Meer controle", - "Keep all your data with you in one place, available at any time": "Bewaar al uw gegevens op één plaats, altijd beschikbaar", - "Track where you sign in to services": "Volg bij welke services u zich aanmeldt", - "Get started": "Begin", + "City": "Stad", + "Coming Soon": "Binnenkort", + "Confirm": "Bevestigen", + "Confirm payment": "Bevestig betaling", + "Contact": "Contact", "Continue": "Doorgaan", - "Write these words down on an analog and secure place": "Schrijf deze woorden op een analoge en veilige plaats op", - "Without these words, you cannot access your wallet again": "Zonder deze woorden heeft u geen toegang meer tot uw portefeuille", - "Yes, I wrote it down": "Ja, ik heb het opgeschreven", - "Give us a few moments": "Geef ons een moment", - "to set up your identity": "om je identiteit in te stellen", - "+ add": "+ voeg toe", - "No local claims": "Geen lokale claims", - "Self-signed": "Zelfondertekend", + "Could not parse JSONWebToken": "Could not parse JSONWebToken", + "Could not receive credentials": "Could not receive credentials", + "Could not save claim": "Could not save claim", + "Could not save external credential": "Could not save external credential", + "Country": "Land", + "Credential offer failed": "Credential offer failed", + "Credential request failed": "Credential request failed", + "Credential response failed": "Credential response failed", + "Damn": "Damn", "Deny": "Weigeren", - "This service is asking you to share the following claims": "Deze service vraagt ​​u om de volgende claims te delen", - "Personal": "Persoonlijk", - "Name": "Naam", + "Document details/claims": "Documentdetails/claims", + "Documents": "Documenten", "Email": "E-mail", - "Given Name": "Gegeven naam", + "Encrypting and storing data locally": "Data lokaal versleutelen en opslaan", + "Enhanced privacy": "Verhoogde privacy", "Family Name": "Achternaam", - "Telephone": "Telefoon", - "Contact": "Contact", + "For": "Voor", + "For security purposes, we need some randomness": "Voor de veiligheid hebben we wat willekeur nodig", + "Forget about long forms and registrations": "Zonder lange formulieren en registraties", + "Fueling with ether": "Ether tanken", + "Get started": "Begin", + "Give us a few moments": "Geef ons een moment", + "Given Name": "Gegeven naam", + "Go back": "Terug", + "Greater control": "Meer controle", + "Instantly access services without using your social media profiles": "Direct toegang tot services, niet via uw sociale media-profielen", + "It's easy": "Simpel", + "Keep all your data with you in one place, available at any time": "Bewaar al uw gegevens op één plaats, altijd beschikbaar", + "Language": "Taal", + "Login records": "Login records", "Mobile Phone": "Mobiel nummer", + "My Identity": "Mijn identiteit", + "Name": "Naam", + "Name of issuer": "Naam van uitgevende instantie", + "No local claims": "Geen lokale claims", + "Oh no": "Oh no", + "Other": "Other", + "Payment method": "Betalingsmiddel", + "Payment request failed": "Payment request failed", + "Payment response failed": "Payment response failed", + "Personal": "Persoonlijk", + "Please tap the screen and draw on it randomly": "Tik op het scherm en teken er willekeurig op", "Postal Address": "Postadres", - "Address Line1": "Adres regel 1", - "Address Line2": "Adres regel 2", "Postal Code": "Postcode", - "City": "Stad", - "Country": "Land", - "Your Jolocom Wallet": "Uw Jolocom Wallet" -} + "Preparing launch": "De lancering voorbereiden", + "Protect your digital self against fraud": "Bescherm uw digitale identiteit tegen fraude", + "Receiving new credential": "Nieuwe inloggegevens ontvangen", + "Registering decentralized identity": "Gedecentraliseerde identiteit registreren", + "Registration failed": "Registration failed", + "Self-signed": "Zelfondertekend", + "Settings": "Instellingen", + "Share claims": "Deel claims", + "Share only the information a service really needs": "Deel alleen de informatie die een service echt nodig heeft", + "Take back control of your digital self and protect your private data against unfair usage": "Neem de controle over uw digitale zelf over en bescherm uw privégegevens tegen oneerlijk gebruik", + "Telephone": "Telefoon", + "There was an error with your request": "Er is een fout opgetreden bij uw aanvraag", + "This service is asking you to share the following claims": "Deze service vraagt ​​u om de volgende claims te delen", + "To": "Naar", + "to set up your identity": "om je identiteit in te stellen", + "Track where you sign in to services": "Volg bij welke services u zich aanmeldt", + "Try again": "Probeer opnieuw", + "Uh oh": "Uh oh", + "Unable to initialize wallet": "Unable to initialize wallet", + "Unknown Error": "Unknown Error", + "version": "versie", + "with your SmartWallet?": "with your SmartWallet?", + "Without these words, you cannot access your wallet again": "Zonder deze woorden heeft u geen toegang meer tot uw portefeuille", + "Would you like to": "Would you like to", + "Write these words down on an analog and secure place": "Schrijf deze woorden op een analoge en veilige plaats op", + "Yes, I wrote it down": "Ja, ik heb het opgeschreven", + "You can scan the qr code now!": "U kunt de QR-code nu scannen!", + "You haven't logged in to any services yet": "You haven't logged in to any services yet", + "Your Jolocom SmartWallet": "Uw Jolocom SmartWallet", + "Your Jolocom Wallet": "Uw Jolocom Wallet", + "Your preferences": "Jouw Voorkeuren" +} \ No newline at end of file diff --git a/src/locales/strings.ts b/src/locales/strings.ts new file mode 100644 index 0000000000..04c8990593 --- /dev/null +++ b/src/locales/strings.ts @@ -0,0 +1,115 @@ +import ErrorCode from '../lib/errorCodes' + +export default { + YOUR_JOLOCOM_WALLET: 'Your Jolocom Wallet', + ALL_CLAIMS: 'All claims', + MY_IDENTITY: 'My Identity', + RECEIVING_NEW_CREDENTIAL: 'Receiving new credential', + SHARE_CLAIMS: 'Share claims', + CONFIRM_PAYMENT: 'Confirm payment', + AUTHORIZATION_REQUEST: 'Authorization request', + DOCUMENTS: 'Documents', + LOGIN_RECORDS: 'Login records', + SETTINGS: 'Settings', + ENCRYPTING_AND_STORING_DATA_LOCALLY: 'Encrypting and storing data locally', + FUELING_WITH_ETHER: 'Fueling with ether', + REGISTERING_DECENTRALIZED_IDENTITY: 'Registering decentralized identity', + PREPARING_LAUNCH: 'Preparing launch', + WOULD_YOU_LIKE_TO: 'Would you like to', + WITH_YOUR_SMARTWALLET: 'with your SmartWallet?', + AUTHORIZE: 'Authorize', + DENY: 'Deny', + GO_BACK: 'Go back', + YOU_CAN_SCAN_THE_QR_CODE_NOW: 'You can scan the qr code now!', + CANCEL: 'Cancel', + ADD_CLAIM: 'Add claim', + NAME_OF_ISSUER: 'Name of issuer', + DOCUMENT_DETAILS_CLAIMS: 'Document details/claims', + COMING_SOON: 'Coming Soon', + TAKE_BACK_CONTROL_OF_YOUR_DIGITAL_SELF_AND_PROTECT_YOUR_PRIVATE_DATA_AGAINST_UNFAIR_USAGE: + 'Take back control of your digital self and protect your private data against unfair usage', + ITS_EASY: "It's easy", + FORGET_ABOUT_LONG_FORMS_AND_REGISTRATIONS: + 'Forget about long forms and registrations', + INSTANTLY_ACCESS_SERVICES_WITHOUT_USING_YOUR_SOCIAL_MEDIA_PROFILES: + 'Instantly access services without using your social media profiles', + ENHANCED_PRIVACY: 'Enhanced privacy', + SHARE_ONLY_THE_INFORMATION_A_SERVICE_REALLY_NEEDS: + 'Share only the information a service really needs', + PROTECT_YOUR_DIGITAL_SELF_AGAINST_FRAUD: + 'Protect your digital self against fraud', + GREATER_CONTROL: 'Greater control', + KEEP_ALL_YOUR_DATA_WITH_YOU_IN_ONE_PLACE_AVAILABLE_AT_ANY_TIME: + 'Keep all your data with you in one place, available at any time', + TRACK_WHERE_YOU_SIGN_IN_TO_SERVICES: 'Track where you sign in to services', + GET_STARTED: 'Get started', + CONFIRM: 'Confirm', + FOR: 'For', + OTHER: 'Other', + TO: 'To', + FOR_SECURITY_PURPOSES_WE_NEED_SOME_RANDOMNESS: + 'For security purposes, we need some randomness', + PLEASE_TAP_THE_SCREEN_AND_DRAW_ON_IT_RANDOMLY: + 'Please tap the screen and draw on it randomly', + CONTINUE: 'Continue', + WRITE_THESE_WORDS_DOWN_ON_AN_ANALOG_AND_SECURE_PLACE: + 'Write these words down on an analog and secure place', + WITHOUT_THESE_WORDS_YOU_CANNOT_ACCESS_YOUR_WALLET_AGAIN: + 'Without these words, you cannot access your wallet again', + YES_I_WROTE_IT_DOWN: 'Yes, I wrote it down', + GIVE_US_A_FEW_MOMENTS: 'Give us a few moments', + TO_SET_UP_YOUR_IDENTITY: 'to set up your identity', + LANGUAGE: 'Language', + YOUR_PREFERENCES: 'Your preferences', + VERSION: 'version', + ADD: 'add', + NO_LOCAL_CLAIMS: 'No local claims', + SELF_SIGNED: 'Self-signed', + THIS_SERVICE_IS_ASKING_YOU_TO_SHARE_THE_FOLLOWING_CLAIMS: + 'This service is asking you to share the following claims', + YOU_HAVENT_LOGGED_IN_TO_ANY_SERVICES_YET: + "You haven't logged in to any services yet", + THERE_WAS_AN_ERROR_WITH_YOUR_REQUEST: 'There was an error with your request', + COUNTRY: 'Country', + CITY: 'City', + POSTAL_CODE: 'Postal Code', + ADDRESS_LINE1: 'Address Line1', + ADDRESS_LINE2: 'Address Line2', + POSTAL_ADDRESS: 'Postal Address', + CONTACT: 'Contact', + TELEPHONE: 'Telephone', + FAMILY_NAME: 'Family Name', + GIVEN_NAME: 'Given Name', + MOBILE_PHONE: 'Mobile Phone', + EMAIL: 'Email', + NAME: 'Name', + PERSONAL: 'Personal', + NO_DOCUMENTS_TO_SEE_HERE: 'No documents to see here', + EXPIRED: 'expired', + + // Error Title: + DAMN: 'Damn', + OH_NO: 'Oh no', + UH_OH: 'Uh oh', + + // Error Codes: + [ErrorCode.Unknown]: 'Unknown Error', + [ErrorCode.WalletInitFailed]: 'Unable to initialize wallet', + [ErrorCode.SaveClaimFailed]: 'Could not save claim', + [ErrorCode.SaveExternalCredentialFailed]: + 'Could not save external credential', + + [ErrorCode.AuthenticationRequestFailed]: 'Authentication request failed', + [ErrorCode.AuthenticationResponseFailed]: 'Authentication response failed', + [ErrorCode.PaymentRequestFailed]: 'Payment request failed', + [ErrorCode.PaymentResponseFailed]: 'Payment response failed', + + [ErrorCode.CredentialOfferFailed]: 'Credential offer failed', + [ErrorCode.CredentialsReceiveFailed]: 'Could not receive credentials', + [ErrorCode.CredentialRequestFailed]: 'Credential request failed', + [ErrorCode.CredentialResponseFailed]: 'Credential response failed', + [ErrorCode.ParseJWTFailed]: 'Could not parse JSONWebToken', + + [ErrorCode.DeepLinkUrlNotFound]: 'Could not find receiving application', + [ErrorCode.RegistrationFailed]: 'Registration failed', +} diff --git a/src/locales/template.pot b/src/locales/template.pot deleted file mode 100644 index 7eb063e8f8..0000000000 --- a/src/locales/template.pot +++ /dev/null @@ -1,217 +0,0 @@ -#: src/actions/registration/loadingStages.ts -msgid "Encrypting and storing data locally" -msgstr "" - -#: src/actions/registration/loadingStages.ts -msgid "Fueling with ether" -msgstr "" - -#: src/actions/registration/loadingStages.ts -msgid "Registering decentralized identity" -msgstr "" - -#: src/actions/registration/loadingStages.ts -msgid "Preparing launch" -msgstr "" - -#: src/routes.ts -msgid "All claims" -msgstr "" - -#: src/routes.ts -msgid "My identity" -msgstr "" - -#: src/routes.ts -msgid "Documents" -msgstr "" - -#: src/routes.ts -msgid "Receiving new credential" -msgstr "" - -#: src/routes.ts -#: src/ui/sso/components/consent.tsx -msgid "Share claims" -msgstr "" - -#: src/ui/generic/exception.tsx -msgid "There was an error with your request" -msgstr "" - -#: src/ui/generic/exception.tsx -msgid "Oops!" -msgstr "" - -#: src/ui/generic/exception.tsx -msgid "Try again" -msgstr "" - -#: src/ui/home/components/claimDetails.tsx -msgid "Add claim" -msgstr "" - -#: src/ui/home/components/credentialDialog.tsx -msgid "Name of issuer" -msgstr "" - -#: src/ui/home/components/credentialDialog.tsx -msgid "Document details/claims" -msgstr "" - -#: src/ui/home/components/interactions.tsx -msgid "Coming Soon" -msgstr "" - -#: src/ui/home/components/qrcodeScanner.tsx -msgid "You can scan the qr code now!" -msgstr "" - -#: src/ui/home/components/qrcodeScanner.tsx -msgid "Cancel" -msgstr "" - -#: src/ui/landing/components/landing.tsx -msgid "Your Jolocom Wallet" -msgstr "" - -#: src/ui/landing/components/landing.tsx -msgid "Take back control of your digital self and protect your private data against unfair usage" -msgstr "" - -#: src/ui/landing/components/landing.tsx -msgid "It's easy" -msgstr "" - -#: src/ui/landing/components/landing.tsx -msgid "Forget about long forms and registrations" -msgstr "" - -#: src/ui/landing/components/landing.tsx -msgid "Instantly access services without using your social media profiles" -msgstr "" - -#: src/ui/landing/components/landing.tsx -msgid "Enhanced privacy" -msgstr "" - -#: src/ui/landing/components/landing.tsx -msgid "Share only the information a service really needs" -msgstr "" - -#: src/ui/landing/components/landing.tsx -msgid "Protect your digital self against fraud" -msgstr "" - -#: src/ui/landing/components/landing.tsx -msgid "Greater control" -msgstr "" - -#: src/ui/landing/components/landing.tsx -msgid "Keep all your data with you in one place, available at any time" -msgstr "" - -#: src/ui/landing/components/landing.tsx -msgid "Track where you sign in to services" -msgstr "" - -#: src/ui/landing/components/landing.tsx -msgid "Get started" -msgstr "" - -#: src/ui/registration/components/entropy.tsx -msgid "For security purposes, we need some randomness" -msgstr "" - -#: src/ui/registration/components/entropy.tsx -msgid "Please tap the screen and draw on it randomly" -msgstr "" - -#: src/ui/registration/components/entropy.tsx -#: src/ui/registration/components/passwordEntry.tsx -msgid "Continue" -msgstr "" - -#: src/ui/registration/components/passwordEntry.tsx -msgid "Set a password to encrypt your data on the device" -msgstr "" - -#: src/ui/registration/components/passwordEntry.tsx -msgid "This password will be stored in your keychain" -msgstr "" - -#: src/ui/registration/components/passwordEntry.tsx -msgid "After setting it, please make sure you have passcode enabled" -msgstr "" - -#: src/ui/registration/components/passwordEntry.tsx -msgid "Password" -msgstr "" - -#: src/ui/registration/components/passwordEntry.tsx -msgid "Repeat password" -msgstr "" - -#: src/ui/registration/components/seedPhrase.tsx -msgid "Write these words down on an analog and secure place" -msgstr "" - -#: src/ui/registration/components/seedPhrase.tsx -msgid "Without these words, you cannot access your wallet again" -msgstr "" - -#: src/ui/registration/components/seedPhrase.tsx -msgid "Yes, I wrote it down" -msgstr "" - -#: src/ui/registration/containers/loading.tsx -msgid "Give us a few moments" -msgstr "" - -#: src/ui/registration/containers/loading.tsx -msgid "to set up your identity" -msgstr "" - -#: src/ui/sso/components/claimCard.tsx -msgid "+ add" -msgstr "" - -#: src/ui/sso/components/claimCard.tsx -msgid "No local claims" -msgstr "" - -#: src/ui/sso/components/claimCard.tsx -msgid "Self-signed" -msgstr "" - -#: src/ui/sso/components/consent.tsx -msgid "Deny" -msgstr "" - -#: src/ui/sso/components/consent.tsx -msgid "This service is asking you to share the following claims" -msgstr "" - -msgid "Personal" -msgstr "" - -msgid "Name" -msgstr "" - -msgid "Email" -msgstr "" - -msgid "Given Name" -msgstr "" - -msgid "Family Name" -msgstr "" - -msgid "Telephone" -msgstr "" - -msgid "Contact" -msgstr "" - -msgid "Mobile Phone" -msgstr "" diff --git a/src/reducers/account/claims.ts b/src/reducers/account/claims.ts index cd56235b6a..4c0318625b 100644 --- a/src/reducers/account/claims.ts +++ b/src/reducers/account/claims.ts @@ -1,5 +1,4 @@ import { AnyAction } from 'redux' -import Immutable from 'immutable' import { ClaimsState, CategorizedClaims, @@ -15,7 +14,9 @@ const categorizedClaims: CategorizedClaims = { familyName: '', }, id: '', - issuer: '', + issuer: { + did: '', + }, subject: '', }, ], @@ -26,7 +27,9 @@ const categorizedClaims: CategorizedClaims = { email: '', }, id: '', - issuer: '', + issuer: { + did: '', + }, subject: '', keyboardType: 'email-address', }, @@ -36,7 +39,9 @@ const categorizedClaims: CategorizedClaims = { telephone: '', }, id: '', - issuer: '', + issuer: { + did: '', + }, subject: '', keyboardType: 'phone-pad', }, @@ -50,53 +55,61 @@ const categorizedClaims: CategorizedClaims = { country: '', }, id: '', - issuer: '', + issuer: { + did: '', + }, subject: '', }, ], + // /** @dev FOR TESTING */ Other: [], } export const initialState: ClaimsState = { - loading: false, selected: { credentialType: '', claimData: {}, id: '', - issuer: '', + issuer: { + did: '', + }, subject: '', }, - pendingExternal: [], + pendingExternal: { + offeror: { + did: '', + }, + offer: [], + }, decoratedCredentials: categorizedClaims, } export const claims = ( - state = Immutable.fromJS(initialState), + state = initialState, action: AnyAction, ): ClaimsState => { switch (action.type) { - case 'TOGGLE_CLAIMS_LOADING': - return state.setIn(['loading'], action.value) case 'SET_CLAIMS_FOR_DID': - return state - .set( - 'decoratedCredentials', - Immutable.fromJS(addDefaultValues(action.claims)), - ) - .set('loading', false) + return { ...state, decoratedCredentials: addDefaultValues(action.claims) } case 'SET_EXTERNAL': - return state.set('pendingExternal', Immutable.fromJS(action.external)) + return { ...state, pendingExternal: action.value } case 'RESET_EXTERNAL': - return state.set('pendingExternal', Immutable.fromJS([])) + return { ...state, pendingExternal: initialState.pendingExternal } // TODO Remove in favor of calling set external with empty array case 'SET_SELECTED': - return state.setIn(['selected'], Immutable.fromJS(action.selected)) + return { ...state, selected: action.selected } case 'RESET_SELECTED': - return state.setIn(['selected'], initialState.selected) - case 'HANLDE_CLAIM_INPUT': - return state.setIn( - ['selected', 'claimData', action.fieldName], - action.fieldValue, - ) + return { ...state, selected: initialState.selected } + case 'HANDLE_CLAIM_INPUT': + return { + ...state, + selected: { + ...state.selected, + claimData: { + ...state.selected.claimData, + [action.fieldName]: action.fieldValue, + }, + }, + } default: return state } diff --git a/src/reducers/account/did.ts b/src/reducers/account/did.ts index 1ecf26729e..1ca76c66b2 100644 --- a/src/reducers/account/did.ts +++ b/src/reducers/account/did.ts @@ -1,18 +1,16 @@ import { AnyAction } from 'redux' -import Immutable from 'immutable' import { DidState } from 'src/reducers/account/' const initialState: DidState = { did: '', } -export const did = ( - state = Immutable.fromJS(initialState), - action: AnyAction, -): string => { +export const did = (state = initialState, action: AnyAction): DidState => { switch (action.type) { case 'DID_SET': - return state.setIn(['did'], action.value) + return { + did: action.value, + } default: return state } diff --git a/src/reducers/account/index.ts b/src/reducers/account/index.ts index 0b04b9ce4e..be25dfadc7 100644 --- a/src/reducers/account/index.ts +++ b/src/reducers/account/index.ts @@ -2,16 +2,24 @@ import { combineReducers } from 'redux' import { did } from 'src/reducers/account/did' import { claims } from 'src/reducers/account/claims' import { loading } from 'src/reducers/account/loading' +import { + CredentialOfferMetadata, + CredentialOfferRenderInfo, +} from 'jolocom-lib/js/interactionTokens/interactionTokens.types' import { SignedCredential } from 'jolocom-lib/js/credentials/signedCredential/signedCredential' -import { ClaimEntry } from 'jolocom-lib/js/credentials/credential/types' +import { IdentitySummary } from '../../actions/sso/types' export interface DecoratedClaims { credentialType: string - claimData: ClaimEntry + claimData: { + [key: string]: any /** @TODO Type correctly */ + } id: string - issuer: string + issuer: IdentitySummary subject: string expires?: Date + renderInfo?: CredentialOfferRenderInfo + metadata?: CredentialOfferMetadata keyboardType?: | 'default' | 'number-pad' @@ -26,10 +34,15 @@ export interface CategorizedClaims { } export interface ClaimsState { - readonly loading: boolean readonly selected: DecoratedClaims readonly decoratedCredentials: CategorizedClaims - readonly pendingExternal: SignedCredential[] + readonly pendingExternal: { + offeror: IdentitySummary + offer: Array<{ + credential: SignedCredential + decoratedClaim: DecoratedClaims + }> + } } export interface DidState { diff --git a/src/reducers/account/loading.ts b/src/reducers/account/loading.ts index 0e9f569948..65751bc9cd 100644 --- a/src/reducers/account/loading.ts +++ b/src/reducers/account/loading.ts @@ -1,5 +1,4 @@ import { AnyAction } from 'redux' -import Immutable from 'immutable' import { LoadingState } from 'src/reducers/account/' const initialState: LoadingState = { @@ -7,12 +6,12 @@ const initialState: LoadingState = { } export const loading = ( - state = Immutable.fromJS(initialState), + state = initialState, action: AnyAction, -): string => { +): LoadingState => { switch (action.type) { case 'SET_LOADING': - return state.setIn(['loading'], action.value) + return { loading: action.value } default: return state } diff --git a/src/reducers/documents/index.ts b/src/reducers/documents/index.ts new file mode 100644 index 0000000000..c728f36632 --- /dev/null +++ b/src/reducers/documents/index.ts @@ -0,0 +1,40 @@ +import { AnyAction } from 'redux' +import { SET_DOC_DETAIL, CLEAR_DOC_DETAIL } from 'src/actions/documents' +import { DecoratedClaims } from '../account' + +export interface DocumentsState { + selectedDocument: DecoratedClaims +} + +const initialState: DocumentsState = { + selectedDocument: { + credentialType: '', + subject: '', + id: '', + issuer: { + did: '', + }, + expires: undefined, + claimData: { + type: '', + documentNumber: '', + }, + }, +} + +export const documentsReducer = ( + state = initialState, + action: AnyAction, +): DocumentsState => { + switch (action.type) { + case SET_DOC_DETAIL: + return { ...state, selectedDocument: action.value } + case CLEAR_DOC_DETAIL: + return { + ...state, + selectedDocument: initialState.selectedDocument, + } + default: + return state + } +} diff --git a/src/reducers/index.ts b/src/reducers/index.ts index 7b566509e8..7abc2463b8 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -1,4 +1,5 @@ import { combineReducers } from 'redux' +import { settingsReducer, SettingsState } from 'src/reducers/settings/' import { accountReducer, AccountState } from 'src/reducers/account/' import { registrationReducer, @@ -7,17 +8,22 @@ import { import { navigationReducer } from 'src/reducers/navigation/' import { NavigationState } from 'react-navigation' import { ssoReducer, SsoState } from 'src/reducers/sso/' +import { documentsReducer, DocumentsState } from './documents' export const rootReducer = combineReducers({ + settings: settingsReducer, account: accountReducer, registration: registrationReducer, navigation: navigationReducer, sso: ssoReducer, + documents: documentsReducer, }) export interface RootState { + readonly settings: SettingsState readonly account: AccountState readonly registration: RegistrationState readonly navigation: NavigationState readonly sso: SsoState + readonly documents: DocumentsState } diff --git a/src/reducers/registration/loading.ts b/src/reducers/registration/loading.ts index b246ee5d68..ccda009da0 100644 --- a/src/reducers/registration/loading.ts +++ b/src/reducers/registration/loading.ts @@ -1,5 +1,4 @@ import { AnyAction } from 'redux' -import Immutable from 'immutable' import { LoadingState } from 'src/reducers/registration/' const initialState: LoadingState = { @@ -7,12 +6,14 @@ const initialState: LoadingState = { } export const loading = ( - state = Immutable.fromJS(initialState), + state = initialState, action: AnyAction, -): string => { +): LoadingState => { switch (action.type) { case 'SET_LOADING_MSG': - return state.setIn(['loadingMsg'], action.value) + return { + loadingMsg: action.value, + } default: return state } diff --git a/src/reducers/settings/index.ts b/src/reducers/settings/index.ts new file mode 100644 index 0000000000..407163b609 --- /dev/null +++ b/src/reducers/settings/index.ts @@ -0,0 +1,26 @@ +import { AnyAction } from 'redux' + +export interface SettingsState { + readonly locale: string +} + +const initialState: SettingsState = { + locale: '', +} + +export const settingsReducer = ( + state = initialState, + action: AnyAction, +): SettingsState => { + switch (action.type) { + case 'LOAD_SETTINGS': + return action.value + case 'SET_LOCALE': + return { + ...state, + locale: action.value, + } + default: + return state + } +} diff --git a/src/reducers/sso/index.ts b/src/reducers/sso/index.ts index 80b9d948f1..8898c1a0dc 100644 --- a/src/reducers/sso/index.ts +++ b/src/reducers/sso/index.ts @@ -1,8 +1,9 @@ import { AnyAction } from 'redux' +import { IdentitySummary } from '../../actions/sso/types' export interface StateVerificationSummary { id: string - issuer: string + issuer: IdentitySummary selfSigned: boolean expires: string | undefined | Date } @@ -15,7 +16,7 @@ export interface StateTypeSummary { export interface StateCredentialRequestSummary { readonly callbackURL: string - readonly requester: string + readonly requester: IdentitySummary readonly availableCredentials: StateTypeSummary[] readonly requestJWT: string } @@ -43,12 +44,13 @@ export interface SsoState { activePaymentRequest: StatePaymentRequestSummary activeAuthenticationRequest: StateAuthenticationRequestSummary deepLinkLoading: boolean - isDeepLinkInteraction: boolean } const initialState: SsoState = { activeCredentialRequest: { - requester: '', + requester: { + did: '', + }, callbackURL: '', availableCredentials: [], requestJWT: '', @@ -63,7 +65,6 @@ const initialState: SsoState = { description: '', paymentRequest: '', }, - isDeepLinkInteraction: false, // add blank authentication request, which is did, public profile? activeAuthenticationRequest: { requester: '', @@ -83,8 +84,6 @@ export const ssoReducer = ( return { ...state, activeCredentialRequest: action.value } case 'SET_PAYMENT_REQUEST': return { ...state, activePaymentRequest: action.value } - case 'SET_DEEP_LINK_FLAG': - return { ...state, isDeepLinkInteraction: action.value } case 'SET_AUTHENTICATION_REQUEST': return { ...state, activeAuthenticationRequest: action.value } case 'SET_DEEP_LINK_LOADING': diff --git a/src/resources/img/back-26.png b/src/resources/img/back-26.png new file mode 100644 index 0000000000..d7fe65362c Binary files /dev/null and b/src/resources/img/back-26.png differ diff --git a/src/resources/img/expired.png b/src/resources/img/expired.png new file mode 100644 index 0000000000..33443ac505 Binary files /dev/null and b/src/resources/img/expired.png differ diff --git a/src/resources/index.ts b/src/resources/index.ts index 25ed92e007..a985bb2e55 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -8,3 +8,7 @@ export const EmailIcon = require('src/resources/svg/EmailIcon').default export const PhoneIcon = require('src/resources/svg/PhoneIcon').default export const AccessibilityIcon = require('src/resources/svg/AccessibilityIcon') .default +export const IdentityMenuIcon = require('./svg/IdentityMenuIcon').default +export const DocumentsMenuIcon = require('./svg/DocumentsMenuIcon').default +export const RecordsMenuIcon = require('./svg/RecordsMenuIcon').default +export const SettingsMenuIcon = require('./svg/SettingsMenuIcon').default diff --git a/src/resources/svg/DocumentsMenuIcon.js b/src/resources/svg/DocumentsMenuIcon.js new file mode 100644 index 0000000000..dfc30d5e51 --- /dev/null +++ b/src/resources/svg/DocumentsMenuIcon.js @@ -0,0 +1,26 @@ +import React from 'react' +import Svg, { G, Rect } from 'react-native-svg' + +const SvgDocumentsMenuIcon = props => ( + + + + + + +) + +export default SvgDocumentsMenuIcon diff --git a/src/resources/svg/IdentityMenuIcon.js b/src/resources/svg/IdentityMenuIcon.js new file mode 100644 index 0000000000..e8d3582b5f --- /dev/null +++ b/src/resources/svg/IdentityMenuIcon.js @@ -0,0 +1,16 @@ +import React from 'react' +import Svg, { G, Path } from 'react-native-svg' + +const SvgIdentityMenuIcon = props => ( + + + + + + +) + +export default SvgIdentityMenuIcon diff --git a/src/resources/svg/RecordsMenuIcon.js b/src/resources/svg/RecordsMenuIcon.js new file mode 100644 index 0000000000..15fd6b2928 --- /dev/null +++ b/src/resources/svg/RecordsMenuIcon.js @@ -0,0 +1,19 @@ +import React from 'react' +import Svg, { G, Path } from 'react-native-svg' + +const SvgRecordsMenuIcon = props => ( + + + + + + + + +) + +export default SvgRecordsMenuIcon diff --git a/src/resources/svg/SettingsMenuIcon.js b/src/resources/svg/SettingsMenuIcon.js new file mode 100644 index 0000000000..eb529157aa --- /dev/null +++ b/src/resources/svg/SettingsMenuIcon.js @@ -0,0 +1,20 @@ +import React from 'react' +import Svg, { G, Path } from 'react-native-svg' + +const SvgSettingsMenuIcon = props => ( + + + + + + + +) + +export default SvgSettingsMenuIcon diff --git a/src/routeList.ios.ts b/src/routeList.ios.ts deleted file mode 100644 index e77dfad1dc..0000000000 --- a/src/routeList.ios.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const enum routeList { - Landing = 'Landing', - Consent = 'Consent', - Loading = 'Loading', - SeedPhrase = 'SeedPhrase', - Home = 'Home', - Claims = 'Claims', - Exception = 'Exception', - ClaimDetails = 'ClaimDetails', - CredentialDialog = 'CredentialDialog', - QRCodeScanner = 'QRCodeScanner', - PaymentConsent = 'PaymentConsent', - AuthenticationConsent = 'AuthenticationConsent', -} diff --git a/src/routeList.ts b/src/routeList.ts index 51240e61c6..929d50804b 100644 --- a/src/routeList.ts +++ b/src/routeList.ts @@ -1,15 +1,24 @@ -export const enum routeList { +// NOTE: don't use 'const' so that values are useable in both .js and .ts files +export enum routeList { Landing = 'Landing', - Consent = 'Consent', + Entropy = 'Entropy', Loading = 'Loading', SeedPhrase = 'SeedPhrase', + Home = 'Home', Claims = 'Claims', Interactions = 'Interactions', - Exception = 'Exception', - ClaimDetails = 'ClaimDetails', - CredentialDialog = 'CredentialDialog', + Documents = 'Documents', + Records = 'Records', + Settings = 'Settings', QRCodeScanner = 'QRCodeScanner', + + CredentialDialog = 'CredentialDialog', + Consent = 'Consent', PaymentConsent = 'PaymentConsent', AuthenticationConsent = 'AuthenticationConsent', + ClaimDetails = 'ClaimDetails', + DocumentDetails = 'DocumentDetails', + + Exception = 'Exception', } diff --git a/src/routes.ios.ts b/src/routes.ios.ts deleted file mode 100644 index 657ff6420a..0000000000 --- a/src/routes.ios.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { StackNavigator, TabBarTop, TabNavigator } from 'react-navigation' -import { Claims, ClaimDetails } from 'src/ui/home/' -import { Landing } from 'src/ui/landing/' -import { PaymentConsent } from 'src/ui/payment' -import { SeedPhrase, Loading } from 'src/ui/registration/' -import { JolocomTheme } from 'src/styles/jolocom-theme' -import { Exception } from 'src/ui/generic/' -import { Consent } from 'src/ui/sso' -import { CredentialReceive } from 'src/ui/home' -import I18n from 'src/locales/i18n' -import { QRScannerContainer } from 'src/ui/generic/qrcodeScanner' -import { AuthenticationConsent } from 'src/ui/authentication' -const backIcon = require('src/resources/img/left-chevron.png') - -const navigationOptions = { - header: null, -} - -const navOptScreenWCancel = { - headerStyle: { backgroundColor: JolocomTheme.primaryColorBlack }, - headerBackImage: backIcon, - headerBackTitleStyle: { color: JolocomTheme.primaryColorWhite }, - headerTintColor: { color: JolocomTheme.primaryColorWhite }, -} - -const headerTitleStyle = { - fontSize: JolocomTheme.headerFontSize, - fontFamily: JolocomTheme.contentFontFamily, - fontWeight: '300', -} - -const commonNavigationOptions = { - headerTitleStyle, - headerStyle: { - backgroundColor: JolocomTheme.primaryColorBlack, - }, - headerTintColor: JolocomTheme.primaryColorWhite, -} - -export const HomeRoutes = TabNavigator( - { - Claims: { - screen: Claims, - navigationOptions: { - tabBarLabel: I18n.t('All claims'), - headerTitle: I18n.t('My identity'), - ...commonNavigationOptions, - }, - }, - }, - { - tabBarOptions: { - upperCaseLabel: false, - activeTintColor: JolocomTheme.primaryColorSand, - inactiveTintColor: JolocomTheme.primaryColorGrey, - labelStyle: { - fontFamily: JolocomTheme.contentFontFamily, - fontSize: JolocomTheme.labelFontSize, - textAlign: 'center', - }, - style: { - backgroundColor: JolocomTheme.primaryColorBlack, - }, - indicatorStyle: { - backgroundColor: JolocomTheme.primaryColorSand, - }, - }, - tabBarComponent: TabBarTop, - tabBarPosition: 'top', - }, -) - -export const Routes = StackNavigator({ - Landing: { screen: Landing, navigationOptions }, - Loading: { screen: Loading, navigationOptions }, - SeedPhrase: { screen: SeedPhrase, navigationOptions }, - Home: { screen: HomeRoutes }, - CredentialDialog: { - screen: CredentialReceive, - navigationOptions: { - headerTitle: I18n.t('Receiving new credential'), - headerTitleStyle: { - fontFamily: JolocomTheme.contentFontFamily, - fontWeight: '100', - fontSize: JolocomTheme.headerFontSize, - }, - headerStyle: { backgroundColor: JolocomTheme.primaryColorBlack }, - headerTintColor: JolocomTheme.primaryColorWhite, - }, - }, - Consent: { - screen: Consent, - navigationOptions: { - headerTitle: I18n.t('Share claims'), - headerTitleStyle: { - fontFamily: JolocomTheme.contentFontFamily, - fontWeight: '100', - fontSize: JolocomTheme.headerFontSize, - }, - headerStyle: { backgroundColor: JolocomTheme.primaryColorBlack }, - headerTintColor: JolocomTheme.primaryColorWhite, - }, - }, - PaymentConsent: { - screen: PaymentConsent, - navigationOptions: { - headerBackImage: backIcon, - headerBackTitleStyle: { color: JolocomTheme.primaryColorWhite }, - headerTitle: I18n.t('Confirm payment'), - headerTitleStyle: { - fontFamily: JolocomTheme.contentFontFamily, - fontWeight: '100', - fontSize: JolocomTheme.headerFontSize, - }, - headerStyle: { - backgroundColor: JolocomTheme.primaryColorBlack, - }, - headerTintColor: JolocomTheme.primaryColorWhite, - }, - }, - AuthenticationConsent: { - screen: AuthenticationConsent, - navigationOptions: { - headerBackImage: backIcon, - headerBackTitleStyle: { color: JolocomTheme.primaryColorWhite }, - headerTitle: I18n.t('Authorization request'), - headerTitleStyle: { - fontFamily: JolocomTheme.contentFontFamily, - fontWeight: '100', - fontSize: JolocomTheme.headerFontSize, - }, - headerStyle: { - backgroundColor: JolocomTheme.primaryColorBlack, - }, - headerTintColor: JolocomTheme.primaryColorWhite, - }, - }, - Exception: { screen: Exception, navigationOptions }, - ClaimDetails: { - screen: ClaimDetails, - navigationOptions: navOptScreenWCancel, - }, - QRCodeScanner: { - screen: QRScannerContainer, - navigationOptions: navOptScreenWCancel, - }, -}) diff --git a/src/routes.ts b/src/routes.ts index 9e37dfb652..760bc798cd 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1,151 +1,202 @@ -import { StackNavigator, TabBarTop, TabNavigator } from 'react-navigation' -import { Claims, Interactions, ClaimDetails } from 'src/ui/home/' +import { Platform } from 'react-native' +import { StackNavigator, TabNavigator } from 'react-navigation' +import { Claims, Records, ClaimDetails } from 'src/ui/home/' +import { Documents, DocumentDetails } from 'src/ui/documents' import { Landing } from 'src/ui/landing/' import { PaymentConsent } from 'src/ui/payment' -import { SeedPhrase, Loading } from 'src/ui/registration/' +import { SeedPhrase, Loading, Entropy } from 'src/ui/registration/' import { JolocomTheme } from 'src/styles/jolocom-theme' -import { Exception } from 'src/ui/generic/' +import { Exception, BottomNavBar } from 'src/ui/generic/' import { Consent } from 'src/ui/sso' import { CredentialReceive } from 'src/ui/home' +import { Settings } from 'src/ui/settings' import I18n from 'src/locales/i18n' -import { QRScannerContainer } from './ui/generic/qrcodeScanner' -import { AuthenticationConsent } from './ui/authentication' -const closeIcon = require('./resources/img/close.png') +import { QRScannerContainer } from 'src/ui/generic/qrcodeScanner' +import { AuthenticationConsent } from 'src/ui/authentication' +import { routeList } from './routeList' +import strings from './locales/strings' + +import { + IdentityMenuIcon, + RecordsMenuIcon, + DocumentsMenuIcon, + SettingsMenuIcon, +} from 'src/resources' + +const headerBackImage = + Platform.OS === 'android' + ? require('./resources/img/close.png') + : require('./resources/img/back-26.png') const navigationOptions = { header: null, } -const navOptScreenWCancel = { - headerStyle: { backgroundColor: JolocomTheme.primaryColorBlack }, - headerBackImage: closeIcon, -} - const headerTitleStyle = { fontSize: JolocomTheme.headerFontSize, fontFamily: JolocomTheme.contentFontFamily, fontWeight: '300', } +const defaultHeaderBackgroundColor = + Platform.OS === 'android' + ? JolocomTheme.primaryColorBlack + : JolocomTheme.primaryColorGrey + +const defaultHeaderTintColor = + Platform.OS === 'android' + ? JolocomTheme.primaryColorWhite + : JolocomTheme.primaryColorBlack + const commonNavigationOptions = { headerTitleStyle, headerStyle: { - backgroundColor: JolocomTheme.primaryColorBlack, + backgroundColor: defaultHeaderBackgroundColor, + borderBottomWidth: 0, + }, + headerTintColor: defaultHeaderTintColor, +} + +const navOptScreenWCancel = { + headerStyle: { + backgroundColor: defaultHeaderBackgroundColor, + }, + headerTitleStyle: { + color: JolocomTheme.primaryColorWhite, }, - headerTintColor: JolocomTheme.primaryColorWhite, + headerBackImage, + ...Platform.select({ + ios: { + headerTintColor: JolocomTheme.primaryColorPurple, + }, + }), } -export const HomeRoutes = TabNavigator( +const bottomNavBarBackground = + Platform.OS === 'android' + ? '#fafafa' // FIXME add to theme + : JolocomTheme.primaryColorBlack + +export const BottomNavRoutes = TabNavigator( { - Claims: { + [routeList.Claims]: { screen: Claims, - navigationOptions: { - tabBarLabel: I18n.t('All claims'), - headerTitle: I18n.t('My identity'), + navigationOptions: () => ({ ...commonNavigationOptions, - }, + headerTitle: I18n.t(strings.MY_IDENTITY), + tabBarIcon: IdentityMenuIcon, + }), }, - Interactions: { - screen: Interactions, - navigationOptions: { - tabBarLabel: I18n.t('Documents'), - headerTitle: I18n.t('My identity'), + [routeList.Documents]: { + screen: Documents, + navigationOptions: () => ({ ...commonNavigationOptions, - }, + headerTitle: I18n.t(strings.DOCUMENTS), + tabBarIcon: (props: { + tintColor: string + focused: boolean + fillColor?: string + }) => { + props.fillColor = bottomNavBarBackground + return new DocumentsMenuIcon(props) + }, + }), + }, + [routeList.Records]: { + screen: Records, + navigationOptions: () => ({ + ...commonNavigationOptions, + headerTitle: I18n.t(strings.LOGIN_RECORDS), + tabBarIcon: RecordsMenuIcon, + }), + }, + [routeList.Settings]: { + screen: Settings, + navigationOptions: () => ({ + ...commonNavigationOptions, + headerTitle: I18n.t(strings.SETTINGS), + tabBarIcon: SettingsMenuIcon, + }), }, }, { tabBarOptions: { - upperCaseLabel: false, - activeTintColor: JolocomTheme.primaryColorSand, - inactiveTintColor: JolocomTheme.primaryColorGrey, - labelStyle: { - fontFamily: JolocomTheme.contentFontFamily, - fontSize: JolocomTheme.labelFontSize, - textAlign: 'center', - }, + ...Platform.select({ + android: { + activeTintColor: JolocomTheme.primaryColorPurple, + inactiveTintColor: '#9B9B9E', // FIXME + }, + ios: { + activeTintColor: JolocomTheme.primaryColorWhite, + inactiveTintColor: 'rgba(255, 255, 255, 0.5)', + }, + }), + showLabel: false, style: { - backgroundColor: JolocomTheme.primaryColorBlack, - }, - indicatorStyle: { - backgroundColor: JolocomTheme.primaryColorSand, + height: 50, + bottom: 0, + backgroundColor: bottomNavBarBackground, }, }, - tabBarComponent: TabBarTop, - tabBarPosition: 'top', + tabBarComponent: BottomNavBar, + tabBarPosition: 'bottom', }, ) export const Routes = StackNavigator({ - Landing: { screen: Landing, navigationOptions }, - Loading: { screen: Loading, navigationOptions }, - SeedPhrase: { screen: SeedPhrase, navigationOptions }, - Home: { screen: HomeRoutes }, - CredentialDialog: { + [routeList.Landing]: { screen: Landing, navigationOptions }, + [routeList.Entropy]: { screen: Entropy, navigationOptions }, + [routeList.Loading]: { screen: Loading, navigationOptions }, + [routeList.SeedPhrase]: { screen: SeedPhrase, navigationOptions }, + + [routeList.Home]: { screen: BottomNavRoutes }, + [routeList.QRCodeScanner]: { + screen: QRScannerContainer, + navigationOptions: () => ({ + ...navOptScreenWCancel, + }), + }, + + [routeList.CredentialDialog]: { screen: CredentialReceive, - navigationOptions: { - headerTitle: I18n.t('Receiving new credential'), - headerTitleStyle: { - fontFamily: JolocomTheme.contentFontFamily, - fontWeight: '100', - fontSize: JolocomTheme.headerFontSize, - }, - headerStyle: { backgroundColor: JolocomTheme.primaryColorBlack }, - headerTintColor: JolocomTheme.primaryColorWhite, - }, + navigationOptions: () => ({ + headerTitle: I18n.t(strings.RECEIVING_NEW_CREDENTIAL), + ...commonNavigationOptions, + }), }, - Consent: { + [routeList.Consent]: { screen: Consent, - navigationOptions: { - headerTitle: I18n.t('Share claims'), - headerTitleStyle: { - fontFamily: JolocomTheme.contentFontFamily, - fontWeight: '100', - fontSize: JolocomTheme.headerFontSize, - }, - headerStyle: { backgroundColor: JolocomTheme.primaryColorBlack }, - headerTintColor: JolocomTheme.primaryColorWhite, - }, + navigationOptions: () => ({ + headerTitle: I18n.t(strings.SHARE_CLAIMS), + ...commonNavigationOptions, + }), }, - PaymentConsent: { + [routeList.PaymentConsent]: { screen: PaymentConsent, - navigationOptions: { - headerBackImage: closeIcon, - headerTitle: I18n.t('Confirm payment'), - headerTitleStyle: { - color: JolocomTheme.primaryColorWhite, - fontFamily: JolocomTheme.contentFontFamily, - fontWeight: '100', - fontSize: JolocomTheme.headerFontSize, - }, - headerStyle: { - backgroundColor: JolocomTheme.primaryColorBlack, - }, - }, + navigationOptions: () => ({ + headerBackImage, + headerTitle: I18n.t(strings.CONFIRM_PAYMENT), + ...commonNavigationOptions, + }), }, - AuthenticationConsent: { + [routeList.AuthenticationConsent]: { screen: AuthenticationConsent, - navigationOptions: { - headerBackImage: closeIcon, - headerTitle: I18n.t('Authorization request'), - headerTitleStyle: { - color: JolocomTheme.primaryColorWhite, - fontFamily: JolocomTheme.contentFontFamily, - fontWeight: '100', - fontSize: JolocomTheme.headerFontSize, - }, - headerStyle: { - backgroundColor: JolocomTheme.primaryColorBlack, - }, - }, + navigationOptions: () => ({ + headerBackImage, + headerTitle: I18n.t(strings.AUTHORIZATION_REQUEST), + ...commonNavigationOptions, + }), }, - Exception: { screen: Exception, navigationOptions }, - ClaimDetails: { + [routeList.ClaimDetails]: { screen: ClaimDetails, - navigationOptions: navOptScreenWCancel, + navigationOptions: () => navOptScreenWCancel, }, - QRCodeScanner: { - screen: QRScannerContainer, - navigationOptions: navOptScreenWCancel, + [routeList.DocumentDetails]: { + screen: DocumentDetails, + navigationOptions: { + ...navOptScreenWCancel, + headerTitleStyle, + }, }, + [routeList.Exception]: { screen: Exception, navigationOptions }, }) diff --git a/src/store.ts b/src/store.ts index 870e7854c7..61beaffc08 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,11 +1,19 @@ -import { createStore, applyMiddleware } from 'redux' -import thunk from 'redux-thunk' +import { + createStore, + applyMiddleware, + AnyAction as OriginalAnyAction, +} from 'redux' +import { NavigationAction } from 'react-navigation' +import thunk, { + ThunkDispatch as OriginalThunkDispatch, + ThunkAction as OriginalThunkAction, +} from 'redux-thunk' import { RootState, rootReducer } from 'src/reducers' import { BackendMiddleware } from 'src/backendMiddleware' import config from 'src/config' import { Store } from 'react-redux' -export function initStore(): Store<{}> { +export function initStore(): Store { const { createReactNavigationReduxMiddleware, } = require('react-navigation-redux-helpers') @@ -22,3 +30,13 @@ export function initStore(): Store<{}> { applyMiddleware(thunk.withExtraArgument(backendMiddleware)), ) } + +export type AnyAction = OriginalAnyAction | NavigationAction +export type ThunkDispatch = OriginalThunkDispatch< + RootState, + BackendMiddleware, + AnyAction +> +export type ThunkAction< + R = AnyAction | Promise +> = OriginalThunkAction diff --git a/src/ui/authentication/components/AuthenticationConsent.tsx b/src/ui/authentication/components/AuthenticationConsent.tsx index 51296b4b98..e9792c52a5 100644 --- a/src/ui/authentication/components/AuthenticationConsent.tsx +++ b/src/ui/authentication/components/AuthenticationConsent.tsx @@ -4,11 +4,12 @@ import { Text, StyleSheet, View } from 'react-native' import I18n from 'src/locales/i18n' import { StateAuthenticationRequestSummary } from 'src/reducers/sso' import { JolocomTheme } from 'src/styles/jolocom-theme' +import strings from '../../../locales/strings' interface Props { activeAuthenticationRequest: StateAuthenticationRequestSummary - cancelAuthenticationRequest: () => void - confirmAuthenticationRequest: () => void + confirmAuthenticationRequest: Function + cancelAuthenticationRequest: Function } interface State {} @@ -59,7 +60,7 @@ export class AuthenticationConsentComponent extends React.Component< private handleConfirm = () => { this.setState({ pending: true }) - this.props.confirmAuthenticationRequest() + return this.props.confirmAuthenticationRequest() } private renderRequesterCard(requester: string, callbackURL: string) { @@ -88,13 +89,13 @@ export class AuthenticationConsentComponent extends React.Component< return ( - {I18n.t('Would you like to')} + {I18n.t(strings.WOULD_YOU_LIKE_TO)} {description} - {I18n.t('with your SmartWallet?')} + {I18n.t(strings.WITH_YOUR_SMARTWALLET)} ) @@ -105,10 +106,10 @@ export class AuthenticationConsentComponent extends React.Component< this.props.cancelAuthenticationRequest()} + handleDeny={() => this.props.cancelAuthenticationRequest()} // TODO Does this get dispatched correctly? verticalPadding={10} /> ) diff --git a/src/ui/authentication/container/AuthenticationConsent.tsx b/src/ui/authentication/container/AuthenticationConsent.tsx index cd1f29c845..efa0296c73 100644 --- a/src/ui/authentication/container/AuthenticationConsent.tsx +++ b/src/ui/authentication/container/AuthenticationConsent.tsx @@ -1,17 +1,18 @@ import React from 'react' import { connect } from 'react-redux' import { AuthenticationConsentComponent } from '../components/AuthenticationConsent' -import { StateAuthenticationRequestSummary } from 'src/reducers/sso' import { RootState } from 'src/reducers' import { cancelSSO } from 'src/actions/sso' import { sendAuthenticationResponse } from 'src/actions/sso/authenticationRequest' +import { ThunkDispatch } from '../../../store' +import { withErrorHandling } from '../../../actions/modifiers' +import { showErrorScreen } from '../../../actions/generic' +import {NavigationParams} from 'react-navigation' -interface ConnectProps {} - -interface Props extends ConnectProps { - activeAuthenticationRequest: StateAuthenticationRequestSummary - confirmAuthenticationRequest: () => void - cancelAuthenticationRequest: () => void +interface Props + extends ReturnType, + ReturnType { + navigation: { state: { params: NavigationParams } } } interface State {} @@ -21,10 +22,11 @@ export class AuthenticationConsentContainer extends React.Component< State > { render() { + const {isDeepLinkInteraction} = this.props.navigation.state.params return ( this.props.confirmAuthenticationRequest(isDeepLinkInteraction)} cancelAuthenticationRequest={this.props.cancelAuthenticationRequest} /> ) @@ -35,9 +37,10 @@ const mapStateToProps = (state: RootState) => ({ activeAuthenticationRequest: state.sso.activeAuthenticationRequest, }) -const mapDispatchToProps = (dispatch: Function) => ({ - confirmAuthenticationRequest: () => dispatch(sendAuthenticationResponse()), - cancelAuthenticationRequest: () => dispatch(cancelSSO()), +const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ + confirmAuthenticationRequest: (isDeepLinkInteraction: boolean) => + dispatch(withErrorHandling(showErrorScreen)(sendAuthenticationResponse(isDeepLinkInteraction))), + cancelAuthenticationRequest: () => dispatch(cancelSSO), }) export const AuthenticationConsent = connect( diff --git a/src/ui/documents/components/documentCard.tsx b/src/ui/documents/components/documentCard.tsx new file mode 100644 index 0000000000..905d456612 --- /dev/null +++ b/src/ui/documents/components/documentCard.tsx @@ -0,0 +1,116 @@ +import React from 'react' +import { View, StyleSheet, Text, Image, ImageBackground } from 'react-native' +import { JolocomTheme } from 'src/styles/jolocom-theme' +import { DocumentValiditySummary } from './documentValidity' +import { DecoratedClaims } from 'src/reducers/account' +import { ClaimInterface } from 'cred-types-jolocom-core' + +export const DOCUMENT_CARD_HEIGHT = 176 +export const DOCUMENT_CARD_WIDTH = 276 + +interface DocumentCardProps { + document: DecoratedClaims +} + +const styles = StyleSheet.create({ + card: { + height: DOCUMENT_CARD_HEIGHT, + backgroundColor: JolocomTheme.primaryColorWhite, + borderColor: 'rgba(0, 0, 0, 0.09)', + borderWidth: 2, + borderRadius: 10, + width: DOCUMENT_CARD_WIDTH, + overflow: 'hidden', + }, + cardBack: { + width: '100%', + height: '100%', + position: 'absolute', + }, + cardContent: { + paddingVertical: 16, + flex: 1, + }, + documentType: { + paddingHorizontal: 15, + fontSize: 28, + fontFamily: JolocomTheme.contentFontFamily, + }, + documentNumber: { + paddingHorizontal: 15, + fontSize: 17, + fontFamily: JolocomTheme.contentFontFamily, + color: 'rgba(5, 5, 13, 0.4)', + }, + validityContainer: { + flexDirection: 'row', + marginTop: 'auto', + alignItems: 'center', + width: '100%', + height: 50, + paddingHorizontal: 15, + }, + validityText: { + marginLeft: 5, + fontSize: 15, + }, + icon: { + marginLeft: 'auto', + width: 42, + height: 42, + }, +}) + +export const DocumentCard: React.SFC = ({ + document, +}): JSX.Element => { + const { renderInfo, expires } = document + const { background = undefined, logo = undefined, text = undefined } = + renderInfo || {} + const claimData = document.claimData as ClaimInterface + + return ( + + + + + {claimData.type || document.credentialType} + + + {claimData.documentNumber} + + + {expires && ( + + )} + {logo ? ( + + ) : ( + + )} + + + + ) +} diff --git a/src/ui/documents/components/documentDetails.tsx b/src/ui/documents/components/documentDetails.tsx new file mode 100644 index 0000000000..c09dd58770 --- /dev/null +++ b/src/ui/documents/components/documentDetails.tsx @@ -0,0 +1,67 @@ +import React from 'react' +import { View, Text, StyleSheet } from 'react-native' +import { IssuerCard } from 'src/ui/documents/components/issuerCard' +import { JolocomTheme } from 'src/styles/jolocom-theme' +import { DecoratedClaims } from 'src/reducers/account' +import { prepareLabel } from 'src/lib/util' + +interface Props { + document: DecoratedClaims +} + +const styles = StyleSheet.create({ + container: { + paddingBottom: 50, + }, + sectionHeader: { + marginTop: 20, + fontSize: 17, + fontFamily: JolocomTheme.contentFontFamily, + color: 'rgba(0,0,0,0.4)', + paddingHorizontal: 16, + marginBottom: 10, + paddingLeft: 16, + }, + claimsContainer: { + borderTopWidth: 1, + borderColor: '#ececec', + }, + claimCard: { + backgroundColor: JolocomTheme.primaryColorWhite, + paddingVertical: 15, + borderBottomWidth: 1, + borderColor: '#ececec', + }, + claimCardTextContainer: { + paddingHorizontal: 30, + }, + claimCardTitle: { + color: 'rgba(0, 0, 0, 0.4)', + fontSize: 17, + fontFamily: JolocomTheme.contentFontFamily, + }, +}) + +export const DocumentDetails: React.SFC = ({ document }) => { + if (!document) return null + + return ( + + Issued by + {IssuerCard(document.issuer)} + Details + + {Object.keys(document.claimData).map(key => ( + + + {prepareLabel(key)} + + {document.claimData[key]} + + + + ))} + + + ) +} diff --git a/src/ui/documents/components/documentValidity.tsx b/src/ui/documents/components/documentValidity.tsx new file mode 100644 index 0000000000..c328770082 --- /dev/null +++ b/src/ui/documents/components/documentValidity.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { compareDates } from 'src/lib/util' +import { Text, Image, View, StyleSheet } from 'react-native' +import Icon from 'react-native-vector-icons/MaterialCommunityIcons' +import { JolocomTheme } from 'src/styles/jolocom-theme' +const expiredIcon = require('src/resources/img/expired.png') + +interface Props { + expires: Date + color?: string +} + +const styles = StyleSheet.create({ + validityContainer: { + flexDirection: 'row', + alignItems: 'baseline', + }, + validityText: { + marginLeft: 5, + fontFamily: JolocomTheme.contentFontFamily, + fontSize: 17, + }, +}) + +// TODO: Refactor home/components/validitySummary.tsx so we just use one + +export const DocumentValiditySummary: React.SFC = ( + props, +): JSX.Element => { + const isValid = compareDates(new Date(Date.now()), props.expires) > 1 + return isValid ? ( + + + + {`Valid until ${props.expires.toLocaleDateString('en-gb')}`} + + + ) : ( + + + + {`Expired on ${props.expires.toLocaleDateString('en-gb')}`} + + + ) +} diff --git a/src/ui/documents/components/documentViewToggle.tsx b/src/ui/documents/components/documentViewToggle.tsx new file mode 100644 index 0000000000..844f8f8da5 --- /dev/null +++ b/src/ui/documents/components/documentViewToggle.tsx @@ -0,0 +1,75 @@ +import React from 'react' +import { View, StyleSheet, Text, TouchableWithoutFeedback } from 'react-native' +import { JolocomTheme } from 'src/styles/jolocom-theme' +import Icon from 'react-native-vector-icons/MaterialCommunityIcons' + +export interface DocumentViewToggleProps { + showingValid: boolean + onTouch: () => void +} + +const styles = StyleSheet.create({ + container: { + paddingTop: 10, + paddingHorizontal: 15, + }, + bar: { + paddingHorizontal: 16, + paddingVertical: 5, + flexDirection: 'row', + alignItems: 'baseline', + justifyContent: 'center', + borderRadius: 4, + }, + barValid: { + backgroundColor: 'rgba(233, 239, 221, 0.57)', + }, + barExpired: { + backgroundColor: 'rgba(255, 222, 188, 0.25)', + }, + icon: { + color: 'rgba(5, 5, 13, 0.48)', + marginRight: 8, + }, + iconExpired: { + color: 'rgba(5, 5, 13, 0.48)', + fontSize: 17, + fontFamily: JolocomTheme.contentFontFamily, + fontWeight: 'bold', + marginRight: 8, + }, + text: { + color: 'rgba(5, 5, 13, 0.48)', + fontSize: 17, + fontFamily: JolocomTheme.contentFontFamily, + }, + underline: { + textDecorationLine: 'underline', + }, +}) + +export const DocumentViewToggle: React.SFC = ( + props, +): JSX.Element => ( + + + {props.showingValid ? ( + + + + Showing valid.{' '} + Tap to show expired. + + + ) : ( + + + + Showing expired.{' '} + Tap to show valid. + + + )} + + +) diff --git a/src/ui/documents/components/documentsCarousel.tsx b/src/ui/documents/components/documentsCarousel.tsx new file mode 100644 index 0000000000..4726a3d7ca --- /dev/null +++ b/src/ui/documents/components/documentsCarousel.tsx @@ -0,0 +1,40 @@ +import * as React from 'react' +import { Dimensions, View } from 'react-native' +import Carousel from 'react-native-snap-carousel' + +import { DecoratedClaims } from 'src/reducers/account' + +import { DocumentDetails } from './documentDetails' +import { DocumentCard, DOCUMENT_CARD_WIDTH } from './documentCard' + +interface DocumentsCarouselProps { + activeIndex: number + documents: DecoratedClaims[] + onActiveIndexChange: (index: number) => void +} + +const renderItem = ({ item }: { item: DecoratedClaims }): JSX.Element => ( + +) + +export const DocumentsCarousel: React.SFC = ( + props, +): JSX.Element => { + const viewWidth: number = Dimensions.get('window').width + const { documents, activeIndex, onActiveIndexChange } = props + + return ( + + + + + ) +} diff --git a/src/ui/documents/components/documentsList.tsx b/src/ui/documents/components/documentsList.tsx new file mode 100644 index 0000000000..15805c995a --- /dev/null +++ b/src/ui/documents/components/documentsList.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import { StyleSheet, TouchableOpacity } from 'react-native' +import { DocumentCard } from './documentCard' +//import Icon from 'react-native-vector-icons/MaterialCommunityIcons' +import { DecoratedClaims } from 'src/reducers/account' +import { SCROLL_PADDING_BOTTOM } from 'src/ui/generic' + +interface DocumentsListProps { + onDocumentPress?: (document: DecoratedClaims) => void + documents: DecoratedClaims[] +} + +const styles = StyleSheet.create({ + documentContainer: { + paddingTop: 15, + paddingBottom: SCROLL_PADDING_BOTTOM, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, +}) + +export const DocumentsList: React.SFC = ( + props, +): JSX.Element => ( + + {props.documents.map((document, idx) => ( + props.onDocumentPress && props.onDocumentPress(document)} + > + + {/* */} + + ))} + +) diff --git a/src/ui/documents/components/issuerCard.tsx b/src/ui/documents/components/issuerCard.tsx new file mode 100644 index 0000000000..969cade1f0 --- /dev/null +++ b/src/ui/documents/components/issuerCard.tsx @@ -0,0 +1,66 @@ +import React from 'react' +import { StyleSheet, Image } from 'react-native' +import { JolocomTheme } from 'src/styles/jolocom-theme' +import { ClaimCard } from '../../sso/components/claimCard' +import { IdentitySummary } from '../../../actions/sso/types' +import { isEmpty } from 'ramda' + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + backgroundColor: JolocomTheme.primaryColorWhite, + paddingVertical: 18, + paddingLeft: 15, + paddingRight: 30, + borderTopWidth: 1, + borderBottomWidth: 1, + borderColor: '#ececec', + }, + icon: { + width: 42, + height: 42, + }, + textContainer: { + marginLeft: 16, + }, + issuerName: JolocomTheme.textStyles.light.textDisplayField, + urlText: { + fontSize: 17, + color: JolocomTheme.primaryColorPurple, + fontFamily: JolocomTheme.contentFontFamily, + }, +}) + +export const IssuerCard = (issuer: IdentitySummary): JSX.Element => { + const { image, primaryText, secondaryText } = convertToClaimCard(issuer) + return ( + } + /> + ) +} + +const convertToClaimCard = ({ did, publicProfile }: IdentitySummary) => { + if (!publicProfile || isEmpty(publicProfile)) { + return { + description: 'No description provided', + image: undefined, + primaryText: (did && did.substr(0, 25) + '...') || 'TODO', + secondaryText: 'Service Name', + } + } else { + return { + primaryText: publicProfile.url, + secondaryText: publicProfile.name, + image: publicProfile.image, + description: publicProfile.description, + } + } +} diff --git a/src/ui/documents/containers/documentDetails.tsx b/src/ui/documents/containers/documentDetails.tsx new file mode 100644 index 0000000000..a956e3ee58 --- /dev/null +++ b/src/ui/documents/containers/documentDetails.tsx @@ -0,0 +1,74 @@ +import React from 'react' +import { connect } from 'react-redux' +import { NavigationScreenProps } from 'react-navigation' +import { View, StyleSheet, ScrollView } from 'react-native' +import { ClaimInterface } from 'cred-types-jolocom-core' +import { RootState } from 'src/reducers/' +import I18n from 'src/locales/i18n' +import { DocumentCard } from '../components/documentCard' +import { DocumentDetails as DocumentDetailsComponent } from '../components/documentDetails' +import strings from '../../../locales/strings' + +interface Props + extends ReturnType, + NavigationScreenProps {} + +interface State {} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + documentCardContainer: { + paddingVertical: 15, + justifyContent: 'center', + alignItems: 'center', + }, +}) + +export class DocumentDetailsContainer extends React.Component { + public static navigationOptions = ({ + navigation, + navigationOptions, + }: NavigationScreenProps) => ({ + ...navigationOptions, + // FIXME needs to be changed to this on update to react-navigation + // headerTitle: navigation.getParam('headerTitle', ''), + headerTitle: navigation.state.params && navigation.state.params.headerTitle, + }) + + public componentWillMount() { + const { document } = this.props + // FIXME use filter from src/lib/filterDecoratedClaims + const isExpired = document.expires + ? document.expires.valueOf() < new Date().valueOf() + : true + + let headerTitle = (document.claimData as ClaimInterface).type + if (isExpired) headerTitle = `[${I18n.t(strings.EXPIRED)}] ${headerTitle}` + this.props.navigation.setParams({ headerTitle }) + } + + public render(): JSX.Element { + const { document } = this.props + + return ( + + + + + + + + + ) + } +} + +const mapStateToProps = (state: RootState) => ({ + document: state.documents.selectedDocument, +}) + +export const DocumentDetails = connect(mapStateToProps)( + DocumentDetailsContainer, +) diff --git a/src/ui/documents/containers/documents.tsx b/src/ui/documents/containers/documents.tsx new file mode 100644 index 0000000000..c11a7e9196 --- /dev/null +++ b/src/ui/documents/containers/documents.tsx @@ -0,0 +1,155 @@ +import React from 'react' +import { connect } from 'react-redux' +import { StyleSheet, Animated, ScrollView } from 'react-native' + +import { openDocumentDetails } from 'src/actions/documents' +import { DecoratedClaims } from 'src/reducers/account' +import { RootState } from 'src/reducers' +import { getDocumentClaims } from 'src/utils/filterDocuments' +import { ThunkDispatch } from 'src/store' +import { Container, Block, CenteredText } from 'src/ui/structure' + +import { JolocomTheme } from 'src/styles/jolocom-theme' +import I18n from 'src/locales/i18n' +import { filters } from 'src/lib/filterDecoratedClaims' + +import { DocumentsCarousel } from '../components/documentsCarousel' +import { DocumentsList } from '../components/documentsList' +import { DocumentViewToggle } from '../components/documentViewToggle' +import strings from '../../../locales/strings' + +interface Props + extends ReturnType, + ReturnType {} + +interface State { + activeDocumentIndex: number + showingValid: boolean +} + +/* +const APPBAR_HEIGHT = Platform.select({ + ios: 44, + android: 56, + default: 64, +}) +*/ + +const styles = StyleSheet.create({ + mainContainer: { + flex: 1, + backgroundColor: JolocomTheme.primaryColorGrey, + }, + topContainer: { + paddingVertical: 15, + justifyContent: 'center', + alignItems: 'center', + }, + centeredText: { + fontFamily: JolocomTheme.contentFontFamily, + fontSize: 30, // FIXME + color: '#959595', // FIXME + }, +}) + +export class DocumentsContainer extends React.Component { + public ScrollViewRef: ScrollView | null = null + + public state = { + activeDocumentIndex: 0, + showingValid: true, + } + + private scrollToTop(): void { + if (this.ScrollViewRef) { + this.ScrollViewRef.scrollTo({ x: 0, y: 0, animated: true }) + } + } + + private onActiveIndexChange(index: number): void { + this.setState({ activeDocumentIndex: index }) + this.scrollToTop() + } + + private handleToggle = () => { + this.setState((prevState: State) => ({ + showingValid: !prevState.showingValid, + activeDocumentIndex: 0, + })) + this.scrollToTop() + } + + public render(): JSX.Element { + const { openDocumentDetails, decoratedCredentials } = this.props + const documents = getDocumentClaims(decoratedCredentials['Other']) + const docFilter = this.state.showingValid + ? filters.filterByValid + : filters.filterByExpired + const displayedDocs = docFilter(documents) + const isEmpty = displayedDocs.length === 0 + const otherIsEmpty = displayedDocs.length === documents.length + + return ( + + {!otherIsEmpty && ( + + )} + (this.ScrollViewRef = ref)} + > + {isEmpty ? ( + + + + + + ) : this.state.showingValid ? ( + + ) : ( + + )} + + + ) + } +} + +const mapStateToProps = ({ + account: { + claims: { decoratedCredentials }, + }, +}: RootState) => ({ + decoratedCredentials, +}) + +const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ + openDocumentDetails: (doc: DecoratedClaims) => + dispatch(openDocumentDetails(doc)), +}) + +export const Documents = connect( + mapStateToProps, + mapDispatchToProps, +)(DocumentsContainer) diff --git a/src/ui/documents/index.tsx b/src/ui/documents/index.tsx new file mode 100644 index 0000000000..7c2e4ccaaa --- /dev/null +++ b/src/ui/documents/index.tsx @@ -0,0 +1,2 @@ +export { Documents } from 'src/ui/documents/containers/documents' +export { DocumentDetails } from 'src/ui/documents/containers/documentDetails' diff --git a/src/ui/generic/bottomActionBar.tsx b/src/ui/generic/bottomActionBar.tsx deleted file mode 100644 index 35108a1e2a..0000000000 --- a/src/ui/generic/bottomActionBar.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { StyleSheet, View, TouchableOpacity } from 'react-native' -import { JolocomTheme } from 'src/styles/jolocom-theme' -import * as React from 'react' -import Icon from 'react-native-vector-icons/MaterialCommunityIcons' - -const NAVIGATION_HEIGHT = 82 -const NAVIGATION_CONTENT_HEIGHT = NAVIGATION_HEIGHT - 32 - -interface ActionBarProps { - openScanner: () => void -} - -const styles = StyleSheet.create({ - navigationContent: { - height: NAVIGATION_CONTENT_HEIGHT, - backgroundColor: '#fafafa', - width: '100%', - flexDirection: 'row', - justifyContent: 'space-around', - alignItems: 'center', - }, - navigationContentItem: { - flex: 1, - flexDirection: 'row', - justifyContent: 'space-around', - alignItems: 'center', - }, - contentLeft: { - marginRight: 36, - }, - contentRight: { - marginLeft: 36, - }, - qrCodeButton: { - position: 'absolute', - bottom: 6, - height: 72, - width: 72, - borderRadius: 35, - backgroundColor: JolocomTheme.primaryColorPurple, - alignItems: 'center', - justifyContent: 'center', - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.8, - shadowRadius: 2, - elevation: 8, - }, -}) - -export class BottomActionBar extends React.Component { - render() { - return ( - - - - - - - - - - - ) - } -} diff --git a/src/ui/generic/bottomNavBar.d.ts b/src/ui/generic/bottomNavBar.d.ts new file mode 100644 index 0000000000..44182e0e3e --- /dev/null +++ b/src/ui/generic/bottomNavBar.d.ts @@ -0,0 +1,4 @@ +import * as React from 'react' +import { TabBarBottomProps } from 'react-navigation' + +declare class BottomNavBar extends React.Component {} diff --git a/src/ui/generic/bottomNavBar.js b/src/ui/generic/bottomNavBar.js new file mode 100644 index 0000000000..7ff3830b90 --- /dev/null +++ b/src/ui/generic/bottomNavBar.js @@ -0,0 +1,414 @@ +/** + * NOTE: this is copied from react-navigation/src/views/TabView/TabBarBottom.js + * and modified to add the QRButton at QR_CODE_BUTTON_INDEX + * + * The class in react-navigation/src/views/TabView/TabBarBottom is not + * exported, but instead it is wrapped using withOrientation (as below) + * so the original class is captured in a closure and is inaccessible, so + * had to be copied here. + * + * Alterations to the original are marked with // + */ + +// +const QR_CODE_BUTTON_INDEX = 2 +import { JolocomTheme } from 'src/styles/jolocom-theme' +import { routeList } from 'src/routeList' +import { TouchableOpacity } from 'react-native' +import Icon from 'react-native-vector-icons/MaterialCommunityIcons' +// + +import React from 'react' +import { + Animated, + TouchableWithoutFeedback, + StyleSheet, + View, + Platform, + Keyboard, +} from 'react-native' +import SafeAreaView from 'react-native-safe-area-view' + +// to use absolute imports +import TabBarIcon from 'react-navigation/src/views/TabView/TabBarIcon' +import NavigationActions from 'react-navigation/src/NavigationActions' +import withOrientation from 'react-navigation/src/views/withOrientation' +// + +const majorVersion = parseInt(Platform.Version, 10) +const isIos = Platform.OS === 'ios' +const isIOS11 = majorVersion >= 11 && isIos +const defaultMaxTabBarItemWidth = 125 + +class TabBarBottom extends React.PureComponent { + // See https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/UIKitUICatalog/UITabBar.html + static defaultProps = { + activeTintColor: '#3478f6', // Default active tint color in iOS 10 + activeBackgroundColor: 'transparent', + inactiveTintColor: '#929292', // Default inactive tint color in iOS 10 + inactiveBackgroundColor: 'transparent', + showLabel: true, + showIcon: true, + allowFontScaling: true, + adaptive: isIOS11, + } + + _renderLabel = scene => { + const { + position, + navigation, + activeTintColor, + inactiveTintColor, + labelStyle, + showLabel, + showIcon, + isLandscape, + allowFontScaling, + } = this.props + if (showLabel === false) { + return null + } + const { index } = scene + const { routes } = navigation.state + // Prepend '-1', so there are always at least 2 items in inputRange + const inputRange = [-1, ...routes.map((x, i) => i)] + const outputRange = inputRange.map(inputIndex => + inputIndex === index ? activeTintColor : inactiveTintColor, + ) + const color = position.interpolate({ + inputRange, + outputRange: outputRange, + }) + + const tintColor = scene.focused ? activeTintColor : inactiveTintColor + const label = this.props.getLabel({ ...scene, tintColor }) + + if (typeof label === 'string') { + return ( + + {label} + + ) + } + + if (typeof label === 'function') { + return label({ ...scene, tintColor }) + } + + return label + } + + _renderIcon = scene => { + const { + position, + navigation, + activeTintColor, + inactiveTintColor, + renderIcon, + showIcon, + showLabel, + } = this.props + if (showIcon === false) { + return null + } + + const horizontal = this._shouldUseHorizontalTabs() + + return ( + + ) + } + + _renderTestIDProps = scene => { + const testIDProps = + this.props.getTestIDProps && this.props.getTestIDProps(scene) + return testIDProps + } + + _tabItemMaxWidth() { + const { tabStyle, layout } = this.props + let maxTabBarItemWidth + + const flattenedTabStyle = StyleSheet.flatten(tabStyle) + + if (flattenedTabStyle) { + if (typeof flattenedTabStyle.width === 'number') { + maxTabBarItemWidth = flattenedTabStyle.width + } else if ( + typeof flattenedTabStyle.width === 'string' && + flattenedTabStyle.width.endsWith('%') + ) { + const width = parseFloat(flattenedTabStyle.width) + if (Number.isFinite(width)) { + maxTabBarItemWidth = layout.width * (width / 100) + } + } else if (typeof flattenedTabStyle.maxWidth === 'number') { + maxTabBarItemWidth = flattenedTabStyle.maxWidth + } else if ( + typeof flattenedTabStyle.maxWidth === 'string' && + flattenedTabStyle.width.endsWith('%') + ) { + const width = parseFloat(flattenedTabStyle.maxWidth) + if (Number.isFinite(width)) { + maxTabBarItemWidth = layout.width * (width / 100) + } + } + } + + if (!maxTabBarItemWidth) { + maxTabBarItemWidth = defaultMaxTabBarItemWidth + } + + return maxTabBarItemWidth + } + + _shouldUseHorizontalTabs() { + const { routes } = this.props.navigation.state + const { isLandscape, layout, adaptive, tabStyle } = this.props + + if (!adaptive) { + return false + } + + let tabBarWidth = layout.width + if (tabBarWidth === 0) { + return Platform.isPad + } + + if (!Platform.isPad) { + return isLandscape + } else { + const maxTabBarItemWidth = this._tabItemMaxWidth() + return routes.length * maxTabBarItemWidth <= tabBarWidth + } + } + + _handleTabPress = index => { + const { jumpToIndex, navigation } = this.props + const currentIndex = navigation.state.index + + if (currentIndex === index) { + let childRoute = navigation.state.routes[index] + if (childRoute.hasOwnProperty('index') && childRoute.index > 0) { + navigation.dispatch(NavigationActions.popToTop({ key: childRoute.key })) + } else { + // TODO: do something to scroll to top + } + } else { + jumpToIndex(index) + } + } + + render() { + const { + position, + navigation, + jumpToIndex, + getOnPress, + getTestIDProps, + activeBackgroundColor, + inactiveBackgroundColor, + style, + animateStyle, + tabStyle, + isLandscape, + } = this.props + const { routes } = navigation.state + const previousScene = routes[navigation.state.index] + // Prepend '-1', so there are always at least 2 items in inputRange + const inputRange = [-1, ...routes.map((x, i) => i)] + + const tabBarStyle = [ + styles.tabBar, + this._shouldUseHorizontalTabs() && !Platform.isPad + ? styles.tabBarCompact + : styles.tabBarRegular, + style, + ] + + // + const openScanner = () => + this.props.navigation.navigate(routeList.QRCodeScanner) + const QRCodeButtonPlaceholder = ( + + ) + const QRCodeButton = ( + + + + ) + // + + return ( + + {/**/ QRCodeButton /**/} + + {routes.map((route, index) => { + const focused = index === navigation.state.index + const scene = { route, index, focused } + const onPress = getOnPress(previousScene, scene) + const outputRange = inputRange.map(inputIndex => + inputIndex === index + ? activeBackgroundColor + : inactiveBackgroundColor, + ) + const backgroundColor = position.interpolate({ + inputRange, + outputRange: outputRange, + }) + + const justifyContent = this.props.showIcon ? 'flex-end' : 'center' + const extraProps = this._renderTestIDProps(scene) || {} + const { testID, accessibilityLabel } = extraProps + + // + // added React.Fragment and QRCodeButtonPlaceholder + // + return ( + + {QR_CODE_BUTTON_INDEX == index && QRCodeButtonPlaceholder} + + onPress + ? onPress({ + previousScene, + scene, + jumpToIndex, + defaultHandler: this._handleTabPress, + }) + : this._handleTabPress(index) + } + > + + + {this._renderIcon(scene)} + {this._renderLabel(scene)} + + + + + ) + })} + + + ) + } +} + +const DEFAULT_HEIGHT = 49 +const COMPACT_HEIGHT = 29 + +const styles = StyleSheet.create({ + tabBar: { + backgroundColor: '#F7F7F7', // Default background color in iOS 10 + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: 'rgba(0, 0, 0, .3)', + flexDirection: 'row', + }, + tabBarCompact: { + height: COMPACT_HEIGHT, + }, + tabBarRegular: { + height: DEFAULT_HEIGHT, + }, + tab: { + flex: 1, + alignItems: isIos ? 'center' : 'stretch', + }, + tabPortrait: { + justifyContent: 'flex-end', + flexDirection: 'column', + }, + tabLandscape: { + justifyContent: 'center', + flexDirection: 'row', + }, + iconWithoutLabel: { + flex: 1, + }, + iconWithLabel: { + flex: 1, + }, + iconWithExplicitHeight: { + height: Platform.isPad ? DEFAULT_HEIGHT : COMPACT_HEIGHT, + }, + label: { + textAlign: 'center', + backgroundColor: 'transparent', + }, + labelBeneath: { + fontSize: 10, + marginBottom: 1.5, + }, + labelBeside: { + fontSize: 13, + marginLeft: 20, + }, + // + qrCodeButton: { + position: 'absolute', + top: -COMPACT_HEIGHT, + height: 72, + width: 72, + borderRadius: 36, + backgroundColor: JolocomTheme.primaryColorPurple, + alignSelf: 'center', + alignItems: 'center', + justifyContent: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.8, + shadowRadius: 2, + elevation: 8, + zIndex: 10, + }, + // +}) + +export const BottomNavBar = withOrientation(TabBarBottom) diff --git a/src/ui/generic/exception.tsx b/src/ui/generic/exception.tsx index 15ec8e8d3e..615afa8bba 100644 --- a/src/ui/generic/exception.tsx +++ b/src/ui/generic/exception.tsx @@ -8,10 +8,14 @@ import { routeList } from 'src/routeList' import I18n from 'src/locales/i18n' import { AppError, errorTitleMessages } from 'src/lib/errors' import { getRandomStringFromArray } from 'src/utils/getRandomStringFromArray' +import strings from 'src/locales/strings' +import { ThunkDispatch } from '../../store' const errorImage = require('src/resources/img/error_image.png') interface ConnectProps { - navigateBack: (routeName: routeList) => void + navigateBack: ( + routeName: string, + ) => ReturnType } interface Props extends ConnectProps { @@ -84,9 +88,12 @@ const styles = StyleSheet.create({ export const ExceptionComponent: React.SFC = (props): JSX.Element => { // TODO: display error code const err = props.navigation.state.params.error + console.log(err) const errorTitle = props.errorTitle || getRandomStringFromArray(errorTitleMessages) - let errorText = err ? err.message : 'There was an error with your request' + let errorText = err + ? err.message + : strings.THERE_WAS_AN_ERROR_WITH_YOUR_REQUEST errorText = I18n.t(errorText) + '.' console.error(err && err.origError ? err.origError : err) @@ -95,8 +102,8 @@ export const ExceptionComponent: React.SFC = (props): JSX.Element => { - {I18n.t(errorTitle)} - {errorText} + {I18n.t(errorTitle) + '.'} + {errorText} @@ -110,7 +117,7 @@ export const ExceptionComponent: React.SFC = (props): JSX.Element => { text: styles.buttonText, }} upperCase={false} - text={I18n.t('Go back')} + text={I18n.t(strings.GO_BACK)} /> @@ -119,8 +126,8 @@ export const ExceptionComponent: React.SFC = (props): JSX.Element => { const mapStateToProps = (): {} => ({}) -const mapDispatchToProps = (dispatch: Function): {} => ({ - navigateBack: (routeName: routeList) => +const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ + navigateBack: (routeName: string) => dispatch(navigationActions.navigatorReset({ routeName })), }) diff --git a/src/ui/generic/index.ts b/src/ui/generic/index.ts index 8c451bf8e7..d8489e7786 100644 --- a/src/ui/generic/index.ts +++ b/src/ui/generic/index.ts @@ -1,4 +1,6 @@ export { Exception } from './exception' export { LoadingScreen } from './loading' export { LoadingSpinner } from './loadingSpinner' -export { BottomActionBar } from './bottomActionBar' +export { BottomNavBar } from './bottomNavBar' + +export const SCROLL_PADDING_BOTTOM = 36 diff --git a/src/ui/generic/qrcodeScanner.tsx b/src/ui/generic/qrcodeScanner.tsx index 8775441ac0..89e28b876d 100644 --- a/src/ui/generic/qrcodeScanner.tsx +++ b/src/ui/generic/qrcodeScanner.tsx @@ -1,13 +1,25 @@ import React from 'react' -import { Text, StyleSheet } from 'react-native' +import { StyleSheet, Text } from 'react-native' import { connect } from 'react-redux' import { Container } from 'src/ui/structure' import { JolocomTheme } from 'src/styles/jolocom-theme' import { Button } from 'react-native-material-ui' import { QrScanEvent } from 'src/ui/generic/qrcodeScanner' -import { ssoActions, navigationActions } from 'src/actions' +import { navigationActions } from 'src/actions' import I18n from 'src/locales/i18n' import { LoadingSpinner } from './loadingSpinner' +import strings from '../../locales/strings' +import { JolocomLib } from 'jolocom-lib' +import { interactionHandlers } from '../../lib/storage/interactionTokens' +import { ThunkDispatch } from '../../store' +import { showErrorScreen } from '../../actions/generic' +import { RootState } from '../../reducers' +import { goBack } from '../../actions/navigation' +import { withErrorHandling, withLoading } from '../../actions/modifiers' +import { NavigationNavigateAction } from 'react-navigation' +import { AppError, ErrorCode } from '../../lib/errors' +import { setDeepLinkLoading } from '../../actions/sso' + const QRScanner = require('react-native-qrcode-scanner').default export interface QrScanEvent { @@ -16,8 +28,8 @@ export interface QrScanEvent { interface Props { loading: boolean - onScannerSuccess: (e: QrScanEvent) => void - onScannerCancel: () => void + onScannerSuccess: (e: QrScanEvent) => Promise + onScannerCancel: () => typeof goBack } interface State {} @@ -37,12 +49,14 @@ export class QRcodeScanner extends React.Component { onScannerSuccess(e)} - topContent={{I18n.t('You can scan the qr code now!')}} + topContent={ + {I18n.t(strings.YOU_CAN_SCAN_THE_QR_CODE_NOW)} + } bottomContent={