From 886fca7fa099751c620f46c22d66c99bc73a0d99 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Mon, 16 Jun 2025 21:21:26 -0230 Subject: [PATCH 1/3] Add new submagic logo (#36413) --- apps/www/components/logos.tsx | 5 +++++ apps/www/public/images/logos/publicity/submagic.svg | 1 + 2 files changed, 6 insertions(+) create mode 100644 apps/www/public/images/logos/publicity/submagic.svg diff --git a/apps/www/components/logos.tsx b/apps/www/components/logos.tsx index b2ef09c79c7a4..5c5608eb2bb4c 100644 --- a/apps/www/components/logos.tsx +++ b/apps/www/components/logos.tsx @@ -118,6 +118,11 @@ const logos = [ alt: 'betashares', name: 'betashares', }, + { + image: `/images/logos/publicity/submagic.svg`, + alt: 'submagic', + name: 'submagic', + }, ] const LogosRow: React.FC<{ className?: string }> = ({ className }) => ( diff --git a/apps/www/public/images/logos/publicity/submagic.svg b/apps/www/public/images/logos/publicity/submagic.svg new file mode 100644 index 0000000000000..84fb19c6bbf4c --- /dev/null +++ b/apps/www/public/images/logos/publicity/submagic.svg @@ -0,0 +1 @@ + \ No newline at end of file From 9ded153928833239b6102b44aa6a59985a951825 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 17 Jun 2025 10:37:07 +0800 Subject: [PATCH 2/3] Mitigate race condition when fetching folder contents storage (#36363) * Mitigate race condition when fetching folder contents storage * Fix --- .../Storage/EmptyBucketModal.tsx | 7 +++++- .../StorageExplorer/FileExplorerRow.tsx | 12 +++++++++- .../StorageExplorer/StorageExplorer.tsx | 23 ++++++++++++++++--- .../StorageExplorer/useSelectedBucket.ts | 11 +++++++++ .../[ref]/storage/buckets/[bucketId].tsx | 10 +++----- apps/studio/state/storage-explorer.tsx | 22 +++++++++++------- 6 files changed, 65 insertions(+), 20 deletions(-) create mode 100644 apps/studio/components/to-be-cleaned/Storage/StorageExplorer/useSelectedBucket.ts diff --git a/apps/studio/components/to-be-cleaned/Storage/EmptyBucketModal.tsx b/apps/studio/components/to-be-cleaned/Storage/EmptyBucketModal.tsx index 0cd2db00acfd0..77696bc4862e3 100644 --- a/apps/studio/components/to-be-cleaned/Storage/EmptyBucketModal.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/EmptyBucketModal.tsx @@ -19,7 +19,12 @@ export const EmptyBucketModal = ({ visible = false, bucket, onClose }: EmptyBuck const { mutate: emptyBucket, isLoading } = useBucketEmptyMutation({ onSuccess: async () => { if (bucket === undefined) return - await fetchFolderContents({ folderId: bucket.id, folderName: bucket.name, index: -1 }) + await fetchFolderContents({ + bucketId: bucket.id, + folderId: bucket.id, + folderName: bucket.name, + index: -1, + }) toast.success(`Successfully deleted bucket ${bucket!.name}`) onClose() }, diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx index 8cfb5ef7cc423..306eb49098fec 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx @@ -22,6 +22,7 @@ import type { ItemRenderer } from 'components/ui/InfiniteList' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { formatBytes } from 'lib/helpers' +import { toast } from 'sonner' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { Checkbox, @@ -50,6 +51,7 @@ import { StorageItem, StorageItemWithColumn } from '../Storage.types' import FileExplorerRowEditing from './FileExplorerRowEditing' import { copyPathToFolder, downloadFile } from './StorageExplorer.utils' import { useCopyUrl } from './useCopyUrl' +import { useSelectedBucket } from './useSelectedBucket' export const RowIcon = ({ view, @@ -114,6 +116,7 @@ const FileExplorerRow: ItemRenderer = ({ openedFolders = [], }) => { const { ref: projectRef, bucketId } = useParams() + const { bucket } = useSelectedBucket() const { selectedBucket, @@ -152,11 +155,18 @@ const FileExplorerRow: ItemRenderer = ({ } const onSelectFolder = async (columnIndex: number, folder: StorageItem) => { + if (!bucket) return toast.error('Unable to retrieve bucket details') + setSelectedFilePreview(undefined) clearSelectedItems(columnIndex + 1) popOpenedFoldersAtIndex(columnIndex - 1) pushOpenedFolderAtIndex(folder, columnIndex) - await fetchFolderContents({ folderId: folder.id, folderName: folder.name, index: columnIndex }) + await fetchFolderContents({ + bucketId: bucket.id, + folderId: folder.id, + folderName: folder.name, + index: columnIndex, + }) } const onCheckItem = (isShiftKeyHeld: boolean) => { diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx index c26df6d1dccd3..e9a20a5e1bd35 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx @@ -67,6 +67,7 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { if (!currentFolder) { // At root of bucket await fetchFolderContents({ + bucketId: bucket.id, folderId: bucket.id, folderName: bucket.name, index: -1, @@ -74,6 +75,7 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { }) } else { await fetchFolderContents({ + bucketId: bucket.id, folderId: currentFolder.id, folderName: currentFolder.name, index: currentFolderIdx, @@ -83,9 +85,15 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { } else { if (!currentFolder) { // At root of bucket - await fetchFolderContents({ folderId: bucket.id, folderName: bucket.name, index: -1 }) + await fetchFolderContents({ + bucketId: bucket.id, + folderId: bucket.id, + folderName: bucket.name, + index: -1, + }) } else { await fetchFolderContents({ + bucketId: bucket.id, folderId: currentFolder.id, folderName: currentFolder.name, index: currentFolderIdx, @@ -93,8 +101,17 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { } } } else if (view === STORAGE_VIEWS.COLUMNS) { - const paths = openedFolders.map((folder) => folder.name) - fetchFoldersByPath({ paths, searchString: itemSearchString, showLoading: true }) + if (openedFolders.length > 0) { + const paths = openedFolders.map((folder) => folder.name) + fetchFoldersByPath({ paths, searchString: itemSearchString, showLoading: true }) + } else { + await fetchFolderContents({ + bucketId: bucket.id, + folderId: bucket.id, + folderName: bucket.name, + index: -1, + }) + } } } diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/useSelectedBucket.ts b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/useSelectedBucket.ts new file mode 100644 index 0000000000000..2023b7c044665 --- /dev/null +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/useSelectedBucket.ts @@ -0,0 +1,11 @@ +import { useParams } from 'common' +import { useBucketsQuery } from 'data/storage/buckets-query' + +export const useSelectedBucket = () => { + const { ref, bucketId } = useParams() + + const { data: buckets = [], isSuccess, isError, error } = useBucketsQuery({ projectRef: ref }) + const bucket = buckets.find((b) => b.id === bucketId) + + return { bucket, isSuccess, isError, error } +} diff --git a/apps/studio/pages/project/[ref]/storage/buckets/[bucketId].tsx b/apps/studio/pages/project/[ref]/storage/buckets/[bucketId].tsx index 6537c2f4b3b6a..24e88ca1ade9b 100644 --- a/apps/studio/pages/project/[ref]/storage/buckets/[bucketId].tsx +++ b/apps/studio/pages/project/[ref]/storage/buckets/[bucketId].tsx @@ -1,23 +1,19 @@ import { useParams } from 'common' -import { find } from 'lodash' import DefaultLayout from 'components/layouts/DefaultLayout' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import StorageBucketsError from 'components/layouts/StorageLayout/StorageBucketsError' import StorageLayout from 'components/layouts/StorageLayout/StorageLayout' import { StorageExplorer } from 'components/to-be-cleaned/Storage' -import { useBucketsQuery } from 'data/storage/buckets-query' +import { useSelectedBucket } from 'components/to-be-cleaned/Storage/StorageExplorer/useSelectedBucket' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import type { NextPageWithLayout } from 'types' const PageLayout: NextPageWithLayout = () => { - const { ref, bucketId } = useParams() + const { bucketId } = useParams() const { project } = useProjectContext() const { projectRef } = useStorageExplorerStateSnapshot() - - const { data, isSuccess, isError, error } = useBucketsQuery({ projectRef: ref }) - const buckets = data ?? [] - const bucket = find(buckets, { id: bucketId }) + const { bucket, error, isSuccess, isError } = useSelectedBucket() if (!project || !projectRef) return null diff --git a/apps/studio/state/storage-explorer.tsx b/apps/studio/state/storage-explorer.tsx index c02859646f429..4150e63eb4a2b 100644 --- a/apps/studio/state/storage-explorer.tsx +++ b/apps/studio/state/storage-explorer.tsx @@ -34,7 +34,6 @@ import { InlineLink } from 'components/ui/InlineLink' import { configKeys } from 'data/config/keys' import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { ProjectStorageConfigResponse } from 'data/config/project-storage-config-query' -import type { Project } from 'data/projects/project-detail-query' import { getQueryClient } from 'data/query-client' import { deleteBucketObject } from 'data/storage/bucket-object-delete-mutation' import { downloadBucketObject } from 'data/storage/bucket-object-download-mutation' @@ -208,12 +207,6 @@ function createStorageExplorerState({ localStorage.setItem(localStorageKey, JSON.stringify({ view, sortBy, sortByOrder })) }, - openBucket: async (bucket: Bucket) => { - const { id, name } = bucket - state.setSelectedBucket(bucket) - await state.fetchFolderContents({ folderId: id, folderName: name, index: -1 }) - }, - // Functions that manage the UI of the Storage Explorer getLatestColumnIndex: () => { @@ -226,6 +219,17 @@ function createStorageExplorerState({ }) }, + openBucket: async (bucket: Bucket) => { + const { id, name } = bucket + state.setSelectedBucket(bucket) + await state.fetchFolderContents({ + bucketId: bucket.id, + folderId: id, + folderName: name, + index: -1, + }) + }, + // ======== Folders CRUD ======== getPathAlongOpenedFolders: (includeBucket = true) => { @@ -305,11 +309,13 @@ function createStorageExplorerState({ }, fetchFolderContents: async ({ + bucketId, folderId, folderName, index, searchString, }: { + bucketId: string folderId: string | null folderName: string index: number @@ -340,8 +346,8 @@ function createStorageExplorerState({ try { const data = await listBucketObjects( { + bucketId, projectRef: state.projectRef, - bucketId: state.selectedBucket.id, path: prefix, options, }, From 7e5850bc701e11e8612a6a4cc42625d8e8578fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Tue, 17 Jun 2025 11:54:45 +0800 Subject: [PATCH 3/3] feat: prefill Stripe email + styling (#36457) - Prefill email in Stripe form to set up Link faster or connect faster - Overwrite a bit of styling so it looks less foreign - Moved the Payment component from the NewOrgForm into it's own file and renamed it --- .../Payment/AddNewPaymentMethodModal.tsx | 3 +- .../Billing/Payment/AddPaymentMethodForm.tsx | 7 +- .../Billing/Payment/Payment.utils.ts | 29 +++++++ .../BillingSettings/CreditTopUp.tsx | 3 +- .../NewPaymentMethodElement.tsx | 67 +++++++++++++++ .../Organization/NewOrg/NewOrgForm.tsx | 86 +++---------------- 6 files changed, 119 insertions(+), 76 deletions(-) create mode 100644 apps/studio/components/interfaces/Billing/Payment/Payment.utils.ts create mode 100644 apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/NewPaymentMethodElement.tsx diff --git a/apps/studio/components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx b/apps/studio/components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx index 7c75bfae4e76b..d24f02b803674 100644 --- a/apps/studio/components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx +++ b/apps/studio/components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx @@ -11,6 +11,7 @@ import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { STRIPE_PUBLIC_KEY } from 'lib/constants' import { useIsHCaptchaLoaded } from 'stores/hcaptcha-loaded-store' import AddPaymentMethodForm from './AddPaymentMethodForm' +import { getStripeElementsAppearanceOptions } from './Payment.utils' interface AddNewPaymentMethodModalProps { visible: boolean @@ -90,7 +91,7 @@ const AddNewPaymentMethodModal = ({ const options = { clientSecret: intent ? intent.client_secret : '', - appearance: { theme: resolvedTheme?.includes('dark') ? 'night' : 'flat', labels: 'floating' }, + appearance: getStripeElementsAppearanceOptions(resolvedTheme), } as any const onLocalCancel = () => { diff --git a/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx b/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx index 7f18498616538..a197cfee60c91 100644 --- a/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx +++ b/apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx @@ -115,7 +115,12 @@ const AddPaymentMethodForm = ({ - + {showSetDefaultCheckbox && (
{ + return { + theme: (resolvedTheme?.includes('dark') ? 'night' : 'flat') as 'night' | 'flat', + variables: { + fontSizeBase: '14px', + colorBackground: resolvedTheme?.includes('dark') + ? 'hsl(0deg 0% 14.1%)' + : 'hsl(0deg 0% 95.3%)', + fontFamily: + 'var(--font-custom, Circular, custom-font, Helvetica Neue, Helvetica, Arial, sans-serif)', + spacingUnit: '4px', + borderRadius: '.375rem', + gridRowSpacing: '4px', + }, + rules: { + '.Label': { + // Hide labels - it is obvious enough what the fields are for + fontSize: '0', + }, + '.TermsText': { + fontSize: '12px', + }, + }, + } +} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx index a4954c689e5bc..c3edcba0847f3 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx @@ -39,6 +39,7 @@ import { import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import PaymentMethodSelection from './Subscription/PaymentMethodSelection' import { PaymentConfirmation } from 'components/interfaces/Billing/Payment/PaymentConfirmation' +import { getStripeElementsAppearanceOptions } from 'components/interfaces/Billing/Payment/Payment.utils' const stripePromise = loadStripe(STRIPE_PUBLIC_KEY) @@ -149,7 +150,7 @@ export const CreditTopUp = ({ slug }: { slug: string | undefined }) => { const options = useMemo(() => { return { clientSecret: paymentIntentSecret, - appearance: { theme: resolvedTheme?.includes('dark') ? 'night' : 'flat', labels: 'floating' }, + appearance: getStripeElementsAppearanceOptions(resolvedTheme), } as any }, [paymentIntentSecret, resolvedTheme]) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/NewPaymentMethodElement.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/NewPaymentMethodElement.tsx new file mode 100644 index 0000000000000..0578878093a8d --- /dev/null +++ b/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/NewPaymentMethodElement.tsx @@ -0,0 +1,67 @@ +/** + * Set up as a separate component, as we need any component using stripe/elements to be wrapped in Elements. + * + * If Elements is on a higher level, we risk losing all form state in case a payment fails. + */ + +import { PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js' +import { PaymentMethod } from '@stripe/stripe-js' +import { getURL } from 'lib/helpers' +import { forwardRef, useImperativeHandle } from 'react' +import { toast } from 'sonner' + +const NewPaymentMethodElement = forwardRef( + ( + { + pending_subscription_flow_enabled, + email, + }: { pending_subscription_flow_enabled: boolean; email?: string }, + ref + ) => { + const stripe = useStripe() + const elements = useElements() + + const createPaymentMethod = async () => { + if (!stripe || !elements) return + await elements.submit() + + if (pending_subscription_flow_enabled) { + // To avoid double 3DS confirmation, we just create the payment method here, as there might be a confirmation step while doing the actual payment + const { error, paymentMethod } = await stripe.createPaymentMethod({ + elements, + }) + if (error || paymentMethod == null) { + toast.error(error?.message ?? ' Failed to process card details') + return + } + return paymentMethod + } else { + const { error, setupIntent } = await stripe.confirmSetup({ + elements, + redirect: 'if_required', + confirmParams: { + return_url: getURL(), + expand: ['payment_method'], + }, + }) + + if (error || !setupIntent.payment_method) { + toast.error(error?.message ?? ' Failed to save card details') + return + } + + return setupIntent.payment_method as PaymentMethod + } + } + + useImperativeHandle(ref, () => ({ + createPaymentMethod, + })) + + return + } +) + +NewPaymentMethodElement.displayName = 'NewPaymentMethodElement' + +export { NewPaymentMethodElement } diff --git a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx index 8d52683c54e19..066177fb1587f 100644 --- a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx +++ b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx @@ -42,6 +42,8 @@ import { SetupIntentResponse } from 'data/stripe/setup-intent-mutation' import { useProfile } from 'lib/profile' import { PaymentConfirmation } from 'components/interfaces/Billing/Payment/PaymentConfirmation' import { getURL } from 'lib/helpers' +import { getStripeElementsAppearanceOptions } from 'components/interfaces/Billing/Payment/Payment.utils' +import { NewPaymentMethodElement } from '../BillingSettings/PaymentMethods/NewPaymentMethodElement' const ORG_KIND_TYPES = { PERSONAL: 'Personal', @@ -119,10 +121,7 @@ const NewOrgForm = ({ onPaymentMethodReset, setupIntent, onPlanSelected }: NewOr () => ({ clientSecret: setupIntent ? setupIntent.client_secret! : '', - appearance: { - theme: resolvedTheme?.includes('dark') ? 'night' : 'flat', - labels: 'floating', - }, + appearance: getStripeElementsAppearanceOptions(resolvedTheme), ...(setupIntent?.pending_subscription_flow_enabled_for_creation === true ? { paymentMethodCreation: 'manual' } : {}), @@ -240,7 +239,7 @@ const NewOrgForm = ({ onPaymentMethodReset, setupIntent, onPlanSelected }: NewOr const stripeOptionsConfirm = useMemo(() => { return { clientSecret: paymentIntentSecret, - appearance: { theme: resolvedTheme?.includes('dark') ? 'night' : 'flat', labels: 'floating' }, + appearance: getStripeElementsAppearanceOptions(resolvedTheme), } as StripeElementsOptions }, [paymentIntentSecret, resolvedTheme]) @@ -551,12 +550,15 @@ const NewOrgForm = ({ onPaymentMethodReset, setupIntent, onPlanSelected }: NewOr {setupIntent && formState.plan !== 'FREE' && ( - + + + )} @@ -656,65 +658,3 @@ const NewOrgForm = ({ onPaymentMethodReset, setupIntent, onPlanSelected }: NewOr } export default NewOrgForm - -/** - * Set up as a separate component, as we need any component using stripe/elements to be wrapped in Elements. - * - * If Elements is on a higher level, we risk losing all form state in case a payment fails. - */ -const Payment = forwardRef( - ( - { - pending_subscription_flow_enabled_for_creation, - }: { pending_subscription_flow_enabled_for_creation: boolean }, - ref - ) => { - const stripe = useStripe() - const elements = useElements() - - const createPaymentMethod = async () => { - if (!stripe || !elements) return - await elements.submit() - - if (pending_subscription_flow_enabled_for_creation) { - // To avoid double 3DS confirmation, we just create the payment method here, as there might be a confirmation step while doing the actual payment - const { error, paymentMethod } = await stripe.createPaymentMethod({ - elements, - }) - if (error || paymentMethod == null) { - toast.error(error?.message ?? ' Failed to process card details') - return - } - return paymentMethod - } else { - const { error, setupIntent } = await stripe.confirmSetup({ - elements, - redirect: 'if_required', - confirmParams: { - return_url: `${getURL()}/new`, - expand: ['payment_method'], - }, - }) - - if (error || !setupIntent.payment_method) { - toast.error(error?.message ?? ' Failed to save card details') - return - } - - return setupIntent.payment_method as PaymentMethod - } - } - - useImperativeHandle(ref, () => ({ - createPaymentMethod, - })) - - return ( - - - - ) - } -) - -Payment.displayName = 'Payment'