Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,12 @@ const AddPaymentMethodForm = ({
<Modal.Content
className={`transition ${isSaving ? 'pointer-events-none opacity-75' : 'opacity-100'}`}
>
<PaymentElement className="[.p-LinkAutofillPrompt]:pt-0" />
<PaymentElement
className="[.p-LinkAutofillPrompt]:pt-0"
options={{
defaultValues: { billingDetails: { email: selectedOrganization?.billing_email } },
}}
/>
{showSetDefaultCheckbox && (
<div className="flex items-center gap-x-2 mt-4 mb-2">
<Checkbox_Shadcn_
Expand Down
29 changes: 29 additions & 0 deletions apps/studio/components/interfaces/Billing/Payment/Payment.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Appearance } from '@stripe/stripe-js'

export const getStripeElementsAppearanceOptions = (
resolvedTheme: string | undefined
): Appearance => {
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',
},
},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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])

Expand Down
Original file line number Diff line number Diff line change
@@ -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 <PaymentElement options={{ defaultValues: { billingDetails: { email } } }} />
}
)

NewPaymentMethodElement.displayName = 'NewPaymentMethodElement'

export { NewPaymentMethodElement }
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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' }
: {}),
Expand Down Expand Up @@ -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])

Expand Down Expand Up @@ -551,12 +550,15 @@ const NewOrgForm = ({ onPaymentMethodReset, setupIntent, onPlanSelected }: NewOr
{setupIntent && formState.plan !== 'FREE' && (
<Panel.Content>
<Elements stripe={stripePromise} options={stripeOptionsPaymentMethod}>
<Payment
ref={paymentRef}
pending_subscription_flow_enabled_for_creation={
setupIntent?.pending_subscription_flow_enabled_for_creation === true
}
/>
<Panel.Content>
<NewPaymentMethodElement
ref={paymentRef}
pending_subscription_flow_enabled={
setupIntent?.pending_subscription_flow_enabled_for_creation === true
}
email={user.profile?.primary_email}
/>
</Panel.Content>
</Elements>
</Panel.Content>
)}
Expand Down Expand Up @@ -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 (
<Panel.Content>
<PaymentElement />
</Panel.Content>
)
}
)

Payment.displayName = 'Payment'
Original file line number Diff line number Diff line change
Expand Up @@ -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()
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -114,6 +116,7 @@ const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> = ({
openedFolders = [],
}) => {
const { ref: projectRef, bucketId } = useParams()
const { bucket } = useSelectedBucket()

const {
selectedBucket,
Expand Down Expand Up @@ -152,11 +155,18 @@ const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> = ({
}

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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,15 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => {
if (!currentFolder) {
// At root of bucket
await fetchFolderContents({
bucketId: bucket.id,
folderId: bucket.id,
folderName: bucket.name,
index: -1,
searchString: itemSearchString,
})
} else {
await fetchFolderContents({
bucketId: bucket.id,
folderId: currentFolder.id,
folderName: currentFolder.name,
index: currentFolderIdx,
Expand All @@ -83,18 +85,33 @@ 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,
})
}
}
} 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,
})
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 }
}
Loading
Loading