Get primary user flow working on desktop

This commit is contained in:
Frank Noirot
2025-01-08 16:56:48 -05:00
parent 5cbd11cec8
commit ca09224c92
9 changed files with 93 additions and 61 deletions

View File

@ -8,3 +8,5 @@ VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000 VITE_KC_CONNECTION_TIMEOUT_MS=5000
# ONLY add your token in .env.development.local if you want to skip auth, otherwise this token takes precedence! # ONLY add your token in .env.development.local if you want to skip auth, otherwise this token takes precedence!
#VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local" #VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local"
# Add a prod token if you want to use the share URL feature in local dev
#VITE_KC_PROD_TOKEN="your token from prod.zoo.dev should go in .env.development.local"

1
interface.d.ts vendored
View File

@ -62,6 +62,7 @@ export interface IElectronAPI {
TEST_SETTINGS_FILE_KEY: string TEST_SETTINGS_FILE_KEY: string
IS_PLAYWRIGHT: string IS_PLAYWRIGHT: string
VITE_KC_DEV_TOKEN: string VITE_KC_DEV_TOKEN: string
VITE_KC_PROD_TOKEN: string
VITE_KC_API_WS_MODELING_URL: string VITE_KC_API_WS_MODELING_URL: string
VITE_KC_API_BASE_URL: string VITE_KC_API_BASE_URL: string
VITE_KC_SITE_BASE_URL: string VITE_KC_SITE_BASE_URL: string

View File

@ -47,7 +47,6 @@ import { AppStateProvider } from 'AppState'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { RouteProvider } from 'components/RouteProvider' import { RouteProvider } from 'components/RouteProvider'
import { ProjectsContextProvider } from 'components/ProjectsContextProvider' import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
import { ProtocolHandler } from 'components/ProtocolHandler'
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
@ -59,7 +58,6 @@ const router = createRouter([
/* Make sure auth is the outermost provider or else we will have /* Make sure auth is the outermost provider or else we will have
* inefficient re-renders, use the react profiler to see. */ * inefficient re-renders, use the react profiler to see. */
element: ( element: (
<ProtocolHandler>
<CommandBarProvider> <CommandBarProvider>
<RouteProvider> <RouteProvider>
<SettingsAuthProvider> <SettingsAuthProvider>
@ -77,7 +75,6 @@ const router = createRouter([
</SettingsAuthProvider> </SettingsAuthProvider>
</RouteProvider> </RouteProvider>
</CommandBarProvider> </CommandBarProvider>
</ProtocolHandler>
), ),
errorElement: <ErrorPage />, errorElement: <ErrorPage />,
children: [ children: [

View File

@ -18,6 +18,8 @@ import Tooltip from './Tooltip'
import { createFileLink } from 'lib/createFileLink' import { createFileLink } from 'lib/createFileLink'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { DEV, VITE_KC_PROD_TOKEN } from 'env'
import { err } from 'lib/trap'
const ProjectSidebarMenu = ({ const ProjectSidebarMenu = ({
project, project,
@ -189,21 +191,41 @@ function ProjectMenuPopover({
Element: 'button', Element: 'button',
children: 'Share link to file', children: 'Share link to file',
onClick: async () => { onClick: async () => {
if (!auth.context.token) { /**
* We don't have a dev shortlink API service,
* so we need to hit the prod API even in local dev.
* This override allows us to shim in an environment variable
* for the prod token.
*/
const token = DEV ? VITE_KC_PROD_TOKEN : auth.context.token
if (DEV && !VITE_KC_PROD_TOKEN) {
toast.error(
'You need to set a prod token in your environment to share a file in development.',
{
duration: 5000,
}
)
return
} else if (!token) {
toast.error('You need to be signed in to share a file.', { toast.error('You need to be signed in to share a file.', {
duration: 5000, duration: 5000,
}) })
return return
} }
const shareUrl = await createFileLink(auth.context.token, { const shareUrl = await createFileLink(token, {
code: codeManager.code, code: codeManager.code,
name: file?.name || '', name: project?.name || '',
units: settings.context.modeling.defaultUnit.current, units: settings.context.modeling.defaultUnit.current,
}) })
console.log(shareUrl) if (err(shareUrl)) {
toast.error(shareUrl.message, {
duration: 5000,
})
return
}
await globalThis.navigator.clipboard.writeText(shareUrl) await globalThis.navigator.clipboard.writeText(shareUrl.url)
toast.success( toast.success(
'Link copied to clipboard. Anyone who clicks this link will get a copy of this file. Share carefully!', 'Link copied to clipboard. Anyone who clicks this link will get a copy of this file. Share carefully!',
{ {

View File

@ -14,6 +14,7 @@ export const VITE_KC_SKIP_AUTH = env.VITE_KC_SKIP_AUTH as string | undefined
export const VITE_KC_CONNECTION_TIMEOUT_MS = export const VITE_KC_CONNECTION_TIMEOUT_MS =
env.VITE_KC_CONNECTION_TIMEOUT_MS as string | undefined env.VITE_KC_CONNECTION_TIMEOUT_MS as string | undefined
export const VITE_KC_DEV_TOKEN = env.VITE_KC_DEV_TOKEN as string | undefined export const VITE_KC_DEV_TOKEN = env.VITE_KC_DEV_TOKEN as string | undefined
export const VITE_KC_PROD_TOKEN = env.VITE_KC_PROD_TOKEN as string | undefined
export const PROD = env.PROD as string | undefined export const PROD = env.PROD as string | undefined
export const TEST = env.TEST as string | undefined export const TEST = env.TEST as string | undefined
export const DEV = env.DEV as string | undefined export const DEV = env.DEV as string | undefined

View File

@ -1,10 +1,7 @@
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models' import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { postUserShortlink } from 'lib/desktop'
import { CREATE_FILE_URL_PARAM } from './constants' import { CREATE_FILE_URL_PARAM } from './constants'
import { stringToBase64 } from './base64' import { stringToBase64 } from './base64'
import withBaseURL from 'lib/withBaseURL' import { ZOO_STUDIO_PROTOCOL } from './link'
import { isDesktop } from 'lib/isDesktop'
export interface FileLinkParams { export interface FileLinkParams {
code: string code: string
name: string name: string
@ -13,41 +10,44 @@ export interface FileLinkParams {
/** /**
* Given a file's code, name, and units, creates shareable link * Given a file's code, name, and units, creates shareable link
* TODO: update the return type to use TS library after its updated
*/ */
export async function createFileLink( export async function createFileLink(
token: string, token: string,
{ code, name, units }: FileLinkParams { code, name, units }: FileLinkParams
) { ): Promise<Error | { key: string; url: string }> {
let urlUserShortlinks = withBaseURL('/users/shortlinks')
// During development, the "handler" needs to first be the web app version, // During development, the "handler" needs to first be the web app version,
// which exists on localhost:3000 typically. // which exists on localhost:3000 typically.
let origin = 'http://localhost:3000' let origin = 'http://localhost:3000'
let urlFileToShare = new URL( let urlFileToShare = new URL(
`/?${CREATE_FILE_URL_PARAM}&name=${encodeURIComponent( `?${CREATE_FILE_URL_PARAM}&name=${encodeURIComponent(
name name
)}&units=${units}&code=${encodeURIComponent(stringToBase64(code))}`, )}&units=${units}&code=${encodeURIComponent(stringToBase64(code))}`,
origin origin
).toString() ).toString()
// Remove this monkey patching /**
function fixTheBrokenShitUntilItsFixedOnDev() { * We don't use our `withBaseURL` function here because
urlUserShortlinks = urlUserShortlinks.replace( * there is no URL shortener service in the dev API.
'https://api.dev.zoo.dev', */
'https://api.zoo.dev' const response = await fetch('https://api.zoo.dev/user/shortlinks', {
)
console.log(urlUserShortlinks)
}
fixTheBrokenShitUntilItsFixedOnDev()
return await fetch(urlUserShortlinks, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-type': 'application/json', 'Content-type': 'application/json',
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify({ url: urlFileToShare }), body: JSON.stringify({
}).then((resp) => resp.json()) url: urlFileToShare,
// In future we can support org-scoped and password-protected shortlinks here
// https://zoo.dev/docs/api/shortlinks/create-a-shortlink-for-a-user?lang=typescript
}),
})
console.log('response', response)
if (!response.ok) {
const error = await response.json()
return new Error(`Failed to create shortlink: ${error.message}`)
} else {
return response.json()
}
} }

View File

@ -1 +1 @@
export const ZOO_STUDIO_PROTOCOL = 'zoo-studio' export const ZOO_STUDIO_PROTOCOL = 'zoo-studio:'

View File

@ -67,7 +67,7 @@ if (process.defaultApp) {
// Must be done before ready event. // Must be done before ready event.
registerStartupListeners() registerStartupListeners()
const createWindow = (filePath?: string, reuse?: boolean): BrowserWindow => { const createWindow = (pathToOpen?: string, reuse?: boolean): BrowserWindow => {
let newWindow let newWindow
if (reuse) { if (reuse) {
@ -92,33 +92,47 @@ const createWindow = (filePath?: string, reuse?: boolean): BrowserWindow => {
}) })
} }
const pathIsCustomProtocolLink = pathToOpen?.startsWith(ZOO_STUDIO_PROTOCOL) ?? false
// and load the index.html of the app. // and load the index.html of the app.
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL).catch(reportRejection) const filteredPath = pathToOpen ? decodeURI(pathToOpen.replace(ZOO_STUDIO_PROTOCOL, '')) : ''
const fullHashBasedUrl = `${MAIN_WINDOW_VITE_DEV_SERVER_URL}/#/${filteredPath}`
newWindow.loadURL(fullHashBasedUrl).catch(reportRejection)
} else { } else {
console.log('Loading from file', filePath) if (pathIsCustomProtocolLink && pathToOpen) {
getProjectPathAtStartup(filePath) // We're trying to open a custom protocol link
.then(async (projectPath) => { const filteredPath = pathToOpen ? decodeURI(pathToOpen.replace(ZOO_STUDIO_PROTOCOL, '')) : ''
const startIndex = path.join( const startIndex = path.join(
__dirname, __dirname,
`../renderer/${MAIN_WINDOW_VITE_NAME}/index.html` `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
) )
newWindow.loadFile(startIndex, {
if (projectPath === null) { hash: filteredPath,
await newWindow.loadFile(startIndex) }).catch(reportRejection)
return } else {
} // otherwise we're trying to open a local file from the command line
getProjectPathAtStartup(pathToOpen)
console.log('Loading file', projectPath) .then(async (projectPath) => {
const startIndex = path.join(
const fullUrl = `/file/${encodeURIComponent(projectPath)}` __dirname,
console.log('Full URL', fullUrl) `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
)
await newWindow.loadFile(startIndex, {
hash: fullUrl, if (projectPath === null) {
await newWindow.loadFile(startIndex)
return
}
const fullUrl = `/file/${encodeURIComponent(projectPath)}`
console.log('Full URL', fullUrl)
await newWindow.loadFile(startIndex, {
hash: fullUrl,
})
}) })
}) .catch(reportRejection)
.catch(reportRejection) }
} }
// Open the DevTools. // Open the DevTools.
@ -467,12 +481,6 @@ function registerStartupListeners() {
) { ) {
event.preventDefault() event.preventDefault()
console.log('open-url', url)
fs.writeFileSync(
'/Users/frankjohnson/open-url.txt',
`at ${new Date().toLocaleTimeString()} opened url: ${url}`
)
// If we have a mainWindow, lets open another window. // If we have a mainWindow, lets open another window.
if (mainWindow) { if (mainWindow) {
createWindow(url) createWindow(url)

View File

@ -188,6 +188,7 @@ contextBridge.exposeInMainWorld('electron', {
'VITE_KC_SKIP_AUTH', 'VITE_KC_SKIP_AUTH',
'VITE_KC_CONNECTION_TIMEOUT_MS', 'VITE_KC_CONNECTION_TIMEOUT_MS',
'VITE_KC_DEV_TOKEN', 'VITE_KC_DEV_TOKEN',
'VITE_KC_PROD_TOKEN',
'IS_PLAYWRIGHT', 'IS_PLAYWRIGHT',
// Really we shouldn't use these and our code should use NODE_ENV // Really we shouldn't use these and our code should use NODE_ENV