diff --git a/e2e/playwright/native-file-menu.spec.ts b/e2e/playwright/native-file-menu.spec.ts index f9c341077..3847c78df 100644 --- a/e2e/playwright/native-file-menu.spec.ts +++ b/e2e/playwright/native-file-menu.spec.ts @@ -197,18 +197,6 @@ test.describe( await clickElectronNativeMenuById(tronApp, 'File.Export current part') await cmdBar.expectCommandName('Export') }) - await test.step('Modeling.File.Share part via Zoo link', async () => { - await page.waitForTimeout(250) - await clickElectronNativeMenuById( - tronApp, - 'File.Share part via Zoo link' - ) - const textToCheck = - 'Link copied to clipboard. Anyone who clicks this link will get a copy of this file. Share carefully!' - // Check if text appears anywhere in the page - const isTextVisible = page.getByText(textToCheck) - await expect(isTextVisible).toBeVisible({ timeout: 10000 }) - }) await test.step('Modeling.File.Preferences.Project settings', async () => { await page.waitForTimeout(250) await clickElectronNativeMenuById( diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png index e232709c4..d01a19cf2 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png index bb98a07e3..1c4772c49 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png index 7b03b8980..1c91b0669 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png index bb98a07e3..1c4772c49 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png index bcf7f4b5f..9fe34b78d 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-linux.png index 3d0ed2c9d..2e5b51e50 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-rectangles-should-look-right-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png index c0e7d2e69..aebf41f90 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png index cd1f000f4..4feb5848a 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-2-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-3-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-3-Google-Chrome-linux.png index aa9f29197..fae651ea7 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-3-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-3-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-4-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-4-Google-Chrome-linux.png index d419ff79d..ca356f3b7 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-4-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-segments-should-look-right-4-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png index 8f797014d..4cf087a09 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Sketch-on-face-with-none-z-up-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-2d-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-2d-1-Google-Chrome-linux.png index a59bc6a32..c61a5eb93 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-2d-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-2d-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-3d-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-3d-1-Google-Chrome-linux.png index 848742965..bffef7458 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-3d-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-3d-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-1-Google-Chrome-linux.png index 2c788df1d..045c4c182 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-opening-window-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-opening-window-1-Google-Chrome-linux.png index 60f4db613..665739b77 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-opening-window-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-opening-window-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-works-with-single-quotes-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-works-with-single-quotes-1-Google-Chrome-linux.png index 745771970..f4f57a768 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-works-with-single-quotes-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/code-color-goober-code-color-goober-works-with-single-quotes-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png index 604cee0f4..39d4622cd 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XY-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png index be12655bc..d50f24d79 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--XZ-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png index 8c37ee47c..1f88d8689 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable--YZ-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-linux.png index cde3cf48e..7a38bb750 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png index 8a41d5dd9..0a85b74d7 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XZ-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png index 6677da605..62f34436f 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-YZ-1-Google-Chrome-linux.png differ diff --git a/rust/kcl-lib/src/simulation_tests.rs b/rust/kcl-lib/src/simulation_tests.rs index 53951ca37..0be20f146 100644 --- a/rust/kcl-lib/src/simulation_tests.rs +++ b/rust/kcl-lib/src/simulation_tests.rs @@ -2770,7 +2770,7 @@ mod clone_w_fillets { /// Test that KCL is executed correctly. #[tokio::test(flavor = "multi_thread")] #[ignore] // turn on when https://github.com/KittyCAD/engine/pull/3380 is merged - // Theres also a test in clone.rs you need to turn too + // There's also a test in clone.rs you need to turn too async fn kcl_test_execute() { super::execute(TEST_NAME, true).await } diff --git a/src/components/ProjectSidebarMenu.tsx b/src/components/ProjectSidebarMenu.tsx index a6ea77049..5e08275d3 100644 --- a/src/components/ProjectSidebarMenu.tsx +++ b/src/components/ProjectSidebarMenu.tsx @@ -15,15 +15,9 @@ import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath' import usePlatform from '@src/hooks/usePlatform' import { APP_NAME } from '@src/lib/constants' import { isDesktop } from '@src/lib/isDesktop' -import { copyFileShareLink } from '@src/lib/links' import { PATHS } from '@src/lib/paths' -import { - codeManager, - engineCommandManager, - kclManager, -} from '@src/lib/singletons' +import { engineCommandManager, kclManager } from '@src/lib/singletons' import { type IndexLoaderData } from '@src/lib/types' -import { useToken } from '@src/lib/singletons' import { commandBarActor } from '@src/lib/singletons' const ProjectSidebarMenu = ({ @@ -108,7 +102,6 @@ function ProjectMenuPopover({ const location = useLocation() const navigate = useNavigate() const filePath = useAbsoluteFilePath() - const token = useToken() const machineManager = useContext(MachineManagerContext) const commands = useSelector(commandBarActor, commandsSelector) @@ -116,7 +109,6 @@ function ProjectMenuPopover({ const insertCommandInfo = { name: 'Insert', groupId: 'code' } const exportCommandInfo = { name: 'Export', groupId: 'modeling' } const makeCommandInfo = { name: 'Make', groupId: 'modeling' } - const shareCommandInfo = { name: 'share-file-link', groupId: 'code' } const findCommand = (obj: { name: string; groupId: string }) => Boolean( commands.find((c) => c.name === obj.name && c.groupId === obj.groupId) @@ -217,19 +209,6 @@ function ProjectMenuPopover({ }) }, }, - { - id: 'share-link', - Element: 'button', - children: 'Share part via Zoo link', - disabled: !findCommand(shareCommandInfo), - onClick: async () => { - await copyFileShareLink({ - token: token ?? '', - code: codeManager.code, - name: project?.name || '', - }) - }, - }, 'break', { id: 'go-home', diff --git a/src/components/ShareButton.tsx b/src/components/ShareButton.tsx index 35f93dfce..149957738 100644 --- a/src/components/ShareButton.tsx +++ b/src/components/ShareButton.tsx @@ -1,53 +1,168 @@ +import { err } from '@src/lib/trap' import { CustomIcon } from '@src/components/CustomIcon' import Tooltip from '@src/components/Tooltip' import usePlatform from '@src/hooks/usePlatform' import { hotkeyDisplay } from '@src/lib/hotkeyWrapper' -import { commandBarActor } from '@src/lib/singletons' +import { billingActor, commandBarActor } from '@src/lib/singletons' +import { useState } from 'react' import { useHotkeys } from 'react-hotkeys-hook' +import { Popover } from '@headlessui/react' +import { useSelector } from '@xstate/react' import { useKclContext } from '@src/lang/KclProvider' +import { Tier } from '@src/machines/billingMachine' +import type { SubscriptionsOrError } from '@src/machines/billingMachine' const shareHotkey = 'mod+alt+s' -const onShareClick = () => - commandBarActor.send({ - type: 'Find and select command', - data: { name: 'share-file-link', groupId: 'code' }, - }) + +const canPasswordProtectShareLinks = ( + subOrErr: undefined | SubscriptionsOrError +): boolean => { + if (subOrErr === undefined || typeof subOrErr === 'number' || err(subOrErr)) + return false + return subOrErr.modeling_app.share_links[0] === 'password_protected' +} /** Share Zoo link button shown in the upper-right of the modeling view */ export const ShareButton = () => { const platform = usePlatform() - useHotkeys(shareHotkey, onShareClick, { + + const [showOptions, setShowOptions] = useState(false) + const [isRestrictedToOrg, setIsRestrictedToOrg] = useState(false) + const [password, setPassword] = useState('') + + const billingContext = useSelector(billingActor, ({ context }) => context) + + const allowOrgRestrict = billingContext.tier === Tier.Organization + const allowPassword = canPasswordProtectShareLinks( + billingContext.subscriptionsOrError + ) + const hasOptions = allowOrgRestrict || allowPassword + + // Prevents Organization and Pro tier users from one-click sharing, + // and give them a chance to set a password and restrict to org. + const onShareClickFreeOrUnknownRestricted = () => { + if (hasOptions) { + setShowOptions(true) + return + } + + commandBarActor.send({ + type: 'Find and select command', + data: { + name: 'share-file-link', + groupId: 'code', + isRestrictedToOrg: false, + }, + }) + } + + const onShareClickProOrOrganization = () => { + setShowOptions(false) + + commandBarActor.send({ + type: 'Find and select command', + data: { + name: 'share-file-link', + groupId: 'code', + isRestrictedToOrg, + password, + }, + }) + } + + useHotkeys(shareHotkey, onShareClickFreeOrUnknownRestricted, { scopes: ['modeling'], }) const kclContext = useKclContext() - const disabled = kclContext.ast.body.some((n) => n.type === 'ImportStatement') + + // It doesn't make sense for the user to be able to click on this + // until we get what their subscription allows for. + const disabled = + kclContext.ast.body.some((n) => n.type === 'ImportStatement') || + billingContext.tier === undefined return ( - + + + + + {showOptions && ( + +
+
+ + setPassword(e.target.value)} + autoCapitalize="off" + autoComplete="off" + autoCorrect="off" + spellCheck="false" + className={`${allowPassword ? 'cursor-pointer' : 'cursor-not-allowed'} text-xs w-full py-1 bg-transparent text-chalkboard-100 placeholder:text-chalkboard-70 dark:text-chalkboard-10 dark:placeholder:text-chalkboard-50 focus:outline-none focus:ring-0`} + type="text" + placeholder="Set a password" + /> +
+
+ + {!allowOrgRestrict && ( + Upgrade to Organization to use this feature. + )} +
+ +
+
+ )} +
) } diff --git a/src/hooks/useCreateFileLinkQueryWatcher.ts b/src/hooks/useCreateFileLinkQueryWatcher.ts index eff918925..627647647 100644 --- a/src/hooks/useCreateFileLinkQueryWatcher.ts +++ b/src/hooks/useCreateFileLinkQueryWatcher.ts @@ -49,6 +49,7 @@ export function useCreateFileLinkQuery( decodeURIComponent(searchParams.get('code') ?? '') ), name: searchParams.get('name') ?? DEFAULT_FILE_NAME, + isRestrictedToOrg: false, } const argDefaultValues: CreateFileSchemaMethodOptional = { diff --git a/src/lib/kclCommands.ts b/src/lib/kclCommands.ts index 935e756e4..968fa0125 100644 --- a/src/lib/kclCommands.ts +++ b/src/lib/kclCommands.ts @@ -32,6 +32,8 @@ interface KclCommandConfig { settings: { defaultUnit: UnitLength_type } + isRestrictedToOrg?: boolean + password?: string } export function kclCommands(commandProps: KclCommandConfig): Command[] { @@ -175,11 +177,13 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] { groupId: 'code', needsReview: false, icon: 'link', - onSubmit: () => { + onSubmit: (input) => { copyFileShareLink({ token: commandProps.authToken, code: codeManager.code, name: commandProps.projectData.project?.name || '', + isRestrictedToOrg: input?.event.data.isRestrictedToOrg ?? false, + password: input?.event.data.password, }).catch(reportRejection) }, }, diff --git a/src/lib/links.test.ts b/src/lib/links.test.ts index 32d2c6dff..bbc2ac9a1 100644 --- a/src/lib/links.test.ts +++ b/src/lib/links.test.ts @@ -11,7 +11,7 @@ describe(`link creation tests`, () => { const expectedEncodedCode = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D` const expectedLink = `${VITE_KC_SITE_APP_URL}/?create-file=true&name=test&code=${expectedEncodedCode}&ask-open-desktop=true` - const result = createCreateFileUrl({ code, name }) + const result = createCreateFileUrl({ code, name, isRestrictedToOrg: false }) expect(result.toString()).toBe(expectedLink) }) }) diff --git a/src/lib/links.ts b/src/lib/links.ts index f03613273..8c2b0d599 100644 --- a/src/lib/links.ts +++ b/src/lib/links.ts @@ -11,6 +11,8 @@ import { err } from '@src/lib/trap' export interface FileLinkParams { code: string name: string + isRestrictedToOrg: boolean + password?: string } export async function copyFileShareLink( @@ -24,7 +26,12 @@ export async function copyFileShareLink( return } const shareUrl = createCreateFileUrl(args) - const shortlink = await createShortlink(token, shareUrl.toString()) + const shortlink = await createShortlink( + token, + shareUrl.toString(), + args.isRestrictedToOrg, + args.password + ) if (err(shortlink)) { toast.error(shortlink.message, { @@ -70,23 +77,32 @@ export function createCreateFileUrl({ code, name }: FileLinkParams) { */ export async function createShortlink( token: string, - url: string + url: string, + isRestrictedToOrg: boolean, + password?: string ): Promise { /** * We don't use our `withBaseURL` function here because * there is no URL shortener service in the dev API. */ + const body: { + url: string + restrict_to_org: boolean + password?: string + } = { + url, + restrict_to_org: isRestrictedToOrg, + } + if (password) { + body.password = password + } const response = await fetch(`${VITE_KC_API_BASE_URL}/user/shortlinks`, { method: 'POST', headers: { 'Content-type': 'application/json', Authorization: `Bearer ${token}`, }, - body: JSON.stringify({ - url, - // 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 - }), + body: JSON.stringify(body), }) if (!response.ok) { const error = await response.json() diff --git a/src/machines/billingMachine.ts b/src/machines/billingMachine.ts index 6d52579e7..6dcd340da 100644 --- a/src/machines/billingMachine.ts +++ b/src/machines/billingMachine.ts @@ -14,11 +14,34 @@ export enum BillingTransition { Wait = 'wait', } +// It's nice to be explicit if we are an Organization, Pro, Free. +// @kittycad/lib offers some types around this, but they aren't as... +// homogeneous: Models['ZooProductSubscriptions_type'], and +// Models['Org_type']. +export enum Tier { + Free = 'free', + Pro = 'pro', + Organization = 'organization', + Unknown = 'unknown', +} + +export type OrgOrError = Models['Org_type'] | number | Error +export type SubscriptionsOrError = + | Models['ZooProductSubscriptions_type'] + | number + | Error +export type TierBasedOn = { + orgOrError: OrgOrError + subscriptionsOrError: SubscriptionsOrError +} + export interface BillingContext { credits: undefined | number allowance: undefined | number error: undefined | Error urlUserService: string + tier: undefined | Tier + subscriptionsOrError: undefined | SubscriptionsOrError } export interface BillingUpdateEvent { @@ -30,9 +53,30 @@ export const BILLING_CONTEXT_DEFAULTS: BillingContext = Object.freeze({ credits: undefined, allowance: undefined, error: undefined, + tier: undefined, + subscriptionsOrError: undefined, urlUserService: '', }) +const toTierFrom = (args: TierBasedOn): Tier => { + if (typeof args.orgOrError !== 'number' && !err(args.orgOrError)) { + return Tier.Organization + } else if ( + typeof args.subscriptionsOrError !== 'number' && + !err(args.subscriptionsOrError) + ) { + const subscriptions: Models['ZooProductSubscriptions_type'] = + args.subscriptionsOrError + if (subscriptions.modeling_app.name === 'pro') { + return Tier.Pro + } else { + return Tier.Free + } + } + + return Tier.Unknown +} + export const billingMachine = setup({ types: { context: {} as BillingContext, @@ -72,33 +116,45 @@ export const billingMachine = setup({ input.event.apiToken ) + const tier = toTierFrom({ + orgOrError, + subscriptionsOrError, + }) + let credits = Number(billing.monthly_api_credits_remaining) + Number(billing.stable_api_credits_remaining) let allowance = undefined - // If user is part of an org, the endpoint will return data. - if (typeof orgOrError !== 'number' && !err(orgOrError)) { - credits = Infinity - // Otherwise they are on a Pro or Free subscription - } else if ( - typeof subscriptionsOrError !== 'number' && - !err(subscriptionsOrError) - ) { - const subscriptions: Models['ZooProductSubscriptions_type'] = - subscriptionsOrError - if (subscriptions.modeling_app.name === 'pro') { + switch (tier) { + case Tier.Organization: + case Tier.Pro: credits = Infinity - } else { - allowance = Number( - subscriptions.modeling_app.monthly_pay_as_you_go_api_credits - ) - } + break + case Tier.Free: + // TS too dumb Tier.Free has the same logic + if ( + typeof subscriptionsOrError !== 'number' && + !err(subscriptionsOrError) + ) { + allowance = Number( + subscriptionsOrError.modeling_app + .monthly_pay_as_you_go_api_credits + ) + } + break + case Tier.Unknown: + break + default: + const _exh: never = tier } + // If nothing matches, we show a credit total. return { error: undefined, + tier, + subscriptionsOrError, credits, allowance, } diff --git a/src/machines/commandBarMachine.ts b/src/machines/commandBarMachine.ts index 4c6a8a504..635724c34 100644 --- a/src/machines/commandBarMachine.ts +++ b/src/machines/commandBarMachine.ts @@ -66,6 +66,10 @@ export type CommandBarMachineEvent = name: string groupId: string argDefaultValues?: { [x: string]: unknown } + + // I'm sorry but the way we did share URL called for this. + isRestrictedToOrg?: boolean + password?: string } } | { @@ -115,7 +119,7 @@ export const commandBarMachine = setup({ } selectedCommand?.onSubmit(resolvedArgs) } else { - selectedCommand?.onSubmit() + selectedCommand?.onSubmit({ context, event }) } }, 'Clear selected command': assign({ diff --git a/src/machines/engineStreamMachine.ts b/src/machines/engineStreamMachine.ts index d099f6d57..013887c4d 100644 --- a/src/machines/engineStreamMachine.ts +++ b/src/machines/engineStreamMachine.ts @@ -317,7 +317,7 @@ export const engineStreamMachine = setup({ [EngineStreamTransition.Play]: { target: EngineStreamState.Playing, }, - // We actually failed inbetween needing to play and sending commands. + // We actually failed in between needing to play and sending commands. [EngineStreamTransition.StartOrReconfigureEngine]: { target: EngineStreamState.WaitingForMediaStream, reenter: true, diff --git a/src/menu/channels.ts b/src/menu/channels.ts index 6312fa8db..f7ebf5f8b 100644 --- a/src/menu/channels.ts +++ b/src/menu/channels.ts @@ -26,7 +26,6 @@ export type MenuLabels = | 'File.Create new folder' | 'File.Add file to project' | 'File.Export current part' - | 'File.Share part via Zoo link' | 'File.Preferences.Project settings' | 'Design.Start sketch' | 'Design.Create an offset plane' diff --git a/src/menu/fileRole.ts b/src/menu/fileRole.ts index 781524139..08774c4b1 100644 --- a/src/menu/fileRole.ts +++ b/src/menu/fileRole.ts @@ -184,15 +184,6 @@ export const modelingFileRole = ( }) }, }, - { - label: 'Share part via Zoo link', - id: 'File.Share part via Zoo link', - click: () => { - typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', { - menuLabel: 'File.Share part via Zoo link', - }) - }, - }, { type: 'separator' }, { label: 'Preferences', diff --git a/src/menu/register.ts b/src/menu/register.ts index eae3a35e2..30bf62eba 100644 --- a/src/menu/register.ts +++ b/src/menu/register.ts @@ -1,13 +1,8 @@ import { AxisNames } from '@src/lib/constants' -import { copyFileShareLink } from '@src/lib/links' import { PATHS } from '@src/lib/paths' import type { Project } from '@src/lib/project' import type { SettingsType } from '@src/lib/settings/initialSettings' -import { - codeManager, - engineCommandManager, - sceneInfra, -} from '@src/lib/singletons' +import { engineCommandManager, sceneInfra } from '@src/lib/singletons' import { reportRejection } from '@src/lib/trap' import { uuidv4 } from '@src/lib/utils' import { authActor, settingsActor } from '@src/lib/singletons' @@ -84,12 +79,6 @@ export function modelingMenuCallbackMostActions( }) } else if (data.menuLabel === 'File.Preferences.Theme color') { navigate(filePath + PATHS.SETTINGS_USER + '#themeColor') - } else if (data.menuLabel === 'File.Share part via Zoo link') { - copyFileShareLink({ - token: token ?? '', - code: codeManager.code, - name: project?.name || '', - }).catch(reportRejection) } else if (data.menuLabel === 'File.Preferences.User default units') { navigate(filePath + PATHS.SETTINGS_USER + '#defaultUnit') } else if (data.menuLabel === 'File.Add file to project') {