Browse Source

Connect docs for general release (#585)

* feat: add connect to todo-list

* feat: updates storage tutorial with connect

* fix: broken links and missing images
fix/connect
Hank Stoever 5 years ago
committed by GitHub
parent
commit
5d84a489e4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 251
      _browser/blockstack_storage.md
  2. BIN
      _browser/images/display-complete.png
  3. BIN
      _browser/images/initial-app.png
  4. BIN
      _browser/images/login-choice.png
  5. BIN
      _browser/images/login-no-auth.png
  6. BIN
      _browser/images/login.gif
  7. BIN
      _browser/images/login.png
  8. BIN
      _browser/images/make-a-list.png
  9. BIN
      _browser/images/multi-player-storage-status.png
  10. BIN
      _browser/images/multiple-lists.png
  11. BIN
      _browser/images/network-connections.gif
  12. BIN
      _browser/images/publish-data-perm.png
  13. BIN
      _browser/images/saving-status.png
  14. BIN
      _browser/images/todo-app.png
  15. BIN
      _browser/images/todo-sign-in.png
  16. 28
      _browser/todo-list.md
  17. 4
      _includes/sign_in.md

251
_browser/blockstack_storage.md

@ -14,7 +14,7 @@ topics:
* TOC * TOC
{:toc} {:toc}
This tutorial does not teach you about authentication. That is covered in depth [in the hello-blockstack tutorial](hello-blockstack). This tutorial does not teach you about authentication. That is covered in depth [in the guide to Blockstack Connect](/develop/connect/get-started).
<!--TODO: authentication tutorial--> <!--TODO: authentication tutorial-->
<!--Strictly speaking not sure it is necessary here to send them out--> <!--Strictly speaking not sure it is necessary here to send them out-->
@ -32,8 +32,7 @@ to follow along, basic familiarity with React.js is helpful. When complete, the
- displaying statuses in the user profile - displaying statuses in the user profile
- looking up the profiles and statuses of other users - looking up the profiles and statuses of other users
The basic identity and storage services are provided by `blockstack.js`. To test The basic identity and storage services are provided by `blockstack.js`.
the application, you need to have already [registered a Blockstack ID](ids-introduction).
For this tutorial, you will use the following tools: For this tutorial, you will use the following tools:
@ -220,48 +219,36 @@ These are the `UserSession.putFile`, `UserSession.getFile`, and `lookupProfile`
1. Open the `src/components/Profile.js` file. 1. Open the `src/components/Profile.js` file.
2. Replace the initial state in the `constructor()` method so that it holds the key properties required by the app. 2. Replace the initial state in the component method so that it holds the key properties required by the app.
This code constructs a Blockstack `Person` object to hold the profile. Your constructor should look like this: This code constructs a Blockstack `Person` object to hold the profile. Your component should look like this:
```javascript ```javascript
constructor(props) { export const Profile = ({ userData, handleSignOut }) => {
super(props); const [newStatus, setNewStatus] = React.useState('');
const [statuses, setStatuses] = React.useState([]);
this.state = { const [statusIndex, setStatusIndex] = React.useState(0);
person: { const [isLoading, setLoading] = React.useState(false);
name() { const [username, setUsername] = React.useState(userData.username);
return 'Anonymous'; const [person, setPerson] = React.useState(new Person(userData.profile));
}, const { authOptions } = useConnect();
avatarUrl() { const { userSession } = authOptions;
return avatarFallbackImage; // ...
},
},
username: "",
newStatus: "",
statuses: [],
statusIndex: 0,
isLoading: false
};
} }
``` ```
3. Locate the `render()` method. 3. Modify the rendered result to add a text input and submit button to the
4. Modify the `render()` method to add a text input and submit button to the
by replacing it with the code below: by replacing it with the code below:
The following code renders the `person.name` and `person.avatarURL` The following code renders the `person.name` and `person.avatarURL`
properties from the profile on the display: properties from the profile on the display:
```javascript ```javascript
render() { export const Profile = ({ userData, handleSignOut }) => {
const { handleSignOut, userSession } = this.props; // ... state setup from before
const { person } = this.state;
const { username } = this.state;
return ( return (
!userSession.isSignInPending() && person ?
<div className="container"> <div className="container">
<div className="row"> <div className="row">
<div className="col-md-offset-3 col-md-6"> <div className="col-md-offset-3 col-md-6">
@ -289,15 +276,15 @@ These are the `UserSession.putFile`, `UserSession.getFile`, and `lookupProfile`
<div className="new-status"> <div className="new-status">
<div className="col-md-12"> <div className="col-md-12">
<textarea className="input-status" <textarea className="input-status"
value={this.state.newStatus} value={newStatus}
onChange={e => this.handleNewStatusChange(e)} onChange={handleNewStatus}
placeholder="Enter a status" placeholder="Enter a status"
/> />
</div> </div>
<div className="col-md-12"> <div className="col-md-12">
<button <button
className="btn btn-primary btn-lg" className="btn btn-primary btn-lg"
onClick={e => this.handleNewStatusSubmit(e)} onClick={handleNewStatusSubmit}
> >
Submit Submit
</button> </button>
@ -306,7 +293,7 @@ These are the `UserSession.putFile`, `UserSession.getFile`, and `lookupProfile`
</div> </div>
</div> </div>
</div> : null </div>
); );
} }
``` ```
@ -315,61 +302,36 @@ These are the `UserSession.putFile`, `UserSession.getFile`, and `lookupProfile`
user's Blockstack ID. To display this, your app must extract the ID from the user's Blockstack ID. To display this, your app must extract the ID from the
user profile data. user profile data.
Notice that the `userSession` property passed into our profile renderer contains 7. Add two methods in the `Profile` component to handle the status input events:
the `isSignInPending()` method which checks if a sign in operation is pending.
5. Locate the `componentWillMount()` method.
6. Add the `username` property below the `person` property.
You'll use the Blockstack `loadUserData()` method in our user session to access the `username`.
```javascript
componentWillMount() {
const { userSession } = this.props
this.setState({
person: new Person(userSession.loadUserData().profile),
username: userSession.loadUserData().username
});
}
```
7. Add two methods in the `Profile` class to handle the status input events:
```javascript ```javascript
handleNewStatusChange(event) { const handleNewStatus = (event) => {
this.setState({newStatus: event.target.value}) setNewStatus(event.target.value);
} }
handleNewStatusSubmit(event) { const handleNewStatusSubmit = async (event) => {
this.saveNewStatus(this.state.newStatus) await saveNewStatus(newStatus);
this.setState({ setNewStatus("");
newStatus: ""
})
} }
``` ```
8. Add a `saveNewStatus()` method to save the new statuses. 8. Add a `saveNewStatus()` method to save the new statuses.
```javascript ```javascript
saveNewStatus(statusText) { const saveNewStatus = async (statusText) => {
const { userSession } = this.props const _statuses = statuses
let statuses = this.state.statuses
let status = { let status = {
id: this.state.statusIndex++, id: statusIndex + 1,
text: statusText.trim(), text: statusText.trim(),
created_at: Date.now() created_at: Date.now()
} }
statuses.unshift(status) _statuses.unshift(status)
const options = { encrypt: false } const options = { encrypt: false }
userSession.putFile('statuses.json', JSON.stringify(statuses), options) await userSession.putFile('statuses.json', JSON.stringify(_statuses), options);
.then(() => { setStatuses(_statuses);
this.setState({ setStatusIndex(statusIndex + 1);
statuses: statuses
})
})
} }
``` ```
@ -396,13 +358,12 @@ Update `Profile.js` again.
```javascript ```javascript
<div className="col-md-12 statuses"> <div className="col-md-12 statuses">
{this.state.isLoading && <span>Loading...</span>} {isLoading && <span>Loading...</span>}
{this.state.statuses.map((status) => ( {statuses.map((status) => (
<div className="status" key={status.id}> <div className="status" key={status.id}>
{status.text} {status.text}
</div> </div>
) ))}
)}
</div> </div>
``` ```
This displays existing state. Your code needs to fetch statuses on page load. This displays existing state. Your code needs to fetch statuses on page load.
@ -410,34 +371,26 @@ Update `Profile.js` again.
4. Add a new method called `fetchData()` after the `saveNewStatus()` method. 4. Add a new method called `fetchData()` after the `saveNewStatus()` method.
```javascript ```javascript
fetchData() { const fetchData = async () => {
const { userSession } = this.props setLoading(true);
this.setState({ isLoading: true })
const options = { decrypt: false } const options = { decrypt: false }
userSession.getFile('statuses.json', options) const file = await userSession.getFile('statuses.json', options)
.then((file) => { const _statuses = JSON.parse(file || '[]')
var statuses = JSON.parse(file || '[]') setStatusIndex(_statuses.length);
this.setState({ setStatuses(_statuses);
person: new Person(userSession.loadUserData().profile), setLoading(false);
username: userSession.loadUserData().username,
statusIndex: statuses.length,
statuses: statuses,
})
})
.finally(() => {
this.setState({ isLoading: false })
})
} }
``` ```
By default, `getFile()` this method decrypts data; because the default `putFile()` encrypts it. In this case, the app shares statuses publicly. So, there is no need to decrypt. By default, `getFile()` this method decrypts data; because the default `putFile()` encrypts it. In this case, the app shares statuses publicly. So, there is no need to decrypt.
5. Call `fetchData()` from the `componentDidMount()` method. 5. Call `fetchData()` from the by using React's `useEffect` method, which will fetch data whenever the component's username state is changed..
```javascript ```javascript
// after setting up your component's state
componentDidMount() { React.useEffect(() => {
this.fetchData() fetchData();
} }, [username]);
``` ```
6. Save the file. 6. Save the file.
@ -578,8 +531,13 @@ process URL paths that contain the `.` (dot) character for example,
3. Add a single method to the `Profile` class that determines if the app is viewing the local user's profile or another user's profile. 3. Add a single method to the `Profile` class that determines if the app is viewing the local user's profile or another user's profile.
```javascript ```javascript
isLocal() { // Make sure you add this new prop!
return this.props.match.params.username ? false : true export const Profile = ({ userData, handleSignOut, match }) => {
// ...
const isLocal = () => {
return match.params.username ? false : true
}
// ...
} }
``` ```
@ -588,37 +546,25 @@ process URL paths that contain the `.` (dot) character for example,
4. Modify the `fetchData()` method like so: 4. Modify the `fetchData()` method like so:
```javascript ```javascript
fetchData() { const fetchData = async () => {
const { userSession } = this.props setLoading(true);
this.setState({ isLoading: true }) if (isLocal()) {
if (this.isLocal()) {
const options = { decrypt: false } const options = { decrypt: false }
userSession.getFile('statuses.json', options) const file = await userSession.getFile('statuses.json', options)
.then((file) => { const _statuses = JSON.parse(file || '[]')
var statuses = JSON.parse(file || '[]') setStatusIndex(_statuses.length);
this.setState({ setStatuses(_statuses);
person: new Person(userSession.loadUserData().profile), setLoading(false);
username: userSession.loadUserData().username,
statusIndex: statuses.length,
statuses: statuses,
})
})
.finally(() => {
this.setState({ isLoading: false })
})
} else { } else {
const username = this.props.match.params.username const username = match.params.username
lookupProfile(username) try {
.then((profile) => { const newProfile = await lookupProfile(username)
this.setState({ setPerson(new Person(newProfile));
person: new Person(profile), setUsername(username);
username: username } catch (error) {
}) console.log('Could not resolve profile');
}) }
.catch((error) => {
console.log('could not resolve profile')
})
} }
} }
``` ```
@ -630,40 +576,25 @@ process URL paths that contain the `.` (dot) character for example,
documentation](http://blockstack.github.io/blockstack.js/#getfile) for documentation](http://blockstack.github.io/blockstack.js/#getfile) for
details. details.
5. Add the following block to `fetchData()` right after the call to `lookupProfile(username)... catch((error)=>{..}` block: 5. Add the following block to `fetchData()` right after the call to `setUsername(username)` block:
```javascript ```javascript
const options = { username: username, decrypt: false } const options = { username: username, decrypt: false }
userSession.getFile('statuses.json', options) const file = await userSession.getFile('statuses.json', options)
.then((file) => { const _statuses = JSON.parse(file || '[]')
var statuses = JSON.parse(file || '[]') setStatusIndex(_statuses.length);
this.setState({ setStatuses(_statuses);
statusIndex: statuses.length, setLoading(false);
statuses: statuses
})
})
.catch((error) => {
console.log('could not fetch statuses')
})
.finally(() => {
this.setState({ isLoading: false })
})
``` ```
This fetches the user statuses. This fetches the user statuses.
Finally, you must conditionally render the logout button, status input textbox, and submit button so they don't show up when viewing another user's profile. Finally, you must conditionally render the logout button, status input textbox, and submit button so they don't show up when viewing another user's profile.
6. Replace the `render()` method with the following: 6. Replace the returned JSX in the `Profile` component with the following:
```javascript ```javascript
render() {
const { handleSignOut, userSession } = this.props;
const { person } = this.state;
const { username } = this.state;
return ( return (
!userSession.isSignInPending() && person ?
<div className="container"> <div className="container">
<div className="row"> <div className="row">
<div className="col-md-offset-3 col-md-6"> <div className="col-md-offset-3 col-md-6">
@ -680,28 +611,28 @@ process URL paths that contain the `.` (dot) character for example,
: 'Nameless Person' }</span> : 'Nameless Person' }</span>
</h1> </h1>
<span>{username}</span> <span>{username}</span>
{this.isLocal() && {isLocal() &&
<span> <span>
&nbsp;|&nbsp; &nbsp;|&nbsp;
<a onClick={ handleSignOut.bind(this) }>(Logout)</a> <a onClick={handleSignOut}>(Logout)</a>
</span> </span>
} }
</div> </div>
</div> </div>
</div> </div>
{this.isLocal() && {isLocal() &&
<div className="new-status"> <div className="new-status">
<div className="col-md-12"> <div className="col-md-12">
<textarea className="input-status" <textarea className="input-status"
value={this.state.newStatus} value={newStatus}
onChange={e => this.handleNewStatusChange(e)} onChange={handleNewStatus}
placeholder="What's on your mind?" placeholder="What's on your mind?"
/> />
</div> </div>
<div className="col-md-12 text-right"> <div className="col-md-12 text-right">
<button <button
className="btn btn-primary btn-lg" className="btn btn-primary btn-lg"
onClick={e => this.handleNewStatusSubmit(e)} onClick={handleNewStatusSubmit}
> >
Submit Submit
</button> </button>
@ -709,19 +640,17 @@ process URL paths that contain the `.` (dot) character for example,
</div> </div>
} }
<div className="col-md-12 statuses"> <div className="col-md-12 statuses">
{this.state.isLoading && <span>Loading...</span>} {isLoading && <span>Loading...</span>}
{this.state.statuses.map((status) => ( {statuses.map((status) => (
<div className="status" key={status.id}> <div className="status" key={status.id}>
{status.text} {status.text}
</div> </div>
) ))}
)} </div>
</div> </div>
</div> </div>
</div> </div>
</div> : null
); );
}
``` ```
This checks to ensure that users are viewing their own profile, by wrapping the **Logout** button and inputs with the `{isLocal() && ...}` condition. This checks to ensure that users are viewing their own profile, by wrapping the **Logout** button and inputs with the `{isLocal() && ...}` condition.

