diff --git a/.env b/.env index a6c6a051..9f63d72a 100644 --- a/.env +++ b/.env @@ -3,6 +3,8 @@ VITE_DEFAULT_DRIVES_URL=https://apps.powerhouse.io/staging/makerdao/switchboard/ VITE_BASE_HREF=/ ASSET_URL=/ +VITE_APP_REQUIRES_HARD_REFRESH=true + ######## RENOWN CONFIG ######## # Renown instance to use for login # VITE_RENOWN_URL=http://localhost:3000 diff --git a/.github/workflows/build-and-deploy-arbitrum-prod.yaml b/.github/workflows/build-and-deploy-arbitrum-prod.yaml index 01fdd227..25ba2aea 100644 --- a/.github/workflows/build-and-deploy-arbitrum-prod.yaml +++ b/.github/workflows/build-and-deploy-arbitrum-prod.yaml @@ -36,6 +36,7 @@ jobs: --build-arg VITE_DISABLE_DELETE_CLOUD_DRIVES=true --build-arg VITE_DISABLE_DELETE_LOCAL_DRIVES=true --build-arg VITE_LOCAL_DRIVES_ENABLED=false + --build-arg VITE_SEARCH_BAR_ENABLED=false --build-arg VITE_DEFAULT_DRIVES_URL=https://apps.powerhouse.io/arbitrum/switchboard/d/arbitrum --build-arg VITE_RENOWN_CHAIN_ID=42161 --build-arg VITE_HIDE_DOCUMENT_MODEL_SELECTION_SETTINGS=true diff --git a/.github/workflows/build-and-deploy-arbitrum-staging.yaml b/.github/workflows/build-and-deploy-arbitrum-staging.yaml index df1647e7..fe0f12ff 100644 --- a/.github/workflows/build-and-deploy-arbitrum-staging.yaml +++ b/.github/workflows/build-and-deploy-arbitrum-staging.yaml @@ -36,6 +36,7 @@ jobs: --build-arg VITE_LOCAL_DRIVES_ENABLED=false --build-arg VITE_DEFAULT_DRIVES_URL=https://apps.powerhouse.io/staging/arbitrum/switchboard/d/arbitrum --build-arg VITE_RENOWN_CHAIN_ID=42161 + --build-arg VITE_SEARCH_BAR_ENABLED=false --build-arg VITE_SENTRY_DSN=${{ secrets.SENTRY_DSN }} --build-arg VITE_SENTRY_ENV=${{ secrets.SENTRY_ENV }} --build-arg VITE_ARBITRUM_ALLOW_LIST=${{secrets.VITE_ARBITRUM_ALLOW_LIST}} diff --git a/.github/workflows/build-and-deploy-makerdao-prod.yaml b/.github/workflows/build-and-deploy-makerdao-prod.yaml index 7dc43ab2..09322a8c 100644 --- a/.github/workflows/build-and-deploy-makerdao-prod.yaml +++ b/.github/workflows/build-and-deploy-makerdao-prod.yaml @@ -28,6 +28,7 @@ jobs: --build-arg VITE_ROUTER_BASENAME=/makerdao/connect --build-arg VITE_SENTRY_DSN=${{ secrets.SENTRY_DSN }} --build-arg VITE_SENTRY_ENV=${{ secrets.SENTRY_ENV }} + --build-arg VITE_SEARCH_BAR_ENABLED=false --build-arg VITE_DEFAULT_DRIVES_URL=https://apps.powerhouse.io/makerdao/switchboard/d/b443d823-3e4d-4e60-ae44-45edafd5f632 --build-arg VITE_RWA_ALLOW_LIST=${{secrets.VITE_RWA_ALLOW_LIST}} process_type: web \ No newline at end of file diff --git a/.github/workflows/build-and-deploy-makerdao-staging.yaml b/.github/workflows/build-and-deploy-makerdao-staging.yaml index bc50acb4..b6c5e93f 100644 --- a/.github/workflows/build-and-deploy-makerdao-staging.yaml +++ b/.github/workflows/build-and-deploy-makerdao-staging.yaml @@ -28,6 +28,7 @@ jobs: --build-arg VITE_ROUTER_BASENAME=/staging/makerdao/connect --build-arg VITE_SENTRY_DSN=${{ secrets.SENTRY_DSN }} --build-arg VITE_SENTRY_ENV=${{ secrets.SENTRY_ENV }} + --build-arg VITE_SEARCH_BAR_ENABLED=false --build-arg VITE_DEFAULT_DRIVES_URL=https://apps.powerhouse.io/staging/makerdao/switchboard/d/280dd289-ec51-40f0-adad-c154967fc2b2 --build-arg VITE_RWA_ALLOW_LIST=${{secrets.VITE_RWA_ALLOW_LIST}} process_type: web \ No newline at end of file diff --git a/.github/workflows/build-and-deploy-powerhouse-develop.yaml b/.github/workflows/build-and-deploy-powerhouse-develop.yaml index 0e006bfd..e45b54d1 100644 --- a/.github/workflows/build-and-deploy-powerhouse-develop.yaml +++ b/.github/workflows/build-and-deploy-powerhouse-develop.yaml @@ -27,4 +27,5 @@ jobs: --build-arg VITE_BASE_HREF=/develop/powerhouse/connect/ --build-arg VITE_ROUTER_BASENAME=/develop/powerhouse/connect --build-arg VITE_DEFAULT_DRIVES_URL=https://apps.powerhouse.io/develop/powerhouse/switchboard/d/core-dev + --build-arg VITE_SEARCH_BAR_ENABLED=false process_type: web \ No newline at end of file diff --git a/.github/workflows/build-and-deploy-powerhouse-prod.yaml b/.github/workflows/build-and-deploy-powerhouse-prod.yaml index 3178de26..a11290f6 100644 --- a/.github/workflows/build-and-deploy-powerhouse-prod.yaml +++ b/.github/workflows/build-and-deploy-powerhouse-prod.yaml @@ -29,4 +29,5 @@ jobs: --build-arg VITE_SENTRY_DSN=${{ secrets.SENTRY_DSN }} --build-arg VITE_SENTRY_ENV=${{ secrets.SENTRY_ENV }} --build-arg VITE_DEFAULT_DRIVES_URL=https://apps.powerhouse.io/powerhouse/switchboard/d/powerhouse + --build-arg VITE_SEARCH_BAR_ENABLED=false process_type: web \ No newline at end of file diff --git a/.github/workflows/build-and-deploy-powerhouse-staging.yaml b/.github/workflows/build-and-deploy-powerhouse-staging.yaml index be64c577..cffca31d 100644 --- a/.github/workflows/build-and-deploy-powerhouse-staging.yaml +++ b/.github/workflows/build-and-deploy-powerhouse-staging.yaml @@ -29,4 +29,5 @@ jobs: --build-arg VITE_SENTRY_DSN=${{ secrets.SENTRY_DSN }} --build-arg VITE_SENTRY_ENV=${{ secrets.SENTRY_ENV }} --build-arg VITE_DEFAULT_DRIVES_URL=https://apps.powerhouse.io/staging/powerhouse/switchboard/d/powerhouse + --build-arg VITE_SEARCH_BAR_ENABLED=false process_type: web \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e1f848fe..b5261bba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,6 @@ ### Features -<<<<<<< HEAD * add animated loader for editors ([0ef0cc5](https://github.com/powerhouse-inc/document-model-electron/commit/0ef0cc587bb04d3fa65a4fd133c4c42f5998c92c)) * add comments ([a5e81b2](https://github.com/powerhouse-inc/document-model-electron/commit/a5e81b22b1d634548a6323ef5b4a5efa2b0a87ec)) * add file icon logic ([3ba0aae](https://github.com/powerhouse-inc/document-model-electron/commit/3ba0aae6d692a2368b19e1defeb655d582c5c220)) @@ -119,11 +118,6 @@ * renown env var ([0b6bb86](https://github.com/powerhouse-inc/document-model-electron/commit/0b6bb86c10a525462f45db82a1cd5607b16a88be)) # [1.0.0-next.4](https://github.com/powerhouse-inc/document-model-electron/compare/v1.0.0-next.3...v1.0.0-next.4) (2024-07-01) -======= -* updated document model deps ([efc3814](https://github.com/powerhouse-inc/document-model-electron/commit/efc38147792002975a87e433d2cf7f4091534b5e)) - -# [1.0.0-dev.55](https://github.com/powerhouse-inc/document-model-electron/compare/v1.0.0-dev.54...v1.0.0-dev.55) (2024-08-02) ->>>>>>> develop ### Features diff --git a/Dockerfile b/Dockerfile index 0ab6e420..7ebc9f08 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,9 +14,6 @@ ENV VITE_RENOWN_NETWORK_ID=${VITE_RENOWN_NETWORK_ID} ARG VITE_RENOWN_CHAIN_ID=1 ENV VITE_RENOWN_CHAIN_ID=${VITE_RENOWN_CHAIN_ID} -ARG VITE_RENOWN_URL="https://renown.id" -ENV VITE_RENOWN_URL=${VITE_RENOWN_URL} - ARG VITE_SENTRY_DSN="" ENV VITE_SENTRY_DSN=${VITE_SENTRY_DSN} diff --git a/package-lock.json b/package-lock.json index f41f4936..7416bdc2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,21 @@ { "name": "@powerhousedao/connect", - "version": "1.0.0-next.6", + "version": "1.0.0-next.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@powerhousedao/connect", - "version": "1.0.0-next.6", + "version": "1.0.0-next.7", "license": "AGPL-3.0-only", "dependencies": { - "@powerhousedao/design-system": "1.0.0-alpha.159", + "@powerhousedao/design-system": "1.0.0-alpha.160", "@sentry/react": "^7.109.0", "@tanstack/react-virtual": "^3.8.1", "did-key-creator": "^1.2.0", "document-drive": "^1.0.0-alpha.88", "document-model": "1.7.0", - "document-model-libs": "^1.82.0", + "document-model-libs": "^1.83.0", "electron-is-dev": "^3.0.1", "electron-squirrel-startup": "^1.0.0", "electron-store": "^8.1.0", @@ -6821,9 +6821,9 @@ } }, "node_modules/@powerhousedao/design-system": { - "version": "1.0.0-alpha.159", - "resolved": "https://registry.npmjs.org/@powerhousedao/design-system/-/design-system-1.0.0-alpha.159.tgz", - "integrity": "sha512-h/DedTdv7SCDziPuaoKuYuY7sLnEUAKWyQYKJT/hE1Ih6qCu4M1AiLucJuQxPMvbX8LUiCzn2A4fCm03SMLQ4Q==", + "version": "1.0.0-alpha.160", + "resolved": "https://registry.npmjs.org/@powerhousedao/design-system/-/design-system-1.0.0-alpha.160.tgz", + "integrity": "sha512-QL/tyyXnguac5vgVOKPC7NzQ9ot/PaMyJ9EJeYBDx8KufmuYFPUM2pRx9Df/5Q5yQ+XK46mOO2HsSVuVYWfgdw==", "dependencies": { "@internationalized/date": "^3.5.1", "@radix-ui/react-dialog": "^1.0.5", @@ -15642,9 +15642,9 @@ } }, "node_modules/document-model-libs": { - "version": "1.82.0", - "resolved": "https://registry.npmjs.org/document-model-libs/-/document-model-libs-1.82.0.tgz", - "integrity": "sha512-718kbi3sdWWlOWGmXaujhk0hOEJE/omLZ7wT+DaSkw4+mlnNz4ItBAAo3GoHRWcqucRDsF3wEm8uj5keGpONDA==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/document-model-libs/-/document-model-libs-1.83.0.tgz", + "integrity": "sha512-GbgsUgW47+K051vu63bazdY05GBKTLNDNnasjoZ9lcLMbsY+FycknMGRbscuvCneNFQk131GQWPuzxD2WxobAw==", "dependencies": { "@acaldas/graphql-codegen-typescript-validation-schema": "^0.12.3", "@graphql-codegen/core": "^4.0.2", diff --git a/package.json b/package.json index f8579e9a..df092b4e 100644 --- a/package.json +++ b/package.json @@ -89,13 +89,13 @@ "xvfb-maybe": "^0.2.1" }, "dependencies": { - "@powerhousedao/design-system": "1.0.0-alpha.159", + "@powerhousedao/design-system": "1.0.0-alpha.160", "@sentry/react": "^7.109.0", "@tanstack/react-virtual": "^3.8.1", "did-key-creator": "^1.2.0", "document-drive": "^1.0.0-alpha.88", "document-model": "1.7.0", - "document-model-libs": "^1.82.0", + "document-model-libs": "^1.83.0", "electron-is-dev": "^3.0.1", "electron-squirrel-startup": "^1.0.0", "electron-store": "^8.1.0", diff --git a/public/service-worker.js b/public/service-worker.js new file mode 100644 index 00000000..97b1e910 --- /dev/null +++ b/public/service-worker.js @@ -0,0 +1,55 @@ +const VERSION_CACHE = 'version-cache'; +const VERSION_KEY = 'app-version'; + +self.addEventListener('install', event => { + self.skipWaiting(); +}); + +self.addEventListener('activate', event => { + event.waitUntil(self.clients.claim()); +}); + + +self.addEventListener('message', async event => { + if (event.data && event.data.type === 'SET_APP_VERSION') { + const cache = await caches.open(VERSION_CACHE); + await cache.put(VERSION_KEY, new Response(event.data.version)); + } + }); + +async function checkForUpdates() { + try { + const response = await fetch('/version.json', { cache: 'no-store' }); + const newVersion = await response.json(); + const cache = await caches.open(VERSION_CACHE); + const cachedResponse = await cache.match(VERSION_KEY); + + let currentVersion = ''; + + if (cachedResponse) { + currentVersion = await cachedResponse.text(); + } + + if (currentVersion === '') { + // Initial cache + await cache.put(VERSION_KEY, new Response(newVersion.version)); + } else if (currentVersion !== newVersion.version) { + // New version detected + console.log('Current version:', currentVersion); + console.log('New version:', newVersion.version); + + const clients = await self.clients.matchAll(); + clients.forEach(client => { + client.postMessage({ type: 'NEW_VERSION_AVAILABLE', requiresHardRefresh: newVersion.requiresHardRefresh }); + }); + + // Update the stored version + await cache.put(VERSION_KEY, new Response(newVersion.version)); + } + } catch (error) { + console.error('Error checking version:', error); + } +} + +// Check for updates every minute +setInterval(checkForUpdates, 60 * 1000); // 60 seconds diff --git a/src/components/editors.tsx b/src/components/editors.tsx index 2386188e..c8d220b4 100644 --- a/src/components/editors.tsx +++ b/src/components/editors.tsx @@ -2,16 +2,11 @@ import { FILE, RevisionHistory } from '@powerhousedao/design-system'; import { Action, ActionErrorCallback, - ActionSigner, BaseAction, Document, EditorContext, Operation, - OperationSignatureContext, - Reducer, - User, actions, - utils, } from 'document-model/document'; import { useAtomValue } from 'jotai'; import { Suspense, useEffect, useMemo, useState } from 'react'; @@ -28,6 +23,7 @@ import { DocumentDispatchCallback, useDocumentDispatch, } from 'src/utils/document-model'; +import { addActionContext, signOperation } from 'src/utils/signature'; import Button from './button'; import { EditorLoader } from './editor-loader'; @@ -43,43 +39,6 @@ export type EditorProps< onChange?: (document: Document) => void; }; -const signOperation = async ( - operation: Operation, - sign: (data: Uint8Array) => Promise, - documentId: string, - document: Document, - reducer?: Reducer, - user?: User, -) => { - if (!user) return operation; - if (!operation.context) return operation; - if (!operation.context.signer) return operation; - if (!reducer) { - logger.error( - `Document model '${document.documentType}' does not have a reducer`, - ); - return operation; - } - - const context: Omit< - OperationSignatureContext, - 'operation' | 'previousStateHash' - > = { - documentId, - signer: operation.context.signer, - }; - - const signedOperation = await utils.buildSignedOperation( - operation, - reducer, - document, - context, - sign, - ); - - return signedOperation; -}; - export function DocumentEditor(props: EditorProps) { const { selectedNode, @@ -118,29 +77,6 @@ export function DocumentEditor(props: EditorProps) { const canRedo = !!document?.clipboard.length; useUndoRedoShortcuts({ undo, redo, canUndo, canRedo }); - function addActionContext(action: Action): Action { - if (!user) return action; - const signer: ActionSigner = { - app: { - name: 'Connect', - key: connectDid || '', - }, - user: { - address: user.address, - networkId: user.networkId, - chainId: user.chainId, - }, - signatures: [], - }; - - return { - ...action, - context: { - signer, - }, - }; - } - function dispatch( action: BaseAction | Action, onErrorCallback?: ActionErrorCallback, @@ -168,7 +104,11 @@ export function DocumentEditor(props: EditorProps) { .catch(logger.error); }; - _dispatch(addActionContext(action), callback, onErrorCallback); + _dispatch( + addActionContext(action, connectDid, user), + callback, + onErrorCallback, + ); } useEffect(() => { diff --git a/src/components/modal/modals/DebugSettingsModal.tsx b/src/components/modal/modals/DebugSettingsModal.tsx index 48a5197f..e3c2a698 100644 --- a/src/components/modal/modals/DebugSettingsModal.tsx +++ b/src/components/modal/modals/DebugSettingsModal.tsx @@ -5,8 +5,9 @@ import { Icon, Modal, } from '@powerhousedao/design-system'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useDocumentDriveServer } from 'src/hooks/useDocumentDriveServer'; +import serviceWorkerManager from 'src/utils/registerServiceWorker'; import { v4 as uuid } from 'uuid'; export interface DebugSettingsModalProps { @@ -26,6 +27,11 @@ export const DebugSettingsModal: React.FC = props => { console.log('autoRegisterPullResponder', autoRegisterPullResponder); + const [appVersion, setAppVersion] = useState(''); + const [serviceWorkerDebugMode, setServiceWorkerDebugMode] = useState({ + label: serviceWorkerManager.debug ? 'Enabled' : 'Disabled', + value: serviceWorkerManager.debug, + }); const [selectedDrive, setSelectedDrive] = useState(); const [selectedDriveTrigger, setSelectedDriveTrigger] = useState(null); @@ -41,6 +47,10 @@ export const DebugSettingsModal: React.FC = props => { registerNewPullResponderTrigger, } = useDocumentDriveServer(); + useEffect(() => { + serviceWorkerManager.setDebug(serviceWorkerDebugMode.value); + }, [serviceWorkerDebugMode]); + console.log('documentDrives', documentDrives); console.log('selectedDrive', selectedDrive); @@ -114,6 +124,13 @@ export const DebugSettingsModal: React.FC = props => {
+ + + App Version: {process.env.APP_VERSION} + +
+ +
Drive Tools:
@@ -240,6 +257,73 @@ export const DebugSettingsModal: React.FC = props => { + +
+ + Service Worker Tools: +
+ +
+
+ + { + setServiceWorkerDebugMode( + value as typeof serviceWorkerDebugMode, + ); + }} + value={serviceWorkerDebugMode} + options={[ + { label: 'Enabled', value: true }, + { label: 'Disabled', value: false }, + ]} + /> +
+
+ +
+
+ + + Version: +
+ } + value={appVersion} + onChange={element => + setAppVersion(element.target.value) + } + /> +
+
+ +
+ ); diff --git a/src/hooks/useDocumentDriveServer.ts b/src/hooks/useDocumentDriveServer.ts index 80f2df79..4530e49a 100644 --- a/src/hooks/useDocumentDriveServer.ts +++ b/src/hooks/useDocumentDriveServer.ts @@ -15,6 +15,8 @@ import { import { isDocumentDrive } from 'document-drive/utils'; import { DocumentDriveAction, + DocumentDriveLocalState, + DocumentDriveState, Trigger, actions, utils as documentDriveUtils, @@ -27,8 +29,11 @@ import { Document, Operation, utils } from 'document-model/document'; import { useCallback, useMemo } from 'react'; import { logger } from 'src/services/logger'; import { useGetDocumentModel } from 'src/store/document-model'; +import { useUser } from 'src/store/user'; import { DefaultDocumentDriveServer } from 'src/utils/document-drive-server'; import { loadFile } from 'src/utils/file'; +import { addActionContext, signOperation } from 'src/utils/signature'; +import { useConnectCrypto, useConnectDid } from './useConnectCrypto'; import { useDocumentDrives } from './useDocumentDrives'; import { useUserPermissions } from './useUserPermissions'; @@ -45,6 +50,9 @@ export function useDocumentDriveServer( ) { const { isAllowedToCreateDocuments, isAllowedToEditDocuments } = useUserPermissions(); + const user = useUser(); + const connectDid = useConnectDid(); + const { sign } = useConnectCrypto(); if (!server) { throw new Error('Invalid Document Drive Server'); @@ -81,7 +89,9 @@ export function useDocumentDriveServer( throw new Error(`Drive with id ${driveId} not found`); } - drive = reducer(drive, action); + const driveCopy = { ...drive }; + + drive = reducer(drive, addActionContext(action, connectDid, user)); const scope = action.scope ?? 'global'; const operations = drive.operations[scope]; const operation = operations.findLast( @@ -91,10 +101,24 @@ export function useDocumentDriveServer( throw new Error('There was an error applying the operation'); } + // sign operation + const signedOperation = await signOperation< + DocumentDriveState, + DocumentDriveAction, + DocumentDriveLocalState + >( + operation as Operation, + sign, + driveId, + driveCopy, + reducer, + user, + ); + try { const result = await server.queueDriveOperation( driveId, - operation, + signedOperation, ); if (result.status !== 'SUCCESS') { @@ -116,7 +140,7 @@ export function useDocumentDriveServer( return drive; } }, - [documentDrives, refreshDocumentDrives, server], + [documentDrives, refreshDocumentDrives, server, sign, user, connectDid], ); const addDocument = useCallback( diff --git a/src/renderer.ts b/src/renderer.ts index a00fd9d6..3aab81b3 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -31,9 +31,12 @@ import App from './components/app'; import './i18n'; import './index.css'; import { DocumentEditorDebugTools } from './utils/document-editor-debug-tools'; +import serviceWorkerManager from './utils/registerServiceWorker'; if (import.meta.env.MODE === 'development') { window.documentEditorDebugTools = new DocumentEditorDebugTools(); +} else { + serviceWorkerManager.registerServiceWorker(false); } createRoot(document.getElementById('app')!).render(App); diff --git a/src/services/logger.ts b/src/services/logger.ts index 3387d9c3..4286dea9 100644 --- a/src/services/logger.ts +++ b/src/services/logger.ts @@ -5,6 +5,16 @@ import { ILogger, setLogger } from 'document-drive/logger'; class ConnectLogger implements ILogger { #logger: ILogger = console; + constructor() { + // Bind all methods to the current instance + this.log = this.log.bind(this); + this.info = this.info.bind(this); + this.warn = this.warn.bind(this); + this.error = this.error.bind(this); + this.debug = this.debug.bind(this); + this.trace = this.trace.bind(this); + } + set logger(logger: ILogger) { this.#logger = logger; } diff --git a/src/utils/registerServiceWorker.ts b/src/utils/registerServiceWorker.ts new file mode 100644 index 00000000..45dbd781 --- /dev/null +++ b/src/utils/registerServiceWorker.ts @@ -0,0 +1,110 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +type SET_APP_VERSION_MESSAGE = { + type: 'SET_APP_VERSION'; + version: string; +}; + +export type ServiceWorkerPostMessage = SET_APP_VERSION_MESSAGE; + +type NEW_VERSION_AVAILABLE_MESSAGE = { + type: 'NEW_VERSION_AVAILABLE'; + requiresHardRefresh: boolean; +}; + +export type ServiceWorkerMessage = NEW_VERSION_AVAILABLE_MESSAGE; + +class ServiceWorkerManager { + ready = false; + debug = false; + registration: ServiceWorkerRegistration | null = null; + + constructor(debug = false) { + this.debug = debug; + } + + setDebug(debug: boolean) { + this.debug = debug; + } + + registerServiceWorker(debug = false) { + this.debug = debug; + + if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker + .register('/service-worker.js') + .then(registration => { + // Listen for messages from the service worker + if (this.debug) { + console.log( + 'ServiceWorker registered: ', + registration, + ); + } + + navigator.serviceWorker.addEventListener( + 'message', + event => { + if (this.debug) { + console.log( + 'ServiceWorker message: ', + event, + ); + } + + if ( + event.data && + event.data.type === + 'NEW_VERSION_AVAILABLE' && + event.data.requiresHardRefresh === true + ) { + if (this.debug) { + console.log('New version available'); + } + window.location.reload(); // Reload the page to load the new version + } + }, + ); + + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage({ + type: 'SET_APP_VERSION', + version: process.env.APP_VERSION, + }); + } + + this.ready = true; + this.registration = registration; + }) + .catch(error => { + console.error( + 'ServiceWorker registration failed: ', + error, + ); + }); + }); + } + } + + sendMessage(message: ServiceWorkerPostMessage) { + if (this.ready && this.registration) { + const serviceWorker = + this.registration.active || + this.registration.waiting || + this.registration.installing; + if (serviceWorker) { + if (this.debug) { + console.log('Sending message to service worker: ', message); + } + serviceWorker.postMessage(message); + } else { + console.error('No active service worker found.'); + } + } else { + console.error('Service Worker is not ready yet.'); + } + } +} + +const serviceWorkerManager = new ServiceWorkerManager(); +export default serviceWorkerManager; diff --git a/src/utils/signature.ts b/src/utils/signature.ts new file mode 100644 index 00000000..b12337c1 --- /dev/null +++ b/src/utils/signature.ts @@ -0,0 +1,77 @@ +import { + Action, + ActionSigner, + Document, + Operation, + OperationSignatureContext, + Reducer, + User, + utils, +} from 'document-model/document'; +import { logger } from 'src/services/logger'; +import type { User as RenownUser } from 'src/services/renown/types'; + +export async function signOperation< + State = unknown, + A extends Action = Action, + LocalState = unknown, +>( + operation: Operation, + sign: (data: Uint8Array) => Promise, + documentId: string, + document: Document, + reducer?: Reducer, + user?: User, +) { + if (!user) return operation; + if (!operation.context) return operation; + if (!operation.context.signer) return operation; + if (!reducer) { + logger.error( + `Document model '${document.documentType}' does not have a reducer`, + ); + return operation; + } + + const context: Omit< + OperationSignatureContext, + 'operation' | 'previousStateHash' + > = { + documentId, + signer: operation.context.signer, + }; + + const signedOperation = await utils.buildSignedOperation< + State, + A, + LocalState + >(operation, reducer, document, context, sign); + + return signedOperation; +} + +export function addActionContext( + action: A, + connectDid?: string, + user?: RenownUser, +) { + if (!user) return action; + + const signer: ActionSigner = { + app: { + name: 'Connect', + key: connectDid || '', + }, + user: { + address: user.address, + networkId: user.networkId, + chainId: user.chainId, + }, + signatures: [], + }; + + return { + context: { signer }, + ...action, + }; +} diff --git a/vite.renderer.config.mts b/vite.renderer.config.mts index 364925f8..e0923b67 100644 --- a/vite.renderer.config.mts +++ b/vite.renderer.config.mts @@ -1,16 +1,47 @@ import react from '@vitejs/plugin-react'; +import fs from 'fs'; import jotaiDebugLabel from 'jotai/babel/plugin-debug-label'; import jotaiReactRefresh from 'jotai/babel/plugin-react-refresh'; import path from 'path'; -import { HtmlTagDescriptor, defineConfig } from 'vite'; -import svgr from 'vite-plugin-svgr'; +import { HtmlTagDescriptor, defineConfig, loadEnv } from 'vite'; import { createHtmlPlugin } from 'vite-plugin-html'; +import svgr from 'vite-plugin-svgr'; +import pkg from './package.json'; import clientConfig from './client.config'; +const appVersion = pkg.version; + +const generateVersionPlugin = (hardRefresh = false) => { + return { + name: 'generate-version', + closeBundle() { + const versionManifest = { + version: appVersion, + requiresHardRefresh: hardRefresh, + }; + + fs.writeFileSync( + path.join('dist', 'version.json'), + JSON.stringify(versionManifest, null, 2), + ); + }, + }; +}; + export default defineConfig(({ mode }) => { - const isProd = mode === "production"; + const isProd = mode === 'production'; + const env = loadEnv(mode, process.cwd()); + + const requiresHardRefresh = env.VITE_APP_REQUIRES_HARD_REFRESH === 'true'; + return { + define: { + 'process.env': { + APP_VERSION: appVersion, + REQUIRES_HARD_REFRESH: isProd ? requiresHardRefresh : false, + }, + }, plugins: [ react({ include: 'src/**/*.tsx', @@ -23,13 +54,14 @@ export default defineConfig(({ mode }) => { minify: true, inject: { tags: [ - ...clientConfig.meta.map((meta) => ({ + ...(clientConfig.meta.map(meta => ({ ...meta, injectTo: 'head', - })) as HtmlTagDescriptor[], - ] + })) as HtmlTagDescriptor[]), + ], }, }), + generateVersionPlugin(isProd ? requiresHardRefresh : false), ], build: { minify: isProd, @@ -39,7 +71,10 @@ export default defineConfig(({ mode }) => { alias: { '@/assets': path.resolve(__dirname, './assets'), src: path.resolve(__dirname, './src'), - 'connect-config': path.resolve(__dirname, './connect.config.ts'), + 'connect-config': path.resolve( + __dirname, + './connect.config.ts', + ), path: 'rollup-plugin-node-polyfills/polyfills/path', events: 'rollup-plugin-node-polyfills/polyfills/events', },