diff --git a/frontend/app/components/redesign/components/StepsIndicator.tsx b/frontend/app/components/redesign/components/StepsIndicator.tsx index 14cccaa4..f747d5cd 100644 --- a/frontend/app/components/redesign/components/StepsIndicator.tsx +++ b/frontend/app/components/redesign/components/StepsIndicator.tsx @@ -1,7 +1,7 @@ import React from 'react' import { SVGGreenVector, SVGErrorVector } from '~/assets/svg' +import type { StepStatus } from '~/stores/toolStore' -export type StepStatus = 'unfilled' | 'filled' | 'error' type TextPosition = 'top' | 'bottom' interface StepProps { diff --git a/frontend/app/components/redesign/components/ToolsWalletAddress.tsx b/frontend/app/components/redesign/components/ToolsWalletAddress.tsx index 37e64ca1..5872386f 100644 --- a/frontend/app/components/redesign/components/ToolsWalletAddress.tsx +++ b/frontend/app/components/redesign/components/ToolsWalletAddress.tsx @@ -1,6 +1,5 @@ import React, { useState, useRef, useEffect } from 'react' import { cx } from 'class-variance-authority' -import { useSnapshot } from 'valtio' import { ToolsSecondaryButton, InputField, Tooltip } from '@/components' import { Heading5 } from '@/typography' import { @@ -11,16 +10,21 @@ import { import { SVGRefresh, SVGSpinner } from '~/assets/svg' import { useConnectWallet } from '~/hooks/useConnectWallet' import type { ElementErrors } from '~/lib/types' -import { toolState, toolActions } from '~/stores/toolStore' import { useUIActions } from '~/stores/uiStore' +import type { WalletActions, WalletStore } from '~/stores/wallet-store' interface Props { + store: WalletStore + walletActions: WalletActions toolName: 'drawer banner' | 'payment widget' | 'offerwall experience' } -export const ToolsWalletAddress = ({ toolName }: Props) => { - const snap = useSnapshot(toolState, { sync: true }) - const { connect, disconnect } = useConnectWallet() +export const ToolsWalletAddress = ({ + store: snap, + walletActions, + toolName, +}: Props) => { + const { connect, disconnect } = useConnectWallet(snap, walletActions) const uiActions = useUIActions() const [error, setError] = useState() const [isLoading, setIsLoading] = useState(false) @@ -57,7 +61,7 @@ export const ToolsWalletAddress = ({ toolName }: Props) => { ) const walletAddressInfo = await getWalletAddress(walletAddressUrl) - toolActions.setWalletAddressId(walletAddressInfo.id) + walletActions.setWalletAddressId(walletAddressInfo.id) await connect() } catch (error) { setError({ @@ -72,10 +76,10 @@ export const ToolsWalletAddress = ({ toolName }: Props) => { const handleWalletAddressChange = ( e: React.ChangeEvent, ) => { - toolActions.setWalletAddress(e.target.value) + walletActions.setWalletAddress(e.target.value) if (snap.walletConnectStep !== 'unfilled') { - toolActions.setConnectWalletStep('unfilled') + walletActions.setConnectWalletStep('unfilled') } if (error) { setError(undefined) diff --git a/frontend/app/components/redesign/components/dialogs/GrantConfirmationDialog.tsx b/frontend/app/components/redesign/components/dialogs/GrantConfirmationDialog.tsx index 428810d5..c467074d 100644 --- a/frontend/app/components/redesign/components/dialogs/GrantConfirmationDialog.tsx +++ b/frontend/app/components/redesign/components/dialogs/GrantConfirmationDialog.tsx @@ -1,16 +1,17 @@ import React from 'react' -import { useSnapshot } from 'valtio' import { ToolsSecondaryButton } from '@/components' import { Heading5, BodyEmphasis } from '@/typography' -import { toolState } from '~/stores/toolStore' import { BaseDialog } from './BaseDialog' interface Props { + walletAddress: string grantRedirect: string } -export const GrantConfirmationDialog: React.FC = ({ grantRedirect }) => { - const { walletAddress } = useSnapshot(toolState) +export const GrantConfirmationDialog: React.FC = ({ + walletAddress, + grantRedirect, +}) => { return ( , ) diff --git a/frontend/app/hooks/useSaveProfile.tsx b/frontend/app/hooks/useSaveProfile.tsx index f1c48edd..fa93bc75 100644 --- a/frontend/app/hooks/useSaveProfile.tsx +++ b/frontend/app/hooks/useSaveProfile.tsx @@ -10,6 +10,7 @@ import { ApiError } from '~/lib/helpers' import { actions as bannerActions } from '~/stores/banner-store' import { actions as offerwallActions } from '~/stores/offerwall-store' import { toolState } from '~/stores/toolStore' +import type { WalletStore } from '~/stores/wallet-store' import { actions as widgetActions } from '~/stores/widget-store' function getToolActions() { @@ -25,7 +26,7 @@ function getToolActions() { } } -export const useSaveProfile = () => { +export const useSaveProfile = (wallet: WalletStore) => { const [openDialog, closeDialog] = useDialog() const save = useCallback( @@ -37,7 +38,10 @@ export const useSaveProfile = () => { if (result.grantRedirect) { openDialog( - , + , ) return } @@ -46,7 +50,7 @@ export const useSaveProfile = () => { actions.commitProfile() if (action === 'script') { - openDialog() + openDialog() } else { openDialog() } diff --git a/frontend/app/hooks/useToolWallet.ts b/frontend/app/hooks/useToolWallet.ts new file mode 100644 index 00000000..c16788dd --- /dev/null +++ b/frontend/app/hooks/useToolWallet.ts @@ -0,0 +1,12 @@ +import { useSnapshot } from 'valtio' +import type { WalletActions, WalletStore } from '~/stores/wallet-store' + +type WalletBundle = { wallet: WalletStore; actions: WalletActions } + +export function useToolWallet( + { wallet, actions }: WalletBundle, + options?: { sync: boolean }, +): [WalletStore, WalletActions] { + const snap = useSnapshot(wallet, options) + return [snap, actions] +} diff --git a/frontend/app/routes/banner.tsx b/frontend/app/routes/banner.tsx index a10e5f71..5bb92965 100644 --- a/frontend/app/routes/banner.tsx +++ b/frontend/app/routes/banner.tsx @@ -29,11 +29,16 @@ import { useGrantResponseHandler } from '~/hooks/useGrantResponseHandler' import { usePathTracker } from '~/hooks/usePathTracker' import { useSaveProfile } from '~/hooks/useSaveProfile' import { useScrollToWalletAddress } from '~/hooks/useScrollToWalletAddress' +import { useToolWallet } from '~/hooks/useToolWallet' import { actions, banner, + bannerWallet, + bannerWalletActions, hydrateProfilesFromStorage, hydrateSnapshotsFromStorage, + loadBannerWallet, + persistBannerWallet, subscribeProfilesToStorage, subscribeProfilesToUpdates, useBannerProfile, @@ -86,11 +91,15 @@ export async function loader({ request, context }: LoaderFunctionArgs) { export default function Banner() { const snap = useSnapshot(toolState) + const [walletSnap, walletActions] = useToolWallet({ + wallet: bannerWallet, + actions: bannerWalletActions, + }) const bannerSnap = useSnapshot(banner) const [profile] = useBannerProfile() const navigate = useNavigate() const uiActions = useUIActions() - const { save, saveLastAction } = useSaveProfile() + const { save, saveLastAction } = useSaveProfile(bannerWallet) const { walletAddressRef, scrollToWalletAddress } = useScrollToWalletAddress() const [isLoading, setIsLoading] = useState(false) const [isLoadingScript, setIsLoadingScript] = useState(false) @@ -109,6 +118,8 @@ export default function Banner() { loadState(OP_WALLET_ADDRESS) persistState() + loadBannerWallet() + persistBannerWallet() return () => { unsubscribeStorage() @@ -121,8 +132,8 @@ export default function Banner() { }) const handleSave = async (action: 'save-success' | 'script') => { - if (!snap.isWalletConnected) { - toolActions.setConnectWalletStep('error') + if (!walletSnap.isWalletConnected) { + walletActions.setConnectWalletStep('error') scrollToWalletAddress() return } @@ -167,7 +178,7 @@ export default function Banner() { { number: 1, label: 'Connect', - status: snap.walletConnectStep, + status: walletSnap.walletConnectStep, }, { number: 2, @@ -183,9 +194,13 @@ export default function Banner() { + -
diff --git a/frontend/app/routes/offerwall.tsx b/frontend/app/routes/offerwall.tsx index 60bc33f8..548412c7 100644 --- a/frontend/app/routes/offerwall.tsx +++ b/frontend/app/routes/offerwall.tsx @@ -27,11 +27,16 @@ import { useGrantResponseHandler } from '~/hooks/useGrantResponseHandler' import { usePathTracker } from '~/hooks/usePathTracker' import { useSaveProfile } from '~/hooks/useSaveProfile' import { useScrollToWalletAddress } from '~/hooks/useScrollToWalletAddress' +import { useToolWallet } from '~/hooks/useToolWallet' import { actions, hydrateProfilesFromStorage, hydrateSnapshotsFromStorage, + loadOfferwallWallet, offerwall, + offerwallWallet, + offerwallWalletActions, + persistOfferwallWallet, subscribeProfilesToStorage, subscribeProfilesToUpdates, } from '~/stores/offerwall-store' @@ -82,9 +87,13 @@ export async function loader({ request, context }: LoaderFunctionArgs) { export default function Offerwall() { const snap = useSnapshot(toolState) + const [walletSnap, walletActions] = useToolWallet({ + wallet: offerwallWallet, + actions: offerwallWalletActions, + }) const offerwallSnap = useSnapshot(offerwall) const navigate = useNavigate() - const { save, saveLastAction } = useSaveProfile() + const { save, saveLastAction } = useSaveProfile(offerwallWallet) const { walletAddressRef, scrollToWalletAddress } = useScrollToWalletAddress() const [isLoading, setIsLoading] = useState(false) const [isLoadingScript, setIsLoadingScript] = useState(false) @@ -101,6 +110,8 @@ export default function Offerwall() { loadState(OP_WALLET_ADDRESS) persistState() + loadOfferwallWallet() + persistOfferwallWallet() return () => { unsubscribeStorage() @@ -113,8 +124,8 @@ export default function Offerwall() { }) const handleSave = async (action: 'save-success' | 'script') => { - if (!snap.isWalletConnected) { - toolActions.setConnectWalletStep('error') + if (!walletSnap.isWalletConnected) { + walletActions.setConnectWalletStep('error') scrollToWalletAddress() return } @@ -156,7 +167,7 @@ export default function Offerwall() { { number: 1, label: 'Connect', - status: snap.walletConnectStep, + status: walletSnap.walletConnectStep, }, { number: 2, @@ -172,9 +183,13 @@ export default function Offerwall() { + -
diff --git a/frontend/app/routes/widget.tsx b/frontend/app/routes/widget.tsx index f883ef42..503ae20c 100644 --- a/frontend/app/routes/widget.tsx +++ b/frontend/app/routes/widget.tsx @@ -25,6 +25,7 @@ import { useGrantResponseHandler } from '~/hooks/useGrantResponseHandler' import { usePathTracker } from '~/hooks/usePathTracker' import { useSaveProfile } from '~/hooks/useSaveProfile' import { useScrollToWalletAddress } from '~/hooks/useScrollToWalletAddress' +import { useToolWallet } from '~/hooks/useToolWallet' import { toolState, toolActions, @@ -37,8 +38,12 @@ import { widget, hydrateProfilesFromStorage, hydrateSnapshotsFromStorage, + loadWidgetWallet, + persistWidgetWallet, subscribeProfilesToStorage, subscribeProfilesToUpdates, + widgetWallet, + widgetWalletActions, } from '~/stores/widget-store' import { commitSession, getSession } from '~/utils/session.server.js' @@ -84,7 +89,11 @@ export default function Widget() { const widgetSnap = useSnapshot(widget) const navigate = useNavigate() const uiActions = useUIActions() - const { save, saveLastAction } = useSaveProfile() + const [walletSnap, walletActions] = useToolWallet({ + wallet: widgetWallet, + actions: widgetWalletActions, + }) + const { save, saveLastAction } = useSaveProfile(widgetWallet) const { walletAddressRef, scrollToWalletAddress } = useScrollToWalletAddress() const [isLoading, setIsLoading] = useState(false) const [isLoadingScript, setIsLoadingScript] = useState(false) @@ -102,6 +111,8 @@ export default function Widget() { loadState(OP_WALLET_ADDRESS) persistState() + loadWidgetWallet() + persistWidgetWallet() return () => { unsubscribeStorage() @@ -114,8 +125,8 @@ export default function Widget() { }) const handleSave = async (action: 'save-success' | 'script') => { - if (!snap.isWalletConnected) { - toolActions.setConnectWalletStep('error') + if (!walletSnap.isWalletConnected) { + walletActions.setConnectWalletStep('error') scrollToWalletAddress() return } @@ -154,7 +165,7 @@ export default function Widget() { { number: 1, label: 'Connect', - status: snap.walletConnectStep, + status: walletSnap.walletConnectStep, }, { number: 2, @@ -170,9 +181,13 @@ export default function Widget() { + -
diff --git a/frontend/app/stores/banner-store.ts b/frontend/app/stores/banner-store.ts index 6adc00c7..3a5a32d0 100644 --- a/frontend/app/stores/banner-store.ts +++ b/frontend/app/stores/banner-store.ts @@ -10,11 +10,19 @@ import { TOOL_BANNER, } from '@shared/types' import type { SaveResult } from '~/lib/types' +import { createWalletStore } from '~/stores/wallet-store' import { getToolProfiles, saveToolProfile } from '~/utils/profile-api' import { patchProxy, splitProfileProperties } from '~/utils/utils.storage' import { createToolStoreUtils, getStorageKeys } from '~/utils/utilts.store' import { toolState } from './toolStore' +export const { + wallet: bannerWallet, + load: loadBannerWallet, + persist: persistBannerWallet, + actions: bannerWalletActions, +} = createWalletStore(TOOL_BANNER) + export type BannerStore = ReturnType const createProfileStoreBanner = (profileName: string) => @@ -79,8 +87,7 @@ export const actions = { }) }, async getProfiles(tool: typeof TOOL_BANNER): Promise> { - const { walletAddress } = toolState - return await getToolProfiles(walletAddress, tool) + return await getToolProfiles(bannerWallet.walletAddress, tool) }, resetProfiles() { bannerStoreUtils.removeProfilesFromStorage() @@ -102,8 +109,12 @@ export const actions = { }, async saveProfile(): Promise { const profile = snapshot(banner.profile) - const { walletAddress, activeTab } = toolState - return await saveToolProfile(walletAddress, TOOL_BANNER, profile, activeTab) + return await saveToolProfile( + bannerWallet.walletAddress, + TOOL_BANNER, + profile, + toolState.activeTab, + ) }, commitProfile() { const profile = snapshot(banner.profile) diff --git a/frontend/app/stores/offerwall-store.ts b/frontend/app/stores/offerwall-store.ts index bd05a20a..0a302f4d 100644 --- a/frontend/app/stores/offerwall-store.ts +++ b/frontend/app/stores/offerwall-store.ts @@ -10,11 +10,19 @@ import { TOOL_OFFERWALL, } from '@shared/types' import type { SaveResult } from '~/lib/types' +import { createWalletStore } from '~/stores/wallet-store' import { getToolProfiles, saveToolProfile } from '~/utils/profile-api' import { patchProxy } from '~/utils/utils.storage' import { createToolStoreUtils, getStorageKeys } from '~/utils/utilts.store' import { toolState } from './toolStore' +export const { + wallet: offerwallWallet, + load: loadOfferwallWallet, + persist: persistOfferwallWallet, + actions: offerwallWalletActions, +} = createWalletStore(TOOL_OFFERWALL) + export type OfferwallStore = ReturnType const createProfileStoreOfferwall = (profileName: string) => @@ -81,8 +89,7 @@ export const actions = { async getProfiles( tool: typeof TOOL_OFFERWALL, ): Promise> { - const { walletAddress } = toolState - return await getToolProfiles(walletAddress, tool) + return await getToolProfiles(offerwallWallet.walletAddress, tool) }, resetProfiles() { offerwallStoreUtils.removeProfilesFromStorage() @@ -103,12 +110,11 @@ export const actions = { }, async saveProfile(): Promise { const profile = snapshot(offerwall.profile) - const { walletAddress, activeTab } = toolState return await saveToolProfile( - walletAddress, + offerwallWallet.walletAddress, TOOL_OFFERWALL, profile, - activeTab, + toolState.activeTab, ) }, commitProfile() { diff --git a/frontend/app/stores/toolStore.ts b/frontend/app/stores/toolStore.ts index 7383bf6e..5946426e 100644 --- a/frontend/app/stores/toolStore.ts +++ b/frontend/app/stores/toolStore.ts @@ -9,12 +9,10 @@ import { TOOL_OFFERWALL, PROFILE_A, } from '@shared/types' -import type { StepStatus } from '~/components/redesign/components/StepsIndicator' import { actions as bannerActions } from '~/stores/banner-store' import { actions as offerwallActions } from '~/stores/offerwall-store' import { actions as widgetActions } from '~/stores/widget-store' import { omit } from '~/utils/utils.storage' -import { captureSnapshotsToStorage } from './banner-store' const STORAGE_KEY = 'valtio-store' @@ -25,6 +23,8 @@ const EXCLUDED_FROM_STORAGE = new Set([ 'cdnUrl', ]) +export type StepStatus = 'unfilled' | 'filled' | 'error' + export const toolState = proxy({ activeTab: PROFILE_A as ProfileId, currentToolType: 'unknown' as Tool, @@ -43,14 +43,7 @@ export const toolState = proxy({ // environment variables opWallet: '', - // wallet and connection state - walletAddress: '', - walletAddressId: '', - grantResponse: '', - isGrantAccepted: false, - isWalletConnected: false, - hasRemoteConfigs: false, - walletConnectStep: 'unfilled' as StepStatus, + // customization steps state buildStep: 'unfilled' as StepStatus, }) @@ -94,9 +87,20 @@ export const toolActions = { break } }, - resetProfiles() { - for (const actions of [bannerActions, widgetActions, offerwallActions]) { - actions.resetProfiles() + resetToolProfiles() { + switch (toolState.currentToolType) { + case TOOL_BANNER: + bannerActions.resetProfiles() + break + case TOOL_WIDGET: + widgetActions.resetProfiles() + break + case TOOL_OFFERWALL: + offerwallActions.resetProfiles() + break + + default: + break } }, setCurrentToolType: (toolType: Tool) => { @@ -111,37 +115,9 @@ export const toolActions = { toolState.loadingState = state }, - setWalletConnected: (connected: boolean) => { - toolState.isWalletConnected = connected - if (connected) { - toolState.walletConnectStep = 'filled' - } else { - toolState.walletConnectStep = 'unfilled' - } - - captureSnapshotsToStorage() - }, - - setConnectWalletStep: (step: StepStatus) => { - toolState.walletConnectStep = step - }, - - setBuildCompleteStep: (step: StepStatus) => { + setBuildCompleteStep(step: StepStatus) { toolState.buildStep = step }, - setWalletAddress: (walletAddress: string) => { - toolState.walletAddress = walletAddress - }, - setWalletAddressId: (walletAddressId: string) => { - toolState.walletAddressId = walletAddressId - }, - setHasRemoteConfigs: (hasRemoteConfigs: boolean) => { - toolState.hasRemoteConfigs = hasRemoteConfigs - }, - setGrantResponse: (grantResponse: string, isGrantAccepted: boolean) => { - toolState.grantResponse = grantResponse - toolState.isGrantAccepted = isGrantAccepted - }, } /** Load from localStorage on init, remove storage if invalid */ @@ -158,8 +134,7 @@ export function loadState(OP_WALLET_ADDRESS: Env['OP_WALLET_ADDRESS']) { Object.keys(parsed).every((key) => key in toolState) if (validKeys) { - const loadedData = parsedStorageData(parsed) - Object.assign(toolState, loadedData) + Object.assign(toolState, omit(parsed, EXCLUDED_FROM_STORAGE)) } else { throw new Error('saved configuration not valid') } @@ -173,15 +148,7 @@ export function persistState() { subscribe(toolState, () => { localStorage.setItem( STORAGE_KEY, - JSON.stringify(createStorageState(toolState)), + JSON.stringify(omit(toolState, EXCLUDED_FROM_STORAGE)), ) }) } - -function createStorageState(state: typeof toolState) { - return omit(state, EXCLUDED_FROM_STORAGE) -} - -function parsedStorageData(parsed: Record) { - return omit(parsed, EXCLUDED_FROM_STORAGE) -} diff --git a/frontend/app/stores/wallet-store.ts b/frontend/app/stores/wallet-store.ts new file mode 100644 index 00000000..5d23c837 --- /dev/null +++ b/frontend/app/stores/wallet-store.ts @@ -0,0 +1,79 @@ +import { proxy, snapshot, subscribe } from 'valtio' +import type { Tool } from '@shared/types' +import type { StepStatus } from './toolStore' + +export type WalletStore = ReturnType + +function getStorageKey(tool: Tool) { + return `wmt-${tool}-wallet` +} + +function createWalletState() { + return { + walletAddress: '', + walletAddressId: '', + isWalletConnected: false, + hasRemoteConfigs: false, + walletConnectStep: 'unfilled' as StepStatus, + } +} + +function createWalletActions(wallet: WalletStore, storageKey: string) { + return { + setWalletConnected(connected: boolean) { + wallet.isWalletConnected = connected + wallet.walletConnectStep = connected ? 'filled' : 'unfilled' + }, + setConnectWalletStep(step: StepStatus) { + wallet.walletConnectStep = step + }, + setWalletAddress(address: string) { + wallet.walletAddress = address + }, + setWalletAddressId(id: string) { + wallet.walletAddressId = id + }, + setHasRemoteConfigs(has: boolean) { + wallet.hasRemoteConfigs = has + }, + clearWalletStorage() { + localStorage.removeItem(storageKey) + }, + } +} + +export type WalletActions = ReturnType + +export function createWalletStore(tool: Tool) { + const wallet = proxy(createWalletState()) + const storageKey = getStorageKey(tool) + const actions = createWalletActions(wallet, storageKey) + + function load() { + try { + const saved = localStorage.getItem(storageKey) + if (!saved) return + + const isValid = (parsed: WalletStore) => + typeof parsed === 'object' && + Object.keys(parsed).every((key) => key in parsed) + + const parsed = JSON.parse(saved) + if (!isValid(parsed)) { + throw new Error('Failed to parse') + } + + Object.assign(wallet, parsed) + } catch { + localStorage.removeItem(storageKey) + } + } + + function persist() { + subscribe(wallet, () => { + localStorage.setItem(storageKey, JSON.stringify(snapshot(wallet))) + }) + } + + return { wallet, actions, load, persist } +} diff --git a/frontend/app/stores/widget-store.ts b/frontend/app/stores/widget-store.ts index 48797b03..3ef857ce 100644 --- a/frontend/app/stores/widget-store.ts +++ b/frontend/app/stores/widget-store.ts @@ -10,11 +10,19 @@ import { TOOL_WIDGET, } from '@shared/types' import type { SaveResult } from '~/lib/types' +import { createWalletStore } from '~/stores/wallet-store' import { getToolProfiles, saveToolProfile } from '~/utils/profile-api' import { patchProxy, splitProfileProperties } from '~/utils/utils.storage' import { createToolStoreUtils, getStorageKeys } from '~/utils/utilts.store' import { toolState } from './toolStore' +export const { + wallet: widgetWallet, + load: loadWidgetWallet, + persist: persistWidgetWallet, + actions: widgetWalletActions, +} = createWalletStore(TOOL_WIDGET) + export type WidgetStore = ReturnType const createProfileStoreWidget = (profileName: string) => @@ -78,8 +86,7 @@ export const actions = { }) }, async getProfiles(tool: typeof TOOL_WIDGET): Promise> { - const { walletAddress } = toolState - return await getToolProfiles(walletAddress, tool) + return await getToolProfiles(widgetWallet.walletAddress, tool) }, resetProfiles() { widgetStoreUtils.removeProfilesFromStorage() @@ -101,8 +108,12 @@ export const actions = { }, async saveProfile(): Promise { const profile = snapshot(widget.profile) - const { walletAddress, activeTab } = toolState - return await saveToolProfile(walletAddress, TOOL_WIDGET, profile, activeTab) + return await saveToolProfile( + widgetWallet.walletAddress, + TOOL_WIDGET, + profile, + toolState.activeTab, + ) }, commitProfile() { const profile = snapshot(widget.profile)