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}
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-->
<!--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
- looking up the profiles and statuses of other users
The basic identity and storage services are provided by `blockstack.js`. To test
the application, you need to have already [registered a Blockstack ID](ids-introduction).
The basic identity and storage services are provided by `blockstack.js`.
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.
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
constructor(props) {
super(props);
this.state = {
person: {
name() {
return 'Anonymous';
},
avatarUrl() {
return avatarFallbackImage;
},
},
username: "",
newStatus: "",
statuses: [],
statusIndex: 0,
isLoading: false
};
export const Profile = ({ userData, handleSignOut }) => {
const [newStatus, setNewStatus] = React.useState('');
const [statuses, setStatuses] = React.useState([]);
const [statusIndex, setStatusIndex] = React.useState(0);
const [isLoading, setLoading] = React.useState(false);
const [username, setUsername] = React.useState(userData.username);
const [person, setPerson] = React.useState(new Person(userData.profile));
const { authOptions } = useConnect();
const { userSession } = authOptions;
// ...
}
```
3. Locate the `render()` method.
4. Modify the `render()` method to add a text input and submit button to the
3. Modify the rendered result to add a text input and submit button to the
by replacing it with the code below:
The following code renders the `person.name` and `person.avatarURL`
properties from the profile on the display:
```javascript
render() {
const { handleSignOut, userSession } = this.props;
const { person } = this.state;
const { username } = this.state;
export const Profile = ({ userData, handleSignOut }) => {
// ... state setup from before
return (
!userSession.isSignInPending() && person ?
<div className="container">
<div className="row">
<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="col-md-12">
<textarea className="input-status"
value={this.state.newStatus}
onChange={e => this.handleNewStatusChange(e)}
value={newStatus}
onChange={handleNewStatus}
placeholder="Enter a status"
/>
</div>
<div className="col-md-12">
<button
className="btn btn-primary btn-lg"
onClick={e => this.handleNewStatusSubmit(e)}
onClick={handleNewStatusSubmit}
>
Submit
</button>
@ -306,7 +293,7 @@ These are the `UserSession.putFile`, `UserSession.getFile`, and `lookupProfile`
</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 profile data.
Notice that the `userSession` property passed into our profile renderer contains
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:
7. Add two methods in the `Profile` component to handle the status input events:
```javascript
handleNewStatusChange(event) {
this.setState({newStatus: event.target.value})
const handleNewStatus = (event) => {
setNewStatus(event.target.value);
}
handleNewStatusSubmit(event) {
this.saveNewStatus(this.state.newStatus)
this.setState({
newStatus: ""
})
const handleNewStatusSubmit = async (event) => {
await saveNewStatus(newStatus);
setNewStatus("");
}
```
8. Add a `saveNewStatus()` method to save the new statuses.
```javascript
saveNewStatus(statusText) {
const { userSession } = this.props
let statuses = this.state.statuses
const saveNewStatus = async (statusText) => {
const _statuses = statuses
let status = {
id: this.state.statusIndex++,
id: statusIndex + 1,
text: statusText.trim(),
created_at: Date.now()
}
statuses.unshift(status)
_statuses.unshift(status)
const options = { encrypt: false }
userSession.putFile('statuses.json', JSON.stringify(statuses), options)
.then(() => {
this.setState({
statuses: statuses
})
})
await userSession.putFile('statuses.json', JSON.stringify(_statuses), options);
setStatuses(_statuses);
setStatusIndex(statusIndex + 1);
}
```
@ -396,13 +358,12 @@ Update `Profile.js` again.
```javascript
<div className="col-md-12 statuses">
{this.state.isLoading && <span>Loading...</span>}
{this.state.statuses.map((status) => (
{isLoading && <span>Loading...</span>}
{statuses.map((status) => (
<div className="status" key={status.id}>
{status.text}
</div>
)
)}
))}
</div>
```
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.
```javascript
fetchData() {
const { userSession } = this.props
this.setState({ isLoading: true })
const fetchData = async () => {
setLoading(true);
const options = { decrypt: false }
userSession.getFile('statuses.json', options)
.then((file) => {
var statuses = JSON.parse(file || '[]')
this.setState({
person: new Person(userSession.loadUserData().profile),
username: userSession.loadUserData().username,
statusIndex: statuses.length,
statuses: statuses,
})
})
.finally(() => {
this.setState({ isLoading: false })
})
const file = await userSession.getFile('statuses.json', options)
const _statuses = JSON.parse(file || '[]')
setStatusIndex(_statuses.length);
setStatuses(_statuses);
setLoading(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.
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
// after setting up your component's state
componentDidMount() {
this.fetchData()
}
React.useEffect(() => {
fetchData();
}, [username]);
```
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.
```javascript
isLocal() {
return this.props.match.params.username ? false : true
// Make sure you add this new prop!
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:
```javascript
fetchData() {
const { userSession } = this.props
this.setState({ isLoading: true })
if (this.isLocal()) {
const fetchData = async () => {
setLoading(true);
if (isLocal()) {
const options = { decrypt: false }
userSession.getFile('statuses.json', options)
.then((file) => {
var statuses = JSON.parse(file || '[]')
this.setState({
person: new Person(userSession.loadUserData().profile),
username: userSession.loadUserData().username,
statusIndex: statuses.length,
statuses: statuses,
})
})
.finally(() => {
this.setState({ isLoading: false })
})
const file = await userSession.getFile('statuses.json', options)
const _statuses = JSON.parse(file || '[]')
setStatusIndex(_statuses.length);
setStatuses(_statuses);
setLoading(false);
} else {
const username = this.props.match.params.username
lookupProfile(username)
.then((profile) => {
this.setState({
person: new Person(profile),
username: username
})
})
.catch((error) => {
console.log('could not resolve profile')
})
const username = match.params.username
try {
const newProfile = await lookupProfile(username)
setPerson(new Person(newProfile));
setUsername(username);
} 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
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
const options = { username: username, decrypt: false }
userSession.getFile('statuses.json', options)
.then((file) => {
var statuses = JSON.parse(file || '[]')
this.setState({
statusIndex: statuses.length,
statuses: statuses
})
})
.catch((error) => {
console.log('could not fetch statuses')
})
.finally(() => {
this.setState({ isLoading: false })
})
const file = await userSession.getFile('statuses.json', options)
const _statuses = JSON.parse(file || '[]')
setStatusIndex(_statuses.length);
setStatuses(_statuses);
setLoading(false);
```
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.
6. Replace the `render()` method with the following:
6. Replace the returned JSX in the `Profile` component with the following:
```javascript
render() {
const { handleSignOut, userSession } = this.props;
const { person } = this.state;
const { username } = this.state;
return (
!userSession.isSignInPending() && person ?
<div className="container">
<div className="row">
<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>
</h1>
<span>{username}</span>
{this.isLocal() &&
{isLocal() &&
<span>
&nbsp;|&nbsp;
<a onClick={ handleSignOut.bind(this) }>(Logout)</a>
<a onClick={handleSignOut}>(Logout)</a>
</span>
}
</div>
</div>
</div>
{this.isLocal() &&
{isLocal() &&
<div className="new-status">
<div className="col-md-12">
<textarea className="input-status"
value={this.state.newStatus}
onChange={e => this.handleNewStatusChange(e)}
value={newStatus}
onChange={handleNewStatus}
placeholder="What's on your mind?"
/>
</div>
<div className="col-md-12 text-right">
<button
className="btn btn-primary btn-lg"
onClick={e => this.handleNewStatusSubmit(e)}
onClick={handleNewStatusSubmit}
>
Submit
</button>
@ -709,19 +640,17 @@ process URL paths that contain the `.` (dot) character for example,
</div>
}
<div className="col-md-12 statuses">
{this.state.isLoading && <span>Loading...</span>}
{this.state.statuses.map((status) => (
{isLoading && <span>Loading...</span>}
{statuses.map((status) => (
<div className="status" key={status.id}>
{status.text}
</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.

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/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
...
const userSession = new UserSession({ appConfig })
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.
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) {
e.preventDefault();
userSession.redirectToSignIn();
}
...
```js
import React from 'react';
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. :

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)
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`.

Loading…
Cancel
Save