BIN
_browser/images/display-complete.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
_browser/images/initial-app.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
_browser/images/login-choice.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
_browser/images/login-no-auth.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

BIN
_browser/images/login.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
_browser/images/login.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

BIN
_browser/images/make-a-list.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
_browser/images/multi-player-storage-status.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
_browser/images/multiple-lists.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

BIN
_browser/images/network-connections.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
_browser/images/publish-data-perm.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
_browser/images/saving-status.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
_browser/images/todo-app.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
_browser/images/todo-sign-in.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

28
_browser/todo-list.md

@ -189,19 +189,27 @@ in and sign out is handled in each of these files:
| `components/Signin.js ` | Code for the initial sign on page. | | `components/Signin.js ` | Code for the initial sign on page. |
| `components/Profile.js` | Application data storage and user sign out. | | `components/Profile.js` | Application data storage and user sign out. |
The `src/components/App.js` code configures an `AppConfig` object and then uses this to create a `UserSession`. Then, the application calls a [`redirectToSignIn()`](https://blockstack.github.io/blockstack.js#redirectToSignIn) function which generates the `authRequest` and redirects the user to the Blockstack authenticator: <!-- The `src/components/App.js` code configures an `AppConfig` object and then uses this to create a `UserSession`. Then, the application calls a [`redirectToSignIn()`](https://blockstack.github.io/blockstack.js#redirectToSignIn) function which generates the `authRequest` and redirects the user to the Blockstack authenticator: -->
```js The `src/components/App.js` code configures a `UserSession` and other `authOptions`, which are passed to the `Connect` component. The `Connect` component acts as a "provider" for the rest of your application, and essentially creates a re-usable configuration for you.
...
const userSession = new UserSession({ appConfig })
export default class App extends Component { In the `src/components/Signin.js` component, we are then calling the `useConnect` hook. This hook returns many helper functions, one of which is `doOpenAuth`. Calling this method will being the authentication process. First, it injects a modal into your application, which acts as a way of "warming up" your user to Blockstack authentication. When the user continues, they are redirected to the Blockstack authenticator, where they can finish signing up.
handleSignIn(e) { ```js
e.preventDefault(); import React from 'react';
userSession.redirectToSignIn(); import { useConnect } from '@blockstack/connect';
}
... export const Signin = () => {
const { doOpenAuth } = useConnect();
return (
<button
onClick={() => doOpenAuth()}
>
Sign In with Blockstack
</button>
)
};
``` ```
Once the user authenticates, the application handles the `authResponse` in the `src/components/Profile.js` file. : Once the user authenticates, the application handles the `authResponse` in the `src/components/Profile.js` file. :

