kopia lustrzana https://github.com/bugout-dev/moonstream
				
				
				
			Merge pull request #227 from bugout-dev/store-onbarding-on-backend
Store onboarding on backendpull/240/head
						commit
						e2416ccdc2
					
				|  | @ -1,7 +1,8 @@ | |||
| import json | ||||
| import logging | ||||
| from typing import Optional | ||||
| from typing import Optional, Dict, Any | ||||
| from enum import Enum | ||||
| import uuid | ||||
| 
 | ||||
| import boto3  # type: ignore | ||||
| from moonstreamdb.models import ( | ||||
|  | @ -14,6 +15,12 @@ from sqlalchemy.orm import Session | |||
| from . import data | ||||
| from .reporter import reporter | ||||
| from .settings import ETHERSCAN_SMARTCONTRACTS_BUCKET | ||||
| from bugout.data import BugoutResource | ||||
| from .settings import ( | ||||
|     MOONSTREAM_APPLICATION_ID, | ||||
|     bugout_client as bc, | ||||
|     BUGOUT_REQUEST_TIMEOUT_SECONDS, | ||||
| ) | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| ETHERSCAN_SMARTCONTRACT_LABEL_NAME = "etherscan_smartcontract" | ||||
|  | @ -144,3 +151,25 @@ def get_address_labels( | |||
|         ) | ||||
| 
 | ||||
|     return addresses_response | ||||
| 
 | ||||
| 
 | ||||
| def create_onboarding_resource( | ||||
|     token: uuid.UUID, | ||||
|     resource_data: Dict[str, Any] = { | ||||
|         "type": data.USER_ONBOARDING_STATE, | ||||
|         "steps": { | ||||
|             "welcome": 0, | ||||
|             "subscriptions": 0, | ||||
|             "stream": 0, | ||||
|         }, | ||||
|         "is_complete": False, | ||||
|     }, | ||||
| ) -> BugoutResource: | ||||
| 
 | ||||
|     resource = bc.create_resource( | ||||
|         token=token, | ||||
|         application_id=MOONSTREAM_APPLICATION_ID, | ||||
|         resource_data=resource_data, | ||||
|         timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS, | ||||
|     ) | ||||
|     return resource | ||||
|  |  | |||
|  | @ -5,6 +5,8 @@ from typing import List, Optional, Dict, Any | |||
| 
 | ||||
| from pydantic import BaseModel, Field | ||||
| 
 | ||||
| USER_ONBOARDING_STATE = "onboarding_state" | ||||
| 
 | ||||
| 
 | ||||
| class SubscriptionTypeResourceData(BaseModel): | ||||
|     id: str | ||||
|  | @ -196,3 +198,8 @@ class AddressLabelsResponse(BaseModel): | |||
| 
 | ||||
| class AddressListLabelsResponse(BaseModel): | ||||
|     addresses: List[AddressLabelsResponse] = Field(default_factory=list) | ||||
| 
 | ||||
| 
 | ||||
| class OnboardingState(BaseModel): | ||||
|     is_complete: bool | ||||
|     steps: Dict[str, int] | ||||
|  |  | |||
|  | @ -5,20 +5,30 @@ import logging | |||
| from typing import Any, Dict | ||||
| import uuid | ||||
| 
 | ||||
| from bugout.data import BugoutToken, BugoutUser | ||||
| from bugout.data import BugoutToken, BugoutUser, BugoutResource | ||||
| from bugout.exceptions import BugoutResponseException | ||||
| from fastapi import FastAPI, Form, Request | ||||
| 
 | ||||
| from fastapi import ( | ||||
|     Body, | ||||
|     FastAPI, | ||||
|     Form, | ||||
|     Request, | ||||
| ) | ||||
| from fastapi.middleware.cors import CORSMiddleware | ||||
| 
 | ||||
| from .. import data | ||||
| from ..middleware import BroodAuthMiddleware, MoonstreamHTTPException | ||||
| 
 | ||||
| from ..settings import ( | ||||
|     MOONSTREAM_APPLICATION_ID, | ||||
|     DOCS_TARGET_PATH, | ||||
|     ORIGINS, | ||||
|     DOCS_PATHS, | ||||
|     bugout_client as bc, | ||||
|     BUGOUT_REQUEST_TIMEOUT_SECONDS, | ||||
| ) | ||||
| from ..version import MOONSTREAM_VERSION | ||||
| from ..actions import create_onboarding_resource | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
|  | @ -166,3 +176,122 @@ async def logout_handler(request: Request) -> uuid.UUID: | |||
|     except Exception as e: | ||||
|         raise MoonstreamHTTPException(status_code=500, internal_error=e) | ||||
|     return token_id | ||||
| 
 | ||||
| 
 | ||||
| @app.post("/onboarding", tags=["users"], response_model=data.OnboardingState) | ||||
| async def set_onboarding_state( | ||||
|     request: Request, | ||||
|     onboarding_data: data.OnboardingState = Body(...), | ||||
| ) -> data.OnboardingState: | ||||
| 
 | ||||
|     token = request.state.token | ||||
|     try: | ||||
|         response = bc.list_resources( | ||||
|             token=token, | ||||
|             params={"type": data.USER_ONBOARDING_STATE}, | ||||
|             timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS, | ||||
|         ) | ||||
|         resource_data = {"type": data.USER_ONBOARDING_STATE, **onboarding_data.dict()} | ||||
|         if response.resources: | ||||
|             resource = bc.update_resource( | ||||
|                 token=token, | ||||
|                 resource_id=str(response.resources[0].id), | ||||
|                 resource_data={"update": resource_data, "drop_keys": []}, | ||||
|             ) | ||||
|         else: | ||||
|             resource = create_onboarding_resource( | ||||
|                 token=token, resource_data=resource_data | ||||
|             ) | ||||
| 
 | ||||
|     except BugoutResponseException as e: | ||||
|         raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail) | ||||
|     except Exception as e: | ||||
|         raise MoonstreamHTTPException(status_code=500) | ||||
| 
 | ||||
