diff --git a/.gitignore b/.gitignore index 11416c2db..4594c405a 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,7 @@ __coverage__ .elasticbeanstalk/ -unused \ No newline at end of file +unused + +tmp + diff --git a/README.md b/README.md index 8d332dc30..9f1dd185c 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,30 @@ The Blockstack Browser allows you to explore the Blockstack internet. --- -### Getting up and running +## Developing Locally 1. Clone this repo from `https://github.com/blockstack/blockstack-browser.git` -2. Run `npm install` from the root directory -3. Run `gulp proxy` which starts the CORS proxy on port 1337 -4. In another terminal, run `gulp dev` (may require installing Gulp globally `npm install gulp -g`) -5. Your browser will automatically be opened and directed to the browser-sync proxy address -6. To prepare assets for production, run the `npm run build` task (Note: the production task does not fire up the browser-sync server, and won't provide you with browser-sync's live reloading. Simply use `gulp dev` during development. More information below) +1. Run `npm install` from the root directory +1. Run `npm run dev` to run locally -Now that `gulp dev` is running, the server is up as well and serving files from the `/build` directory. Any changes in the `/app` directory will be automatically processed by Gulp and the changes will be injected to any open browsers pointed at the proxy address. +*Note: When you do `npm run dev` you're running two concurrent processes. One starts a CORS proxy on port 1337. The other runs a BrowserSync process that watches the assets in `/app`, then builds them and places them in `/build`, and in turn serves them up on port 3000. Anytime changes are made to the original files, they are rebuilt and resynced to the browser frames you have open.* ---- +## Building for the Web + +1. Make sure you've cloned the repo and installed all npm assets (as shown above) +1. Run `npm run web` + +## Building for macOS + +1. Make sure you have a working installation of Xcode 8 or higher +1. Run `npm install nexe -g` to install the "node to native" binary tool globally +1. Run `npm run mac` + +*Note: You only need to run `nexe` once but the first build will take a while as `nexe` downloads and compiles a source copy of node. Then it creates and copies the needed proxy binaries into place and copies a built version of the browser web app into the source tree.* + +*Note: This has only been tested on macOS Sierra 10.12.* + +## Tech Stack This app uses the latest versions of the following libraries: @@ -33,16 +45,12 @@ This app uses the latest versions of the following libraries: Along with many Gulp libraries (these can be seen in either `package.json`, or at the top of each task in `/gulp/tasks/`). ---- - -### Running tests +## Testing 1. If you haven't already, follow steps 1 & 2 above -2. If you haven't already run `gulp dev` or `npm run build` at least once, run `npm run build` -3. Run all tests in the `tests/` directory with the `gulp test` command - * A single file can be run by specifing an `-f` flag: `gulp test -f ` +2. If you haven't already run `npm run dev` or `npm run build` at least once, run `npm run build` +3. Run all tests in the `tests/` directory with the `npm run test` command + * A single file can be run by specifing an `-f` flag: `npm run test -f ` * In the `PATH_TO_TEST_FILE`, it is possible to omit the `tests/` prefix, as well as the `.test.js` suffix. They will be automatically added if not detected. -##### Code coverage - -When running tests, code coverage will be automatically calculated and output to an HTML file using the [Istanbul](https://github.com/gotwarlost/istanbul) library. These files can be seen in the generated `coverage/` directory. +*Note: When running tests, code coverage will be automatically calculated and output to an HTML file using the [Istanbul](https://github.com/gotwarlost/istanbul) library. These files can be seen in the generated `coverage/` directory.* \ No newline at end of file diff --git a/api b/api new file mode 100644 index 000000000..e69de29bb diff --git a/app/images/app-blockstack.png b/app/images/app-blockstack.png new file mode 100644 index 000000000..4f0d4bce9 Binary files /dev/null and b/app/images/app-blockstack.png differ diff --git a/app/images/app-hello-blockstack.png b/app/images/app-hello-blockstack.png new file mode 100644 index 000000000..637327500 Binary files /dev/null and b/app/images/app-hello-blockstack.png differ diff --git a/app/images/app-identity.png b/app/images/app-identity.png new file mode 100644 index 000000000..eb8e9d40f Binary files /dev/null and b/app/images/app-identity.png differ diff --git a/app/images/app-settings.png b/app/images/app-settings.png new file mode 100644 index 000000000..a368fdee9 Binary files /dev/null and b/app/images/app-settings.png differ diff --git a/app/js/App.js b/app/js/App.js index 6f822f490..ab72c8de9 100644 --- a/app/js/App.js +++ b/app/js/App.js @@ -5,45 +5,21 @@ import { connect } from 'react-redux' import Sidebar from './components/Sidebar' import Navbar from './components/Navbar' +import { AccountActions } from './store/account' function mapStateToProps(state) { return { - accountCreated: state.account.accountCreated } } -class MainScreen extends Component { - static propTypes = { - children: PropTypes.element.isRequired - } - - render() { - return ( -
- -
- - {this.props.children} -
-
- ) - } -} - -class WelcomeScreen extends Component { - render() { - return ( -
- {this.props.children} -
- ) - } +function mapDispatchToProps(dispatch) { + return bindActionCreators(AccountActions, dispatch) } class App extends Component { static propTypes = { children: PropTypes.element.isRequired, - accountCreated: PropTypes.bool.isRequired + initializeWallet: PropTypes.func.isRequired, } static contextTypes = { @@ -52,36 +28,22 @@ class App extends Component { constructor(props) { super(props) - this.state = {} - } - - componentHasNewProps(accountCreated) { - if (!accountCreated) { - this.context.router.push('/account/create') - } } componentWillMount() { - this.componentHasNewProps(this.props.accountCreated) - } - - componentWillReceiveProps(nextProps) { - if (nextProps.accountCreated !== this.props.accountCreated) { - this.componentHasNewProps(nextProps.accountCreated) - } + this.props.initializeWallet("password", null) } render() { - if (this.props.accountCreated) { - return () - } else { - return () - } + return ( +
+ {this.props.children} +
+ ) } } - -export default connect(mapStateToProps)(App) +export default connect(mapStateToProps, mapDispatchToProps)(App) /* { diff --git a/app/js/components/Navbar.js b/app/js/components/Navbar.js index f8a340daf..0d5591bd6 100644 --- a/app/js/components/Navbar.js +++ b/app/js/components/Navbar.js @@ -2,7 +2,6 @@ import React, { Component, PropTypes } from 'react' import { Link } from 'react-router' import BackButton from '../components/BackButton' import ForwardButton from '../components/ForwardButton' -import AddressBar from './AddressBar' class Navbar extends Component { static propTypes = { @@ -33,20 +32,6 @@ class Navbar extends Component { -
- -
- - Account -
- -
- - -
-
- -
diff --git a/app/js/pages/DashboardPage.js b/app/js/pages/DashboardPage.js index e242109b2..481a55a9c 100644 --- a/app/js/pages/DashboardPage.js +++ b/app/js/pages/DashboardPage.js @@ -24,30 +24,41 @@ class DashboardPage extends Component {    return (
-
-
Featured Apps, coming soon...
-
-
- +
+ +
+ +
+ +
+

Profiles

+
-
- +
+ +
+ +
+ +
+

Settings

+
-
- +
+ +
+ +
+
+
+

Hello, Blockstack

+
-
-

Browse the decentralized web

-
-
-
-
Blockstack browser is the world’s first browser that enables you to browse the decentralized web
-
-
   ) diff --git a/app/js/pages/IdentityPage.js b/app/js/pages/IdentityPage.js new file mode 100644 index 000000000..336c71808 --- /dev/null +++ b/app/js/pages/IdentityPage.js @@ -0,0 +1,103 @@ +import React, { Component, PropTypes } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' +import { Link } from 'react-router' +import { Person } from 'blockstack-profiles' + +import AddressBar from '../components/AddressBar' +import { IdentityItem } from '../components/index' +import { IdentityActions } from '../store/identities' +import { AccountActions } from '../store/account' + +function mapStateToProps(state) { + return { + localIdentities: state.identities.localIdentities, + lastNameLookup: state.identities.lastNameLookup, + identityAddresses: state.account.identityAccount.addresses, + addressLookupUrl: state.settings.api.addressLookupUrl || '' + } +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators(Object.assign({}, IdentityActions, AccountActions), dispatch) +} + +class IdentityPage extends Component { + static propTypes = { + localIdentities: PropTypes.object.isRequired, + createNewIdentity: PropTypes.func.isRequired, + refreshIdentities: PropTypes.func.isRequired, + addressLookupUrl: PropTypes.string.isRequired, + lastNameLookup: PropTypes.array.isRequired + } + + constructor(props) { + super(props) + + this.state = { + localIdentities: this.props.localIdentities + } + } + + componentWillMount() { + this.props.refreshIdentities( + this.props.identityAddresses, + this.props.addressLookupUrl, + this.props.localIdentities, + this.props.lastNameLookup + ) + } + + componentWillReceiveProps(nextProps) { + this.setState({ + localIdentities: nextProps.localIdentities + }) + } + + render() { + return ( +
+ + +
+

Personas

+
+
+ + Register + + + Import + +
+
+
    + {Object.keys(this.state.localIdentities).map((domainName) => { + const identity = this.state.localIdentities[domainName], + person = new Person(identity.profile) + if (identity.domainName) { + return ( + + ) + } + })} +
+
+
+ ) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(IdentityPage) \ No newline at end of file diff --git a/app/js/pages/account/SettingsPage.js b/app/js/pages/account/SettingsPage.js index 6c9c6b2c1..9998d89a9 100644 --- a/app/js/pages/account/SettingsPage.js +++ b/app/js/pages/account/SettingsPage.js @@ -2,12 +2,17 @@ import React, { Component, PropTypes } from 'react' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import RadioGroup from 'react-radio-group' +import { SELF_HOSTED_S3, BLOCKSTACK_INC, DROPBOX } from '../../utils/storage/index' +import { DROPBOX_APP_ID, getDropboxAccessTokenFromHash } from '../../utils/storage/dropbox' import { InputGroup, AccountSidebar, SaveButton, PageHeader } from '../../components/index' import { SettingsActions } from '../../store/settings' +var Dropbox = require('dropbox') + + function mapStateToProps(state) { return { api: state.settings.api @@ -32,10 +37,13 @@ class SettingsPage extends Component { api: this.props.api } + this.onValueChange = this.onValueChange.bind(this) this.updateApi = this.updateApi.bind(this) this.resetApi = this.resetApi.bind(this) this.onHostedDataValueChange = this.onHostedDataValueChange.bind(this) + this.connectDropbox = this.connectDropbox.bind(this) + this.disconnectDropbox = this.disconnectDropbox.bind(this) } componentWillReceiveProps(nextProps) { @@ -44,6 +52,17 @@ class SettingsPage extends Component { }) } + componentDidMount() { + let api = this.state.api + const dropboxAccessToken = getDropboxAccessTokenFromHash(window.location.hash) + if(dropboxAccessToken != null) { + api['dropboxAccessToken'] = dropboxAccessToken + this.setState({ api: api }) + window.location.hash = "" + this.props.updateApi(api) + } + } + onValueChange(event) { let api = this.state.api api[event.target.name] = event.target.value @@ -65,6 +84,19 @@ class SettingsPage extends Component { this.props.resetApi() } + connectDropbox() { + var dbx = new Dropbox({ clientId: DROPBOX_APP_ID }) + window.location = dbx.getAuthenticationUrl('http://localhost:3000/account/settings') + } + disconnectDropbox() { + let api = this.state.api + var dbx = new Dropbox({ accessToken: api.dropboxAccessToken }) + dbx.authTokenRevoke() + api.dropboxAccessToken = null + this.setState({ api: api }) + this.props.updateApi(api) + } + render() { return (
@@ -100,28 +132,31 @@ class SettingsPage extends Component {
)} - { this.state.api.hostedDataLocation === 'self-hosted-S3' ? + { this.state.api.hostedDataLocation === DROPBOX ?
- - - + { this.state.api.dropboxAccessToken == null ? + + : + + }
: null } diff --git a/app/js/pages/apps/AppPage.js b/app/js/pages/apps/AppPage.js deleted file mode 100644 index 6a5d2221f..000000000 --- a/app/js/pages/apps/AppPage.js +++ /dev/null @@ -1,91 +0,0 @@ -import React, { Component, PropTypes } from 'react' -import { bindActionCreators } from 'redux' -import { connect } from 'react-redux' -import { Link } from 'react-router' - -import { PageHeader } from '../../components/index' - -function mapStateToProps(state) { - return { - } -} - -function mapDispatchToProps(dispatch) { - let actions = {} - return bindActionCreators(actions, dispatch) -} - -class AppPage extends Component { - constructor(props) { - super(props) - - this.state = { - currentURI: null, - isLoading: true - } - } - - componentHasNewRouteParams(props) { - if (props.routeParams.name) { - let routeName = props.routeParams.name.replace('.app', '') - let currentURI = null - - let routes = { - 'blockstack': 'https://blockstack.org', - 'openbazaar': 'https://openbazaar.org', - 'mediachain': 'http://www.mediachain.io', - 'ipfs': 'https://ipfs.io', - 'arcadecity': 'http://arcade.city', - 'bitcoincore': 'https://bitcoincore.org', - 'coinbase': 'https://www.coinbase.com', - '21': 'https://21.co', - 'helloworld': 'https://blockstack-hello-world.firebaseapp.com/' - } - - if (routeName in routes) { - currentURI = routes[routeName] - } - - this.setState({ - currentURI: currentURI, - isLoading: false - }) - } - } - - componentWillMount() { - this.componentHasNewRouteParams(this.props) - } - - componentWillReceiveProps(nextProps) { - if (nextProps.routeParams !== this.props.routeParams) { - this.componentHasNewRouteParams(nextProps) - } - } - - render() { - return ( -
- { this.state.currentURI !== null ? - - - : -
- {this.state.isLoading ? -

- Loading... -

- : -

- Site not found -

- } -
- } -
- ) - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(AppPage) \ No newline at end of file diff --git a/app/js/pages/profiles/EditProfilePage.js b/app/js/pages/profiles/EditProfilePage.js index e0f0b6359..bd04e7b9d 100644 --- a/app/js/pages/profiles/EditProfilePage.js +++ b/app/js/pages/profiles/EditProfilePage.js @@ -9,7 +9,7 @@ import { InputGroup, SaveButton, ProfileEditingSidebar, PageHeader } from '../../components/index' import { IdentityActions } from '../../store/identities' -import { getNameParts, uploadFile } from '../../utils/index' +import { getNameParts, uploadProfile, uploadPhoto } from '../../utils/index' import BasicInfoTab from './BasicInfoTab' import PhotosTab from './PhotosTab' @@ -51,8 +51,8 @@ class EditProfilePage extends Component { } this.saveProfile = this.saveProfile.bind(this) - this.uploadProfile = this.uploadProfile.bind(this) this.changeTabs = this.changeTabs.bind(this) + this.uploadProfilePhoto = this.uploadProfilePhoto.bind(this) } componentHasNewLocalIdentities(props) { @@ -79,18 +79,14 @@ class EditProfilePage extends Component { componentWillUnmount() { this.saveProfile(this.state.profile) - this.uploadProfile() } saveProfile(newProfile) { this.props.updateProfile(this.props.routeParams.index, newProfile) - const analyticsId = this.props.analyticsId mixpanel.track('Save profile', { distinct_id: analyticsId }) mixpanel.track('Perform action', { distinct_id: analyticsId }) - } - uploadProfile() { const filename = this.state.domainName + '.json' const keypair = this.props.identityKeypairs[0], @@ -101,12 +97,23 @@ class EditProfilePage extends Component { tokenRecord = wrapToken(token), tokenRecords = [tokenRecord] const data = JSON.stringify(tokenRecords, null, 2) - uploadFile(this.props.api, filename, data, ({ url, err, res }) => { - if (err) { - console.log(res) - console.log('profile not uploaded to s3') - } + + uploadProfile(this.props.api, this.state.domainName, data).catch((err) => { + console.error(err) + console.error('profile not uploaded ') }) + + } + + + uploadProfilePhoto(file, index) { + + const analyticsId = this.props.analyticsId + mixpanel.track('Upload photo', { distinct_id: analyticsId }) + mixpanel.track('Perform action', { distinct_id: analyticsId }) + const name = this.state.domainName + return uploadPhoto(this.props.api, name, file, index) + } changeTabs(tabName) { @@ -148,7 +155,8 @@ class EditProfilePage extends Component { return ( + saveProfile={this.saveProfile} + uploadProfilePhoto={this.uploadProfilePhoto} /> ) case "Social Accounts": return ( diff --git a/app/js/pages/profiles/PhotosTab.js b/app/js/pages/profiles/PhotosTab.js index 13386df6d..0b9e5cc03 100644 --- a/app/js/pages/profiles/PhotosTab.js +++ b/app/js/pages/profiles/PhotosTab.js @@ -2,16 +2,22 @@ import React, { Component, PropTypes } from 'react' import { InputGroup, SaveButton } from '../../components/index' +var Dropbox = require('dropbox'); + +var Dropzone = require('react-dropzone'); + class PhotosTab extends Component { static propTypes = { profile: PropTypes.object.isRequired, - saveProfile: PropTypes.func.isRequired + saveProfile: PropTypes.func.isRequired, + uploadProfilePhoto: PropTypes.func.isRequired } constructor(props) { super(props) this.state = { - profile: null + profile: null, + files: [] } this.onChange = this.onChange.bind(this) this.saveProfile = this.saveProfile.bind(this) @@ -65,10 +71,43 @@ class PhotosTab extends Component { this.setState({profile: profile}) } + onDrop(acceptedFiles, rejectedFiles, index) { + let files = this.state.files + let profile = this.state.profile + + files[index] = acceptedFiles[0] // only accept 1 file + files[index].uploaded = false + + this.setState({ + files: files + }) + + this.props.uploadProfilePhoto(files[index], index) + .then((avatarUrl) => { + profile.image[index].contentUrl = avatarUrl + files[index].uploaded = true + this.setState({ + files: files, + profile: profile + }) + }) + .catch((error) => { + console.error(error) + }) + + + } + + onOpenClick() { + this.refs.dropzone.open(); + } + + render() { const profile = this.state.profile, images = this.state.profile.hasOwnProperty('image') ? - this.state.profile.image : [] + this.state.profile.image : [], + files = this.state.hasOwnProperty('files') ? this.state.files : [] return (
@@ -82,10 +121,35 @@ class PhotosTab extends Component {
{ image.name === 'avatar' ? - {this.onChange(event, index)}} /> +
+ +
+ { this.onDrop(acceptedFiles, rejectedFiles, index) } } + multiple={false} maxSize={5242880} accept="image/*" + className="dropzone" activeClassName="dropzone-active"> + { !files[index] && !image.contentUrl ? +
+
Drop your photo here or click/tap to select a file!
+
+
+ : +
+ { image.contentUrl ? + + : + + } +
+ } +
+
+ + {this.onChange(event, index)}} /> +
: null }