Skip to content

Commit

Permalink
First Release (#146)
Browse files Browse the repository at this point in the history
* speed up transaction list render. Add loading spinner when fetching next page

* fix notifications scroll and sizes styles. Fixed AcountBalanceWidget styles and add refresh swipe

* add waving loading animation to AccountBalanceWidget

* add Notification top margin of SideBar height

* fix Sidebar TitleBar position. Handle big balance of selected account in Sidebar

* disallow to create account with an existed name

* create account. add error message if private key is already imported and if it is invalid

* Added version 4 and changed branch names

* Solved #3 Wrong amount on invoice

* Solved #11 changing account when changing node

* fix AccountBalanceWidget huge balance go off of the screen bug. Add ScrollView

* fix naming of delete account. Fix modal styles. Add descriptions

* fix button disable for the empty name and invalid address on AddContact

* add address qr to AccountDetails

* add privateKey verify for create account via pk qr

* add harvesting not enough balance message view

* unhardcode harvesting copy

* add support of privateKey scanner for InputAddress

* CreateAccount. Remove qr button. Add InputAddress

* disable sidebar account list while loading

* Solved #8 Harvesting fees earned not taking into account divisibility

* Solved #8 Harvesting fees earned not taking into account divisibility

* Solved #16 Language switch doesn't work properly

* Solved #38 Backup account is hanging on passcode

* Solved #3 Compatibility with old mobile wallet versions doesn't work

* created QRImage component

* add address support to QRImage. Remove qr logic from AccountDetails

* add secret view to QRImage

* add password modals to QRImage and InputAdress:

* add QR error messages

* remlace ContactProfile qr logic with QRImage component

* add transaction support to QRImage. Replace Receive screen qr logic with component

* create QRScanner screen

* add support of Address and PrivateKey support to QRScanner. Add set up props to Send and CreateAccount screens

* add access to Account Details from balance widget

* add new icon for qr scanner

* add transaction qr support to scanner

* add QRService

* add privateKey logic to QR scanner. Ad password modal

* add privateKey logic to QRService

* add the CreateAccount privateKey prop. Get mosaic name at QRService

* optimizeformat mosaic reqest calls

* add SymbolPageView to ScanQR.Change styles

* fix qr service logic. Add qr scanner action buttons

* fix qr scanner buttons style

* fix missing arg QRService

* fix invalid qr image network type

* add create qr button to Receive screen. Add onReady and isVisible props to QR image. Fix bug of delay qr rendering

* add props to AddContact screen

* add qr service check for transfer tx type

* fix qr scanner back button closes the app

* add support of unencrypted private key qr

* hide private key qr from Account Details

* fix navigation styles

* Removed harvesting image

* Improved translations support added EN as default language

* Added japanese

* Update Home.js

* Fixed format date propagated to news

* Update Send.js

* Removed bad testnet node

* Added margin to sidebar for iphones

* add number check to amount input

* Japanese translations

* Added translations and fixed current ones

* Updated testnet and added dynamic fees

* Fixed #27 Account details. Show seed account index

* Fixed #75 Editing address book contact double entry

* Fixed #75 default "0" when making transactions

* Fixed #64 Removed the ability to hide the main account

* Added translations and removed console.logs

* Solved #79

* Fixed #74

* Fixed #89

* Added flex grow to backup page for lower resolutions

* Fixed #4

* Fixed #57

* Fixed #87

* Fixed #43

* Fixed #69

* Fixed #97

* Fixed #75

* Fixed #36

* Fixed #36

* Fixed #13

* Fixed #72

* Fixed #71

* Fixed iOS build for iPad

* Added fixed height to input, refresh mosaics and fixed receive translations

* New Harvesting logic. First steps

* Added harvesting linked keys

* Setting default node to us-west-1

* Added translations

* Resolved #103

* add warning for multi-mosaic transfer qr

* add warning text type

* add notification and disallow to put unowned mosaicName to Send form

* upf

* validate send amount value

* upd

* disallow negative amount input

* replace copy with translate function

* Account refactor

* Added change to testnet confirmation modal

* Resolved #79

* Resolved #106

* Added derivation path to index convertion to util func

* Added translation

* Removed experimental node

* Added missing translations

* Added experimental node #129

* Fixed app crashing when there's no internet #126

* Fixed broken Home design on Rus translation #118

* Fixed Harvesting Activation progress doesn't change #123

* Fix Android "Node is down" #128

* Added There is no way to add a node #121

* Resolved Mainnet - transaction list is not loading #130

* Fixed node public key for harvesting

* Added support for OptIn accounts with old curve, broadcast offline transactions, harvesting fix, custom node fix

* Resolved review

* Resolved #136,#134,#114 and added translations

* Resolved #136,#134,#114 and added translations

* Fixed mnemonic keys (#142)

* Fixed mnemonic keys

* Added circleci builds for this branch

* Using legacy mnemonics key

* Remove unused migration 1

* Save mnemonic on migration

* Save mnemonic on migration

* Save mnemonic on migration

* Update optin from mnemonic

* Save mnemonic on migration

* Loading screen on opt in preparation

* Save mnemonic on migration

* Save mnemonic on migration

* Changed build id method and target sdk version

* Added terms and conditions and removed keys from env

Co-authored-by: AdriaCarrera <[email protected]>

Co-authored-by: Oleh Makarenko <[email protected]>
Co-authored-by: AdriaCarrera <[email protected]>
Co-authored-by: Steven Liu <[email protected]>
Co-authored-by: OlegMakarenko <[email protected]>
  • Loading branch information
5 people authored Mar 12, 2021
1 parent e178af4 commit 4acb0f1
Show file tree
Hide file tree
Showing 109 changed files with 4,695 additions and 1,211 deletions.
5 changes: 3 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:

- run:
name: Fastlane
command: cd ios && export BUILD_NUMBER=${CIRCLE_BUILD_NUM}00 && export VERSION_NUMBER=3.0.${BUILD_NUMBER} && bundle exec fastlane $FASTLANE_LANE
command: cd ios && export BUILD_NUMBER=${CIRCLE_BUILD_NUM} && export VERSION_NUMBER=4 && bundle exec fastlane $FASTLANE_LANE

- store_artifacts:
path: ios/output
Expand All @@ -56,4 +56,5 @@ workflows:
filters:
branches:
only:
- master
- main
- dev
2 changes: 1 addition & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
android:theme="@style/AppTheme">
<meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_name"
android:value="Nem Catapult Wallet"/>
Expand Down
6 changes: 3 additions & 3 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
buildscript {
ext {
minSdkVersion = 23
compileSdkVersion = 28
targetSdkVersion = 28
compileSdkVersion = 29
targetSdkVersion = 29
supportLibVersion = "28.0.0"
RNNKotlinVersion = "1.3.61" // Or any version above 1.3.x
RNNKotlinStdlib = "kotlin-stdlib-jdk8"
Expand Down Expand Up @@ -55,4 +55,4 @@ subprojects { subproject ->
}
}
}
}
}
4 changes: 2 additions & 2 deletions android/tools/script-git-version.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ ext {
git = Grgit.open(currentDir: projectDir)
gitVersionName = git.describe()
gitShortVersionName = gitVersionName ? gitVersionName.replace("v", "") : ""
gitVersionCode = git.tag.list().size()
gitVersionCode = git.log().size()
gitVersionCodeTime = git.head().time
buildDate = new Date()
customVersionName = "${gitShortVersionName}-${git.head().abbreviatedId}-${buildDate.format('yyMMdd')}"
Expand All @@ -25,4 +25,4 @@ task printVersion() {
println("Version Code: $gitVersionCode")
println("Version Code Time: $gitVersionCodeTime")
println("buildData: ${buildDate.format('yyMMdd')}")
}
}
35 changes: 23 additions & 12 deletions env/default.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"sessionTimeoutInSeconds": 10,
"marketCurrencyName": "nem",
"newsURL": "https://nemflash.io/feed",
"explorerURL": "http://explorer-0.10.0.x-01.symboldev.network/",
"faucetURL": "http://faucet-0.10.0.x-01.symboldev.network/",
"explorerURL": "http://explorer.testnet.symboldev.network/",
"faucetURL": "http://faucet.testnet.symboldev.network/",
"aboutURL": "https://nem.io",
"currencies": {
"USD": "usd",
Expand All @@ -22,20 +22,31 @@
"nodeTypes": { "MAINNET": "mainnet", "TESTNET": "testnet", "CUSTOM": "custom" },
"networks": {
"mainnet": {
"nodes": []
"nodes": [
"http://api.experimental.symboldev.network:3000",
"http://ngl-api-501.symbolblockchain.io:3000",
"http://ngl-api-601.symbolblockchain.io:3000",
"http://ngl-api-401.symbolblockchain.io:3000",
"http://ngl-api-001.symbolblockchain.io:3000",
"http://ngl-api-101.symbolblockchain.io:3000",
"http://ngl-api-301.symbolblockchain.io:3000",
"http://ngl-api-201.symbolblockchain.io:3000"
]
},
"testnet": {
"nodes": [
"http://api-01.eu-central-1.0.10.0.x.symboldev.network:3000",
"http://api-01.eu-west-1.0.10.0.x.symboldev.network:3000",
"http://api-01.us-east-1.0.10.0.x.symboldev.network:3000",
"http://api-01.us-west-1.0.10.0.x.symboldev.network:3000",
"http://api-01.us-west-2.0.10.0.x.symboldev.network:3000",
"http://api-01.ap-southeast-1.0.10.0.x.symboldev.network:3000",
"http://api-01.ap-northeast-1.0.10.0.x.symboldev.network:3000"
"http://api-01.us-west-1.testnet.symboldev.network:3000",
"http://api-01.ap-northeast-1.testnet.symboldev.network:3000",
"http://api-01.ap-southeast-1.testnet.symboldev.network:3000",
"http://api-01.eu-central-1.testnet.symboldev.network:3000",
"http://api-01.eu-west-1.testnet.symboldev.network:3000"
]
}
},
"defaultNetworkType": "testnet",
"nativeMosaicId": "5B66E76BECAD0860"
"defaultNetworkType": "mainnet",
"nativeMosaicId": "5F160D7851F3CB30",
"optInWhiteList" : {
"testnet": [],
"mainnet": []
}
}
30 changes: 2 additions & 28 deletions ios/NEMCatapultWallet/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -32,34 +32,6 @@
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
<key>nem.ninja</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
<key>nemtech.network</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
<key>rssmix.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
<key>symboldev.network</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
<key>NSAppleMusicUsageDescription</key>
<string>$(PRODUCT_NAME) does not request this permission or utilize this functionality but it is included in our info.plist since our app utilizes the react-native-permissions library, which references this permission in its code.</string>
Expand Down Expand Up @@ -105,6 +77,8 @@
<array>
<string>armv7</string>
</array>
<key>UIRequiresFullScreen</key>
<true/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
Expand Down
2 changes: 2 additions & 0 deletions ios/SymbolWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.nemgrouplimited.symbolwallet;
PRODUCT_NAME = Symbol;
PROVISIONING_PROFILE_SPECIFIER = "";
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
Expand Down Expand Up @@ -450,6 +451,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.nemgrouplimited.symbolwallet;
PRODUCT_NAME = Symbol;
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.nemgrouplimited.symbolwallet";
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "NEMCatapultWallet",
"name": "symbol-mobile-wallet",
"version": "0.0.1",
"private": true,
"scripts": {
Expand Down Expand Up @@ -80,6 +80,7 @@
"react-native-switch": "^2.0.0",
"react-native-tab-view": "^2.9.0",
"react-native-tcp": "^3.2.1",
"react-native-text-ticker": "^1.10.0",
"react-native-touch-id": "^4.4.1",
"react-native-udp": "^2.1.0",
"react-native-url": "^0.0.2",
Expand All @@ -100,10 +101,10 @@
"stream-http": "^3.0.0",
"string_decoder": "~0.10.25",
"symbol-address-book": "^1.0.0",
"symbol-hd-wallets": "0.14.0-alpha-202010231206",
"symbol-qr-library": "0.13.1-alpha-202011121108",
"symbol-hd-wallets": "0.14.1-alpha-202103051108",
"symbol-paper-wallets": "^1.0.2",
"symbol-sdk": "0.21.1-alpha-202011162035",
"symbol-qr-library": "0.14.1-alpha-202103081047",
"symbol-sdk": "0.23.0-alpha.202101131900",
"timers-browserify": "^1.0.1",
"tls-browserify": "^0.2.2",
"tty-browserify": "0.0.0",
Expand Down
19 changes: 16 additions & 3 deletions scripts/fix-qr-code-workaround.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
console.log('[-] Fixing QR Code workaround...');
const fs = require('fs');

const FILE_PATH = __dirname + '/../node_modules/symbol-qr-library/dist/src/QRCode.js';
const MAIN_FILE_PATH = __dirname + '/../node_modules/symbol-qr-library/dist/src/QRCode.js';

fs.readFile(FILE_PATH, 'utf8', function(err, data) {
fs.readFile(MAIN_FILE_PATH, 'utf8', function(err, data) {
const formatted = data.replace(/require\("qrcode"\)/g, 'require("qrcode/lib")');

fs.writeFile(FILE_PATH, formatted, 'utf8', function(err) {
fs.writeFile(MAIN_FILE_PATH, formatted, 'utf8', function(err) {
if (err) return console.log(err);
else console.log('[+] Fixed QR Code...');
});
});

const MODULE_FILE_PATH = __dirname + '/../node_modules/symbol-paper-wallets/node_modules/symbol-qr-library/dist/src/QRCode.js';

if (fs.existsSync(MODULE_FILE_PATH)) {
fs.readFile(MODULE_FILE_PATH, 'utf8', function(err, data) {
const formatted = data.replace(/require\("qrcode"\)/g, 'require("qrcode/lib")');

fs.writeFile(MODULE_FILE_PATH, formatted, 'utf8', function(err) {
if (err) return console.log(err);
else console.log('[+] Fixed QR Code under paper-wallets module...');
});
});
}
52 changes: 25 additions & 27 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import { hasUserSetPinCode } from '@haskkor/react-native-pincode';
import * as Config from './config/environment';
import { setI18nConfig } from './locales/i18n';
import { Router } from './Router';
import { AsyncCache } from './utils/storage/AsyncCache';
import {AsyncCache} from './utils/storage/AsyncCache';
import store from '@src/store';
import { MnemonicSecureStorage } from '@src/storage/persistence/MnemonicSecureStorage';
import { AccountSecureStorage } from '@src/storage/persistence/AccountSecureStorage';
import { deletePasscode } from '@src/utils/passcode';
import {CURRENT_DATA_SCHEMA, migrateDataSchema} from "@src/utils/DataSchemaMigrations";

// Handle passcode after 30 secs of inactivity
let appState: string = '';
Expand Down Expand Up @@ -43,50 +44,45 @@ export const handleAppStateChange = async (nextAppState: any) => {

const initStore = async () => {
try {
store.dispatchAction({ type: 'settings/initState' });
} catch {}
try {
store.dispatchAction({ type: 'market/loadMarketData' });
} catch {}
try {
store.dispatchAction({ type: 'network/initState' });
} catch {}
try {
store.dispatchAction({ type: 'news/loadNews' });
} catch {}
try {
store.dispatchAction({ type: 'addressBook/loadAddressBook' });
await store.dispatchAction({ type: 'settings/initState' });
await store.dispatchAction({ type: 'network/initState' });
} catch {}
// store.dispatchAction({ type: 'market/loadMarketData' });
store.dispatchAction({ type: 'news/loadNews' });
store.dispatchAction({ type: 'addressBook/loadAddressBook' });
};

export const startApp = async () => {
setGlobalCustomFont();

await initStore();
const dataSchemaVersion = await AsyncCache.getDataSchemaVersion();

/* TODO: REGISTER CORRECT LANGUAGE
const language = await SettingsHelper.getActiveLanguage();
*/
const selectedLanguage = await AsyncCache.getSelectedLanguage();
setI18nConfig(selectedLanguage);

if (dataSchemaVersion !== CURRENT_DATA_SCHEMA) {
SplashScreen.hide();
Router.goToWalletLoading({
promiseToRun: async () => await migrateDataSchema(dataSchemaVersion),
callbackAction: launchWallet,
});
} else {
await launchWallet();
SplashScreen.hide();
}
};

const launchWallet = async () => {
await initStore();

const mnemonic = await MnemonicSecureStorage.retrieveMnemonic();
const isPin = await hasUserSetPinCode();

SplashScreen.hide();

if (mnemonic) {
scheduleBackgroundJob();
if (isPin) Router.showPasscode({ resetPasscode: false, onSuccess: () => Router.goToDashboard() });
else Router.goToDashboard();
} else {
/* TODO: SELECT FIRST PAGE
goToOnBoarding({
// goToDashboard: () => goToOptinWelcomeAsRoot(),
goToDashboard: () => goToNetworkSelector({}),
goToPasscode: (props: Object) => goToPasscode(props),
});
*/
Router.goToTermsAndPrivacy({});
}
};
Expand Down Expand Up @@ -119,9 +115,11 @@ export const setGlobalCustomFont = () => {

Text.defaultProps = Text.defaultProps || {};
Text.defaultProps.maxFontSizeMultiplier = 1.3;
Text.defaultProps.allowFontScaling = false;

TextInput.defaultProps = TextInput.defaultProps || {};
TextInput.defaultProps.maxFontSizeMultiplier = 1.3;
TextInput.defaultProps.allowFontScaling = false;
};

export const logout = async () => {
Expand Down
27 changes: 26 additions & 1 deletion src/Router.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@ import ContactProfile from '@src/screens/ContactProfile';
import AccountDetails from '@src/screens/AccountDetails';
import CreateAccount from '@src/screens/CreateAccount';
import Receive from '@src/screens/Receive';
import QRScanner from '@src/screens/QRScanner';
import ScanGenericQRCode from '@src/screens/ScanGenericQRCode';
import CustomFlashMessage from '@src/components/organisms/CustomFlashMessage';
import { showMessage } from 'react-native-flash-message';
import LinkedKeysDetails from "@src/screens/LinkedKeysDetails";



export const BASE_SCREEN_NAME = 'com.nemgroup.wallet';
export const CUSTOM_FLASH_MESSAGE = `${BASE_SCREEN_NAME}.CUSTOM_FLASH_MESSAGE`;
Expand Down Expand Up @@ -59,6 +64,8 @@ export const ADD_CONTACT_SCREEN = `${BASE_SCREEN_NAME}.ADD_CONTACT_SCREEN`;
export const CONTACT_PROFILE_SCREEN = `${BASE_SCREEN_NAME}.CONTACT_PROFILE_SCREEN`;
export const ACCOUNT_DETAILS_SCREEN = `${BASE_SCREEN_NAME}.ACCOUNT_DETAILS_SCREEN`;
export const CREATE_ACCOUNT_SCREEN = `${BASE_SCREEN_NAME}.CREATE_ACCOUNT_SCREEN`;
export const QR_SCANNER_SCREEN = `${BASE_SCREEN_NAME}.QR_SCANNER_SCREEN`;
export const SHOW_LINKED_KEYS_SCREEN = `${BASE_SCREEN_NAME}.SHOW_LINKED_KEYS_SCREEN`;

/**
* Class to handle Routing between screens
Expand Down Expand Up @@ -91,7 +98,9 @@ export class Router {
[HARVEST_SCREEN, Harvest],
[ACCOUNT_DETAILS_SCREEN, AccountDetails],
[CREATE_ACCOUNT_SCREEN, CreateAccount],
[CONTACT_PROFILE_SCREEN, ContactProfile],
[CONTACT_PROFILE_SCREEN, ContactProfile],
[QR_SCANNER_SCREEN, QRScanner],
[SHOW_LINKED_KEYS_SCREEN, LinkedKeysDetails]
];

static registerScreens() {
Expand Down Expand Up @@ -180,6 +189,12 @@ export class Router {
}
static goToContactProfile(passProps, parentComponent?) {
return this.goToScreen(CONTACT_PROFILE_SCREEN, passProps, parentComponent);
}
static goToSend(passProps, parentComponent?) {
return this.goToScreen(SEND_SCREEN, passProps, parentComponent);
}
static goToShowLinkedKeys(passProps, parentComponent?) {
return this.goToScreen(SHOW_LINKED_KEYS_SCREEN, passProps, parentComponent);
}

static goToScreen(screen: string, passProps, parentComponent?) {
Expand All @@ -200,6 +215,16 @@ export class Router {
}

static showFlashMessageOverlay = (): Promise<any> => showOverlay(CUSTOM_FLASH_MESSAGE, {});

static showMessage = (message: string, type: 'danger' | 'warning' | 'success' = 'success') => {
Router.showFlashMessageOverlay().then(() => {
showMessage({
message: message,
type: type,
duration: type === 'danger' ? 6000 : 3000
});
});
};
}

/**
Expand Down
Binary file modified src/assets/backgrounds/harvest.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/backgrounds/harvest_clear.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/icons/qr_scanner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/icons/qr_scanner_light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/icons/warning_yellow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/icons/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/icons/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 4acb0f1

Please sign in to comment.