├── .babelrc
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── DEVELOPMENT.md
├── LICENSE
├── README.md
├── dist
├── context
│ └── index.js
└── index.js
├── netlify.toml
├── package.json
├── src
├── context
│ └── index.js
└── index.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-react",
4 | "@babel/preset-env"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | react-blockstack-context-*.tgz
4 | .DS_Store
5 | dist
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /example
2 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | ## [Unreleased]
6 |
7 | - Document use with showBlockstackConnect
8 |
9 | ## [0.6.10] - 2020-04-28
10 |
11 | - Minor correction
12 |
13 | ## [0.6.9] - 2020-04-28
14 |
15 | - The useFile hook doesn't fail with SDK 21 when the file is missing
16 | - Documenting the `useConnectOptions` hook for Connect integration
17 |
18 | ## [0.6.8] - 2020-03-27
19 |
20 | - Unofficial `useConnectOptions` hook for Connect integration
21 |
22 | ## [0.6.7] - 2020-03-27
23 |
24 | - New `didConnect` function to support Blockstack Connect integration
25 |
26 | ## [0.6.6] - 2020-03-26
27 |
28 | - Update documentation
29 |
30 | ## [0.6.5] - 2020-03-26
31 |
32 | - New `authenticated` property returned by useBlockstack()
33 |
34 | ## [0.6.4] - 2019-11-22
35 |
36 | - Example extracted to https://github.com/REBL-Stack/starter-app
37 |
38 | ## [0.6.3] - 2019-10-19
39 |
40 | - Guard in `useFile` against concurrent operations on the same file.
41 |
42 | ## [0.6.0] - 2019-10-12
43 |
44 | ### Added
45 |
46 | - `useFile` hook
47 |
48 | ### Changed
49 |
50 | - Default export is `initBlockstack` instead of the `Blockstack` context provider object (breaking).
51 |
--------------------------------------------------------------------------------
/DEVELOPMENT.md:
--------------------------------------------------------------------------------
1 | # Development Guide
2 |
3 | For the react-blockstack package.
4 |
5 | ## Publish to npm
6 |
7 | The react-blockstack package is published at:
8 |
9 | https://www.npmjs.com/package/react-blockstack
10 |
11 | First test in the example or another local project (see other section).
12 |
13 | To publish a new version, first update the version build the distribution:
14 |
15 | npm install
16 | npm run build
17 | npm version [patch | minor | major]
18 | npm publish
19 |
20 | # Test react-blockstack in a local project
21 |
22 | During development of this package, it can be used/tested in a local project
23 | without first publishing to npm (but see issues below).
24 |
25 | 1. In this directory, execute:
26 |
27 | npm install
28 | npm run build
29 | npm link
30 |
31 | 2. In the dependent project top directory, execute:
32 |
33 | npm link react-blockstack
34 | npm install
35 |
36 | If `npm install` reports the package is not found, try deleting the package-lock.json or see this for other options:
37 |
38 | https://stackoverflow.com/questions/24550515/npm-after-npm-link-module-is-not-found
39 |
40 | If `npm start` reports missing modules when loading react-blockstack, try running `npm install` in this module.
41 |
42 | If there are problems related to "[react hook] can only be called inside the body of a function component" suggesting missing or mismatching modules (like react-dom)
43 | use `npm ls react-dom` in using project to investigate. Removing package-lock.json and
44 | reinstalling is a potential fix. Also make sure to use same case for imported names (`React`). And `npm link` doesn't work well with hooks, says posters at:
45 | https://github.com/facebook/react/issues/13991
46 | https://github.com/webpack/webpack/issues/8607#issuecomment-453068938
47 | https://github.com/transitive-bullshit/create-react-library/issues/99
48 |
49 | Verified work-around for link issue is to change the dependency in project to:
50 | "react": "file:../node_modules/react"
51 | But npm install has to be run first... so it's a drag.
52 |
53 | Adding this to webpack.config.js appears to have done the trick:
54 | "resolve": { "alias": { "react": "./node_modules/react" }}
55 |
56 | Other possibility:
57 | "Solution was as simple as adding these lines to webpack.config.js:""
58 | module.exports = {
59 | externals: [
60 | "react",
61 | "react-dom"
62 | ],
63 | };
64 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Terje Norderhaug
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Blockstack
2 |
3 | React hooks to use the [Blockstack SDK](https://blockstack.github.io/blockstack.js/)
4 | with [react function components](https://reactjs.org/docs/components-and-props.html).
5 |
6 | Includes backward compatibility with react class components.
7 |
8 | ## Installation
9 |
10 | npm install react-blockstack
11 |
12 | ## Blockstack Authentication
13 |
14 | Execute as early as possible to initialize the Blockstack SDK and user authentication:
15 |
16 | ````javascript
17 | import ReactBlockstack from 'react-blockstack'
18 |
19 | const blockstack = ReactBlockstack()
20 | ````
21 |
22 | Consider placing this code in the main index.js file of your project. For customization of the authentication, use the same options argument as for [UserSession](https://blockstack.github.io/blockstack.js/classes/usersession.html) in the Blockstack SDK:
23 |
24 | ````javascript
25 | import { AppConfig } from 'blockstack'
26 |
27 | const appConfig = new AppConfig(['store_write', 'publish_data'])
28 | const blockstack = ReactBlockstack({appConfig})
29 | ````
30 |
31 | The `blockstack.userSession` property is available in case you need to access to the blockstack SDK on toplevel. It is typically preferable to get `userSession` from the `useBlockstack` hook, ignoring the return value from `ReactBlockstack`.
32 |
33 | ## React Hook for Function Components
34 |
35 | The package provides a `useBlockStack` React hook for use in function components. It provides access to the Blockstack SDK and eventually an authenticated user:
36 |
37 | const {userSession, userData, signIn, signOut, person} = useBlockstack()
38 |
39 | The hook returns these properties:
40 |
41 | * `userSession` (UserSession interface for the Blockstack SDK)
42 | * `userData` (UserData interface from the Blockstack SDK; `null` unless authenticated)
43 | * `authenticated` (true when authentication is complete)
44 | * `signIn` (function to sign in the user; `null` when already logged in or pending authentication)
45 | * `signOut` (function to sign out the user; `null` when not logged in or pending authentication)
46 | * `person` (if authenticated, a Person instance containing the user profile)
47 |
48 | Only `userSession` and `signIn` are available before authentication.
49 | After authentication, `signIn` is null, but there are bindings for
50 | `userData`, `authenticated`, `signOut` and `person`. This can be used for conditional rendering
51 | depending on the authentication status. Note that the user can neither sign in nor sign out when the authentication is pending, so:
52 |
53 | ```javascript
54 | const pendingAuthentication = !signIn && !signOut
55 | ```
56 |
57 | ### Example: Authentication Button
58 |
59 | Here is a react function component that implements an authentication button.
60 | It handles both signin and logout, adapting the label depending on status,
61 | disabling the button while authentication is pending:
62 |
63 | ````javascript
64 | import { useBlockstack } from 'react-blockstack'
65 |
66 | function Auth () {
67 | const { signIn, signOut } = useBlockstack()
68 | return (
69 |
71 | { signIn ? "Sign In" : signOut ? "Sign Out" : "..." }
72 |
73 | )
74 | }
75 | ````
76 |
77 | To include the button in jsx:
78 |
79 |
80 |
81 | ## Persistent Data Storage
82 |
83 | The `useFile(path: string)` hook is used to access the app's data store, covering
84 | the functionality of `getFile`, `putFile` and `deleteFile` in the Blockstack SDK.
85 |
86 | The argument is a pathname in the app's data store. The file does not have to exists before the call.
87 |
88 | The `useFile` hook returns the content of the file like `getFile`, with a function to change the file content as second value. The returned content is `undefined` until the file has been accessed and `null` if the file is determined not to exist. The setter accepts the same content types as `putFile`, and will delete the file if called with `null`. The content returned by `useFile` is conservatively updated, not reflecting the change until after storing the content is completed.
89 |
90 | ### Example
91 |
92 | ```javascript
93 | const [content, setContent] = useFile("content")
94 | ```
95 |
96 | ## Blockstack Connect
97 |
98 | React Blockstack can be used with
99 | [Blockstack Connect](https://github.com/blockstack/ux/tree/master/packages/connect).
100 |
101 | To ensure that the state is properly updated after Connect authentication,
102 | make Connect's `authOptions.finished` callback function call `didConnect()`.
103 |
104 | The `useConnectOptions()` hook can be used to fill in default options like the
105 | `userSession`. The argument is the same as the authOptions in Connect.
106 | The hook provides sensible defaults if called without an argument.
107 |
108 | ### Example
109 |
110 | ```javascript
111 | import { didConnect, useConnectOptions } from 'react-blockstack'
112 |
113 | const connectOptions = {
114 | redirectTo: '/',
115 | finished: ({ userSession }) => {
116 | didConnect({ userSession })
117 | }
118 | }
119 |
120 | function Register (props) {
121 | const authOptions = useConnectOptions(connectOptions)
122 | const signIn = useCallback(authOptions && (() => {
123 | showBlockstackConnect(authOptions)
124 | }), [authOptions])
125 | return(
126 | Sign In
127 | )
128 | }
129 |
130 | ```
131 |
132 | ## React Class Components
133 |
134 | For conventional React class components, the package provides an optional
135 | [React context object](https://reactjs.org/docs/context.html)
136 | that pass properties from `useBlockstack` down to components.
137 |
138 | Enclose top level elements in a shared Blockstack context:
139 |
140 | ````javascript
141 | import { Blockstack } from 'react-blockstack/dist/context'
142 |
143 | ReactDOM.render( , document.getElementById('app-root'))
144 | ````
145 |
146 | The Blockstack SDK properties are implicitly passed through the component tree and can be used as any other React context.
147 |
148 | ### Example
149 |
150 | The App component below will automatically be updated whenever there is a change in the Blockstack status. Note the use of the `this.context` containing the properties and
151 | that the class is required to have `contextType = BlockstackContext`.
152 |
153 | ````javascript
154 | import React, { Component } from 'react'
155 | import BlockstackContext from 'react-blockstack/dist/context'
156 |
157 | export default class App extends Component {
158 | static contextType = BlockstackContext
159 | render() {
160 | const { person } = this.context
161 | const avatarUrl = person && person.avatarUrl && person.avatarUrl()
162 | const personName = person && person.name && person.name()
163 | return(
164 |
165 |
166 | { personName }
167 |
168 |
169 | )
170 | }
171 | }
172 | ````
173 |
174 | If there are multiple Blockstack components they will all share the same context.
175 |
176 | ## Live Demo
177 |
178 | The REBL Stack [starter app](https://github.com/REBL-Stack/starter-app)
179 | is a reimplementation of the
180 | [Blockstack react template](https://github.com/blockstack/blockstack-app-generator/tree/master/react/templates).
181 |
182 | It demonstrates different ways of using react-blockStack.
183 | You are encouraged to use the example as a starting point for your own projects.
184 |
185 | Live at:
186 | [](https://react-blockstack.netlify.com)
187 |
--------------------------------------------------------------------------------
/dist/context/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | Object.defineProperty(exports, "Blockstack", {
7 | enumerable: true,
8 | get: function get() {
9 | return _index.Blockstack;
10 | }
11 | });
12 | exports["default"] = void 0;
13 |
14 | var _index = require("../index.js");
15 |
16 | var _default = _index.BlockstackContext;
17 | exports["default"] = _default;
--------------------------------------------------------------------------------
/dist/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.useBlockstack = useBlockstack;
7 | exports.setContext = setContext;
8 | exports.initBlockstack = initBlockstack;
9 | exports.Blockstack = Blockstack;
10 | exports.didConnect = didConnect;
11 | exports.useConnectOptions = useConnectOptions;
12 | exports.useFile = useFile;
13 | exports.useFilesList = useFilesList;
14 | exports.useFileUrl = useFileUrl;
15 | exports.useFetch = useFetch;
16 | exports.useStored = useStored;
17 | exports.usePersistent = usePersistent;
18 | exports.Persistent = Persistent;
19 | exports.createAppManifestHook = createAppManifestHook;
20 | exports.useAppManifest = useAppManifest;
21 | exports.AuthenticatedDocumentClass = AuthenticatedDocumentClass;
22 | exports.useProfile = useProfile;
23 | exports.BlockstackContext = exports["default"] = void 0;
24 |
25 | var _react = _interopRequireWildcard(require("react"));
26 |
27 | var _blockstack = require("blockstack");
28 |
29 | var _reactAtom = require("@dbeining/react-atom");
30 |
31 | var _lodash = require("lodash");
32 |
33 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; }
34 |
35 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
36 |
37 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
38 |
39 | function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
40 |
41 | function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
42 |
43 | function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); }
44 |
45 | function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
46 |
47 | function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
48 |
49 | function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
50 |
51 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(n); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
52 |
53 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
54 |
55 | function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
56 |
57 | function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
58 |
59 | function _readOnlyError(name) { throw new Error("\"" + name + "\" is read-only"); }
60 |
61 | var defaultValue = {
62 | userData: null,
63 | signIn: null,
64 | signOut: null,
65 | authenticated: false
66 | };
67 |
68 | var contextAtom = _reactAtom.Atom.of(defaultValue);
69 | /**
70 | * React hook for the Blockstack SDK
71 | *
72 | * @return {{userSession: UserSession, userData: ?UserData, signIn: ?function, signOut: ?function, person: ?Person}} Blockstack SDK context
73 | *
74 | * @example
75 | *
76 | * useBlockstack()
77 | */
78 |
79 |
80 | function useBlockstack() {
81 | return (0, _reactAtom.useAtom)(contextAtom);
82 | }
83 |
84 | function setContext(update) {
85 | // use sparingly as it triggers all using components to update
86 | (0, _reactAtom.swap)(contextAtom, function (state) {
87 | return (0, _lodash.merge)({}, state, (0, _lodash.isFunction)(update) ? update(state) : update);
88 | });
89 | }
90 |
91 | function signIn(e) {
92 | var _deref = (0, _reactAtom.deref)(contextAtom),
93 | userSession = _deref.userSession;
94 |
95 | var update = {
96 | signIn: null
97 | };
98 | setContext(update);
99 | userSession.redirectToSignIn(); // window.location.pathname
100 | }
101 |
102 | function signOut(e) {
103 | var _deref2 = (0, _reactAtom.deref)(contextAtom),
104 | userSession = _deref2.userSession;
105 |
106 | var update = {
107 | userData: null,
108 | signIn: signIn,
109 | signOut: null,
110 | authenticated: false,
111 | person: null
112 | };
113 | setContext(update);
114 | userSession.signUserOut();
115 | }
116 |
117 | function handleAuthenticated(userData) {
118 | window.history.replaceState({}, document.title, window.location.pathname);
119 | var update = {
120 | userData: userData,
121 | person: new _blockstack.Person(userData.profile),
122 | signIn: null,
123 | authenticated: true,
124 | signOut: signOut
125 | };
126 | setContext(update);
127 | }
128 |
129 | function initBlockstack(options) {
130 | // Idempotent
131 | var _deref3 = (0, _reactAtom.deref)(contextAtom),
132 | userSession = _deref3.userSession;
133 |
134 | if (!userSession) {
135 | var _userSession = new _blockstack.UserSession(options);
136 |
137 | var update = {
138 | userSession: _userSession
139 | };
140 | setContext(update);
141 |
142 | if (_userSession.isSignInPending()) {
143 | _userSession.handlePendingSignIn().then(handleAuthenticated);
144 | } else if (_userSession.isUserSignedIn()) {
145 | handleAuthenticated(_userSession.loadUserData());
146 | } else {
147 | setContext({
148 | signIn: signIn
149 | });
150 | }
151 |
152 | return {
153 | userSession: _userSession
154 | };
155 | } else {
156 | return {
157 | userSession: userSession
158 | };
159 | }
160 | }
161 |
162 | var _default = initBlockstack;
163 | exports["default"] = _default;
164 | var BlockstackContext = (0, _react.createContext)(defaultValue);
165 | exports.BlockstackContext = BlockstackContext;
166 |
167 | function Blockstack(props) {
168 | var context = useBlockstack();
169 | return /*#__PURE__*/_react["default"].createElement(BlockstackContext.Provider, {
170 | value: context
171 | }, props.children);
172 | }
173 |
174 | function didConnect(_ref) {
175 | var session = _ref.userSession;
176 |
177 | var _deref4 = (0, _reactAtom.deref)(contextAtom),
178 | userSession = _deref4.userSession,
179 | authenticated = _deref4.authenticated;
180 |
181 | if (userSession != session) {
182 | userSession = (_readOnlyError("userSession"), session);
183 | setContext({
184 | userSession: userSession
185 | });
186 | }
187 |
188 | if (!authenticated) {
189 | var userData = userSession.loadUserData();
190 | handleAuthenticated(userData);
191 | }
192 | }
193 |
194 | function useConnectOptions(options) {
195 | var _useBlockstack = useBlockstack(),
196 | userSession = _useBlockstack.userSession;
197 |
198 | var authOptions = {
199 | redirectTo: '/',
200 | manifest: '/manifest.json',
201 | finished: function finished(_ref2) {
202 | var userSession = _ref2.userSession;
203 | didConnect({
204 | userSession: userSession
205 | });
206 | },
207 | userSession: userSession,
208 | appDetails: {
209 | name: "Blockstack App",
210 | icon: '/logo.svg'
211 | }
212 | };
213 | return (0, _lodash.merge)({}, authOptions, options);
214 | }
215 |
216 | function useFile(path, options) {
217 | var _useStateWithGaiaStor = useStateWithGaiaStorage(path, (0, _lodash.merge)({
218 | reader: _lodash.identity,
219 | writer: _lodash.identity,
220 | initial: null
221 | }, options)),
222 | _useStateWithGaiaStor2 = _slicedToArray(_useStateWithGaiaStor, 2),
223 | value = _useStateWithGaiaStor2[0],
224 | setValue = _useStateWithGaiaStor2[1];
225 |
226 | return [value, !(0, _lodash.isUndefined)(value) ? setValue : null];
227 | }
228 | /*
229 | function gaiaReducer (state, event) {
230 | // state machine for gaia file operations
231 | console.debug("Gaia:", state, event)
232 | switch (state.status) {
233 | case "start":
234 | switch (event.action) {
235 | case "reading": return({...state, status: "reading", pending: true})
236 | }
237 | case "reading":
238 | switch (event.action) {
239 | case "read-error": return({...state, status: "read-error", error: event.error})
240 | case "no-file": return ({...state, status: "no-file"})
241 | case "read-success": return({...state, status: "ready", content: event.content, pending: false})
242 | }
243 | case "no-file":
244 | switch (event.action) {
245 | case "writing": return({...state, status: "writing", content: event.content})
246 | }
247 | case "writing":
248 | switch (event.action) {
249 | case "write-error": return({...state, status: "write-error", error: event.error})
250 | case "write-complete": return({...state, status: "ready", content: event.content})
251 | }
252 | case "ready":
253 | switch (event.action) {
254 | case "writing": return({...state, status: "writing", content: event.content})
255 | case "deleting": return({...state, status: "deleting", content: event.content})
256 | }
257 | case "deleting":
258 | switch (event.action) {
259 | case "delete-error": return({...state, status: "delete-error", error: event.error})
260 | case "delete-complete": return ({...state, status: "no-file", content: null})
261 | }
262 | default: return (state)
263 | }
264 | }
265 | */
266 |
267 |
268 | function useStateWithGaiaStorage(path, _ref3) {
269 | var _ref3$reader = _ref3.reader,
270 | reader = _ref3$reader === void 0 ? _lodash.identity : _ref3$reader,
271 | _ref3$writer = _ref3.writer,
272 | writer = _ref3$writer === void 0 ? _lodash.identity : _ref3$writer,
273 | _ref3$initial = _ref3.initial,
274 | initial = _ref3$initial === void 0 ? null : _ref3$initial;
275 |
276 | /* Low level gaia file hook
277 | Note: Does not guard against multiple hooks for the same file
278 | Possbly an issue that change is set then value, could introduce inconsisitent state
279 | Return value:
280 | 1. File read -> content of file
281 | 2. File not existing or empty -> {} // could also be null
282 | 3. File not yet accessed or inaccessible -> undefined
283 | Attempting to set value when undefined throws an error.
284 | */
285 | var _useState = (0, _react.useState)(undefined),
286 | _useState2 = _slicedToArray(_useState, 2),
287 | value = _useState2[0],
288 | setValue = _useState2[1];
289 |
290 | var _useState3 = (0, _react.useState)(undefined),
291 | _useState4 = _slicedToArray(_useState3, 2),
292 | change = _useState4[0],
293 | setChange = _useState4[1];
294 |
295 | var _useState5 = (0, _react.useState)(false),
296 | _useState6 = _slicedToArray(_useState5, 2),
297 | pending = _useState6[0],
298 | setPending = _useState6[1]; // const [{status, pending}, dispatch] = useReducer(gaiaReducer, {:status: "start"})
299 |
300 |
301 | var updateValue = function updateValue(update) {
302 | // ##FIX: properly handle update being a fn, call with (change || value)
303 | //console.log("[File] Update:", path, update)
304 | if (!(0, _lodash.isUndefined)(value)) {
305 | if ((0, _lodash.isFunction)(update)) {
306 | setChange(function (change) {
307 | return update(!(0, _lodash.isUndefined)(change) ? change : value);
308 | });
309 | } else {
310 | setChange(update);
311 | }
312 | } else {
313 | throw "Premature attempt to update file:" + path;
314 | }
315 | };
316 |
317 | var _useBlockstack2 = useBlockstack(),
318 | userSession = _useBlockstack2.userSession,
319 | userData = _useBlockstack2.userData,
320 | authenticated = _useBlockstack2.authenticated; // React roadmap is to support data loading with Suspense hook
321 |
322 |
323 | (0, _react.useEffect)(function () {
324 | if ((0, _lodash.isNil)(value)) {
325 | if (authenticated && path) {
326 | setPending(true);
327 | userSession.getFile(path).then(function (stored) {
328 | //console.info("[File] Get:", path, value, stored)
329 | var content = !(0, _lodash.isNil)(stored) ? reader(stored) : initial;
330 | setValue(content);
331 | })["catch"](function (error) {
332 | if (error.code === "does_not_exist") {
333 | // SDK 21 errs when file does not exist
334 | setValue(initial);
335 | } else {
336 | console.error("[File] Get error:", error);
337 | }
338 | })["finally"](function () {
339 | return setPending(false);
340 | });
341 | } else if (path) {
342 | console.info("Waiting for user to sign on before reading file:", path);
343 | } else {
344 | console.warn("[File] No file path");
345 | }
346 | } else {//console.log("[File] Get skip:", value)
347 | }
348 | }, [userSession, authenticated, path]);
349 | (0, _react.useEffect)(function () {
350 | if (!(0, _lodash.isUndefined)(change) && !pending) {
351 | if (!authenticated) {
352 | console.warn("[File] User not logged in");
353 | } else if (!(0, _lodash.isEqual)(change, value)) {
354 | if ((0, _lodash.isNull)(change)) {
355 | setPending(true);
356 | userSession.deleteFile(path).then(function () {
357 | return setValue(null);
358 | })["catch"](function (err) {
359 | return console.warn("Failed deleting:", path, err);
360 | })["finally"](function () {
361 | return setPending(false);
362 | });
363 | } else {
364 | var content = writer(change);
365 | var original = value; // setValue(change) // Cannot delay until saved? as it may cause inconsistent state
366 |
367 | setPending(true);
368 | userSession.putFile(path, content).then(function () {
369 | // console.info("[File] Put", path, content);
370 | setValue(change);
371 | setPending(false);
372 | })["catch"](function (err) {
373 | // Don't revert on error for now as it impairs UX
374 | // setValue(original)
375 | setPending(false); // FIX: delay before retry?
376 |
377 | console.warn("[File] Put error: ", path, err);
378 | });
379 | }
380 | } else {// console.log("[File] Put noop:", path)
381 | }
382 | }
383 | }, [change, userSession, pending]); // FIX: deliver eventual error as third value?
384 |
385 | return [value, updateValue];
386 | }
387 | /*
388 | =======================================================================
389 | EXPERIMENTAL FUNCTIONALITY
390 | APT TO CHANGE WITHOUT FURTHER NOTICE
391 | =======================================================================
392 | */
393 |
394 | /* Low-level hooks for Gaia file system */
395 |
396 |
397 | function useFilesList() {
398 | /* First value is a list of files, defaults to empty list.
399 | Better of undefined until names retrieved?
400 | Second value is null then number of files when list is complete.
401 | FIX: Is number of files useful as output? What about errors? */
402 | var _useBlockstack3 = useBlockstack(),
403 | userSession = _useBlockstack3.userSession,
404 | userData = _useBlockstack3.userData,
405 | authenticated = _useBlockstack3.authenticated;
406 |
407 | var _useState7 = (0, _react.useState)([]),
408 | _useState8 = _slicedToArray(_useState7, 2),
409 | value = _useState8[0],
410 | setValue = _useState8[1];
411 |
412 | var _useState9 = (0, _react.useState)(null),
413 | _useState10 = _slicedToArray(_useState9, 2),
414 | fileCount = _useState10[0],
415 | setCount = _useState10[1];
416 |
417 | var appendFile = (0, _react.useCallback)(function (path) {
418 | setValue(function (value) {
419 | return [].concat(_toConsumableArray(value), [path]);
420 | });
421 | return true;
422 | });
423 | (0, _react.useEffect)(function () {
424 | if (userSession && authenticated) {
425 | userSession.listFiles(appendFile).then(setCount)["catch"](function (err) {
426 | return console.warn("Failed retrieving files list:", err);
427 | });
428 | }
429 | }, [userSession, authenticated]);
430 | return [value, fileCount];
431 | }
432 |
433 | function useFileUrl(path) {
434 | // FIX: Should combine with others?
435 | var _useBlockstack4 = useBlockstack(),
436 | userSession = _useBlockstack4.userSession,
437 | userData = _useBlockstack4.userData;
438 |
439 | var _useState11 = (0, _react.useState)(null),
440 | _useState12 = _slicedToArray(_useState11, 2),
441 | value = _useState12[0],
442 | setValue = _useState12[1];
443 |
444 | (0, _react.useEffect)(function () {
445 | if (userSession) {
446 | if (path) {
447 | userSession.getFileUrl(path).then(setValue)["catch"](function (err) {
448 | return console.warn("Failed getting file url:", err);
449 | });
450 | } else {
451 | setValue(null);
452 | }
453 | }
454 | }, [userSession, path]);
455 | return value;
456 | }
457 |
458 | function useFetch(path, init) {
459 | // For internal uses, likely better covered by other libraries
460 | var url = useFileUrl(path);
461 |
462 | var _useState13 = (0, _react.useState)(null),
463 | _useState14 = _slicedToArray(_useState13, 2),
464 | value = _useState14[0],
465 | setValue = _useState14[1];
466 |
467 | (0, _react.useEffect)(function () {
468 | if (url) {
469 | fetch(url, init).then(setValue)["catch"](function (err) {
470 | return console.warn("Failed fetching url:", err);
471 | });
472 | } else {
473 | setValue(null);
474 | }
475 | }, [url]);
476 | return value;
477 | }
478 |
479 | function useStateWithLocalStorage(storageKey) {
480 | var stored = localStorage.getItem(storageKey);
481 | var content = typeof stored != 'undefined' ? JSON.parse(stored) : null;
482 | console.log("PERSISTENT local:", stored, _typeof(stored));
483 |
484 | var _useState15 = (0, _react.useState)(content),
485 | _useState16 = _slicedToArray(_useState15, 2),
486 | value = _useState16[0],
487 | setValue = _useState16[1];
488 |
489 | _react["default"].useEffect(function () {
490 | localStorage.setItem(storageKey, JSON.stringify(value || null));
491 | }, [value]);
492 |
493 | return [value, setValue];
494 | }
495 |
496 | function useStored(props) {
497 | // Generalized persistent property storage
498 | var property = props.property,
499 | overwrite = props.overwrite,
500 | value = props.value,
501 | setValue = props.setValue;
502 | var version = props.version || 0;
503 | var path = props.path || property;
504 |
505 | var _ref4 = props.local ? useStateWithLocalStorage(path) : useStateWithGaiaStorage(path, {
506 | reader: JSON.parse,
507 | writer: JSON.stringify,
508 | initial: {}
509 | }),
510 | _ref5 = _slicedToArray(_ref4, 2),
511 | stored = _ref5[0],
512 | setStored = _ref5[1];
513 |
514 | (0, _react.useEffect)(function () {
515 | // Load data from file
516 | if (!(0, _lodash.isUndefined)(stored) && !(0, _lodash.isNil)(stored) && !(0, _lodash.isEqual)(value, stored)) {
517 | console.info("STORED load:", path, stored, "Current:", value);
518 |
519 | if (stored.version && version != stored.version) {
520 | // ## Fix: better handling of version including migration
521 | console.error("Mismatching version in file", path, " - expected", version, "got", stored.version);
522 | }
523 |
524 | if ((0, _lodash.isFunction)(setValue)) {
525 | setValue(stored.content);
526 | } else {
527 | console.warn("Missing setValue property for storing:", property);
528 | }
529 | } else {
530 | console.log("STORED pass:", path, stored, "Current:", value);
531 | }
532 | }, [stored]);
533 | (0, _react.useEffect)(function () {
534 | // Store content to file
535 | if (!(0, _lodash.isUndefined)(stored) && !(0, _lodash.isUndefined)(value) && !(0, _lodash.isEqual)(value, stored && stored.content)) {
536 | var content = stored && stored.content;
537 | var replacement = overwrite ? value : (0, _lodash.merge)({}, content, value);
538 | console.info("STORED save:", path, replacement, "Was:", value);
539 | setStored({
540 | version: version,
541 | property: property,
542 | content: replacement
543 | });
544 | } else {
545 | console.log("STORED noop:", path, value, stored);
546 | }
547 | }, [value, stored]);
548 | return stored;
549 | }
550 |
551 | function usePersistent(props) {
552 | // Make context state persistent
553 | var property = props.property,
554 | overwrite = props.overwrite;
555 | var context = useBlockstack(); // useContext(BlockstackContext) // ## FIX: call useBlockstack() instead??
556 |
557 | var value = property ? context[property] : null;
558 | var setValue = property ? function (value) {
559 | return setContext((0, _lodash.set)({}, property, value));
560 | } : null;
561 | var stored = useStored((0, _lodash.merge)({}, props, {
562 | value: value,
563 | setValue: setValue
564 | }));
565 | return stored;
566 | }
567 |
568 | function Persistent(props) {
569 | // perhaps should only bind value to context for its children?
570 | // ##FIX: validate input properties, particularly props.property
571 | var property = props.property,
572 | debug = props.debug,
573 | overwrite = props.overwrite;
574 | var result = usePersistent(props);
575 | var context = useBlockstack(); // useContext(BlockstackContext) // ## FIX: call useBlockstack() instead??
576 |
577 | var content = property ? context[property] : null;
578 | return debug ? /*#__PURE__*/_react["default"].createElement("div", null, /*#__PURE__*/_react["default"].createElement("h1", null, "Persistent ", property), /*#__PURE__*/_react["default"].createElement("p", null, "Stored: ", JSON.stringify(stored)), /*#__PURE__*/_react["default"].createElement("p", null, "Context: ", JSON.stringify(content))) : null;
579 | }
580 | /* External Dapps */
581 |
582 |
583 | function getAppManifestAtom(appUri) {
584 | // Out: Atom promise containing a either an app manifest or a null value.
585 | // Avoid passing outside this module to avoid conflicts if there are multiple react-atom packages in the project
586 | var atom = _reactAtom.Atom.of(null);
587 |
588 | var setValue = function setValue(value) {
589 | return (0, _reactAtom.swap)(atom, function (state) {
590 | return value;
591 | });
592 | };
593 |
594 | try {
595 | var manifestUri = appUri + "/manifest.json";
596 | var controller = new AbortController();
597 |
598 | var cleanup = function cleanup() {
599 | return controller.abort();
600 | };
601 |
602 | console.info("FETCHING:", manifestUri);
603 | fetch(manifestUri, {
604 | signal: controller.signal
605 | }).then(function (response) {
606 | response.json().then(setValue);
607 | })["catch"](function (err) {
608 | console.warn("Failed to get manifest for:", appUri, err);
609 | }); // .finally (() => setValue({}))
610 | } catch (err) {
611 | console.warn("Failed fetching when mounting:", err);
612 | setValue({
613 | error: err
614 | });
615 | }
616 |
617 | return atom;
618 | }
619 |
620 | function createAppManifestHook(appUri) {
621 | var atom = getAppManifestAtom(appUri);
622 | return function () {
623 | return (0, _reactAtom.useAtom)(atom);
624 | };
625 | }
626 |
627 | function useAppManifest(appUri) {
628 | // null when pending
629 | var _useState17 = (0, _react.useState)(null),
630 | _useState18 = _slicedToArray(_useState17, 2),
631 | value = _useState18[0],
632 | setValue = _useState18[1]; // ## FIX bug: May start another request while pending for a response
633 |
634 |
635 | (0, _react.useEffect)(function () {
636 | // #FIX: consider useCallback instead
637 | try {
638 | var manifestUri = appUri + "/manifest.json";
639 | var controller = new AbortController();
640 |
641 | var cleanup = function cleanup() {
642 | return controller.abort();
643 | };
644 |
645 | console.info("FETCHING:", manifestUri);
646 | fetch(manifestUri, {
647 | signal: controller.signal
648 | }).then(function (response) {
649 | response.json().then(setValue);
650 | })["catch"](function (err) {
651 | console.warn("Failed to get manifest for:", appUri, err);
652 | }); // .finally (() => setValue({}))
653 |
654 | return cleanup;
655 | } catch (err) {
656 | console.warn("Failed fetching when mounting:", err);
657 | setValue({
658 | error: err
659 | });
660 | }
661 | }, [appUri]);
662 | return value;
663 | }
664 | /* Update document element class */
665 |
666 |
667 | function AuthenticatedDocumentClass(props) {
668 | // declare a classname decorating the document element when authenticated
669 | var className = props.name;
670 |
671 | var _useBlockstack5 = useBlockstack(),
672 | userData = _useBlockstack5.userData;
673 |
674 | (0, _react.useEffect)(function () {
675 | console.log("Updating documentElement classes to reflect signed in status:", !!userData);
676 |
677 | if (userData) {
678 | document.documentElement.classList.add(className);
679 | document.documentElement.classList.remove('reloading');
680 | } else {
681 | document.documentElement.classList.remove(className);
682 | }
683 | }, [userData]);
684 | return null;
685 | }
686 | /* User Profiles ================================ */
687 |
688 |
689 | function useProfile(username, zoneFileLookupURL) {
690 | // FIX: don't lookup if username is current profile...
691 | var _useState19 = (0, _react.useState)(null),
692 | _useState20 = _slicedToArray(_useState19, 2),
693 | value = _useState20[0],
694 | setValue = _useState20[1];
695 |
696 | var _useBlockstack6 = useBlockstack(),
697 | userSession = _useBlockstack6.userSession;
698 |
699 | (0, _react.useEffect)(function () {
700 | if (userSession && username) {
701 | (0, _blockstack.lookupProfile)(username, zoneFileLookupURL).then(setValue)["catch"](function (err) {
702 | return console.warn("Failed to use profile:", err);
703 | });
704 | }
705 | }, [userSession, username, zoneFileLookupURL]);
706 | return value;
707 | }
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | command = "cd example; npm install; npm run build; mv cors/* build"
3 | publish = "example/build"
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-blockstack",
3 | "version": "0.6.10",
4 | "description": "React hooks for the Blockstack SDK",
5 | "directories": {
6 | "context": "./dist/context"
7 | },
8 | "main": "./dist/index.js",
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/njordhov/react-blockstack.git"
12 | },
13 | "keywords": [
14 | "react-hooks",
15 | "react",
16 | "blockstack",
17 | "hooks"
18 | ],
19 | "author": "Terje Norderhaug",
20 | "license": "MIT",
21 | "bugs": {
22 | "url": "https://github.com/njordhov/react-blockstack/issues"
23 | },
24 | "homepage": "https://github.com/njordhov/react-blockstack#readme",
25 | "peerDependencies": {
26 | "blockstack": "^21.0.0",
27 | "react": "^16.8.0"
28 | },
29 | "dependencies": {
30 | "@dbeining/react-atom": "^4.1.4",
31 | "lodash": "^4.17.5"
32 | },
33 | "devDependencies": {
34 | "@babel/cli": "^7.8.4",
35 | "@babel/core": "^7.9.0",
36 | "@babel/preset-env": "^7.9.0",
37 | "@babel/preset-react": "^7.9.4",
38 | "@babel/runtime": "^7.9.2",
39 | "babel-loader": "^8.1.0"
40 | },
41 | "scripts": {
42 | "build": "babel src --out-dir dist"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/context/index.js:
--------------------------------------------------------------------------------
1 | import { Blockstack, BlockstackContext } from '../index.js'
2 |
3 | export {Blockstack}
4 | export default BlockstackContext
5 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, createContext, useState, useEffect, useContext, useCallback, useReducer } from 'react'
2 | import { UserSession, AppConfig, Person, lookupProfile } from 'blockstack'
3 | import { Atom, swap, useAtom, deref} from "@dbeining/react-atom"
4 | import { isNil, isNull, isEqual, isFunction, isUndefined, merge, set, identity } from 'lodash'
5 |
6 | const defaultValue = {userData: null, signIn: null, signOut: null, authenticated: false}
7 |
8 | const contextAtom = Atom.of(defaultValue)
9 |
10 | /**
11 | * React hook for the Blockstack SDK
12 | *
13 | * @return {{userSession: UserSession, userData: ?UserData, signIn: ?function, signOut: ?function, person: ?Person}} Blockstack SDK context
14 | *
15 | * @example
16 | *
17 | * useBlockstack()
18 | */
19 |
20 | export function useBlockstack () {
21 | return( useAtom(contextAtom) )
22 | }
23 |
24 | export function setContext(update) {
25 | // use sparingly as it triggers all using components to update
26 | swap(contextAtom, state => merge({}, state, isFunction(update) ? update(state) : update))
27 | }
28 |
29 | function signIn(e) {
30 | const { userSession } = deref(contextAtom)
31 | const update = {signIn: null}
32 | setContext( update )
33 | userSession.redirectToSignIn() // window.location.pathname
34 | }
35 |
36 | function signOut(e) {
37 | const { userSession } = deref(contextAtom)
38 | const update = { userData: null,
39 | signIn: signIn,
40 | signOut: null,
41 | authenticated: false,
42 | person: null }
43 | setContext( update )
44 | userSession.signUserOut()
45 | }
46 |
47 | function handleAuthenticated (userData) {
48 | window.history.replaceState({}, document.title, window.location.pathname)
49 | const update = { userData: userData,
50 | person: new Person(userData.profile),
51 | signIn: null,
52 | authenticated: true,
53 | signOut: signOut }
54 | setContext( update )
55 | }
56 |
57 | export function initBlockstack (options) {
58 | // Idempotent
59 | const { userSession } = deref(contextAtom)
60 | if (!userSession) {
61 | const userSession = new UserSession(options)
62 | const update = { userSession: userSession }
63 | setContext( update )
64 | if (userSession.isSignInPending()) {
65 | userSession.handlePendingSignIn().then( handleAuthenticated )
66 | } else if (userSession.isUserSignedIn()) {
67 | handleAuthenticated (userSession.loadUserData())
68 | } else {
69 | setContext( { signIn: signIn })
70 | }
71 | return({ userSession: userSession })
72 | } else {
73 | return({ userSession: userSession })
74 | }
75 | }
76 |
77 | export default initBlockstack
78 |
79 | export const BlockstackContext = createContext(defaultValue)
80 |
81 | export function Blockstack(props) {
82 | const context = useBlockstack()
83 | return
84 | {props.children}
85 |
86 | }
87 |
88 | export function didConnect ({userSession: session}) {
89 | const { userSession, authenticated } = deref(contextAtom)
90 | if (userSession != session) {
91 | userSession = session;
92 | setContext({ userSession })
93 | }
94 | if (!authenticated) {
95 | const userData = userSession.loadUserData();
96 | handleAuthenticated(userData)
97 | }
98 | }
99 |
100 | export function useConnectOptions (options) {
101 | const {userSession} = useBlockstack()
102 | const authOptions = {
103 | redirectTo: '/',
104 | manifest: '/manifest.json',
105 | finished: ({userSession}) => {
106 | didConnect({userSession})
107 | },
108 | userSession: userSession,
109 | appDetails: {
110 | name: "Blockstack App",
111 | icon: '/logo.svg'
112 | }
113 | }
114 | return merge({}, authOptions, options)
115 | }
116 |
117 | export function useFile (path, options) {
118 | const [value, setValue] = useStateWithGaiaStorage (path, merge({reader:identity, writer:identity, initial: null}, options))
119 | return ([value, !isUndefined(value) ? setValue : null ])
120 | }
121 |
122 | /*
123 | function gaiaReducer (state, event) {
124 | // state machine for gaia file operations
125 | console.debug("Gaia:", state, event)
126 | switch (state.status) {
127 | case "start":
128 | switch (event.action) {
129 | case "reading": return({...state, status: "reading", pending: true})
130 | }
131 | case "reading":
132 | switch (event.action) {
133 | case "read-error": return({...state, status: "read-error", error: event.error})
134 | case "no-file": return ({...state, status: "no-file"})
135 | case "read-success": return({...state, status: "ready", content: event.content, pending: false})
136 | }
137 | case "no-file":
138 | switch (event.action) {
139 | case "writing": return({...state, status: "writing", content: event.content})
140 | }
141 | case "writing":
142 | switch (event.action) {
143 | case "write-error": return({...state, status: "write-error", error: event.error})
144 | case "write-complete": return({...state, status: "ready", content: event.content})
145 | }
146 | case "ready":
147 | switch (event.action) {
148 | case "writing": return({...state, status: "writing", content: event.content})
149 | case "deleting": return({...state, status: "deleting", content: event.content})
150 | }
151 | case "deleting":
152 | switch (event.action) {
153 | case "delete-error": return({...state, status: "delete-error", error: event.error})
154 | case "delete-complete": return ({...state, status: "no-file", content: null})
155 | }
156 | default: return (state)
157 | }
158 | }
159 | */
160 |
161 | function useStateWithGaiaStorage (path, {reader=identity, writer=identity, initial=null}) {
162 | /* Low level gaia file hook
163 | Note: Does not guard against multiple hooks for the same file
164 | Possbly an issue that change is set then value, could introduce inconsisitent state
165 | Return value:
166 | 1. File read -> content of file
167 | 2. File not existing or empty -> {} // could also be null
168 | 3. File not yet accessed or inaccessible -> undefined
169 | Attempting to set value when undefined throws an error.
170 | */
171 | const [value, setValue] = useState(undefined)
172 | const [change, setChange] = useState(undefined)
173 | const [pending, setPending] = useState(false)
174 | // const [{status, pending}, dispatch] = useReducer(gaiaReducer, {:status: "start"})
175 |
176 | const updateValue = (update) => {
177 | // ##FIX: properly handle update being a fn, call with (change || value)
178 | //console.log("[File] Update:", path, update)
179 | if (!isUndefined(value)) {
180 | if (isFunction(update)) {
181 | setChange(change => update(!isUndefined(change) ? change : value))
182 | } else {
183 | setChange(update)
184 | }
185 | } else {
186 | throw "Premature attempt to update file:" + path
187 | }
188 | }
189 | const { userSession, userData, authenticated } = useBlockstack()
190 | // React roadmap is to support data loading with Suspense hook
191 | useEffect (() => {
192 | if ( isNil(value) ) {
193 | if (authenticated && path) {
194 | setPending(true)
195 | userSession.getFile(path)
196 | .then(stored => {
197 | //console.info("[File] Get:", path, value, stored)
198 | const content = !isNil(stored) ? reader(stored) : initial
199 | setValue(content)
200 | })
201 | .catch(error => {
202 | if (error.code === "does_not_exist") {
203 | // SDK 21 errs when file does not exist
204 | setValue(initial)
205 | } else {
206 | console.error("[File] Get error:", error)
207 | }
208 | })
209 | .finally(() => setPending(false))
210 | } else if (path) {
211 | console.info("Waiting for user to sign on before reading file:", path)
212 | } else {
213 | console.warn("[File] No file path")
214 | }
215 | } else {
216 | //console.log("[File] Get skip:", value)
217 | }}, [userSession, authenticated, path])
218 |
219 | useEffect(() => {
220 | if ( !isUndefined(change) && !pending ) {
221 | if (!authenticated) {
222 | console.warn("[File] User not logged in")
223 | } else if (!isEqual(change, value)){
224 | if (isNull(change)) {
225 | setPending(true)
226 | userSession.deleteFile(path)
227 | .then(() => setValue(null))
228 | .catch((err) => console.warn("Failed deleting:", path, err))
229 | .finally(() => setPending(false))
230 | } else {
231 | const content = writer(change)
232 | const original = value
233 | // setValue(change) // Cannot delay until saved? as it may cause inconsistent state
234 | setPending(true)
235 | userSession.putFile(path, content)
236 | .then(() => {
237 | // console.info("[File] Put", path, content);
238 | setValue(change)
239 | setPending(false)})
240 | .catch((err) => {
241 | // Don't revert on error for now as it impairs UX
242 | // setValue(original)
243 | setPending(false) // FIX: delay before retry?
244 | console.warn("[File] Put error: ", path, err)
245 | })}
246 | } else {
247 | // console.log("[File] Put noop:", path)
248 | }
249 | }},[change, userSession, pending])
250 | // FIX: deliver eventual error as third value?
251 | return [value, updateValue]
252 | }
253 |
254 | /*
255 | =======================================================================
256 | EXPERIMENTAL FUNCTIONALITY
257 | APT TO CHANGE WITHOUT FURTHER NOTICE
258 | =======================================================================
259 | */
260 |
261 | /* Low-level hooks for Gaia file system */
262 |
263 | export function useFilesList () {
264 | /* First value is a list of files, defaults to empty list.
265 | Better of undefined until names retrieved?
266 | Second value is null then number of files when list is complete.
267 | FIX: Is number of files useful as output? What about errors? */
268 | const { userSession, userData, authenticated } = useBlockstack()
269 | const [value, setValue] = useState([])
270 | const [fileCount, setCount] = useState(null)
271 | const appendFile = useCallback(path => {
272 | setValue((value) => [...value, path]);
273 | return true})
274 | useEffect( () => {
275 | if (userSession && authenticated) {
276 | userSession.listFiles(appendFile)
277 | .then(setCount)
278 | .catch((err) => console.warn("Failed retrieving files list:", err))
279 | }}, [userSession, authenticated])
280 | return ([value, fileCount])
281 | }
282 |
283 | export function useFileUrl (path) {
284 | // FIX: Should combine with others?
285 | const { userSession, userData } = useBlockstack()
286 | const [value, setValue] = useState(null)
287 | useEffect( () => {
288 | if (userSession) {
289 | if (path) {
290 | userSession.getFileUrl(path)
291 | .then(setValue)
292 | .catch((err) => console.warn("Failed getting file url:", err))
293 | } else {
294 | setValue(null)
295 | }
296 | }
297 | }, [userSession, path])
298 | return(value)
299 | }
300 |
301 | export function useFetch (path, init) {
302 | // For internal uses, likely better covered by other libraries
303 | const url = useFileUrl(path)
304 | const [value, setValue] = useState(null)
305 | useEffect ( () => {
306 | if (url) {
307 | fetch(url, init)
308 | .then(setValue)
309 | .catch((err) => console.warn("Failed fetching url:", err))
310 | } else {
311 | setValue(null)
312 | }
313 | }, [url])
314 | return (value)
315 | }
316 |
317 | function useStateWithLocalStorage (storageKey) {
318 | const stored = localStorage.getItem(storageKey)
319 | const content = (typeof stored != 'undefined') ? JSON.parse(stored) : null
320 | console.log("PERSISTENT local:", stored, typeof stored)
321 | const [value, setValue] = useState(content)
322 | React.useEffect(() => {
323 | localStorage.setItem(storageKey, JSON.stringify(value || null));
324 | }, [value])
325 | return [value, setValue];
326 | }
327 |
328 | export function useStored (props) {
329 | // Generalized persistent property storage
330 | const {property, overwrite, value, setValue} = props
331 | const version = props.version || 0
332 | const path = props.path || property
333 | const [stored, setStored] = props.local
334 | ? useStateWithLocalStorage(path)
335 | : useStateWithGaiaStorage(path, {reader: JSON.parse, writer: JSON.stringify, initial: {}})
336 | useEffect(() => {
337 | // Load data from file
338 | if (!isUndefined(stored) && !isNil(stored) && !isEqual (value, stored)) {
339 | console.info("STORED load:", path, stored, "Current:", value)
340 | if (stored.version && version != stored.version) {
341 | // ## Fix: better handling of version including migration
342 | console.error("Mismatching version in file", path, " - expected", version, "got", stored.version)
343 | }
344 | if (isFunction(setValue)) {
345 | setValue(stored.content)
346 | } else {
347 | console.warn("Missing setValue property for storing:", property)
348 | }
349 | } else {
350 | console.log("STORED pass:", path, stored, "Current:", value)
351 | }
352 | }, [stored])
353 |
354 | useEffect(() => {
355 | // Store content to file
356 | if (!isUndefined(stored) && !isUndefined(value) && !isEqual (value, stored && stored.content)) {
357 | const content = stored && stored.content
358 | const replacement = overwrite ? value : merge({}, content, value)
359 | console.info("STORED save:", path, replacement, "Was:", value)
360 | setStored({version: version, property: property, content: replacement})
361 | } else {
362 | console.log("STORED noop:", path, value, stored)
363 | }
364 | }, [value, stored])
365 | return ( stored )
366 | }
367 |
368 | export function usePersistent (props){
369 | // Make context state persistent
370 | const {property, overwrite} = props
371 | const context = useBlockstack() // useContext(BlockstackContext) // ## FIX: call useBlockstack() instead??
372 | const value = property ? context[property] : null
373 | const setValue = property ? (value) => setContext( set({}, property, value )) : null
374 | const stored = useStored (merge ({}, props, {value: value, setValue: setValue}))
375 | return( stored )
376 | }
377 |
378 | export function Persistent (props) {
379 | // perhaps should only bind value to context for its children?
380 | // ##FIX: validate input properties, particularly props.property
381 | const {property, debug, overwrite} = props
382 | const result = usePersistent(props)
383 | const context = useBlockstack() // useContext(BlockstackContext) // ## FIX: call useBlockstack() instead??
384 | const content = property ? context[property] : null
385 | return (
386 | debug ?
387 |
388 |
Persistent {property}
389 |
Stored: { JSON.stringify( stored ) }
390 |
Context: { JSON.stringify( content ) }
391 |
392 | :null)
393 | }
394 |
395 | /* External Dapps */
396 |
397 | function getAppManifestAtom (appUri) {
398 | // Out: Atom promise containing a either an app manifest or a null value.
399 | // Avoid passing outside this module to avoid conflicts if there are multiple react-atom packages in the project
400 | const atom = Atom.of(null)
401 | const setValue = (value) => swap(atom, state => value)
402 | try {
403 | const manifestUri = appUri + "/manifest.json"
404 | const controller = new AbortController()
405 | const cleanup = () => controller.abort()
406 | console.info("FETCHING:", manifestUri)
407 | fetch(manifestUri, {signal: controller.signal})
408 | .then ( response => {response.json().then( setValue )})
409 | .catch ( err => {console.warn("Failed to get manifest for:", appUri, err)})
410 | // .finally (() => setValue({}))
411 | } catch (err) {
412 | console.warn("Failed fetching when mounting:", err)
413 | setValue({error: err})
414 | }
415 | return (atom)
416 | }
417 |
418 | export function createAppManifestHook (appUri) {
419 | const atom = getAppManifestAtom(appUri)
420 | return( () => useAtom(atom) )
421 | }
422 |
423 | export function useAppManifest (appUri) {
424 | // null when pending
425 | const [value, setValue] = useState(null)
426 | // ## FIX bug: May start another request while pending for a response
427 | useEffect(() => { // #FIX: consider useCallback instead
428 | try {
429 | const manifestUri = appUri + "/manifest.json"
430 | const controller = new AbortController()
431 | const cleanup = () => controller.abort()
432 | console.info("FETCHING:", manifestUri)
433 | fetch(manifestUri, {signal: controller.signal})
434 | .then ( response => {response.json().then( setValue )})
435 | .catch ( err => {console.warn("Failed to get manifest for:", appUri, err)})
436 | // .finally (() => setValue({}))
437 | return (cleanup)
438 | } catch (err) {
439 | console.warn("Failed fetching when mounting:", err)
440 | setValue({error: err})
441 | }
442 | }, [appUri])
443 | return (value)
444 | }
445 |
446 | /* Update document element class */
447 |
448 | export function AuthenticatedDocumentClass (props) {
449 | // declare a classname decorating the document element when authenticated
450 | const className = props.name
451 | const { userData } = useBlockstack()
452 | useEffect(() => {
453 | console.log("Updating documentElement classes to reflect signed in status:", !!userData)
454 | if (userData) {
455 | document.documentElement.classList.add(className)
456 | document.documentElement.classList.remove('reloading')
457 | } else {
458 | document.documentElement.classList.remove(className)
459 | }}, [userData])
460 | return (null)
461 | }
462 |
463 | /* User Profiles ================================ */
464 |
465 | export function useProfile (username, zoneFileLookupURL) {
466 | // FIX: don't lookup if username is current profile...
467 | const [value, setValue] = useState(null)
468 | const { userSession } = useBlockstack()
469 | useEffect(() => {
470 | if (userSession && username) {
471 | lookupProfile(username, zoneFileLookupURL)
472 | .then(setValue)
473 | .catch((err) => console.warn("Failed to use profile:", err))
474 | }}, [userSession, username, zoneFileLookupURL])
475 | return (value)
476 | }
477 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | module.exports = {
3 | entry: ['./src/index.js', './src/context.js'],
4 | target: 'web',
5 | output: {
6 | path: path.resolve(__dirname, 'build'),
7 | filename: 'index.js',
8 | libraryTarget: 'commonjs2'
9 | },
10 | module: {
11 | rules: [
12 | {
13 | test: /\.js$/,
14 | include: path.resolve(__dirname, 'src'),
15 | exclude: /(node_modules|bower_components|build)/,
16 | use: {
17 | loader: 'babel-loader',
18 | options: {
19 | presets: ['env']
20 | }
21 | }
22 | }
23 | ]
24 | },
25 | externals: {
26 | 'react': 'commonjs react'
27 | }
28 | };
29 |
--------------------------------------------------------------------------------