|     if ( | ||||
|         resource.resource_data.get("is_complete") is None | ||||
|         or resource.resource_data.get("steps") is None | ||||
|     ): | ||||
|         logger.error( | ||||
|             f"Resources did not return correct onboarding object. Resource id:{resource.id}" | ||||
|         ) | ||||
|         raise MoonstreamHTTPException(status_code=500) | ||||
| 
 | ||||
|     result = data.OnboardingState( | ||||
|         is_complete=resource.resource_data.get("is_complete", False), | ||||
|         steps=resource.resource_data.get("steps", {}), | ||||
|     ) | ||||
|     return result | ||||
| 
 | ||||
| 
 | ||||
| @app.get("/onboarding", tags=["users"], response_model=data.OnboardingState) | ||||
| async def get_onboarding_state(request: Request) -> data.OnboardingState: | ||||
|     token = request.state.token | ||||
|     try: | ||||
|         response = bc.list_resources( | ||||
|             token=token, | ||||
|             params={"type": data.USER_ONBOARDING_STATE}, | ||||
|             timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS, | ||||
|         ) | ||||
| 
 | ||||
|         if response.resources: | ||||
|             resource = response.resources[0] | ||||
|         else: | ||||
|             resource = create_onboarding_resource(token=token) | ||||
|     except BugoutResponseException as e: | ||||
|         raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail) | ||||
|     except Exception as e: | ||||
| 
 | ||||
|         raise MoonstreamHTTPException(status_code=500) | ||||
| 
 | ||||
|     if ( | ||||
|         resource.resource_data.get("is_complete") is None | ||||
|         or resource.resource_data.get("steps") is None | ||||
|     ): | ||||
|         logger.error( | ||||
|             f"Resources did not return correct onboarding object. Resource id:{resource.id}" | ||||
|         ) | ||||
|         raise MoonstreamHTTPException(status_code=500) | ||||
|     result = data.OnboardingState( | ||||
|         is_complete=resource.resource_data.get("is_complete", False), | ||||
|         steps=resource.resource_data.get("steps", {}), | ||||
|     ) | ||||
|     return result | ||||
| 
 | ||||
| 
 | ||||
| @app.delete("/onboarding", tags=["users"], response_model=data.OnboardingState) | ||||
| async def delete_onboarding_state(request: Request) -> data.OnboardingState: | ||||
|     token = request.state.token | ||||
|     try: | ||||
|         response = bc.list_resources( | ||||
|             token=token, | ||||
|             params={"type": data.USER_ONBOARDING_STATE}, | ||||
|             timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS, | ||||
|         ) | ||||
|         if not response.resources: | ||||
|             raise MoonstreamHTTPException(status_code=404, detail="not found") | ||||
|         if response.resources: | ||||
|             resource: BugoutResource = bc.delete_resource( | ||||
|                 token=token, | ||||
|                 resource_id=response.resources[0].id, | ||||
|                 timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS, | ||||
|             ) | ||||
| 
 | ||||
