diff --git a/common/styles/index.js b/common/styles/index.js index 01118e1d..77065f1f 100644 --- a/common/styles/index.js +++ b/common/styles/index.js @@ -72,7 +72,7 @@ const theme = { accent: '#0CCABA' }), grey: Object.assign('#5B647C', { - light: '#E6E9EE', + light: '#F9F9FC', mid: '#7588A2', dark: '#142144' }), @@ -94,7 +94,7 @@ const wrapperStyle = css` padding-right: 20px; ${below.md` padding-left: 20px; - padding-right: 20px; + padding-right: 20px; `}; ` diff --git a/components/maker/index.js b/components/maker/index.js index 82a5c5c0..f84c5e70 100644 --- a/components/maker/index.js +++ b/components/maker/index.js @@ -5,7 +5,6 @@ import Status from './status' import Notification from './notification' import { SidebarButton } from './styled' - export default { SidebarButton, PaymentDetails, diff --git a/components/maker/modal.js b/components/maker/modal.js index 8d857dc3..7d944520 100644 --- a/components/maker/modal.js +++ b/components/maker/modal.js @@ -5,7 +5,11 @@ import styled from 'styled-components' import { CloseIcon } from '@components/svg/maker' const customStyles = { - content : { + overlay: { + // above headroom header library + zIndex: 100 + }, + content: { top: '50%', left: '50%', right: 'auto', @@ -27,7 +31,7 @@ const CloseButtonContainer = styled.div` const CloseButton = ({ handleClick }) => ( - + ) @@ -36,12 +40,11 @@ const MakerModal = ({ isOpen, handleClose, children }) => ( isOpen={isOpen} onRequestClose={handleClose} style={customStyles} - ariaHideApp={false} + appElement={typeof document !== 'undefined' ? document.querySelector('#__next') : null} > {children} ) - export default MakerModal diff --git a/components/maker/payment/content.js b/components/maker/payment/content.js new file mode 100644 index 00000000..6f5256c4 --- /dev/null +++ b/components/maker/payment/content.js @@ -0,0 +1,51 @@ +import React from 'react' +import { Flex, Box, Type } from 'blockstack-ui' +import { MakerCardHeader, MakerCardText, MakerButton, MakerField } from '../styled' + +export const PaymentContainer = ({ children }) => ( + + + {children} + + +) + +export const PaymentHeader = MakerCardHeader + +export const PaymentDescription = () => ( + + This is where you will receive your App Mining payments. + Currently, payments are made in Bitcoin (BTC). Payments will be made + in Stacks (STX) in the future. + +) + +export const PaymentHelpText = () => ( + + {"Don't"} have a Stacks address? Download the Stacks wallet to get one + +) + +export const PaymentBtcField = props => ( + +) + +export const PaymentStxField = props => ( + +) + +export const PaymentButton = ({ children, ...props }) => ( + + {children} + +) diff --git a/components/maker/payment/helpers.js b/components/maker/payment/helpers.js new file mode 100644 index 00000000..c59925d8 --- /dev/null +++ b/components/maker/payment/helpers.js @@ -0,0 +1,21 @@ +import { address, networks } from 'bitcoinjs-lib' +import * as c32Check from 'c32check' +import memoize from 'lodash/memoize' + +export const validateBTC = memoize(addr => { + try { + address.toOutputScript(addr, networks.bitcoin) + return true + } catch (error) { + return false + } +}) + +export const validateSTX = memoize(addr => { + try { + c32Check.c32addressDecode(addr) + return true + } catch (error) { + return false + } +}) diff --git a/components/maker/payment/index.js b/components/maker/payment/index.js index f7f402d6..d2623f49 100644 --- a/components/maker/payment/index.js +++ b/components/maker/payment/index.js @@ -1,117 +1,75 @@ import React, { useState } from 'react' -import { Flex, Box, Type } from 'blockstack-ui' -import { address, networks } from 'bitcoinjs-lib' -import * as c32Check from 'c32check' -import Notification from '../notification' - +import { savePaymentDetails } from '@stores/maker/actions' +import { validateBTC, validateSTX } from './helpers' import { - MakerCardHeader, - MakerCardText, - MakerButton, - MakerField -} from '../styled' - -const validateBTC = (addr) => { - try { - address.toOutputScript(addr, networks.bitcoin) - return true - } catch (error) { - return false - } -} - -const validateSTX = (addr) => { - try { - c32Check.c32addressDecode(addr) - return true - } catch (error) { - return false - } -} + PaymentContainer, + PaymentHeader, + PaymentDescription, + PaymentHelpText, + PaymentBtcField, + PaymentStxField, + PaymentButton +} from './content' -const PaymentDetails = ({ app, apiServer, accessToken, user }) => { +const PaymentDetails = ({ app, apiServer, accessToken, user, dispatch }) => { const [btcAddress, setBTCAddress] = useState(app.BTCAddress) const [stxAddress, setSTXAddress] = useState(app.stacksAddress) - const [showNotification, setShowNotification] = useState(false) - const [btcValid, setBtcValid] = useState(true) - const [stxValid, setStxValid] = useState(true) const [saving, setSaving] = useState(false) + const [hasAttemptedSaved, setHasAttemptedSaved] = useState(false) + const [savedValues, setSavedValue] = useState({ btcAddress, stxAddress }) - const notify = () => { - setShowNotification(true) - setTimeout(() => { - setShowNotification(false) - }, 10000) - } + const isSaved = ( + btcAddress === savedValues.btcAddress && + btcAddress !== '' && + stxAddress === savedValues.stxAddress && + stxAddress !== '' + ) const save = async () => { - let isValid = true - if (!validateBTC(btcAddress)) { - isValid = false - setBtcValid(false) - } - if (!validateSTX(stxAddress)) { - isValid = false - setStxValid(false) - } - if (!isValid) { - return - } else { - setBtcValid(true) - setStxValid(true) - } + setHasAttemptedSaved(true) + if (!validateBTC(btcAddress) || !validateSTX(stxAddress)) return setSaving(true) - console.log(stxAddress) - const response = await fetch(`${apiServer}/api/maker/apps?appId=${app.id}`, { - method: 'POST', - headers: new Headers({ - 'Content-Type': 'application/json', - authorization: `Bearer ${user.jwt}` - }), - body: JSON.stringify({ - BTCAddress: btcAddress, - stacksAddress: stxAddress - }) - }) - await response.json() - notify() + await savePaymentDetails({ apiServer, appId: app.id, jwt: user.jwt, btcAddress, stxAddress })(dispatch) setSaving(false) + setSavedValue({ btcAddress, stxAddress }) + } + + const buttonText = () => { + if (saving) return 'Saving…' + if (isSaved) return 'Saved' + return 'Save' + } + + const createInputError = ({ validateFn, currencySymbol }) => addressHash => { + if (!hasAttemptedSaved) return null + if (!validateFn(addressHash)) return `Please enter a valid ${currencySymbol} address` + return null } + const getBtcError = createInputError({ validateFn: validateBTC, currencySymbol: 'BTC' }) + const getStxError = createInputError({ validateFn: validateSTX, currencySymbol: 'STX' }) return ( - - - Payment details - {showNotification && } - - This is where you will receive your App Mining payments. - Currently, payments are made in Bitcoin (BTC). Payments will be made - in Stacks (STX) in the future. - - setBTCAddress(e.target.value)} - value={btcAddress || ''} - error={!btcValid ? 'Please enter a valid BTC address' : null} - /> - setSTXAddress(e.target.value)} - value={stxAddress || ''} - error={!stxValid ? 'Please enter a valid STX address' : null} - /> - - {"Don't"} have a Stacks address? Download the Stacks wallet to get one - - save({ btcAddress, stxAddress, apiServer, accessToken })}> - {saving ? 'Saving...' : 'Save'} - - - + + Payment details + + setBTCAddress(e.target.value)} + value={btcAddress || ''} + error={getBtcError(btcAddress)} + /> + setSTXAddress(e.target.value)} + value={stxAddress || ''} + error={getStxError(stxAddress)} + /> + + save({ btcAddress, stxAddress, apiServer, accessToken })} + > + {buttonText()} + + ) } diff --git a/components/maker/styled.js b/components/maker/styled.js index 3a4957e1..30b2904f 100644 --- a/components/maker/styled.js +++ b/components/maker/styled.js @@ -54,6 +54,14 @@ export const MakerContainer = ({ children }) => ( ) +export const MakerTitle = styled(Type.h2)` + display: block; + font-weight: 500; + font-size: 24px; + line-height: 28px; + color: #0F1117; + margin-bottom: 16px; +` export const MakerCardHeader = styled(Type.h2)` font-weight: 500; diff --git a/components/page/index.js b/components/page/index.js index 43d93b09..cd2eb60e 100644 --- a/components/page/index.js +++ b/components/page/index.js @@ -7,7 +7,7 @@ import { StyledPage } from './styled' const Page = ({ isErrorPage = false, children, admin = false, wrap, innerPadding = [2, 0], ...rest }) => ( - + {children} diff --git a/components/submit/index.js b/components/submit/index.js new file mode 100644 index 00000000..1cdf16e4 --- /dev/null +++ b/components/submit/index.js @@ -0,0 +1,32 @@ +import React from 'react' +import Link from 'next/link' +import { Type, Box, Button } from 'blockstack-ui' + +const SuccessCard = ({ isAppMiningEligible }) => ( + + + + Success! + + + + + Thanks for your submission! Your app will need to be approved before being public on app.co. + + {isAppMiningEligible && ( + <> + + To update your app's details and enroll in App Mining, visit our Maker Portal + + + + + + )} + + +) + +export default SuccessCard diff --git a/package.json b/package.json index f1e36928..37ac0eaa 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@mdx-js/tag": "^0.15.0", "@next/bundle-analyzer": "^9.0.6", "@next/mdx": "^9.0.6", + "@types/react-redux": "^7.1.5", "accounting": "^0.4.1", "async": "^2.6.0", "bitcoinjs-lib": "^5.0.5", @@ -63,6 +64,7 @@ "grid-styled": "5.0.2", "intersection-observer": "^0.5.1", "isomorphic-unfetch": "^3.0.0", + "js-cookie": "^2.2.1", "lodash": "^4.17.11", "lodash.debounce": "^4.0.8", "lru-cache": "^4.1.3", diff --git a/pages/admin/app.js b/pages/admin/app.js index 1cadeb4b..540075ff 100644 --- a/pages/admin/app.js +++ b/pages/admin/app.js @@ -88,7 +88,7 @@ class App extends React.Component { async fetchApp() { const qs = queryString.parse(document.location.search) if (qs.id && this.props.jwt) { - const request = await fetch(`${this.propsapiServer}/api/admin/apps/${qs.id}`, { + const request = await fetch(`${this.props.apiServer}/api/admin/apps/${qs.id}`, { method: 'GET', headers: new Headers({ Authorization: `Bearer ${this.props.jwt}`, @@ -151,10 +151,6 @@ class App extends React.Component { // const app = this.props.selectedApp // const { name } = this.state; const app = this.state -<<<<<<< HEAD - const { categories, authentications, storageNetworks, blockchains } = this.props -======= ->>>>>>> origin/develop if (!app.name) { return

Loading

} @@ -210,6 +206,10 @@ class App extends React.Component {

Magic link: {`/maker/${app.accessToken}`}

+

+ Maker portal: {`/maker/apps/${app.id}`} +

+ - - - - - - - ) - } -} +const AllAppsPage = () => ( + + + + + + + +) export default AllAppsPage diff --git a/pages/maker/apps/index.js b/pages/maker/apps/index.js new file mode 100644 index 00000000..64e30c92 --- /dev/null +++ b/pages/maker/apps/index.js @@ -0,0 +1,122 @@ +import React from 'react' +import { useRouter } from 'next/router' +import { Flex, Box, Type } from 'blockstack-ui' +import { connect } from 'react-redux' +import isNaN from 'lodash/isNaN' + +import { selectMaker, selectAppList, selectCurrentApp, selectCompetionStatus } from '@stores/maker/selectors' +import { fetchApps, selectAppAction } from '@stores/maker/actions' +import { selectApiServer, selectUser } from '@stores/apps/selectors' +import { MakerContainer, MakerContentBox, MakerStickyStatusBox } from '@components/maker/styled' +import { Page } from '@components/page' +import Head from '@containers/head' +import Maker from '@components/maker' + +const mapStateToProps = (state) => ({ + user: selectUser(state), + apiServer: selectApiServer(state), + maker: selectMaker(state), + appList: selectAppList(state), + selectedApp: selectCurrentApp(state), + completionStatus: selectCompetionStatus(state) +}) + +const handleChangingApp = (event, fn) => (dispatch) => { + event.persist() + const id = event.target.value + dispatch(selectAppAction(id)) + fn(id) +} + +const getAppId = (params) => { + if (!params) return undefined + const id = parseInt(params.appId, 10) + if (isNaN(id)) return undefined + return id +} + +const LoadingPage = ({ message = 'Loading...' }) => ( + + + + + {message} + + + + +) + +const MakerPortal = connect()(({ maker, selectedApp, appList, appId, apiServer, completionStatus, user, dispatch }) => { + const router = useRouter() + + const updateMakerRoute = (id) => + router.push('/maker/apps', `/maker/apps/${id}`, { + shallow: true + }) + + if (maker.loading || !selectedApp) return + + return ( + + + + + {appList.length && ( + + )} + + + {selectedApp.name} + + + + + + + + + + + + + + + + + + + + ) +}) + +MakerPortal.getInitialProps = async ({ req, reduxStore }) => { + const { params, universalCookies } = req + const userCookie = universalCookies.cookies.jwt + const appId = getAppId(params) + const apiServer = selectApiServer(reduxStore.getState()) + await fetchApps({ apiServer, user: { jwt: userCookie } })(reduxStore.dispatch) + let selectedApp = {} + if (appId) { + reduxStore.dispatch(selectAppAction(appId)) + selectedApp = selectCurrentApp(reduxStore.getState()) + } else { + const firstApp = selectAppList(reduxStore.getState())[0] + selectedApp = firstApp + } + const props = mapStateToProps(reduxStore.getState()) + + return { appId, ...props, selectedApp, dispatch: reduxStore.dispatch } +} + +export default MakerPortal diff --git a/pages/maker/index.js b/pages/maker/index.js deleted file mode 100644 index fb8fd053..00000000 --- a/pages/maker/index.js +++ /dev/null @@ -1,98 +0,0 @@ -import React from 'react' -import { Flex, Box, Type } from 'blockstack-ui' -import { connect } from 'react-redux' - -import { selectApiServer, selectUser } from '@stores/apps/selectors' -import { Page } from '@components/page' -import Head from '@containers/head' -import Maker from '@components/maker' -import { MakerContainer, MakerContentBox, MakerStickyStatusBox } from '@components/maker/styled' - -class MakerPortal extends React.Component { - - state = { - loading: true, - apps: [], - errorMessage: null - } - - componentDidMount() { - this.fetchApps() - } - - async fetchApps() { - const { apiServer, user } = this.props - if (!(user && user.jwt)) { - this.setState({ - loading: false, - errorMessage: 'Please sign in to access the maker portal.' - }) - return - } - - const uri = `${apiServer}/api/maker/apps` - const response = await fetch(uri, { - headers: { - Authorization: `Bearer ${user.jwt}` - } - }) - const { apps } = await response.json() - this.setState({ - loading: false, - apps - }) - } - - render() { - const { apiServer, user } = this.props - - const { loading, errorMessage, apps } = this.state - const [app] = apps // TODO: this is for now until we handle multiple apps - - if (loading || errorMessage) { - return ( - - - - {loading ? 'Loading...' : errorMessage} - - - - ) - } - - return ( - - - - - {app.name} - - - - - - - - - - - - - - - - - - - - ) - } -} - -const mapStateToProps = (state) => ({ - user: selectUser(state), - apiServer: selectApiServer(state) -}) - -export default connect(mapStateToProps)(MakerPortal) diff --git a/pages/maker/magic-link.js b/pages/maker/magic-link.js index 02563bc1..1b72c72d 100644 --- a/pages/maker/magic-link.js +++ b/pages/maker/magic-link.js @@ -14,6 +14,7 @@ class MakerMagicLink extends React.Component { const { accessToken } = query const apiServer = selectApiServer(reduxStore.getState()) const appResult = await fetch(`${apiServer}/api/magic-link/${accessToken}`) + console.log(appResult) const { app } = await appResult.json() return { @@ -85,7 +86,8 @@ class MakerMagicLink extends React.Component { {app.name} is now owned by {app.adminBlockstackId || user.user.blockstackUsername} - Your Magic Link is now no longer functional. Instead, you will use your Blockstack ID to sign in to the developer portal. + Your Magic Link is now no longer functional. Instead, you will use your Blockstack ID to sign in to + the developer portal. @@ -97,9 +99,12 @@ class MakerMagicLink extends React.Component { Sign in with Blockstack to claim {app.name} - You will use this Blockstack ID to make changes to your app, and remove or modify your listing in the future. + You will use this Blockstack ID to make changes to your app, and remove or modify your listing in + the future. - + )} @@ -117,4 +122,7 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = (dispatch) => bindActionCreators({ ...UserStore.actions }, dispatch) -export default connect(mapStateToProps, mapDispatchToProps)(MakerMagicLink) +export default connect( + mapStateToProps, + mapDispatchToProps +)(MakerMagicLink) diff --git a/pages/submit.js b/pages/submit.js index 1274151f..c65f58ba 100644 --- a/pages/submit.js +++ b/pages/submit.js @@ -2,13 +2,13 @@ import React from 'react' import 'isomorphic-unfetch' import { connect } from 'react-redux' import Head from '@containers/head' -import Link from 'next/link' import { bindActionCreators } from 'redux' +import { MakerTitle, MakerCardHeader, MakerButton } from '@components/maker/styled' import { Page } from '@components/page' -import { Type, Field, Flex, Box, Button } from 'blockstack-ui' +import { Type, Field, Flex, Box } from 'blockstack-ui' import { selectAppConstants, selectApiServer } from '@stores/apps/selectors' -import { string, boolean } from 'yup' -import { FormSection, ErrorMessage, Bar, sections as getSections } from '@containers/submit' +import { FormSection, ErrorMessage, sections as getSections } from '@containers/submit' +import SuccessCard from '@components/submit' import debounce from 'lodash.debounce' import UserStore from '@stores/user' @@ -92,13 +92,8 @@ const Submit = ({ appConstants, setState, state, errors, submit, user, loading, return ( - Add an app to App.co - - Add any user-ready decentralized app: It could be an app you built, or an app you discovered. We manually - verify all information before publishing to App.co. Contact details are not displayed and we promise to keep - your information private and safe. - - + Submit your app + Personal details {user && user.user && ( @@ -121,19 +116,16 @@ const Submit = ({ appConstants, setState, state, errors, submit, user, loading, )}
- {sections.map((section) => ( - <> - - - + {sections.map(section => ( + ))} {errors ? : null} {!(user && user.jwt) ? ( @@ -141,10 +133,10 @@ const Submit = ({ appConstants, setState, state, errors, submit, user, loading, To submit your app, first login with Blockstack. You'll be able to use your Blockstack ID to manage your app's listing. - + {loading ? 'Loading...' : 'Login with Blockstack'} ) : ( - + {loading ? 'Loading...' : 'Submit App'} )}
@@ -263,51 +255,27 @@ class SubmitDapp extends React.Component { const { appConstants } = this.props return ( - + - - {this.state.success ? ( - <> - - - - Success! - - - - Thanks for your submission! Your app will need to be approved before being public on app.co. - {this.appMiningEligible() && ( - <> - To update your app's details and enroll in App Mining, visit our Maker Portal - - - - - )} - - - - ) : ( - this.setState(args), 100)} - state={this.state.values} - errors={this.state.errorCount > 0 && this.state.errors} - signIn={this.props.signIn} - user={this.props.user} - isAppMiningEligible={this.appMiningEligible()} - /> - )} + + { + this.state.success + ? + : ( + this.setState(args), 100)} + state={this.state.values} + errors={this.state.errorCount > 0 && this.state.errors} + signIn={this.props.signIn} + user={this.props.user} + isAppMiningEligible={this.appMiningEligible()} + /> + ) + } ) diff --git a/server.js b/server.js index 4f9ae9b5..9014fb82 100644 --- a/server.js +++ b/server.js @@ -66,7 +66,8 @@ async function renderAndCache(req, res, pagePath, serverData) { ...serverData, blockstackRankedApps, appMiningMonths, - appMiningApps + appMiningApps, + params: req.params } const html = await app.renderToHTML(req, res, pagePath, dataToPass) if (cacheKey) { @@ -220,7 +221,9 @@ app.prepare().then(() => { /** * Maker pages */ - server.get('/maker', (req, res) => renderAndCache(req, res, '/maker')) + server.get('/maker', (req, res) => res.redirect('/maker/apps')) + server.get('/maker/apps', (req, res) => renderAndCache(req, res, '/maker/apps')) + server.get('/maker/apps/:appId', (req, res) => renderAndCache(req, res, '/maker/apps')) server.get('/maker/:accessToken', (req, res) => renderAndCache(req, res, '/maker/magic-link', { accessToken: req.params.accessToken })) apps.platforms.forEach((platform) => { diff --git a/stores/index.js b/stores/index.js index 6c24d38a..ee8a36d5 100644 --- a/stores/index.js +++ b/stores/index.js @@ -8,6 +8,7 @@ import newsletter from '@stores/newsletter' import RouterStore from '@stores/router' import makeMiningReducer from '@stores/mining/reducer' import AdminMiningReducer from '@stores/mining-admin/reducer' +import makerReducer from '@stores/maker/reducer' export default (data) => { const composeEnhancers = (typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose @@ -21,7 +22,8 @@ export default (data) => { newsletter, router: RouterStore.reducer, mining: makeMiningReducer(data), - miningAdmin: AdminMiningReducer + miningAdmin: AdminMiningReducer, + maker: makerReducer }) return finalCreateStore(Reducer) diff --git a/stores/maker/actions.js b/stores/maker/actions.js new file mode 100644 index 00000000..a2452c54 --- /dev/null +++ b/stores/maker/actions.js @@ -0,0 +1,78 @@ +export const SET_LOADING_DONE = 'maker-page/SET_LOADING_DONE' +export const MAKER_AUTH_ERROR = 'maker-page/MAKER_AUTH_ERROR' + +export const SET_PAYMENT_DETAILS_COMPLETE = 'maker-page/SET_PAYMENT_DETAILS_COMPLETE' +export const SAVE_PAYMENT_DETAILS = 'maker-page/SAVE_PAYMENT_DETAILS' +export const SAVE_PAYMENT_DETAILS_DONE = 'maker-page/SAVE_PAYMENT_DETAILS_DONE' +export const SAVE_PAYMENT_DETAILS_FAIL = 'maker-page/SAVE_PAYMENT_DETAILS_FAIL' + +export const SET_KYC_COMPLETE = 'maker-page/SET_KYC_COMPLETE' +export const SET_LEGAL_COMPLETE = 'maker-page/SET_LEGAL_COMPLETE' + +export const FETCH_APPS = 'maker-page/FETCH_APPS' +export const FETCH_APPS_DONE = 'maker-page/FETCH_APPS_DONE' +export const FETCH_APPS_FAIL = 'maker-page/FETCH_APPS_FAIL' + +export const SELECT_APP = 'maker-page/SELECT_APP' + +export const errorAction = () => ({ type: MAKER_AUTH_ERROR }) +export const setLoadingDoneAction = () => ({ type: SET_LOADING_DONE }) +export const savePaymentDetailsAction = () => ({ type: SAVE_PAYMENT_DETAILS }) +export const savePaymentDetailsDoneAction = (addresses) => ({ + type: SAVE_PAYMENT_DETAILS_DONE, + payload: addresses +}) +export const savePaymentDetailsFailAction = () => ({ type: SAVE_PAYMENT_DETAILS_FAIL }) + +export const setKycComplete = () => ({ type: SET_KYC_COMPLETE }) +export const setLegalComplete = () => ({ type: SET_LEGAL_COMPLETE }) + +export const fetchAppsAction = () => ({ type: FETCH_APPS }) + +export const fetchAppsDoneAction = (payload) => ({ type: FETCH_APPS_DONE, payload }) + +export const fetchAppsFailAction = () => ({ type: FETCH_APPS_FAIL }) + +export const selectAppAction = (payload) => ({ type: SELECT_APP, payload }) + +export const savePaymentDetails = ({ apiServer, appId, jwt, btcAddress, stxAddress }) => async (dispatch) => { + dispatch(savePaymentDetailsAction()) + try { + const response = await fetch(`${apiServer}/api/maker/apps?appId=${appId}`, { + method: 'POST', + headers: new Headers({ + 'Content-Type': 'application/json', + authorization: `Bearer ${jwt}` + }), + body: JSON.stringify({ + BTCAddress: btcAddress, + stacksAddress: stxAddress + }) + }) + await response.json() + dispatch(savePaymentDetailsDoneAction({ appId, btcAddress, stxAddress })) + } catch (error) { + dispatch(savePaymentDetailsFailAction(error)) + } +} + +export const fetchApps = ({ user, apiServer }) => async (dispatch) => { + if (!(user && user.jwt)) { + dispatch(errorAction()) + return + } + + dispatch(fetchAppsAction()) + try { + const uri = `${apiServer}/api/maker/apps` + const response = await fetch(uri, { + headers: { + Authorization: `Bearer ${user.jwt}` + } + }) + const data = await response.json() + await dispatch(fetchAppsDoneAction(data)) + } catch (error) { + dispatch(fetchAppsFailAction()) + } +} diff --git a/stores/maker/reducer.js b/stores/maker/reducer.js new file mode 100644 index 00000000..69145591 --- /dev/null +++ b/stores/maker/reducer.js @@ -0,0 +1,51 @@ +import keyBy from 'lodash/keyBy' +import * as MakerActions from './actions' + +const initialState = { + loading: true, + apps: [], + appIds: [], + appEntities: {}, + selectedAppId: null, + errorMessage: null +} + +function makerReducer(state = initialState, action) { + switch (action.type) { + case MakerActions.MAKER_AUTH_ERROR: + return { + ...state, + loading: false, + errorMessage: 'Please sign in to access the maker portal.' + } + + case MakerActions.FETCH_APPS_DONE: + return { + ...state, + loading: false, + appIds: action.payload.apps.map((app) => app.id), + appEntities: keyBy(action.payload.apps, 'id') + } + + case MakerActions.SELECT_APP: + return { ...state, selectedAppId: action.payload } + + case MakerActions.SAVE_PAYMENT_DETAILS_DONE: + return { + ...state, + appEntities: { + ...state.appEntities, + [action.payload.appId]: { + ...state.appEntities[action.payload.appId], + BTCAddress: action.payload.btcAddress, + stacksAddress: action.payload.stxAddress + } + } + } + + default: + return state + } +} + +export default makerReducer diff --git a/stores/maker/selectors.js b/stores/maker/selectors.js new file mode 100644 index 00000000..966940f0 --- /dev/null +++ b/stores/maker/selectors.js @@ -0,0 +1,30 @@ +const updateEntity = (state, id, newProps) => ({ + ...state, + appEntities: { + ...state.appEntities, + [id]: { + ...state.appEntities[id], + ...newProps + } + } +}) + +export const selectMaker = (state) => state.maker + +export const selectIsMakerLoading = (state) => state.maker.loading + +export const selectAppList = (state) => state.maker.appIds.map((id) => state.maker.appEntities[id]) + +export const selectCurrentApp = (state) => state.maker.appEntities[state.maker.selectedAppId] + +export const selectCompetionStatus = (state) => { + const selectedApp = state.maker.appEntities[state.maker.selectedAppId] + if (!selectedApp) { + return {} + } + return { + paymentDetailsComplete: selectedApp.BTCAddress && selectedApp.stacksAddress, + kycComplete: selectedApp.isKYCVerified, + legalComplete: false + } +} diff --git a/stores/user/index.js b/stores/user/index.js index dd1efe05..c85b2db7 100644 --- a/stores/user/index.js +++ b/stores/user/index.js @@ -1,4 +1,5 @@ import { UserSession, AppConfig } from 'blockstack' +import Cookies from 'js-cookie' const host = typeof document === 'undefined' ? 'https://app.co' : document.location.origin const appConfig = new AppConfig(['store_write'], host) @@ -24,35 +25,43 @@ const signingIn = () => ({ const signedIn = (data) => ({ type: constants.SIGNED_IN, - token: data.token, - user: data.user, - userId: data.user.id -}) - -const signOut = () => ({ - type: constants.SIGNING_OUT + payload: { + token: data.token, + user: data.user, + userId: data.user.id + } }) -const handleSignIn = (apiServer) => - async function innerHandleSignIn(dispatch) { - const token = userSession.getAuthResponseToken() - if (!token) { - return true - } - if (userSession.isUserSignedIn()) { - userSession.signUserOut() - } +const signOut = () => { + Cookies.remove('jwt') + return { + type: constants.SIGNING_OUT + } +} - dispatch(signingIn()) - await userSession.handlePendingSignIn() - const url = `${apiServer}/api/authenticate?authToken=${token}` - const response = await fetch(url, { - method: 'POST' - }) - const json = await response.json() - dispatch(signedIn(json)) +const handleSignIn = (apiServer) => async (dispatch) => { + const token = userSession.getAuthResponseToken() + if (!token) { return true } + if (userSession.isUserSignedIn()) { + userSession.signUserOut() + } + + dispatch(signingIn()) + await userSession.handlePendingSignIn() + const url = `${apiServer}/api/authenticate?authToken=${token}` + const response = await fetch(url, { + method: 'POST' + }) + const json = await response.json() + dispatch(signedIn(json)) + const cookie = Cookies.get('jwt') + if (!cookie) { + Cookies.set('jwt', json.token) + } + return true +} const signIn = (redirectPath = 'admin') => { const redirect = `${window.location.origin}/${redirectPath}` @@ -67,25 +76,23 @@ const actions = { signOut } -const reducer = (state = initialState, action) => { - switch (action.type) { +const reducer = (state = initialState, { type, payload }) => { + switch (type) { case constants.SIGNING_IN: - return Object.assign({}, state, { + return { + ...state, signingIn: true - }) + } case constants.SIGNED_IN: - return Object.assign({}, state, { + return { + ...state, signingIn: false, - jwt: action.token, - userId: action.userId, - user: action.user - }) + jwt: payload.token, + userId: payload.userId, + user: payload.user + } case constants.SIGNING_OUT: - return Object.assign({}, state, { - user: null, - userId: null, - jwt: null - }) + return { ...state, user: null, userId: null, jwt: null } default: return state } diff --git a/yarn.lock b/yarn.lock index b77b2a98..d5ee2cb8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2098,6 +2098,14 @@ dependencies: "@types/bn.js" "*" +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/node@*": version "10.12.24" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.24.tgz#b13564af612a22a20b5d95ca40f1bffb3af315cf" @@ -2118,6 +2126,24 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.2.tgz#0e58ae66773d7fd7c372a493aff740878ec9ceaa" integrity sha512-f8JzJNWVhKtc9dg/dyDNfliTKNOJSLa7Oht/ElZdF/UbMUmAH3rLmAk3ODNjw0mZajDEgatA03tRjB4+Dp/tzA== +"@types/react-redux@^7.1.5": + version "7.1.5" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.5.tgz#c7a528d538969250347aa53c52241051cf886bd3" + integrity sha512-ZoNGQMDxh5ENY7PzU7MVonxDzS1l/EWiy8nUhDqxFqUZn4ovboCyvk4Djf68x6COb7vhGTKjyjxHxtFdAA5sUA== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + +"@types/react@*": + version "16.9.11" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.11.tgz#70e0b7ad79058a7842f25ccf2999807076ada120" + integrity sha512-UBT4GZ3PokTXSWmdgC/GeCGEJXE5ofWyibCcecRLUVN2ZBpXQGVgQGtG2foS7CrTKFKlQVVswLvf7Js6XA/CVQ== + dependencies: + "@types/prop-types" "*" + csstype "^2.2.0" + "@types/react@^16.9.2": version "16.9.2" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.2.tgz#6d1765431a1ad1877979013906731aae373de268" @@ -6628,7 +6654,7 @@ hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0, hoist-non-react- resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== -hoist-non-react-statics@^3.1.0: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA== @@ -7738,6 +7764,11 @@ jest@^22.4.3: resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.4.tgz#2c89d6889b5eac522a7eea32c14521559c6cbf02" integrity sha1-LInWiJterFIqfuoywUUhVZxsvwI= +js-cookie@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" + integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== + js-levenshtein@^1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" @@ -10794,6 +10825,14 @@ redux@^3.7.2: loose-envify "^1.1.0" symbol-observable "^1.0.3" +redux@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796" + integrity sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q== + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + redux@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.1.tgz#436cae6cc40fbe4727689d7c8fae44808f1bfef5"