diff --git a/Navigation.js b/Navigation.js index 30109faa6..fc38aa98b 100644 --- a/Navigation.js +++ b/Navigation.js @@ -29,6 +29,10 @@ import WalletExport from './screen/wallets/export'; import WalletXpub from './screen/wallets/xpub'; import BuyBitcoin from './screen/wallets/buyBitcoin'; import HodlHodl from './screen/wallets/hodlHodl'; +import HodlHodlViewOffer from './screen/wallets/hodlHodlViewOffer'; +import HodlHodlLogin from './screen/wallets/hodlHodlLogin'; +import HodlHodlWebview from './screen/wallets/hodlHodlWebview'; +import HodlHodlMyContracts from './screen/wallets/hodlHodlMyContracts'; import Marketplace from './screen/wallets/marketplace'; import ReorderWallets from './screen/wallets/reorderWallets'; import SelectWallet from './screen/wallets/selectWallet'; @@ -201,6 +205,17 @@ const LNDCreateInvoiceRoot = () => ( ); +const HodlHodlStack = createStackNavigator(); +const HodlHodlRoot = () => ( + + + + + + + +); + // LightningScanInvoiceStackNavigator === ScanLndInvoiceStack const ScanLndInvoiceStack = createStackNavigator(); const ScanLndInvoiceRoot = () => ( @@ -236,6 +251,7 @@ const Navigation = () => ( + } */ @@ -523,6 +526,51 @@ export class AppStorage { return finalBalance; } + async getHodlHodlApiKey() { + try { + return await this.getItem(AppStorage.HODL_HODL_API_KEY); + } catch (_) {} + return false; + } + + async getHodlHodlSignatureKey() { + try { + return await this.getItem(AppStorage.HODL_HODL_SIGNATURE_KEY); + } catch (_) {} + return false; + } + + /** + * Since we cant fetch list of contracts from hodlhodl api yet, we have to keep track of it ourselves + * + * @returns {Promise} String ids of contracts in an array + */ + async getHodlHodlContracts() { + try { + let json = await this.getItem(AppStorage.HODL_HODL_CONTRACTS); + return JSON.parse(json); + } catch (_) {} + return []; + } + + async addHodlHodlContract(id) { + let json; + try { + json = await this.getItem(AppStorage.HODL_HODL_CONTRACTS); + json = JSON.parse(json); + } catch (_) { + json = []; + } + + json.push(id); + return this.setItem(AppStorage.HODL_HODL_CONTRACTS, JSON.stringify(json)); + } + + async setHodlHodlApiKey(key, sigKey) { + if (sigKey) await this.setItem(AppStorage.HODL_HODL_SIGNATURE_KEY, sigKey); + return this.setItem(AppStorage.HODL_HODL_API_KEY, key); + } + async isAdancedModeEnabled() { try { return !!(await this.getItem(AppStorage.ADVANCED_MODE_ENABLED)); diff --git a/class/hodl-hodl-api.js b/class/hodl-hodl-api.js index be827fa73..a98224ebb 100644 --- a/class/hodl-hodl-api.js +++ b/class/hodl-hodl-api.js @@ -1,4 +1,5 @@ import Frisbee from 'frisbee'; +const CryptoJS = require('crypto-js'); export class HodlHodlApi { static PAGINATION_LIMIT = 'limit'; // int @@ -74,18 +75,27 @@ export class HodlHodlApi { async getMyCountryCode() { const _api = new Frisbee({ baseURI: 'https://ifconfig.co/' }); + const _api2 = new Frisbee({ baseURI: 'https://geolocation-db.com/' }); let response; let allowedTries = 6; while (allowedTries > 0) { // this API fails a lot, so lets retry several times - response = await _api.get('/country-iso', { - headers: { 'Access-Control-Allow-Origin': '*' }, - }); + response = await _api.get('/country-iso', { headers: { 'Access-Control-Allow-Origin': '*' } }); let body = response.body; if (typeof body === 'string') body = body.replace('\n', ''); if (!body || body.length !== 2) { + // trying api2 + const response = await _api2.get('/json/', { headers: { 'Access-Control-Allow-Origin': '*' } }); + body = response.body; + let json; + try { + json = JSON.parse(body); + } catch (_) {} + if (json && json.country_code) return (this._myCountryCode = json.country_code); + // failed, retry + allowedTries--; await (async () => new Promise(resolve => setTimeout(resolve, 3000)))(); // sleep } else { @@ -118,6 +128,17 @@ export class HodlHodlApi { return (this._currencies = json.currencies.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1))); } + async getOffer(id) { + const response = await this._api.get('/api/v1/offers/' + id, this._getHeadersWithoutAuthorization()); + + const json = response.body; + if (!json || !json.offer || json.status === 'error') { + throw new Error('API failure: ' + JSON.stringify(response)); + } + + return json.offer; + } + async getOffers(pagination = {}, filters = {}, sort = {}) { const uri = []; for (const key in sort) { @@ -138,4 +159,141 @@ export class HodlHodlApi { return (this._offers = json.offers); } + + createSignature(apiKey, sigKey, nonce) { + const sourceMessageForSigning = apiKey + ':' + nonce; // : + return CryptoJS.HmacSHA256(sourceMessageForSigning, sigKey).toString(CryptoJS.enc.Hex); + } + + /** + * @see https://gitlab.com/hodlhodl-public/public_docs/-/blob/master/autologin.md + * + * @param apiSigKey {string} + * @param nonce {integer|null} Optional unix timestamp (sec, not msec), or nothing + * @returns {Promise} Token usable for autologin (works only once and only about 30 seconds) + */ + async requestAutologinToken(apiSigKey, nonce) { + nonce = nonce || Math.floor(+new Date() / 1000); + const signature = this.createSignature(this.apiKey, apiSigKey, nonce); + + const response = await this._api.get('/api/v1/users/login_token?nonce=' + nonce + '&hmac=' + signature, this._getHeaders()); + + const json = response.body; + if (!json || !json.token || json.status === 'error') { + throw new Error('API failure: ' + JSON.stringify(response)); + } + + return json.token; + } + + async getMyself() { + const response = await this._api.get('/api/v1/users/me', this._getHeaders()); + + const json = response.body; + if (!json || !json.user || json.status === 'error') { + throw new Error('API failure: ' + JSON.stringify(response)); + } + + return (this._user = json.user); + } + + async acceptOffer(id, version, paymentMethodInstructionId, paymentMethodInstructionVersion, value) { + const response = await this._api.post( + '/api/v1/contracts', + Object.assign({}, this._getHeaders(), { + body: { + contract: { + offer_id: id, + offer_version: version, + payment_method_instruction_id: paymentMethodInstructionId, + payment_method_instruction_version: paymentMethodInstructionVersion, + comment: 'I accept your offer', + value, + }, + }, + }), + ); + + const json = response.body; + if (!json || !json.contract || json.status === 'error') { + if (json && json.validation_errors) throw new Error(this.validationErrorsToReadable(json.validation_errors)); + throw new Error('API failure: ' + JSON.stringify(response)); + } + + return json.contract; + } + + validationErrorsToReadable(errorz) { + const ret = []; + for (const er of Object.keys(errorz)) { + ret.push(errorz[er].join('; ')); + } + + return ret.join('\n'); + } + + async getContract(id) { + const response = await this._api.get('/api/v1/contracts/' + id, this._getHeaders()); + + const json = response.body; + if (!json || !json.contract || json.status === 'error') { + throw new Error('API failure: ' + JSON.stringify(response)); + } + + return json.contract; + } + + verifyEscrowAddress(encryptedSeed, encryptPassword, index, address, witnessScript) { + // TODO + // @see https://gitlab.com/hodlhodl-public/hodl-client-js + return true; + } + + /** + * This method is used to confirm that client-side validation of escrow data was successful. + * This method should be called immediately after escrow address appeared in Getting contract response and this escrow address has been verified locally by the client. + * + * @param id + * @returns {Promise<{}>} + */ + async markContractAsConfirmed(id) { + const response = await this._api.post('/api/v1/contracts/' + id + '/confirm', this._getHeaders()); + + const json = response.body; + if (!json || !json.contract || json.status === 'error') { + throw new Error('API failure: ' + JSON.stringify(response)); + } + + return json.contract; + } + + /** + * Buyer (and only buyer) should call this method when fiat payment was made. + * This method could be called only if contract’s status is "in_progress". + * + * @param id + * @returns {Promise<{}>} + */ + async markContractAsPaid(id) { + const response = await this._api.post('/api/v1/contracts/' + id + '/mark_as_paid', this._getHeaders()); + + const json = response.body; + if (!json || !json.contract || json.status === 'error') { + throw new Error('API failure: ' + JSON.stringify(response)); + } + + return json.contract; + } + + async cancelContract(id) { + const response = await this._api.post('/api/v1/contracts/' + id + '/cancel', this._getHeaders()); + + const json = response.body; + if (!json || !json.contract || json.status === 'error') { + if (json && json.validation_errors) throw new Error(this.validationErrorsToReadable(json.validation_errors)); + throw new Error('API failure: ' + JSON.stringify(response)); + } + + return json.contract; + } } diff --git a/img/hodlhodl-default-avatar.png b/img/hodlhodl-default-avatar.png new file mode 100644 index 000000000..590a2d5ec Binary files /dev/null and b/img/hodlhodl-default-avatar.png differ diff --git a/package-lock.json b/package-lock.json index fe8672e11..1dd3cd0be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2644,7 +2644,6 @@ "version": "6.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", - "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2825,7 +2824,6 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, "requires": { "safer-buffer": "~2.1.0" } @@ -2857,8 +2855,26 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assign": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/assign/-/assign-0.1.7.tgz", + "integrity": "sha1-5jv+Ooh7hjCRPCdmPkzJv/Hd0l8=", + "requires": { + "fusing": "0.4.x" + }, + "dependencies": { + "fusing": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/fusing/-/fusing-0.4.0.tgz", + "integrity": "sha1-yZBo9Uyj4R3AEYkCFSq/Nnq6Sk0=", + "requires": { + "emits": "1.0.x", + "predefine": "0.1.x" + } + } + } }, "assign-symbols": { "version": "1.0.0", @@ -2893,8 +2909,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { "version": "2.1.2", @@ -2919,14 +2934,12 @@ "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", - "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==", - "dev": true + "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" }, "babel-cli": { "version": "6.26.0", @@ -3727,6 +3740,14 @@ "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", "dev": true }, + "back": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/back/-/back-1.0.2.tgz", + "integrity": "sha1-qT9ebOaXKZhNWQGiuxbjsBpNY2k=", + "requires": { + "xtend": "^4.0.0" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -3819,7 +3840,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, "requires": { "tweetnacl": "^0.14.3" } @@ -4592,16 +4612,53 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.1.0.tgz", "integrity": "sha512-6S062WDQUXi6hOfkO/sBPVwE5ASXY4G2+b4atvhJfSsuUUhIaUKlkjLe9692Ipyt5/a+IPF5aVTu3V5gvXq5cg==" }, + "colornames": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/colornames/-/colornames-0.0.2.tgz", + "integrity": "sha1-2BH9bIT1kClJmorEQ2ICk1uSvjE=" + }, "colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" }, + "colorspace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.0.1.tgz", + "integrity": "sha1-yZx5btMRKLmHalLh7l7gOkpxl0k=", + "requires": { + "color": "0.8.x", + "text-hex": "0.0.x" + }, + "dependencies": { + "color": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/color/-/color-0.8.0.tgz", + "integrity": "sha1-iQwHw/1OZJU3Y4kRz2keVFi2/KU=", + "requires": { + "color-convert": "^0.5.0", + "color-string": "^0.3.0" + } + }, + "color-convert": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", + "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=" + }, + "color-string": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", + "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", + "requires": { + "color-name": "^1.0.0" + } + } + } + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -4951,7 +5008,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, "requires": { "assert-plus": "^1.0.0" } @@ -5104,8 +5160,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "denodeify": { "version": "1.2.1", @@ -5326,6 +5381,16 @@ } } }, + "diagnostics": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.0.1.tgz", + "integrity": "sha1-rM2wgMgrsl0N1zQwqeaof7tDFUE=", + "requires": { + "colorspace": "1.0.x", + "enabled": "1.0.x", + "kuler": "0.0.x" + } + }, "didyoumean": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.1.tgz", @@ -5413,7 +5478,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -5466,11 +5530,24 @@ "minimalistic-crypto-utils": "^1.0.0" } }, + "emits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/emits/-/emits-1.0.2.tgz", + "integrity": "sha1-2yDsZmgyUHHDE0QeMM/ipp6nOFk=" + }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, + "enabled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", + "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", + "requires": { + "env-variable": "0.0.x" + } + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -5497,6 +5574,11 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==" }, + "env-variable": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz", + "integrity": "sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg==" + }, "envinfo": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.5.0.tgz", @@ -6494,8 +6576,7 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "extend-shallow": { "version": "3.0.2", @@ -6516,6 +6597,11 @@ } } }, + "extendible": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/extendible/-/extendible-0.1.1.tgz", + "integrity": "sha1-4qN+2HEp+0+VM+io11BiMKU5yQU=" + }, "external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -6585,11 +6671,49 @@ } } }, + "extract-github": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/extract-github/-/extract-github-0.0.5.tgz", + "integrity": "sha1-9UJTbbjBm5g6O+yduW0u8qX/GoY=" + }, + "extract-zip": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", + "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "requires": { + "concat-stream": "^1.6.2", + "debug": "^2.6.9", + "mkdirp": "^0.5.4", + "yauzl": "^2.10.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fancy-log": { "version": "1.3.3", @@ -6605,8 +6729,7 @@ "fast-deep-equal": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", - "dev": true + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" }, "fast-diff": { "version": "1.2.0", @@ -6616,8 +6739,7 @@ "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "fast-levenshtein": { "version": "2.0.6", @@ -6682,6 +6804,14 @@ } } }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "requires": { + "pend": "~1.2.0" + } + }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -6841,14 +6971,12 @@ "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -7472,6 +7600,14 @@ "resolved": "https://registry.npmjs.org/funpermaproxy/-/funpermaproxy-1.0.1.tgz", "integrity": "sha512-9pEzs5vnNtR7ZGihly98w/mQ7blsvl68Wj30ZCDAXy7qDN4CWLLjdfjtH/P2m6whsnaJkw15hysCNHMXue+wdA==" }, + "fusing": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/fusing/-/fusing-0.2.3.tgz", + "integrity": "sha1-0O76+YXSuv3tRK+LGFMW9uQp4ds=", + "requires": { + "predefine": "0.1.x" + } + }, "fwd-stream": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/fwd-stream/-/fwd-stream-1.0.4.tgz", @@ -7549,11 +7685,27 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, "requires": { "assert-plus": "^1.0.0" } }, + "githulk": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/githulk/-/githulk-0.0.7.tgz", + "integrity": "sha1-2Wyinw7EMRfFOOUh1mNWbqhLTv8=", + "requires": { + "debug": "0.7.x", + "extract-github": "0.0.x", + "mana": "0.1.x" + }, + "dependencies": { + "debug": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", + "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=" + } + } + }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -7606,14 +7758,12 @@ "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, "requires": { "ajv": "^6.5.5", "har-schema": "^2.0.0" @@ -7631,7 +7781,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" }, @@ -7639,8 +7788,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" } } }, @@ -7767,7 +7915,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -8217,8 +8364,7 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "istanbul-lib-coverage": { "version": "2.0.5", @@ -8974,8 +9120,7 @@ "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "jsc-android": { "version": "245459.0.0", @@ -9046,14 +9191,12 @@ "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "json-stable-stringify": { "version": "1.0.1", @@ -9072,8 +9215,7 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "json5": { "version": "2.1.3", @@ -9100,7 +9242,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -9134,8 +9275,15 @@ "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" + }, + "kuler": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-0.0.0.tgz", + "integrity": "sha1-tmu0a5NOVQ9Z2BiEjgq7pPf1VTw=", + "requires": { + "colornames": "0.0.2" + } }, "lcid": { "version": "2.0.0", @@ -9348,6 +9496,30 @@ "type-check": "~0.3.2" } }, + "licenses": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/licenses/-/licenses-0.0.20.tgz", + "integrity": "sha1-8YpXsmp46vKKhz4qN4oz6B9Z0TY=", + "requires": { + "async": "0.6.x", + "debug": "0.8.x", + "fusing": "0.2.x", + "githulk": "0.0.x", + "npm-registry": "0.1.x" + }, + "dependencies": { + "async": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.6.2.tgz", + "integrity": "sha1-Qf0DijgSwKi8GELs8IumPrA5K+8=" + }, + "debug": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.8.1.tgz", + "integrity": "sha1-IP9NJvXkIstoobrLu2EDmtjBwTA=" + } + } + }, "load-json-file": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", @@ -9477,6 +9649,41 @@ "tmpl": "1.0.x" } }, + "mana": { + "version": "0.1.41", + "resolved": "https://registry.npmjs.org/mana/-/mana-0.1.41.tgz", + "integrity": "sha1-fLE/cyGGaGVCKWNcT8Wxfib5O30=", + "requires": { + "assign": ">=0.1.7", + "back": "1.0.x", + "diagnostics": "1.0.x", + "eventemitter3": "1.2.x", + "fusing": "1.0.x", + "millisecond": "0.1.x", + "request": "2.x.x" + }, + "dependencies": { + "emits": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emits/-/emits-3.0.0.tgz", + "integrity": "sha1-MnUrupXhcHshlWI4Srm7ix/WL3A=" + }, + "eventemitter3": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", + "integrity": "sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=" + }, + "fusing": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fusing/-/fusing-1.0.0.tgz", + "integrity": "sha1-VQwV12r5Jld4qgUezkTUAAoJjUU=", + "requires": { + "emits": "3.0.x", + "predefine": "0.1.x" + } + } + } + }, "map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", @@ -10270,6 +10477,11 @@ "to-regex": "^3.0.2" } }, + "millisecond": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/millisecond/-/millisecond-0.1.2.tgz", + "integrity": "sha1-bMWtOGJByrjniv+WT4cCjuyS2sU=" + }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -10532,6 +10744,30 @@ "remove-trailing-separator": "^1.0.1" } }, + "npm-registry": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/npm-registry/-/npm-registry-0.1.13.tgz", + "integrity": "sha1-nl2LL9/Bq1mQ1H99674jHXmp6CI=", + "requires": { + "debug": "0.8.x", + "extract-github": "0.0.x", + "licenses": "0.0.x", + "mana": "0.1.x", + "semver": "2.2.x" + }, + "dependencies": { + "debug": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.8.1.tgz", + "integrity": "sha1-IP9NJvXkIstoobrLu2EDmtjBwTA=" + }, + "semver": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-2.2.1.tgz", + "integrity": "sha1-eUEYKz/8xYC/8cF5QqzfeVHA0hM=" + } + } + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -10567,8 +10803,7 @@ "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, "ob1": { "version": "0.56.4", @@ -11033,11 +11268,15 @@ "sha.js": "^2.4.8" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "picomatch": { "version": "2.2.2", @@ -11194,6 +11433,14 @@ "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" }, + "predefine": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/predefine/-/predefine-0.1.2.tgz", + "integrity": "sha1-KqkrRJa8H4VU5DpF92v75Q0z038=", + "requires": { + "extendible": "0.1.x" + } + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -11276,7 +11523,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.2.tgz", "integrity": "sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA==", - "dev": true, "requires": { "kleur": "^3.0.3", "sisteransi": "^1.0.4" @@ -11320,8 +11566,7 @@ "psl": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", - "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==", - "dev": true + "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==" }, "pump": { "version": "3.0.0", @@ -11335,8 +11580,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "pushdata-bitcoin": { "version": "1.0.1", @@ -12182,12 +12426,20 @@ "integrity": "sha512-es/c1yPRc5aQXNjKuNr0nCgYvuD126bGDRAhq5OaKpnccWHGQorctqxKYRKyNjloOM/NGc97C7DDNnfF1cCfJw==" }, "react-native-webview": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-6.11.1.tgz", - "integrity": "sha512-0OaNCEzdyywJZ70y6Z4fmmuAd2AHYq2AEByIr15z3YetpMXhm9lXUe2V/8BXQIZXeC9FJosj2DAKu48lpZ0nEg==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-9.0.2.tgz", + "integrity": "sha512-HnQ0+8jt3556QocsYTtmG3Y8eAG88GzmBodDT0PTz4TuLzPJukubj41OFkP7hytZlSdXIxyoV9A06GruyxXeLQ==", "requires": { - "escape-string-regexp": "1.0.5", - "invariant": "2.2.4" + "escape-string-regexp": "2.0.0", + "invariant": "2.2.4", + "rnpm-plugin-windows": "^0.5.1-0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + } } }, "react-refresh": { @@ -12477,7 +12729,6 @@ "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "dev": true, "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -12504,8 +12755,7 @@ "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" } } }, @@ -12643,6 +12893,63 @@ } } }, + "rnpm-plugin-windows": { + "version": "0.5.1-0", + "resolved": "https://registry.npmjs.org/rnpm-plugin-windows/-/rnpm-plugin-windows-0.5.1-0.tgz", + "integrity": "sha512-0EX2shP1OI18MylpVHmZRhDX5GSdvHDgSQoFDZx/Ir73dt3dPVtz7iNviiz3vPa8/8HgTOog3Xzn/gXxfPRrnw==", + "requires": { + "chalk": "^1.1.3", + "extract-zip": "^1.6.7", + "fs-extra": "^7.0.1", + "npm-registry": "^0.1.13", + "prompts": "^2.3.0", + "request": "^2.88.0", + "semver": "^6.1.1", + "valid-url": "^1.0.9" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, "rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -12952,8 +13259,7 @@ "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" }, "sjcl": { "version": "1.0.8", @@ -13178,7 +13484,6 @@ "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -13558,6 +13863,11 @@ } } }, + "text-hex": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-0.0.0.tgz", + "integrity": "sha1-V4+8haapJjbkLdF7QdAhjM6esrM=" + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -13694,7 +14004,6 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, "requires": { "psl": "^1.1.28", "punycode": "^2.1.1" @@ -13741,7 +14050,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, "requires": { "safe-buffer": "^5.0.1" } @@ -13749,8 +14057,7 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "type-check": { "version": "0.3.2", @@ -13905,7 +14212,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, "requires": { "punycode": "^2.1.0" } @@ -14029,6 +14335,11 @@ "user-home": "^1.1.1" } }, + "valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=" + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -14055,7 +14366,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -14358,6 +14668,15 @@ "camelcase": "^5.0.0", "decamelize": "^1.2.0" } + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } } } } diff --git a/package.json b/package.json index b475f48f5..4eebb1d6a 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "react-native-tooltip": "git+https://github.com/marcosrdz/react-native-tooltip.git", "react-native-vector-icons": "6.6.0", "react-native-watch-connectivity": "0.4.2", - "react-native-webview": "6.11.1", + "react-native-webview": "9.0.2", "react-test-render": "1.1.2", "readable-stream": "3.6.0", "rn-nodeify": "10.2.0", diff --git a/prompt.js b/prompt.js index 366962bb7..7e8c41312 100644 --- a/prompt.js +++ b/prompt.js @@ -1,6 +1,12 @@ +import { Platform } from 'react-native'; import prompt from 'react-native-prompt-android'; module.exports = (title, text, isCancelable = true, type = 'secure-text') => { + if (Platform.OS === 'ios' && type === 'numeric') { + // `react-native-prompt-android` on ios does not support numeric input + type = 'plain-text'; + } + return new Promise((resolve, reject) => { const buttons = isCancelable ? [ diff --git a/screen/lnd/browser.js b/screen/lnd/browser.js index 3c95c8c6f..ea4b30cf2 100644 --- a/screen/lnd/browser.js +++ b/screen/lnd/browser.js @@ -249,6 +249,7 @@ const styles = StyleSheet.create({ flex: 1, marginLeft: 4, minHeight: 33, + color: '#81868e', }, safeURLHome: { alignContent: 'flex-end', @@ -455,6 +456,7 @@ export default class Browser extends Component { onChangeText={text => this.setState({ stateURL: text })} value={this.state.stateURL} numberOfLines={1} + placeholderTextColor="#81868e" style={styles.safeURLText} editable onSubmitEditing={() => { @@ -486,11 +488,7 @@ export default class Browser extends Component { { - const reloadUrl = this.state.url; - this.setState({ url: 'about:blank' }); - processedInvoices = {}; - setTimeout(() => this.setState({ url: reloadUrl }), 500); - // this.webview.reload(); + this.webview.reload(); }} > {!this.state.pageIsLoading ? ( diff --git a/screen/wallets/hodlHodl.js b/screen/wallets/hodlHodl.js index b297ddd63..b202e8192 100644 --- a/screen/wallets/hodlHodl.js +++ b/screen/wallets/hodlHodl.js @@ -1,321 +1,51 @@ /* global alert */ import React, { Component } from 'react'; import { - Linking, - StyleSheet, - View, - Text, + ActivityIndicator, FlatList, - TouchableHighlight, - TouchableOpacity, + Image, Keyboard, KeyboardAvoidingView, + Linking, Platform, - Image, + RefreshControl, + SectionList, + StyleSheet, + Text, TextInput, - ScrollView, + TouchableHighlight, + TouchableOpacity, + View, } from 'react-native'; -import { BlueNavigationStyle, BlueLoading, BlueCard, SafeBlueArea } from '../../BlueComponents'; +import { BlueButtonLink, BlueNavigationStyle, SafeBlueArea } from '../../BlueComponents'; import PropTypes from 'prop-types'; import { HodlHodlApi } from '../../class/hodl-hodl-api'; import Modal from 'react-native-modal'; import { Icon } from 'react-native-elements'; +import { AppStorage } from '../../class'; +import NavigationService from '../../NavigationService'; + +const BlueApp: AppStorage = require('../../BlueApp'); const A = require('../../analytics'); const CURRENCY_CODE_ANY = '_any'; const METHOD_ANY = '_any'; -const styles = StyleSheet.create({ - header: { - alignItems: 'center', - flex: 1, - }, - headerRow: { - flexDirection: 'row', - }, - poweredBy: { - position: 'absolute', - top: -10, - left: 0, - fontSize: 10, - color: '#0c2550', - }, - title: { - fontWeight: 'bold', - fontSize: 34, - color: '#0c2550', - }, - chooseSide: { - backgroundColor: '#EEF0F4', - borderRadius: 20, - width: 100, - height: 35, - top: 3, - paddingLeft: 2, - paddingBottom: 6, - paddingTop: 6, - paddingRight: 0, - justifyContent: 'center', - alignItems: 'center', - flex: 0.65, - flexDirection: 'row', - }, - chooseSideText: { - fontSize: 17, - fontWeight: '600', - color: '#9AA0AA', - }, - chooseSideIcon: { - paddingLeft: 0, - paddingRight: 0, - }, - filter: { - backgroundColor: '#EEF0F4', - borderRadius: 20, - height: 44, - justifyContent: 'center', - alignItems: 'center', - marginTop: 15, - }, - filterRow: { - width: '100%', - alignItems: 'center', - flex: 1, - flexDirection: 'row', - }, - filterLocation: { - top: 0, - left: 5, - color: '#0c2550', - fontSize: 20, - fontWeight: '500', - }, - filterOpenTouch: { - backgroundColor: '#CCDDF9', - borderRadius: 20, - width: 110, - flex: 1, - flexDirection: 'row', - height: 36, - paddingLeft: 8, - justifyContent: 'center', - alignItems: 'center', - right: 4, - position: 'absolute', - }, - filterOpenText: { - color: '#2f5fb3', - fontSize: 18, - fontWeight: '600', - }, - - modal: { - justifyContent: 'flex-end', - margin: 0, - }, - modalContent: { - backgroundColor: '#FFFFFF', - padding: 22, - justifyContent: 'center', - alignItems: 'center', - borderTopLeftRadius: 16, - borderTopRightRadius: 16, - borderColor: 'rgba(0, 0, 0, 0.1)', - minHeight: 400, - height: 400, - }, - modalContentShort: { - backgroundColor: '#FFFFFF', - padding: 22, - justifyContent: 'center', - alignItems: 'center', - borderTopLeftRadius: 16, - borderTopRightRadius: 16, - borderColor: 'rgba(0, 0, 0, 0.1)', - minHeight: 200, - height: 200, - }, - modalSearch: { - flexDirection: 'row', - borderColor: '#EEF0F4', - borderBottomColor: '#EEF0F4', - borderWidth: 1.0, - borderBottomWidth: 0.5, - backgroundColor: '#EEF0F4', - minHeight: 48, - height: 48, - marginHorizontal: 20, - alignItems: 'center', - marginVertical: 8, - borderRadius: 26, - width: '100%', - }, - modalSearchText: { - flex: 1, - marginHorizontal: 8, - minHeight: 33, - paddingLeft: 6, - paddingRight: 6, - }, - modalSearchText2: { - flex: 1, - fontSize: 17, - marginHorizontal: 8, - minHeight: 33, - paddingLeft: 6, - paddingRight: 6, - }, - modalSearchIcon: { - left: -10, - }, - modalList: { - width: '100%', - }, - - itemRoot: { - backgroundColor: 'white', - }, - item: { - backgroundColor: 'white', - flex: 1, - flexDirection: 'row', - paddingTop: 20, - paddingBottom: 20, - }, - itemText: { - fontSize: 20, - color: '#0c2550', - }, - itemTextBold: { - fontWeight: 'bold', - }, - itemTextNormal: { - fontWeight: 'normal', - }, - itemRow: { - paddingLeft: 10, - flex: 1, - flexDirection: 'row', - }, - itemValue: { - color: '#9AA0AA', - right: 0, - position: 'absolute', - }, - itemValueText1: { - fontSize: 18, - color: '#9AA0AA', - }, - itemValueText2: { - fontSize: 20, - color: '#9AA0AA', - }, - - offers: { - paddingHorizontal: 24, - }, - offersList: { - marginTop: 24, - flex: 1, - }, - offersListContent: { - flex: 1, - justifyContent: 'center', - paddingHorizontal: 0, - }, - noOffers: { - textAlign: 'center', - color: '#9AA0AA', - paddingHorizontal: 16, - }, - - offer: { - backgroundColor: 'white', - paddingTop: 16, - paddingBottom: 16, - }, - offerRow: { - backgroundColor: 'white', - flex: 1, - flexDirection: 'row', - }, - offerAvatar: { - width: 40, - height: 40, - borderRadius: 40, - }, - offerNickname: { - color: '#0c2550', - fontSize: 18, - fontWeight: '600', - }, - offerRating: { - color: '#9AA0AA', - }, - offerText: { - color: '#9AA0AA', - paddingTop: 10, - }, - offerFooter: { - flex: 1, - flexDirection: 'row', - paddingTop: 10, - paddingBottom: 10, - alignItems: 'center', - }, - offerPrice: { - backgroundColor: '#EEF0F4', - borderRadius: 20, - paddingLeft: 8, - paddingRight: 8, - height: 26, - justifyContent: 'center', - alignItems: 'center', - }, - offerPriceText: { - fontWeight: '600', - fontSize: 14, - color: '#9AA0AA', - }, - offerAmount: { - color: '#9AA0AA', - fontSize: 12, - paddingLeft: 10, - }, - - // allOffersText: { - // fontSize: 12, - // color: '#9AA0AA', - // position: 'absolute', - // top: 0, - // left: 15, - // }, - - paddingLeft: { - paddingLeft: 10, - }, - separator: { - height: 0.5, - width: '100%', - backgroundColor: '#C8C8C8', - }, -}); - -const HodlApi = new HodlHodlApi(); +const HodlHodlListSections = { OFFERS: 'OFFERS' }; export default class HodlHodl extends Component { - static navigationOptions = ({ navigation }) => ({ - ...BlueNavigationStyle(), - title: '', - }); - constructor(props) { super(props); - /** @type {AbstractWallet} */ - const wallet = props.route.params.wallet; + props.navigation.setParams({ + handleLoginPress: this.handleLoginPress, + displayLoginButton: true, + handleMyContractsPress: this.handleMyContractsPress, + }); this.state = { + HodlApi: false, isLoading: true, + isRenderOfferVisible: false, isChooseSideModalVisible: false, isChooseCountryModalVisible: false, isFiltersModalVisible: false, @@ -324,7 +54,6 @@ export default class HodlHodl extends Component { currency: false, // means no currency filtering is enabled by default method: false, // means no payment method filtering is enabled by default side: HodlHodlApi.FILTERS_SIDE_VALUE_SELL, // means 'show me sell offers as Im buying' - wallet, offers: [], countries: [], // list of hodlhodl supported countries. filled later via api currencies: [], // list of hodlhodl supported currencies. filled later via api @@ -334,6 +63,21 @@ export default class HodlHodl extends Component { }; } + handleLoginPress = () => { + const handleLoginCallback = (hodlApiKey, hodlHodlSignatureKey) => { + BlueApp.setHodlHodlApiKey(hodlApiKey, hodlHodlSignatureKey); + const displayLoginButton = !hodlApiKey; + const HodlApi = new HodlHodlApi(hodlApiKey); + this.setState({ HodlApi, hodlApiKey }); + this.props.navigation.setParams({ displayLoginButton }); + }; + NavigationService.navigate('HodlHodlRoot', { params: { cb: handleLoginCallback }, screen: 'HodlHodlLogin' }); + }; + + handleMyContractsPress = () => { + NavigationService.navigate('HodlHodlMyContracts'); + }; + /** * Fetch offers and set those offers into state * @@ -344,13 +88,16 @@ export default class HodlHodl extends Component { [HodlHodlApi.PAGINATION_LIMIT]: 200, }; const filters = { - [HodlHodlApi.FILTERS_COUNTRY]: this.state.country, [HodlHodlApi.FILTERS_SIDE]: this.state.side, [HodlHodlApi.FILTERS_ASSET_CODE]: HodlHodlApi.FILTERS_ASSET_CODE_VALUE_BTC, [HodlHodlApi.FILTERS_INCLUDE_GLOBAL]: this.state.country === HodlHodlApi.FILTERS_COUNTRY_VALUE_GLOBAL, [HodlHodlApi.FILTERS_ONLY_WORKING_NOW]: true, // so there wont be any offers which user tries to open website says 'offer not found' }; + if (this.state.country !== HodlHodlApi.FILTERS_COUNTRY_VALUE_GLOBAL) { + filters[HodlHodlApi.FILTERS_COUNTRY] = this.state.country; + } + if (this.state.currency) { filters[HodlHodlApi.FILTERS_CURRENCY_CODE] = this.state.currency; } @@ -363,7 +110,7 @@ export default class HodlHodl extends Component { [HodlHodlApi.SORT_BY]: HodlHodlApi.SORT_BY_VALUE_PRICE, [HodlHodlApi.SORT_DIRECTION]: HodlHodlApi.SORT_DIRECTION_VALUE_ASC, }; - const offers = await HodlApi.getOffers(pagination, filters, sort); + const offers = await this.state.HodlApi.getOffers(pagination, filters, sort); this.setState({ offers, @@ -371,7 +118,7 @@ export default class HodlHodl extends Component { } async fetchMyCountry() { - const myCountryCode = await HodlApi.getMyCountryCode(); + const myCountryCode = await this.state.HodlApi.getMyCountryCode(); this.setState({ myCountryCode, country: myCountryCode, // we start with orders from current country @@ -384,7 +131,7 @@ export default class HodlHodl extends Component { * @returns {Promise} **/ async fetchListOfCountries() { - const countries = await HodlApi.getCountries(); + const countries = await this.state.HodlApi.getCountries(); this.setState({ countries }); } @@ -394,7 +141,7 @@ export default class HodlHodl extends Component { * @returns {Promise} **/ async fetchListOfCurrencies() { - const currencies = await HodlApi.getCurrencies(); + const currencies = await this.state.HodlApi.getCurrencies(); this.setState({ currencies }); } @@ -404,7 +151,7 @@ export default class HodlHodl extends Component { * @returns {Promise} **/ async fetchListOfMethods() { - const methods = await HodlApi.getPaymentMethods(this.state.country || HodlHodlApi.FILTERS_COUNTRY_VALUE_GLOBAL); + const methods = await this.state.HodlApi.getPaymentMethods(this.state.country || HodlHodlApi.FILTERS_COUNTRY_VALUE_GLOBAL); this.setState({ methods }); } @@ -412,6 +159,13 @@ export default class HodlHodl extends Component { console.log('wallets/hodlHodl - componentDidMount'); A(A.ENUM.NAVIGATED_TO_WALLETS_HODLHODL); + const hodlApiKey = await BlueApp.getHodlHodlApiKey(); + const displayLoginButton = !hodlApiKey; + + const HodlApi = new HodlHodlApi(hodlApiKey); + this.setState({ HodlApi, hodlApiKey }); + this.props.navigation.setParams({ displayLoginButton }); + try { await this.fetchMyCountry(); await this.fetchOffers(); @@ -429,22 +183,15 @@ export default class HodlHodl extends Component { this.fetchListOfMethods(); } - async _refresh() { - this.setState( - { - isLoading: true, - }, - async () => { - await this.fetchOffers(); - this.setState({ - isLoading: false, - }); - }, - ); - } - _onPress(item) { - Linking.openURL('https://hodlhodl.com/offers/' + item.id); + const offers = this.state.offers.filter(value => value.id === item.id); + if (offers && offers[0]) { + NavigationService.navigate('HodlHodlViewOffer', { + offerToDisplay: offers[0], + }); + } else { + Linking.openURL('https://hodlhodl.com/offers/' + item.id); + } } _onCountryPress(item) { @@ -565,7 +312,7 @@ export default class HodlHodl extends Component { return ( { Keyboard.dismiss(); this.setState({ isChooseSideModalVisible: false }); @@ -575,8 +322,8 @@ export default class HodlHodl extends Component { } + style={styles.modalFlatList} + ItemSeparatorComponent={() => } data={[ { code: HodlHodlApi.FILTERS_SIDE_VALUE_SELL, name: "I'm buying bitcoin" }, { code: HodlHodlApi.FILTERS_SIDE_VALUE_BUY, name: "I'm selling bitcoin" }, @@ -588,10 +335,8 @@ export default class HodlHodl extends Component { onHideUnderlay={separators.unhighlight} onPress={() => this._onSidePress(item)} > - - - {item.name} - + + {item.name} )} @@ -606,7 +351,7 @@ export default class HodlHodl extends Component { return ( { if (this.state.openNextModal) { const openNextModal = this.state.openNextModal; @@ -625,8 +370,8 @@ export default class HodlHodl extends Component { } + style={styles.modalFlatList} + ItemSeparatorComponent={() => } data={[ { code: 'currency', native_name: 'Currency' }, { code: 'method', native_name: 'Payment method' }, @@ -641,16 +386,19 @@ export default class HodlHodl extends Component { if (item.code === 'method') this.setState({ isFiltersModalVisible: false, openNextModal: 'isChooseMethodVisible' }); }} > - - - - {item.native_name} - + + + + {item.native_name} + {item.code === 'currency' && ( - {this.state.currency ? this.state.currency + ' ❯' : 'Detail ❯'} + + {' '} + {this.state.currency ? this.state.currency + ' ❯' : 'Detail ❯'}{' '} + )} {item.code === 'method' && ( - + {' '} {this.state.method ? this.getMethodName(this.state.method) + ' ❯' : 'Detail ❯'} @@ -707,7 +455,7 @@ export default class HodlHodl extends Component { return ( { Keyboard.dismiss(); this.setState({ isChooseCountryModalVisible: false }); @@ -715,20 +463,20 @@ export default class HodlHodl extends Component { > - + this.setState({ countrySearchInput: text })} placeholder="Search.." placeholderTextColor="#9AA0AA" value={this.state.countrySearchInput || ''} numberOfLines={1} - style={styles.modalSearchText2} + style={styles.searchTextInput} /> - + } + style={styles.modalFlatList} + ItemSeparatorComponent={() => } data={countries2render} keyExtractor={(item, index) => item.code} renderItem={({ item, index, separators }) => ( @@ -737,10 +485,10 @@ export default class HodlHodl extends Component { onShowUnderlay={separators.highlight} onHideUnderlay={separators.unhighlight} > - - - - + + + + {item.native_name} @@ -783,7 +531,7 @@ export default class HodlHodl extends Component { return ( { Keyboard.dismiss(); this.setState({ isChooseCurrencyVisible: false }); @@ -791,20 +539,20 @@ export default class HodlHodl extends Component { > - + this.setState({ currencySearchInput: text })} placeholder="Search.." placeholderTextColor="#9AA0AA" value={this.state.currencySearchInput || ''} numberOfLines={1} - style={styles.modalSearchText} + style={styles.curSearchInput} /> - + } + style={styles.modalFlatList} + ItemSeparatorComponent={() => } data={currencies2render} keyExtractor={(item, index) => item.code} renderItem={({ item, index, separators }) => ( @@ -813,17 +561,10 @@ export default class HodlHodl extends Component { onShowUnderlay={separators.highlight} onHideUnderlay={separators.unhighlight} > - - - - + + + + {item.name} {item.code !== CURRENCY_CODE_ANY && '[' + item.code + ']'} @@ -866,7 +607,7 @@ export default class HodlHodl extends Component { return ( { Keyboard.dismiss(); this.setState({ isChooseMethodVisible: false }); @@ -874,20 +615,20 @@ export default class HodlHodl extends Component { > - + this.setState({ methodSearchInput: text })} placeholder="Search.." placeholderTextColor="#9AA0AA" value={this.state.methodSearchInput || ''} numberOfLines={1} - style={styles.modalSearchText} + style={styles.mthdSearchInput} /> - + } + style={styles.modalFlatList} + ItemSeparatorComponent={() => } data={methods2render} keyExtractor={(item, index) => item.id} renderItem={({ item, index, separators }) => ( @@ -896,16 +637,15 @@ export default class HodlHodl extends Component { onShowUnderlay={separators.highlight} onHideUnderlay={separators.unhighlight} > - - - + + + {item.name} {item.id !== METHOD_ANY && '[' + item.type + ']'} @@ -921,108 +661,154 @@ export default class HodlHodl extends Component { ); }; - render() { + _onRefreshOffers = async () => { + this.setState({ + showShowFlatListRefreshControl: true, + }); + + try { + await this.fetchOffers(); + } catch (_) {} + + this.setState({ + showShowFlatListRefreshControl: false, + }); + }; + + renderHeader = () => { return ( - - - - Powered by HodlHodl® - Local Trader + + + Powered by HodlHodl® + + Local Trader { this.setState({ isChooseSideModalVisible: true }); }} > - {this.state.side === HodlHodlApi.FILTERS_SIDE_VALUE_SELL ? 'Buying' : 'Selling'} - + {this.state.side === HodlHodlApi.FILTERS_SIDE_VALUE_SELL ? 'Buying' : 'Selling'} + - - - {/* All offers */} - - - this.setState({ isChooseCountryModalVisible: true }) /* this.changeCountry() */}> - {this.getNativeCountryName()} - - + + + + {this.state.isLoading ? ( + + ) : ( + this.setState({ isChooseCountryModalVisible: true })}> + {this.getNativeCountryName()} + + )} { this.setState({ isFiltersModalVisible: true }); }} > - Filters + Filters - + - - {(this.state.isLoading && ) || ( - - this._refresh()} - refreshing={this.state.isLoading} - style={styles.offersList} - contentContainerStyle={styles.offersListContent} - ItemSeparatorComponent={() => } - data={this.state.offers} - ListEmptyComponent={() => No offers. Try to change "Near me" to Global offers!} - renderItem={({ item, index, separators }) => ( - this._onPress(item)} - onShowUnderlay={separators.highlight} - onHideUnderlay={separators.unhighlight} - > - - - - - - - {item.trader.login} - - {item.trader.trades_count > 0 ? Math.round(item.trader.rating * 100) + '%' : 'No rating'} - - - + + + ); + }; - {this.getItemText(item)} - - - - {this.getItemPrice(item)} - - - - Min/Max: {item.min_amount.replace('.00', '')} - {item.max_amount.replace('.00', '')} {item.currency_code} - - + renderItem = ({ item, index, separators }) => { + return ( + + this._onPress(item)} + onShowUnderlay={separators.highlight} + onHideUnderlay={separators.unhighlight} + > + + + + + {item.trader.online_status === 'online' && ( + + - - )} - /> - - )} + )} + + + + {item.trader.strong_hodler && ( + + )} + {item.trader.login} + + + {item.trader.trades_count > 0 + ? Math.round(item.trader.rating * 100) + '% / ' + item.trader.trades_count + ' trades' + : 'No rating'} + + + + {this.getItemText(item)} + + + + {this.getItemPrice(item)} + + + + Min/Max: {item.min_amount.replace('.00', '')} - {item.max_amount.replace('.00', '')} {item.currency_code} + + + + + + ); + }; + + sectionListKeyExtractor = (item, index) => { + return `${item}${index}}`; + }; + + renderSectionFooter = () => { + return this.state.offers.length <= 0 ? ( + + No offers. Try to change "Near me" to Global offers! + + ) : undefined; + }; + + render() { + return ( + + } + renderItem={this.renderItem} + keyExtractor={this.sectionListKeyExtractor} + renderSectionHeader={this.renderHeader} + contentInset={{ top: 0, left: 0, bottom: 60, right: 0 }} + sections={[{ key: HodlHodlListSections.OFFERS, data: this.state.offers }]} + style={styles.offersSectionList} + ItemSeparatorComponent={() => } + renderSectionFooter={this.renderSectionFooter} + /> {this.renderChooseSideModal()} - {this.renderChooseContryModal()} - {this.renderFiltersModal()} - {this.renderChooseCurrencyModal()} - {this.renderChooseMethodModal()} ); @@ -1030,9 +816,212 @@ export default class HodlHodl extends Component { } HodlHodl.propTypes = { - route: PropTypes.shape({ - params: PropTypes.shape({ - wallet: PropTypes.object, - }), + navigation: PropTypes.shape({ + navigate: PropTypes.func, + setParams: PropTypes.func, }), }; + +HodlHodl.navigationOptions = ({ navigation, route }) => ({ + ...BlueNavigationStyle(navigation, true), + title: '', + headerLeft: () => { + return route.params.displayLoginButton ? ( + + ) : ( + + ); + }, +}); + +const styles = StyleSheet.create({ + grayDropdownText: { + fontSize: 15, + fontWeight: '600', + color: '#9AA0AA', + }, + modalContent: { + backgroundColor: '#FFFFFF', + padding: 22, + justifyContent: 'center', + alignItems: 'center', + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + borderColor: 'rgba(0, 0, 0, 0.1)', + minHeight: 400, + height: 400, + }, + modalContentShort: { + backgroundColor: '#FFFFFF', + padding: 22, + justifyContent: 'center', + alignItems: 'center', + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + borderColor: 'rgba(0, 0, 0, 0.1)', + minHeight: 200, + height: 200, + }, + bottomModal: { + justifyContent: 'flex-end', + margin: 0, + }, + Title: { + fontWeight: 'bold', + fontSize: 30, + color: '#0c2550', + }, + BottomLine: { + fontSize: 10, + color: '#0c2550', + }, + grayDropdownTextContainer: { + backgroundColor: '#EEF0F4', + borderRadius: 20, + height: 35, + top: 3, + paddingLeft: 16, + paddingRight: 8, + paddingVertical: 6, + justifyContent: 'center', + alignItems: 'center', + flexDirection: 'row', + }, + grayTextContainerContainer: { + backgroundColor: '#EEF0F4', + borderRadius: 20, + height: 44, + justifyContent: 'center', + alignItems: 'center', + marginTop: 15, + }, + grayTextContainer: { + width: '100%', + alignItems: 'center', + flex: 1, + flexDirection: 'row', + }, + blueText: { + color: '#2f5fb3', + fontSize: 15, + fontWeight: '600', + }, + allOffersText: { + fontSize: 12, + color: '#9AA0AA', + position: 'absolute', + top: 0, + left: 15, + }, + locationText: { + top: 0, + left: 5, + color: '#0c2550', + fontSize: 20, + fontWeight: '500', + }, + nicknameText: { + color: '#0c2550', + fontSize: 18, + fontWeight: '600', + }, + blueTextContainer: { + backgroundColor: '#CCDDF9', + borderRadius: 20, + width: 110, + flex: 1, + flexDirection: 'row', + height: 36, + paddingLeft: 8, + justifyContent: 'center', + alignItems: 'center', + right: 4, + position: 'absolute', + }, + searchInputContainer: { + flexDirection: 'row', + borderColor: '#EEF0F4', + borderBottomColor: '#EEF0F4', + borderWidth: 1.0, + borderBottomWidth: 0.5, + backgroundColor: '#EEF0F4', + minHeight: 48, + height: 48, + marginHorizontal: 20, + alignItems: 'center', + marginVertical: 8, + borderRadius: 26, + width: '100%', + }, + circleWhite: { + position: 'absolute', + bottom: 4, + right: 3, + backgroundColor: 'white', + width: 9, + height: 9, + borderRadius: 4, + }, + circleGreen: { + position: 'absolute', + bottom: 1, + right: 1, + backgroundColor: '#00d327', + width: 7, + height: 7, + borderRadius: 4, + }, + itemPrice: { fontWeight: '600', fontSize: 14, color: '#9AA0AA' }, + minmax: { color: '#9AA0AA', fontSize: 12, paddingLeft: 10 }, + noOffersWrapper: { height: '100%', justifyContent: 'center' }, + noOffersText: { textAlign: 'center', color: '#9AA0AA', paddingHorizontal: 16 }, + modalFlatList: { width: '100%' }, + itemSeparator: { height: 0.5, width: '100%', backgroundColor: '#C8C8C8' }, + itemNameWrapper: { backgroundColor: 'white', flex: 1, flexDirection: 'row', paddingTop: 20, paddingBottom: 20 }, + itemNameBold: { fontSize: 20, color: '#0c2550', fontWeight: 'bold' }, + itemNameNormal: { fontSize: 20, color: '#0c2550', fontWeight: 'normal' }, + whiteBackground: { backgroundColor: 'white' }, + filterCurrencyText: { fontSize: 16, color: '#9AA0AA' }, + filteCurrencyTextWrapper: { color: '#9AA0AA', right: 0, position: 'absolute' }, + currencyNativeName: { fontSize: 20, color: '#0c2550' }, + currencyWrapper: { paddingLeft: 10, flex: 1, flexDirection: 'row' }, + methodNameText: { fontSize: 16, color: '#9AA0AA' }, + searchTextInput: { fontSize: 17, flex: 1, marginHorizontal: 8, minHeight: 33, paddingLeft: 6, paddingRight: 6 }, + iconWithOffset: { left: -10 }, + paddingLeft10: { paddingLeft: 10 }, + countryNativeNameBold: { fontSize: 20, color: '#0c2550', fontWeight: 'bold' }, + countryNativeNameNormal: { fontSize: 20, color: '#0c2550', fontWeight: 'normal' }, + curSearchInput: { flex: 1, marginHorizontal: 8, minHeight: 33, paddingLeft: 6, paddingRight: 6 }, + mthdSearchInput: { flex: 1, marginHorizontal: 8, minHeight: 33, paddingLeft: 6, paddingRight: 6 }, + currencyTextBold: { + fontSize: 20, + color: '#0c2550', + fontWeight: 'bold', + }, + currencyTextNormal: { + fontSize: 20, + color: '#0c2550', + fontWeight: 'normal', + }, + noPaddingLeftOrRight: { paddingLeft: 0, paddingRight: 0 }, + flexRow: { flexDirection: 'row' }, + traderRatingText2: { color: '#9AA0AA' }, + itemPriceWrapper: { + backgroundColor: '#EEF0F4', + borderRadius: 20, + paddingLeft: 8, + paddingRight: 8, + height: 26, + justifyContent: 'center', + alignItems: 'center', + }, + itemText: { color: '#9AA0AA', paddingTop: 10 }, + itemPriceWrapperWrapper: { flex: 1, flexDirection: 'row', paddingTop: 10, paddingBottom: 10, alignItems: 'center' }, + offersSectionList: { marginTop: 24, flex: 1 }, + marginHorizontal20: { marginHorizontal: 20 }, + avatarImage: { width: 40, height: 40, borderRadius: 40 }, + avatarWrapper: { backgroundColor: 'white', flex: 1, flexDirection: 'row' }, + avatarWrapperWrapper: { backgroundColor: 'white', paddingTop: 16, paddingBottom: 16 }, + headerWrapper: { marginHorizontal: 20, marginBottom: 8 }, + verifiedIcon: { marginTop: 5, marginRight: 5 }, +}); diff --git a/screen/wallets/hodlHodlLogin.js b/screen/wallets/hodlHodlLogin.js new file mode 100644 index 000000000..adf434478 --- /dev/null +++ b/screen/wallets/hodlHodlLogin.js @@ -0,0 +1,75 @@ +import React, { Component } from 'react'; +import { WebView } from 'react-native-webview'; +import { BlueNavigationStyle, SafeBlueArea } from '../../BlueComponents'; +import PropTypes from 'prop-types'; + +const url = 'https://accounts.hodlhodl.com/accounts/request_access?attributes=api_key,api_signature_key'; + +let lastTimeIvebeenHere = 0; + +const INJECTED_JAVASCRIPT = `(function() { + + window.postMessage = function (data) { + window.ReactNativeWebView && window.ReactNativeWebView.postMessage(data); + } + +})();`; + +export default class HodlHodlLogin extends Component { + static navigationOptions = ({ navigation }) => ({ + ...BlueNavigationStyle(navigation, true), + title: 'Login', + headerLeft: null, + }); + + constructor(props) { + super(props); + + this.state = { + url: url, + }; + } + + render() { + return ( + + (this.webview = ref)} + source={{ uri: this.state.url }} + onMessage={e => { + // this is a handler which receives messages sent from within the browser + + if (lastTimeIvebeenHere && +new Date() - lastTimeIvebeenHere < 5000) return; + lastTimeIvebeenHere = +new Date(); + // page can post messages several times, and that can confuse our react navigation, so we have protection + // against that + + let json = false; + try { + json = JSON.parse(e.nativeEvent.data); + } catch (_) {} + + if (json && json.allowed && json.data && json.data.api_key) { + this.props.route.params.cb(json.data.api_key, json.data.api_signature_key); + this.props.navigation.pop(); + } + }} + /> + + ); + } +} + +HodlHodlLogin.propTypes = { + route: PropTypes.shape({ + params: PropTypes.shape({ + cb: PropTypes.func.isRequired, + }), + }), + navigation: PropTypes.shape({ + getParam: PropTypes.func, + navigate: PropTypes.func, + pop: PropTypes.func, + }), +}; diff --git a/screen/wallets/hodlHodlMyContracts.js b/screen/wallets/hodlHodlMyContracts.js new file mode 100644 index 000000000..99b9e2eed --- /dev/null +++ b/screen/wallets/hodlHodlMyContracts.js @@ -0,0 +1,418 @@ +/* global alert */ +import React, { Component } from 'react'; +import { + Alert, + FlatList, + Keyboard, + KeyboardAvoidingView, + Linking, + Platform, + StyleSheet, + Text, + TouchableHighlight, + View, +} from 'react-native'; +import { BlueButton, BlueLoading, BlueNavigationStyle, BlueSpacing10, BlueSpacing20, SafeBlueArea } from '../../BlueComponents'; +import { AppStorage } from '../../class'; +import { HodlHodlApi } from '../../class/hodl-hodl-api'; +import Modal from 'react-native-modal'; +import NavigationService from '../../NavigationService'; + +const BlueApp: AppStorage = require('../../BlueApp'); + +export default class HodlHodlMyContracts extends Component { + static navigationOptions = ({ navigation }) => ({ + ...BlueNavigationStyle(navigation, true), + title: 'My contracts', + headerLeft: null, + }); + + constructor(props) { + super(props); + + this.state = { + contracts: [], + isLoading: true, + }; + } + + componentWillUnmount() { + clearInterval(this.state.inverval); + } + + async componentDidMount() { + const hodlApiKey = await BlueApp.getHodlHodlApiKey(); + const hodlApi = new HodlHodlApi(hodlApiKey); + this.setState({ hodlApi: hodlApi, contracts: [] }); + + const inverval = setInterval(async () => { + await this.refetchContracts(); + }, 60 * 1000); + + this.setState({ inverval }); + await this.refetchContracts(); + } + + render() { + if (this.state.isLoading) return ; + return ( + + { + return item.id; + }} + ListEmptyComponent={() => You dont have any contracts in progress} + style={styles.flatList} + ItemSeparatorComponent={() => } + data={this.state.contracts} + renderItem={({ item: contract, index, separators }) => ( + this._onContractPress(contract)} + > + + + + {contract.status} + + + + + + + {contract.volume_breakdown.goes_to_buyer} {contract.asset_code} + + {contract.your_role === 'buyer' ? 'buying' : 'selling'} + + + + {contract.statusText} + + + + + )} + /> + {this.renderContract()} + + ); + } + + async refetchContracts() { + this.setState({ + isLoading: true, + }); + + const hodlApi = this.state.hodlApi; + const contracts = []; + let contractToDisplay = this.state.contractToDisplay; + + const contractIds = await BlueApp.getHodlHodlContracts(); + + /* + * Initiator sends “Getting contract” request once every 1-3 minutes until contract.escrow.address is not null (thus, waiting for offer’s creator to confirm his payment password in case he uses the website) + * Each party verifies the escrow address locally + * Each party sends “Confirming contract’s escrow validity” request to the server + */ + for (const id of contractIds) { + let contract; + try { + contract = await hodlApi.getContract(id); + } catch (_) { + continue; + } + if (contract.status === 'canceled') continue; + if (contract.escrow && contract.escrow.address && hodlApi.verifyEscrowAddress()) { + await hodlApi.markContractAsConfirmed(id); + contract.isDepositedEnought = + contract.escrow.confirmations >= contract.confirmations && +contract.escrow.amount_deposited >= +contract.volume; + // technically, we could fetch balance of escrow address ourselved and verify, but we are relying on api here + + contract.statusText = 'Waiting for seller to deposit bitcoins to escrow...'; + if (contract.isDepositedEnought && contract.status !== 'paid') + contract.statusText = 'Bitcoins are in escrow! Please pay seller\nvia agreed payment method'; + if (contract.status === 'paid') contract.statusText = 'Waiting for seller to release coins from escrow'; + if (contract.status === 'in_progress' && contract.your_role === 'buyer') + contract.statusText = 'Coins are in escrow, please pay seller'; + + if (contract.status === 'completed') contract.statusText = 'All done!'; + } + + contracts.push(contract); + + if (contractToDisplay && contract.id === this.state.contractToDisplay.id) { + // refreshing contract that is currently being displayed + contractToDisplay = contract; + } + } + + this.setState({ hodlApi: hodlApi, contracts, contractToDisplay, isLoading: false }); + } + + _onContractPress(contract) { + this.setState({ + contractToDisplay: contract, + isRenderContractVisible: true, + }); + } + + renderContract = () => { + if (!this.state.contractToDisplay) return; + + return ( + { + Keyboard.dismiss(); + this.setState({ isRenderContractVisible: false }); + }} + > + + + + + {this.state.contractToDisplay.volume_breakdown.goes_to_buyer} {this.state.contractToDisplay.asset_code} + + + + + {this.state.contractToDisplay.price} {this.state.contractToDisplay.currency_code} + + + + + To + + + Linking.openURL(`https://blockstream.info/address/${this.state.contractToDisplay.release_address}`)} + > + {this.state.contractToDisplay.release_address} + + + + + + Escrow + + + Linking.openURL(`https://blockstream.info/address/${this.state.contractToDisplay.escrow.address}`)} + > + {this.state.contractToDisplay.escrow.address} + + + + + + {this.isAllowedToMarkContractAsPaid() ? ( + + How to pay + + + {this.state.contractToDisplay.payment_method_instruction.details} + + + + ) : ( + + )} + + + + {this.isAllowedToMarkContractAsPaid() ? ( + + this._onMarkContractAsPaid()} /> + + + ) : ( + + )} + + + + {this.state.contractToDisplay.can_be_canceled && ( + this._onCancelContract()} style={styles.cancelContractText}> + Cancel contract + + )} + + this._onOpenContractOnWebsite()} style={styles.openChatText}> + Open chat with counterparty + + + + + ); + }; + + /** + * If you are the buyer, DO NOT SEND PAYMENT UNTIL CONTRACT STATUS IS "in_progress". + */ + _onMarkContractAsPaid() { + if (!this.state.contractToDisplay) return; + + Alert.alert( + 'Are you sure you want to mark this contract as paid?', + `Do this only if you sent funds to the seller via agreed payment method`, + [ + { + text: 'Yes', + onPress: async () => { + const hodlApi = this.state.hodlApi; + try { + await hodlApi.markContractAsPaid(this.state.contractToDisplay.id); + this.setState({ isRenderContractVisible: false }); + await this.refetchContracts(); + } catch (Error) { + alert(Error); + } + }, + style: 'default', + }, + { + text: 'Cancel', + onPress: () => {}, + style: 'cancel', + }, + ], + { cancelable: true }, + ); + } + + async _onOpenContractOnWebsite() { + if (!this.state.contractToDisplay) return; + const hodlApi = this.state.hodlApi; + const sigKey = await BlueApp.getHodlHodlSignatureKey(); + if (!sigKey) { + alert('Error: signature key not set'); // should never happen + return; + } + + const autologinKey = await hodlApi.requestAutologinToken(sigKey); + const uri = 'https://hodlhodl.com/contracts/' + this.state.contractToDisplay.id + '?sign_in_token=' + autologinKey; + this.setState({ isRenderContractVisible: false }, () => { + NavigationService.navigate('HodlHodlWebview', { uri }); + }); + } + + _onCancelContract() { + if (!this.state.contractToDisplay) return; + + Alert.alert( + 'Are you sure you want to cancel this contract?', + ``, + [ + { + text: 'Yes, cancel contract', + onPress: async () => { + const hodlApi = this.state.hodlApi; + try { + await hodlApi.cancelContract(this.state.contractToDisplay.id); + this.setState({ isRenderContractVisible: false }); + await this.refetchContracts(); + } catch (Error) { + alert(Error); + } + }, + style: 'default', + }, + { + text: 'No', + onPress: () => {}, + style: 'cancel', + }, + ], + { cancelable: true }, + ); + } + + isAllowedToMarkContractAsPaid() { + return this.state.contractToDisplay.status === 'in_progress' && this.state.contractToDisplay.your_role === 'buyer'; + } +} + +const styles = StyleSheet.create({ + bottomModal: { + justifyContent: 'flex-end', + margin: 0, + }, + modalContent: { + backgroundColor: '#FFFFFF', + padding: 22, + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + borderColor: 'rgba(0, 0, 0, 0.1)', + minHeight: 425, + }, + modalContentCentered: { + justifyContent: 'center', + alignItems: 'center', + }, + statusGreenWrapper: { + backgroundColor: '#d2f8d5', + borderRadius: 20, + height: 28, + justifyContent: 'center', + alignItems: 'center', + margin: 15, + paddingLeft: 15, + paddingRight: 15, + }, + statusGreenText: { + fontSize: 12, + color: '#37bfa0', + }, + statusGrayWrapper: { + backgroundColor: '#ebebeb', + borderRadius: 20, + height: 28, + justifyContent: 'center', + alignItems: 'center', + margin: 15, + paddingLeft: 15, + paddingRight: 15, + }, + statusGrayText: { + fontSize: 12, + color: 'gray', + }, + statusGrayWrapper2: { + backgroundColor: '#f8f8f8', + borderRadius: 5, + height: 28, + justifyContent: 'center', + alignItems: 'center', + paddingLeft: 15, + paddingRight: 15, + }, + statusGrayText2: { + fontSize: 12, + color: 'gray', + }, + btcText: { + fontWeight: 'bold', + fontSize: 18, + color: '#0c2550', + }, + subheaderText: { + fontSize: 12, + fontWeight: 'bold', + color: '#0c2550', + }, + emptyComponentText: { textAlign: 'center', color: '#9AA0AA', paddingHorizontal: 16 }, + itemSeparatorComponent: { height: 0.5, width: '100%', backgroundColor: '#C8C8C8' }, + flexDirectionRow: { flexDirection: 'row' }, + flexDirectionColumn: { flexDirection: 'column' }, + volumeBreakdownText: { fontSize: 18, color: '#0c2550' }, + contractStatusText: { fontSize: 14, color: 'gray', fontWeight: 'normal' }, + cancelContractText: { color: '#d0021b', fontSize: 15, paddingTop: 20, fontWeight: '500', textAlign: 'center' }, + openChatText: { color: '#1b02d0', fontSize: 15, paddingTop: 20, fontWeight: '500', textAlign: 'center' }, + flatList: { paddingTop: 30 }, + roleText: { fontSize: 14, color: 'gray', padding: 5 }, +}); diff --git a/screen/wallets/hodlHodlViewOffer.js b/screen/wallets/hodlHodlViewOffer.js new file mode 100644 index 000000000..8395efc99 --- /dev/null +++ b/screen/wallets/hodlHodlViewOffer.js @@ -0,0 +1,351 @@ +/* global alert */ +import React, { Component } from 'react'; +import { Alert, FlatList, Image, KeyboardAvoidingView, Platform, ScrollView, StyleSheet, Text, View } from 'react-native'; +import { BlueButton, BlueLoading, BlueNavigationStyle, BlueSpacing10, SafeBlueArea } from '../../BlueComponents'; +import PropTypes from 'prop-types'; +import { HodlHodlApi } from '../../class/hodl-hodl-api'; +import { Icon } from 'react-native-elements'; +import { AppStorage } from '../../class'; +import NavigationService from '../../NavigationService'; + +const BlueApp: AppStorage = require('../../BlueApp'); +const prompt = require('../../prompt'); + +export default class HodlHodlViewOffer extends Component { + static navigationOptions = ({ navigation }) => ({ + ...BlueNavigationStyle(), + title: '', + }); + + constructor(props) { + super(props); + + const offerToDisplay = props.route.params.offerToDisplay; + + const horizontalScrollData = []; + horizontalScrollData.push({ id: 'window', body: offerToDisplay.payment_window_minutes + ' min' }); + horizontalScrollData.push({ + id: 'min / max', + body: + offerToDisplay.min_amount.replace('.00', '') + + ' - ' + + offerToDisplay.max_amount.replace('.00', '') + + ' ' + + offerToDisplay.currency_code, + }); + offerToDisplay.first_trade_limit && + horizontalScrollData.push({ + id: '1st trade', + body: offerToDisplay.first_trade_limit.replace('.00', '') + ' ' + offerToDisplay.currency_code, + }); + + for (const paymentInstruction of offerToDisplay.payment_method_instructions || []) { + horizontalScrollData.push({ + id: paymentInstruction.id + paymentInstruction.version, + header: paymentInstruction.payment_method_type, + body: paymentInstruction.payment_method_name, + }); + } + + horizontalScrollData.push({ id: 'confirmations', body: offerToDisplay.confirmations }); + + this.state = { + hodlApi: false, + isLoading: true, + horizontalScrollData, + offerToDisplay, + }; + } + + async componentDidMount() { + console.log('wallets/hodlHodlViewOffer - componentDidMount'); + + const hodlApiKey = await BlueApp.getHodlHodlApiKey(); + const hodlApi = new HodlHodlApi(hodlApiKey); + this.setState({ hodlApi, hodlApiKey }); + + this.setState({ + isLoading: false, + }); + } + + async _onAcceptOfferPress(offer) { + if (!this.state.hodlApiKey) { + alert('Please login to HodlHodl to accept offers'); + return; + } + const myself = await this.state.hodlApi.getMyself(); + if (!myself.encrypted_seed || myself.encrypted_seed.length < 10) { + const buttons = [ + { + text: 'Yes', + onPress: async a => { + const sigKey = await BlueApp.getHodlHodlSignatureKey(); + if (!sigKey) { + alert('Error: signature key not set'); // should never happen + return; + } + + const autologinKey = await this.state.hodlApi.requestAutologinToken(sigKey); + const uri = 'https://hodlhodl.com/dashboards/settings?sign_in_token=' + autologinKey; + NavigationService.navigate('HodlHodlWebview', { uri }); + }, + }, + { + text: 'Cancel', + onPress: async a => {}, + }, + ]; + Alert.alert('HodlHodl', `Looks like you didn't finish setting up account on HodlHodl, would you like to finish setup now?`, buttons, { + cancelable: true, + }); + return; + } + + let fiatValue; + try { + fiatValue = await prompt('How much ' + offer.currency_code + ' do you want to buy?', 'For example 100', true, 'numeric'); + } catch (_) { + return; + } + if (!fiatValue) return; + + const buttons = []; + for (const paym of offer.payment_method_instructions) { + buttons.push({ + text: paym.payment_method_name + ' (' + paym.payment_method_type + ')', + onPress: async a => { + let noError = true; + this.setState({ isLoading: true }); + let contract; + try { + contract = await this.state.hodlApi.acceptOffer(offer.id, offer.version, paym.id, paym.version, fiatValue); + } catch (Error) { + noError = false; + alert(Error); + } + this.setState({ isLoading: false }); + + if (noError && contract.id) { + await BlueApp.addHodlHodlContract(contract.id); + NavigationService.navigate('HodlHodlMyContracts'); + } + }, + }); + } + Alert.alert('Choose payment method', ``, buttons, { cancelable: true }); + } + + _renderHorizontalScrollItem(item) { + return ( + + {item.item.header || item.item.id} + {item.item.body} + + ); + } + + render() { + return this.state.isLoading ? ( + + ) : ( + + + + + {this.state.offerToDisplay.title} + + {/* horizontal panel with bubbles */} + + + + + {this.state.offerToDisplay.country} + + + + + + {this.state.offerToDisplay.price} {this.state.offerToDisplay.currency_code} + + + + {/* end */} + + {this.state.offerToDisplay.description} + + + + + + + + {/* avatar and rating */} + + + + {this.state.offerToDisplay.trader.online_status === 'online' && ( + + + + )} + + + + {this.state.offerToDisplay.trader.strong_hodler && ( + + )} + {this.state.offerToDisplay.trader.login} + + + {this.state.offerToDisplay.trader.trades_count > 0 + ? Math.round(this.state.offerToDisplay.trader.rating * 100) + + '%' + + ' / ' + + this.state.offerToDisplay.trader.trades_count + + ' trades' + : 'No rating'} + + + + {/* end */} + + {this.state.offerToDisplay.side === 'sell' ? ( + + + + { + await this._onAcceptOfferPress(this.state.offerToDisplay); + }} + /> + + + ) : ( + + )} + + + + + ); + } +} + +HodlHodlViewOffer.propTypes = { + route: PropTypes.shape({ + params: PropTypes.shape({ + offerToDisplay: PropTypes.object, + }), + }), +}; + +const styles = StyleSheet.create({ + modalContent: { + backgroundColor: '#FFFFFF', + padding: 22, + }, + Title: { + fontWeight: '600', + fontSize: 24, + color: '#0c2550', + }, + circleWhite: { + position: 'absolute', + bottom: 0, + right: 3, + backgroundColor: 'white', + width: 13, + height: 13, + borderRadius: 6, + }, + circleGreen: { + position: 'absolute', + bottom: 1, + right: 1, + backgroundColor: '#00d327', + width: 10, + height: 10, + borderRadius: 5, + }, + grayTextContainerContainer: { + backgroundColor: '#EEF0F4', + borderRadius: 20, + height: 30, + justifyContent: 'center', + alignItems: 'center', + marginTop: 15, + marginRight: 10, + paddingRight: 20, + }, + greenTextContainerContainer: { + backgroundColor: '#d2f8d5', + borderRadius: 20, + height: 30, + justifyContent: 'center', + alignItems: 'center', + marginTop: 15, + paddingLeft: 15, + paddingRight: 15, + }, + grayTextContainer: { + width: '100%', + alignItems: 'center', + flex: 1, + flexDirection: 'row', + }, + priceText: { + top: 0, + color: '#37bfa0', + fontSize: 14, + fontWeight: '500', + }, + descriptionText: { + top: 0, + color: '#818893', + fontSize: 14, + paddingTop: 20, + paddingBottom: 20, + fontWeight: '500', + minHeight: 150, + lineHeight: 23, + }, + nicknameText: { + color: '#0c2550', + fontSize: 16, + fontWeight: 'bold', + }, + traderRatingText: { + color: '#9AA0AA', + fontSize: 12, + }, + locationText: { + color: '#9BA0A9', + }, + horizontalScrollIemHeader: { fontSize: 12, color: '#9AA0AA' }, + horizontalScrollItemBody: { fontSize: 14, fontWeight: 'bold', color: '#0c2550' }, + horizontalScrollWrapper: { flexDirection: 'column', paddingTop: 20, paddingBottom: 20, paddingRight: 40 }, + flexDirRow: { flexDirection: 'row' }, + iconWithPadding: { paddingLeft: 16 }, + _hr: { + borderWidth: 0, + borderBottomWidth: 1, + borderColor: '#ebebeb', + }, + avatarImg: { width: 60, height: 60, borderRadius: 60 }, + avatarWrapper: { backgroundColor: 'white', flex: 1, flexDirection: 'column', alignItems: 'center', marginTop: 32 }, + verifiedIcon: { marginTop: 3, marginRight: 5 }, + traderWrapper: { alignItems: 'center', marginTop: 8 }, + acceptOfferButtonWrapper: { width: '70%', alignItems: 'center' }, + acceptOfferButtonWrapperWrapper: { marginTop: 24, alignItems: 'center' }, +}); diff --git a/screen/wallets/hodlHodlWebview.js b/screen/wallets/hodlHodlWebview.js new file mode 100644 index 000000000..9d2d5f6ee --- /dev/null +++ b/screen/wallets/hodlHodlWebview.js @@ -0,0 +1,38 @@ +import React, { Component } from 'react'; +import { WebView } from 'react-native-webview'; +import { BlueNavigationStyle, SafeBlueArea } from '../../BlueComponents'; +import PropTypes from 'prop-types'; + +export default class HodlHodlWebview extends Component { + static navigationOptions = ({ navigation }) => ({ + ...BlueNavigationStyle(navigation, true), + title: '', + headerLeft: null, + }); + + constructor(props) { + super(props); + + const uri = props.route.params.uri; + + this.state = { + uri, + }; + } + + render() { + return ( + + + + ); + } +} + +HodlHodlWebview.propTypes = { + route: PropTypes.shape({ + params: PropTypes.shape({ + uri: PropTypes.string.isRequired, + }), + }), +}; diff --git a/screen/wallets/list.js b/screen/wallets/list.js index 3cb29c37d..eae81f4da 100644 --- a/screen/wallets/list.js +++ b/screen/wallets/list.js @@ -436,7 +436,7 @@ export default class WalletsList extends Component { return ( { - this.props.navigation.navigate('HodlHodl', { fromWallet: this.state.wallet }); + this.props.navigation.navigate('HodlHodlRoot', { params: { wallet: this.state.wallet }, screen: 'HodlHodl' }); }} style={styles.ltRoot} > diff --git a/tests/integration/HodlHodl.test.js b/tests/integration/HodlHodl.test.js index 89c2ef76e..0b43d3f15 100644 --- a/tests/integration/HodlHodl.test.js +++ b/tests/integration/HodlHodl.test.js @@ -5,6 +5,20 @@ import { HodlHodlApi } from '../../class/hodl-hodl-api'; const bitcoin = require('bitcoinjs-lib'); const assert = require('assert'); +it.skip('can verify escrow address', () => { + const encryptedSeed = + 'ES1:b2dc8bd89782f70ef11ff1d1c6bf6adde0bea78fb959391de48f49acbf7f9766ca128b89c1a9a013d158b6c4dabee77997f8a15764d1b083f213b1d6aa9fb3a14a1edb406930a25423a1df3be72306f120b08972cea669dba1284bd8:bf5af8737529b419cc20935a1c05c742:pbkdf2:10000'; + const encryptPassword = 'Qwert12345'; + const address = '34n3rBtPA16BQYWycphnhK7C9DoucWb527'; + const index = 10298; + const witnessScript = + '522103dc0edfea797214be15a69148bfb1dffa1c8295c05300b7632143a77d918b4a0821031fec42b60942633616aff7e245796b5caae6bf59ef5ba688b0a59f33f08b2896210351fd6e52d38a37b9834909e3f8345c471346e1f5990ec00dafcc53e238d3c7c553ae'; + + const Hodl = new HodlHodlApi(); + assert.ok(Hodl.verifyEscrowAddress(encryptedSeed, encryptPassword, index, address, witnessScript)); + assert.ok(!Hodl.verifyEscrowAddress(encryptedSeed, encryptPassword, index, '3QDf45WU88t2kEBJTHcTPvtrXZx88SkmKC', witnessScript)); +}); + it('can create escrow address', () => { const keyPairServer = bitcoin.ECPair.fromPrivateKey( Buffer.from('9a8cfd0e33a37c90a46d358c84ca3d8dd089ed35409a6eb1973148c0df492288', 'hex'), @@ -109,12 +123,58 @@ describe('HodlHodl API', function () { assert.ok(offers[0].asset_code === 'BTC'); assert.ok(offers[0].country_code); assert.ok(offers[0].side === HodlHodlApi.FILTERS_SIDE_VALUE_SELL); - assert.ok(offers[0].title || offers[0].description || offers[1].title || offers[1].description, JSON.stringify(offers[0], null, 2)); + assert.ok(typeof offers[0].title !== 'undefined', JSON.stringify(offers[0], null, 2)); + assert.ok(typeof offers[0].description !== 'undefined', JSON.stringify(offers[0], null, 2)); assert.ok(offers[0].price); assert.ok(offers[0].payment_method_instructions); assert.ok(offers[0].trader); }); + it('can get offer', async () => { + if (!process.env.HODLHODL_OFFER_ID) return; + const Hodl = new HodlHodlApi(); + const offer = await Hodl.getOffer(process.env.HODLHODL_OFFER_ID); + assert.ok(offer.id); + assert.ok(offer.version); + }); + + it('can accept offer', async () => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000; + if (!process.env.HODLHODL_OFFER_ID) return; + const Hodl = new HodlHodlApi(); + const offer = await Hodl.getOffer(process.env.HODLHODL_OFFER_ID); + assert.strictEqual(offer.side, 'sell'); + const paymentMethodInstructionId = offer.payment_method_instructions[0].id; + const paymentMethodInstructionVersion = offer.payment_method_instructions[0].version; + const fiatValue = 100; + const contract = await Hodl.acceptOffer( + offer.id, + offer.version, + paymentMethodInstructionId, + paymentMethodInstructionVersion, + fiatValue, + ); + console.warn({ contract }); + }); + + it('can get contract', async () => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000; + if (!process.env.HODLHODL_CONTRACT_ID) return; + const Hodl = new HodlHodlApi(); + const contract = await Hodl.getContract(process.env.HODLHODL_CONTRACT_ID); + assert.ok(contract.your_role); + assert.ok(contract.volume); + assert.ok(contract.escrow); + }); + + it('can mark contract as confirmed', async () => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 * 1000; + if (!process.env.HODLHODL_CONTRACT_ID) return; + const Hodl = new HodlHodlApi(); + const result = await Hodl.markContractAsConfirmed(process.env.HODLHODL_CONTRACT_ID); + console.warn(result); + }); + it('can get payment methods', async () => { const Hodl = new HodlHodlApi(); const methods = await Hodl.getPaymentMethods(HodlHodlApi.FILTERS_COUNTRY_VALUE_GLOBAL); @@ -132,4 +192,20 @@ describe('HodlHodl API', function () { assert.ok(currencies[0].name); assert.ok(currencies[0].type); }); + + it('cat get myself', async () => { + const Hodl = new HodlHodlApi(); + const myself = await Hodl.getMyself(); + assert.ok(myself.encrypted_seed); + }); + + it('can create signature for autologin', async () => { + const Hodl = new HodlHodlApi(''); + const sig = Hodl.createSignature( + 'iqZC7uUmx4sVeIwFQN2YqGT5SyrXNLhxVX7QMGUeJK1CDdy87OcrOt3QvPE5LFC56Lgu7WLlg12U55Vy', + 'cce14197a08ebab7cfbb41cfce9fe91e0f31d572d3f48571ca3c30bfd516f769', + 1589980224, + ); + assert.strictEqual(sig, '1d2a51ca2c54ff9107a3460b22f01bc877e527a9a719d81b32038741332159fc'); + }); });