From 8ede15e9b351176c7aefea49ae85823305892f12 Mon Sep 17 00:00:00 2001 From: Yan Sern Date: Thu, 14 Sep 2023 21:26:26 +0300 Subject: [PATCH 1/3] Added basic routing for shareable urls. --- src/lib/redux/cache.js | 8 +- src/state/index.js | 6 +- src/state/request/reducer.js | 2 +- src/state/router.js | 143 +++++++++++++++++++++++++++++++++++ src/state/ui/reducer.js | 7 +- 5 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 src/state/router.js diff --git a/src/lib/redux/cache.js b/src/lib/redux/cache.js index 9326c5b..ee666d0 100644 --- a/src/lib/redux/cache.js +++ b/src/lib/redux/cache.js @@ -18,7 +18,13 @@ function deserialize( state, reducer ) { return reducer( state, { type: DESERIALIZE } ); } -export function loadInitialState( initialState, reducer ) { +export function loadInitialState( initialState, initialStateFromUrl, reducer ) { + + // URL state overrides local storage state + if ( initialStateFromUrl ) { + return reducer( initialStateFromUrl, { type: DESERIALIZE } ); + } + const localStorageState = JSON.parse( localStorage.getItem( STORAGE_KEY ) ) || {}; if ( localStorageState._timestamp && localStorageState._timestamp + MAX_AGE < Date.now() ) { return initialState; diff --git a/src/state/index.js b/src/state/index.js index b95b768..c72d5b7 100644 --- a/src/state/index.js +++ b/src/state/index.js @@ -1,14 +1,14 @@ import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; - +import router, { getStateFromUrl } from './router'; import reducer from './reducer'; import { boot } from './security/actions'; import { loadInitialState, persistState } from '../lib/redux/cache'; const store = createStore( reducer, - loadInitialState( {}, reducer ), - applyMiddleware( thunk ) + loadInitialState( {}, getStateFromUrl(), reducer ), + applyMiddleware( thunk, router ) ); persistState( store, reducer ); store.dispatch( boot() ); diff --git a/src/state/request/reducer.js b/src/state/request/reducer.js index a072d2b..9111eed 100644 --- a/src/state/request/reducer.js +++ b/src/state/request/reducer.js @@ -11,7 +11,7 @@ import { } from '../actions'; import schema from './schema'; -const defaultState = { +export const defaultState = { method: 'GET', endpoint: false, pathValues: {}, diff --git a/src/state/router.js b/src/state/router.js new file mode 100644 index 0000000..cddccfd --- /dev/null +++ b/src/state/router.js @@ -0,0 +1,143 @@ + +import { REQUEST_TRIGGER, API_VERSIONS_RECEIVE, REQUEST_SELECT_ENDPOINT } from './actions'; +import { defaultState as defaultRequestState } from './request/reducer'; +import { defaultState as defaultUiState } from './ui/reducer'; +import { loadEndpoints } from './endpoints/actions'; +import { getEndpoints } from './endpoints/selectors'; + +function getUrlParams() { + return new URL( window.location.href ).searchParams; +} + +function encodeString( val ) { + return ( val === null || val === undefined ) ? '' : encodeURIComponent( val ); +} + +function encodeObject( val ) { + return ( typeof val === 'object' ) && Object.keys( val ).length === 0 ? '' : encodeURIComponent( JSON.stringify( val ) ); +} + +function decodeString( val ) { + return decodeURIComponent( val ); +} + +function decodeObject( val ) { + return JSON.parse( decodeURIComponent( val ) ); +} + +export const getUrlFromState = state => { + + const { request, ui } = state; + + const params = { + bodyParams: encodeObject( request.bodyParams ), + endpoint: encodeString( request.endpoint ? request.endpoint.pathLabeled : '' ), + method: encodeString( request.method ), + pathValues: encodeObject( request.pathValues ), + queryParams: encodeObject( request.queryParams ), + url: encodeString( request.url ), + api: encodeString( ui.api ), + version: encodeString( ui.version ), + }; + + // Discard empty params + Object.keys( params ).forEach( key => ! params[ key ] && ( delete params[ key ] ) ); + + // Construct url + const urlParams = new URLSearchParams( params ); + const url = window.location.origin + window.location.pathname + '?' + urlParams.toString(); + + return url; +}; + +export const getStateFromUrl = () => { + const urlParams = getUrlParams(); + + if ( urlParams.toString() === '' ) { + return false; + } + + const state = { request: { ...defaultRequestState }, ui: { ...defaultUiState } }; + + try { + for ( const [ key, value ] of urlParams.entries() ) { + switch ( key ) { + case 'bodyParams': + state.request.bodyParams = decodeObject( value ); + break; + case 'method': + state.request.method = decodeString( value ); + break; + case 'pathValues': + state.request.pathValues = decodeObject( value ); + break; + case 'queryParams': + state.request.queryParams = decodeObject( value ); + break; + case 'url': + state.request.url = decodeString( value ); + break; + case 'api': + state.ui.api = decodeString( value ); + break; + case 'version': + state.ui.version = decodeString( value ); + break; + default: + break; + } + } + } catch ( e ) { + // Fail without breaking the app + console.error( 'Could not parse state from url params.', e ); + return false; + } + + return state; +}; + +const router = ( { getState, dispatch } ) => { + + // Determine if we need to load and select the endpoint + let isInitializingEndpoint = false; + + const { ui = {} } = getStateFromUrl(); + const endpointUrlParam = getUrlParams().get( 'endpoint' ); + const endpointPathLabeled = endpointUrlParam ? decodeURIComponent( endpointUrlParam ) : false; + const apiName = ui.api; + const apiVersion = ui.version; + + if ( endpointPathLabeled && apiName && apiVersion ) { + loadEndpoints( apiName, apiVersion )( dispatch ); + isInitializingEndpoint = true; + } + + return next => action => { + const state = getState(); + + switch ( action.type ) { + case REQUEST_TRIGGER: + const url = getUrlFromState( state ); + console.log( state ); + window.history.pushState( {}, document.title, url ); + break; + case API_VERSIONS_RECEIVE: + // Once the endpoint is loaded, select the endpoint. + if ( isInitializingEndpoint ) { + const endpoints = getEndpoints( state, apiName, apiVersion ); + const endpoint = endpoints.find( ( { pathLabeled } ) => pathLabeled === endpointPathLabeled ); + if ( endpoint ) { + dispatch( { type: REQUEST_SELECT_ENDPOINT, payload: { endpoint } } ); + } + isInitializingEndpoint = false; + } + break; + default: + break; + } + + return next( action ); + }; +}; + +export default router; diff --git a/src/state/ui/reducer.js b/src/state/ui/reducer.js index c75032d..3da6048 100644 --- a/src/state/ui/reducer.js +++ b/src/state/ui/reducer.js @@ -3,7 +3,12 @@ import { UI_SELECT_API, UI_SELECT_VERSION } from '../actions'; import { getDefault } from '../../api'; import schema from './schema'; -const reducer = createReducer( { api: getDefault().name, version: null }, { +export const defaultState = { + api: getDefault().name, + version: null, +}; + +const reducer = createReducer( defaultState, { [ UI_SELECT_API ]: ( state, { payload } ) => { return ( { version: null, From 8ba207ab209dd01264257748f515c71b939265b5 Mon Sep 17 00:00:00 2001 From: Yan Sern Date: Thu, 14 Sep 2023 21:58:46 +0300 Subject: [PATCH 2/3] Remove console.log. --- src/state/router.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/state/router.js b/src/state/router.js index cddccfd..aa37990 100644 --- a/src/state/router.js +++ b/src/state/router.js @@ -118,7 +118,6 @@ const router = ( { getState, dispatch } ) => { switch ( action.type ) { case REQUEST_TRIGGER: const url = getUrlFromState( state ); - console.log( state ); window.history.pushState( {}, document.title, url ); break; case API_VERSIONS_RECEIVE: From fe601db603cbe0151b44c480bcf9472e5e66cefe Mon Sep 17 00:00:00 2001 From: Yan Sern Date: Fri, 22 Sep 2023 14:00:50 +0300 Subject: [PATCH 3/3] After signing-in, redirect to the same url you were left before. --- .nvmrc | 1 + src/api/index.js | 2 +- src/config.sample.json | 3 +-- 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..cb406c6 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +16.20.2 diff --git a/src/api/index.js b/src/api/index.js index 27fdafb..3791afb 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -16,7 +16,7 @@ if ( wpcomConfig ) { id: 'WPCOM', baseUrl: 'https://public-api.wordpress.com/oauth2', userUrl: 'https://public-api.wordpress.com/rest/v1.1/me', - redirectUrl: wpcomConfig.redirectUrl || wpcomConfig.redirect_uri, + redirectUrl: window.location.href, clientId: wpcomConfig.clientID || wpcomConfig.clientId || wpcomConfig.client_id, scope: 'global', }; diff --git a/src/config.sample.json b/src/config.sample.json index 47eead1..d70bc98 100644 --- a/src/config.sample.json +++ b/src/config.sample.json @@ -1,6 +1,5 @@ { "wordpress.com": { - "client_id": "12345", - "redirect_uri": "http://example.com/path/to/app" + "client_id": "12345" } }