|     except BugoutResponseException as e: | ||||
|         raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail) | ||||
|     except Exception as e: | ||||
|         raise MoonstreamHTTPException(status_code=500) | ||||
| 
 | ||||
|     if ( | ||||
|         resource.resource_data.get("is_complete") is None | ||||
|         or resource.resource_data.get("steps") is None | ||||
|     ): | ||||
|         logger.error( | ||||
|             f"Resources did not return correct onboarding object. Resource id:{resource.id}" | ||||
|         ) | ||||
|         raise MoonstreamHTTPException(status_code=500) | ||||
|     result = data.OnboardingState( | ||||
|         is_complete=resource.resource_data.get("is_complete", False), | ||||
|         steps=resource.resource_data.get("steps", {}), | ||||
|     ) | ||||
|     return result | ||||
|  |  | |||
|  | @ -70,7 +70,7 @@ const Welcome = () => { | |||
|       ui.setOnboardingStep(ui.onboardingStep + 1); | ||||
|       scrollRef?.current?.scrollIntoView(); | ||||
|     } else { | ||||
|       ui.setisOnboardingComplete(true); | ||||
|       ui.setOnboardingComplete(true); | ||||
|       router.push("/stream"); | ||||
|     } | ||||
|   }; | ||||
|  |  | |||
|  | @ -115,12 +115,6 @@ const AppNavbar = () => { | |||
|                   </RouteButton> | ||||
|                 ))} | ||||
|                 {USER_NAV_PATHES.map((item, idx) => { | ||||
|                   console.log( | ||||
|                     "item.path:", | ||||
|                     item.path, | ||||
|                     "pathname:", | ||||
|                     router.nextRouter.pathname | ||||
|                   ); | ||||
|                   return ( | ||||
|                     <RouteButton | ||||
|                       key={`${idx}-${item.title}-navlink`} | ||||
|  |  | |||
|  | @ -1,10 +1,26 @@ | |||
| import React, { useState, useContext, useEffect } from "react"; | ||||
| import React, { useState, useContext, useEffect, useCallback } from "react"; | ||||
| import { useBreakpointValue } from "@chakra-ui/react"; | ||||
| import { useStorage, useQuery, useRouter } from "../../hooks"; | ||||
| import UIContext from "./context"; | ||||
| import UserContext from "../UserProvider/context"; | ||||
| import ModalContext from "../ModalProvider/context"; | ||||
| import { v4 as uuid4 } from "uuid"; | ||||
| import { PreferencesService } from "../../services"; | ||||
| 
 | ||||
| const onboardingSteps = [ | ||||
|   { | ||||
|     step: "welcome", | ||||
|     description: "Basics of how Moonstream works", | ||||
|   }, | ||||
|   { | ||||
|     step: "subscriptions", | ||||
|     description: "Learn how to subscribe to blockchain events", | ||||
|   }, | ||||
|   { | ||||
|     step: "stream", | ||||
|     description: "Learn how to use your Moonstream to analyze blah blah blah", | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const UIProvider = ({ children }) => { | ||||
|   const router = useRouter(); | ||||
|  | @ -53,14 +69,6 @@ const UIProvider = ({ children }) => { | |||
|     } | ||||
|   }, [isAppView, user, isLoggingOut]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (isInit && router.nextRouter.isReady) { | ||||
|       setAppReady(true); | ||||
|     } else { | ||||
|       setAppReady(false); | ||||
|     } | ||||
|   }, [isInit, router]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (user && user.username) { | ||||
|       setLoggedIn(true); | ||||
|  | @ -155,90 +163,109 @@ const UIProvider = ({ children }) => { | |||
| 
 | ||||
|   // *********** Onboarding state **********************
 | ||||
| 
 | ||||
|   const onboardingSteps = [ | ||||
|     { | ||||
|       step: "welcome", | ||||
|       description: "Basics of how Moonstream works", | ||||
|     }, | ||||
|     { | ||||
|       step: "subscriptions", | ||||
|       description: "Learn how to subscribe to blockchain events", | ||||
|     }, | ||||
|     { | ||||
|       step: "stream", | ||||
|       description: "Learn how to use your Moonstream to analyze blah blah blah", | ||||
|     }, | ||||
|   ]; | ||||
|   const [onboardingState, setOnboardingState] = useState(false); | ||||
|   const [onboardingStep, setOnboardingStep] = useState(); | ||||
|   const [onboardingStateInit, setOnboardingStateInit] = useState(false); | ||||
| 
 | ||||
|   const [onboardingState, setOnboardingState] = useStorage( | ||||
|     window.localStorage, | ||||
|     "onboardingState", | ||||
|     { | ||||
|       welcome: 0, | ||||
|       subscriptions: 0, | ||||
|       stream: 0, | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
|   const [onboardingStep, setOnboardingStep] = useState(() => { | ||||
|     //First find onboarding step that was viewed once (resume where left)
 | ||||
|     // If none - find step that was never viewed
 | ||||
|     // if none - set onboarding to zero
 | ||||
|     let step = onboardingSteps.findIndex( | ||||
|       (event) => onboardingState[event.step] === 1 | ||||
|     ); | ||||
|     step = | ||||
|       step === -1 | ||||
|         ? onboardingSteps.findIndex( | ||||
|             (event) => onboardingState[event.step] === 0 | ||||
|           ) | ||||
|         : step; | ||||
|     if (step === -1 && isOnboardingComplete) return 0; | ||||
|     else if (step === -1 && !isOnboardingComplete) return 0; | ||||
|     else return step; | ||||
|   }); | ||||
| 
 | ||||
|   const [isOnboardingComplete, setisOnboardingComplete] = useStorage( | ||||
|     window.localStorage, | ||||
|     "isOnboardingComplete", | ||||
|     isLoggedIn ? true : false | ||||
|   const setOnboardingComplete = useCallback( | ||||
|     (newState) => { | ||||
|       setOnboardingState({ ...onboardingState, is_complete: newState }); | ||||
|     }, | ||||
|     [onboardingState] | ||||
|   ); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (isLoggedIn && !isOnboardingComplete) { | ||||
|     //If onboarding state not exists - fetch it from backend
 | ||||
|     //If it exists but init is not set - set init true
 | ||||
|     //If it exists and is init -> post update to backend
 | ||||
|     if (!onboardingState && user) { | ||||
|       const currentOnboardingState = async () => | ||||
|         PreferencesService.getOnboardingState().then((response) => { | ||||
|           return response.data; | ||||
|         }); | ||||
| 
 | ||||
|       currentOnboardingState().then((response) => { | ||||
|         setOnboardingState(response); | ||||
|       }); | ||||
|     } else if (user && onboardingState && !onboardingStateInit) { | ||||
|       setOnboardingStateInit(true); | ||||
|     } else if (onboardingStateInit) { | ||||
|       PreferencesService.setOnboardingState(onboardingState); | ||||
|     } | ||||
|     // eslint-disable-next-line
 | ||||
|   }, [onboardingState, user]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     //This will set step after state is fetched from backend
 | ||||
|     if (!Number.isInteger(onboardingStep) && onboardingState) { | ||||
|       const step = onboardingSteps.findIndex( | ||||
|         (event) => onboardingState[event.step] === 0 | ||||
|       ); | ||||
|       if (step === -1 && onboardingState["is_complete"]) | ||||
|         setOnboardingStep(onboardingSteps.length - 1); | ||||
|       else if (step === -1 && !onboardingState["is_complete"]) | ||||
|         return setOnboardingStep(0); | ||||
|       else setOnboardingStep(step); | ||||
|     } | ||||
|   }, [onboardingState, onboardingStep]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     //redirect to welcome page if yet not completed onboarding
 | ||||
|     if ( | ||||
|       isLoggedIn && | ||||
|       isAppReady && | ||||
|       onboardingState && | ||||
|       !onboardingState?.is_complete | ||||
|     ) { | ||||
|       router.replace("/welcome"); | ||||
|     } | ||||
|     // eslint-disable-next-line
 | ||||
|   }, [isLoggedIn, isOnboardingComplete]); | ||||
|   }, [isLoggedIn, onboardingState?.is_complete, isAppReady]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if ( | ||||
|       onboardingSteps.findIndex( | ||||
|         (event) => onboardingState[event.step] === 0 | ||||
|       ) === -1 | ||||
|     ) { | ||||
|       setisOnboardingComplete(true); | ||||
|     //This will set up onboarding complete once user finishes each view at least once
 | ||||
|     if (onboardingState?.steps && user && isAppReady) { | ||||
|       if ( | ||||
|         onboardingSteps.findIndex( | ||||
|           (event) => onboardingState.steps[event.step] === 0 | ||||
|         ) === -1 | ||||
|       ) { | ||||
|         !onboardingState.is_complete && setOnboardingComplete(true); | ||||
|       } | ||||
|     } | ||||
|     //eslint-disable-next-line
 | ||||
|   }, [onboardingState]); | ||||
|   }, [onboardingState, user, isAppReady, setOnboardingComplete]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (router.nextRouter.pathname === "/welcome") { | ||||
|       const newOnboardingState = { | ||||
|     //This will update onboardingState when step changes
 | ||||
|     if ( | ||||
|       router.nextRouter.pathname === "/welcome" && | ||||
|       isAppReady && | ||||
|       user && | ||||
|       Number.isInteger(onboardingStep) && | ||||
|       onboardingState | ||||
|     ) { | ||||
|       setOnboardingState({ | ||||
|         ...onboardingState, | ||||
|         [`${onboardingSteps[onboardingStep].step}`]: | ||||
|           onboardingState[onboardingSteps[onboardingStep].step] + 1, | ||||
|       }; | ||||
| 
 | ||||
|       setOnboardingState(newOnboardingState); | ||||
|         steps: { | ||||
|           ...onboardingState.steps, | ||||
|           [`${onboardingSteps[onboardingStep].step}`]: | ||||
|             onboardingState.steps[onboardingSteps[onboardingStep].step] + 1, | ||||
|         }, | ||||
|       }); | ||||
|     } | ||||
|     // eslint-disable-next-line
 | ||||
|   }, [onboardingStep, router.nextRouter.pathname]); | ||||
| 
 | ||||
|   // const ONBOARDING_STEP_NUM = steps.length;
 | ||||
|   }, [onboardingStep, router.nextRouter.pathname, user, isAppReady]); | ||||
| 
 | ||||
|   // ********************************************************
 | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (isInit && router.nextRouter.isReady && onboardingState) { | ||||
|       setAppReady(true); | ||||
|     } else { | ||||
|       setAppReady(false); | ||||
|     } | ||||
|   }, [isInit, router, onboardingState]); | ||||
| 
 | ||||