4
_includes/sign_in.md

@ -4,9 +4,9 @@ A decentralized application (DApp) and the Blockstack Browser communicate during
![](/storage/images/app-sign-in.png) ![](/storage/images/app-sign-in.png)
When a user chooses to **Sign in with Blockstack** on a DApp, it calls the `redirectToSignIn()` method which sends an `authRequest` to the Blockstack Browser. Blockstack passes the token in via a URL query string in the `authRequest` parameter: When a user chooses to **Sign in with Blockstack** on a DApp, it calls the `doOpenAuth()` method which sends an `authRequest` to the Blockstack Authenticator. Blockstack passes the token in via a URL query string in the `authRequest` parameter:
`https://browser.blockstack.org/auth?authRequest=j902120cn829n1jnvoa...` `https://app.blockstack.org/#/sign-up?authRequest=j902120cn829n1jnvoa...`
When the Blockstack Browser receives the request, it generates an (`authResponse`) token to the application using an _ephemeral transit key_ . The ephemeral transit key is just used for the particular instance of the application, in this case, to sign the `authRequest`. The application stores the ephemeral transit key during the request generation. The public portion of the transit key is passed in the `authRequest` token. The Blockstack Browser uses the public portion of the key to encrypt an _app-private key_ which is returned via the `authResponse`. When the Blockstack Browser receives the request, it generates an (`authResponse`) token to the application using an _ephemeral transit key_ . The ephemeral transit key is just used for the particular instance of the application, in this case, to sign the `authRequest`. The application stores the ephemeral transit key during the request generation. The public portion of the transit key is passed in the `authRequest` token. The Blockstack Browser uses the public portion of the key to encrypt an _app-private key_ which is returned via the `authResponse`.

Loading…
Cancel
Save