|   return ( | ||||
|     <UIContext.Provider | ||||
|       value={{ | ||||
|  | @ -268,8 +295,7 @@ const UIProvider = ({ children }) => { | |||
|         isEntryDetailView, | ||||
|         onboardingStep, | ||||
|         setOnboardingStep, | ||||
|         isOnboardingComplete, | ||||
|         setisOnboardingComplete, | ||||
|         setOnboardingComplete, | ||||
|         onboardingSteps, | ||||
|         setOnboardingState, | ||||
|         onboardingState, | ||||
|  |  | |||
|  | @ -1,23 +1,18 @@ | |||
| import { http } from "../utils"; | ||||
| 
 | ||||
| const API = process.env.NEXT_PUBLIC_SIMIOTICS_JOURNALS_URL; | ||||
| const PREFERENCES_API = `${API}/preferences`; | ||||
| const API_URL = process.env.NEXT_PUBLIC_MOONSTREAM_API_URL; | ||||
| export const PREFERENCES_URL = `${API_URL}/users`; | ||||
| 
 | ||||
| export const getDefaultJournal = () => | ||||
| export const getOnboardingState = () => | ||||
|   http({ | ||||
|     method: "GET", | ||||
|     url: `${PREFERENCES_API}/default_journal`, | ||||
|     url: `${PREFERENCES_URL}/onboarding`, | ||||
|   }); | ||||
| 
 | ||||
| export const setDefaultJournal = (journalId) => | ||||
|   http({ | ||||
| export const setOnboardingState = (data) => { | ||||
|   return http({ | ||||
|     method: "POST", | ||||
|     url: `${PREFERENCES_API}/default_journal`, | ||||
|     data: { id: journalId }, | ||||
|   }); | ||||
| 
 | ||||
| export const unsetDefaultJournal = () => | ||||
|   http({ | ||||
|     method: "DELETE", | ||||
|     url: `${PREFERENCES_API}/default_journal`, | ||||
|     url: `${PREFERENCES_URL}/onboarding`, | ||||
|     data, | ||||
|   }); | ||||
| }; | ||||
|  |  | |||
		Ładowanie…
	
		Reference in New Issue
	
	 Sergei Sumarokov
						Sergei Sumarokov