Skip to content

Commit 7e5850b

Browse files
authored
feat: prefill Stripe email + styling (supabase#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
1 parent 9ded153 commit 7e5850b

File tree

6 files changed

+119
-76
lines changed

6 files changed

+119
-76
lines changed

apps/studio/components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
1111
import { STRIPE_PUBLIC_KEY } from 'lib/constants'
1212
import { useIsHCaptchaLoaded } from 'stores/hcaptcha-loaded-store'
1313
import AddPaymentMethodForm from './AddPaymentMethodForm'
14+
import { getStripeElementsAppearanceOptions } from './Payment.utils'
1415

1516
interface AddNewPaymentMethodModalProps {
1617
visible: boolean
@@ -90,7 +91,7 @@ const AddNewPaymentMethodModal = ({
9091

9192
const options = {
9293
clientSecret: intent ? intent.client_secret : '',
93-
appearance: { theme: resolvedTheme?.includes('dark') ? 'night' : 'flat', labels: 'floating' },
94+
appearance: getStripeElementsAppearanceOptions(resolvedTheme),
9495
} as any
9596

9697
const onLocalCancel = () => {

apps/studio/components/interfaces/Billing/Payment/AddPaymentMethodForm.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,12 @@ const AddPaymentMethodForm = ({
115115
<Modal.Content
116116
className={`transition ${isSaving ? 'pointer-events-none opacity-75' : 'opacity-100'}`}
117117
>
118-
<PaymentElement className="[.p-LinkAutofillPrompt]:pt-0" />
118+
<PaymentElement
119+
className="[.p-LinkAutofillPrompt]:pt-0"
120+
options={{
121+
defaultValues: { billingDetails: { email: selectedOrganization?.billing_email } },
122+
}}
123+
/>
119124
{showSetDefaultCheckbox && (
120125
<div className="flex items-center gap-x-2 mt-4 mb-2">
121126
<Checkbox_Shadcn_
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { Appearance } from '@stripe/stripe-js'
2+
3+
export const getStripeElementsAppearanceOptions = (
4+
resolvedTheme: string | undefined
5+
): Appearance => {
6+
return {
7+
theme: (resolvedTheme?.includes('dark') ? 'night' : 'flat') as 'night' | 'flat',
8+
variables: {
9+
fontSizeBase: '14px',
10+
colorBackground: resolvedTheme?.includes('dark')
11+
? 'hsl(0deg 0% 14.1%)'
12+
: 'hsl(0deg 0% 95.3%)',
13+
fontFamily:
14+
'var(--font-custom, Circular, custom-font, Helvetica Neue, Helvetica, Arial, sans-serif)',
15+
spacingUnit: '4px',
16+
borderRadius: '.375rem',
17+
gridRowSpacing: '4px',
18+
},
19+
rules: {
20+
'.Label': {
21+
// Hide labels - it is obvious enough what the fields are for
22+
fontSize: '0',
23+
},
24+
'.TermsText': {
25+
fontSize: '12px',
26+
},
27+
},
28+
}
29+
}

apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
4040
import PaymentMethodSelection from './Subscription/PaymentMethodSelection'
4141
import { PaymentConfirmation } from 'components/interfaces/Billing/Payment/PaymentConfirmation'
42+
import { getStripeElementsAppearanceOptions } from 'components/interfaces/Billing/Payment/Payment.utils'
4243

4344
const stripePromise = loadStripe(STRIPE_PUBLIC_KEY)
4445

@@ -149,7 +150,7 @@ export const CreditTopUp = ({ slug }: { slug: string | undefined }) => {
149150
const options = useMemo(() => {
150151
return {
151152
clientSecret: paymentIntentSecret,
152-
appearance: { theme: resolvedTheme?.includes('dark') ? 'night' : 'flat', labels: 'floating' },
153+
appearance: getStripeElementsAppearanceOptions(resolvedTheme),
153154
} as any
154155
}, [paymentIntentSecret, resolvedTheme])
155156

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Set up as a separate component, as we need any component using stripe/elements to be wrapped in Elements.
3+
*
4+
* If Elements is on a higher level, we risk losing all form state in case a payment fails.
5+
*/
6+
7+
import { PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js'
8+
import { PaymentMethod } from '@stripe/stripe-js'
9+
import { getURL } from 'lib/helpers'
10+
import { forwardRef, useImperativeHandle } from 'react'
11+
import { toast } from 'sonner'
12+
13+
const NewPaymentMethodElement = forwardRef(
14+
(
15+
{
16+
pending_subscription_flow_enabled,
17+
email,
18+
}: { pending_subscription_flow_enabled: boolean; email?: string },
19+
ref
20+
) => {
21+
const stripe = useStripe()
22+
const elements = useElements()
23+
24+
const createPaymentMethod = async () => {
25+
if (!stripe || !elements) return
26+
await elements.submit()
27+
28+
if (pending_subscription_flow_enabled) {
29+
// To avoid double 3DS confirmation, we just create the payment method here, as there might be a confirmation step while doing the actual payment
30+
const { error, paymentMethod } = await stripe.createPaymentMethod({
31+
elements,
32+
})
33+
if (error || paymentMethod == null) {
34+
toast.error(error?.message ?? ' Failed to process card details')
35+
return
36+
}
37+
return paymentMethod
38+
} else {
39+
const { error, setupIntent } = await stripe.confirmSetup({
40+
elements,
41+
redirect: 'if_required',
42+
confirmParams: {
43+
return_url: getURL(),
44+
expand: ['payment_method'],
45+
},
46+
})
47+
48+
if (error || !setupIntent.payment_method) {
49+
toast.error(error?.message ?? ' Failed to save card details')
50+
return
51+
}
52+
53+
return setupIntent.payment_method as PaymentMethod
54+
}
55+
}
56+
57+
useImperativeHandle(ref, () => ({
58+
createPaymentMethod,
59+
}))
60+
61+
return <PaymentElement options={{ defaultValues: { billingDetails: { email } } }} />
62+
}
63+
)
64+
65+
NewPaymentMethodElement.displayName = 'NewPaymentMethodElement'
66+
67+
export { NewPaymentMethodElement }

apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx

Lines changed: 13 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import { SetupIntentResponse } from 'data/stripe/setup-intent-mutation'
4242
import { useProfile } from 'lib/profile'
4343
import { PaymentConfirmation } from 'components/interfaces/Billing/Payment/PaymentConfirmation'
4444
import { getURL } from 'lib/helpers'
45+
import { getStripeElementsAppearanceOptions } from 'components/interfaces/Billing/Payment/Payment.utils'
46+
import { NewPaymentMethodElement } from '../BillingSettings/PaymentMethods/NewPaymentMethodElement'
4547

4648
const ORG_KIND_TYPES = {
4749
PERSONAL: 'Personal',
@@ -119,10 +121,7 @@ const NewOrgForm = ({ onPaymentMethodReset, setupIntent, onPlanSelected }: NewOr
119121
() =>
120122
({
121123
clientSecret: setupIntent ? setupIntent.client_secret! : '',
122-
appearance: {
123-
theme: resolvedTheme?.includes('dark') ? 'night' : 'flat',
124-
labels: 'floating',
125-
},
124+
appearance: getStripeElementsAppearanceOptions(resolvedTheme),
126125
...(setupIntent?.pending_subscription_flow_enabled_for_creation === true
127126
? { paymentMethodCreation: 'manual' }
128127
: {}),
@@ -240,7 +239,7 @@ const NewOrgForm = ({ onPaymentMethodReset, setupIntent, onPlanSelected }: NewOr
240239
const stripeOptionsConfirm = useMemo(() => {
241240
return {
242241
clientSecret: paymentIntentSecret,
243-
appearance: { theme: resolvedTheme?.includes('dark') ? 'night' : 'flat', labels: 'floating' },
242+
appearance: getStripeElementsAppearanceOptions(resolvedTheme),
244243
} as StripeElementsOptions
245244
}, [paymentIntentSecret, resolvedTheme])
246245

@@ -551,12 +550,15 @@ const NewOrgForm = ({ onPaymentMethodReset, setupIntent, onPlanSelected }: NewOr
551550
{setupIntent && formState.plan !== 'FREE' && (
552551
<Panel.Content>
553552
<Elements stripe={stripePromise} options={stripeOptionsPaymentMethod}>
554-
<Payment
555-
ref={paymentRef}
556-
pending_subscription_flow_enabled_for_creation={
557-
setupIntent?.pending_subscription_flow_enabled_for_creation === true
558-
}
559-
/>
553+
<Panel.Content>
554+
<NewPaymentMethodElement
555+
ref={paymentRef}
556+
pending_subscription_flow_enabled={
557+
setupIntent?.pending_subscription_flow_enabled_for_creation === true
558+
}
559+
email={user.profile?.primary_email}
560+
/>
561+
</Panel.Content>
560562
</Elements>
561563
</Panel.Content>
562564
)}
@@ -656,65 +658,3 @@ const NewOrgForm = ({ onPaymentMethodReset, setupIntent, onPlanSelected }: NewOr
656658
}
657659

658660
export default NewOrgForm
659-
660-
/**
661-
* Set up as a separate component, as we need any component using stripe/elements to be wrapped in Elements.
662-
*
663-
* If Elements is on a higher level, we risk losing all form state in case a payment fails.
664-
*/
665-
const Payment = forwardRef(
666-
(
667-
{
668-
pending_subscription_flow_enabled_for_creation,
669-
}: { pending_subscription_flow_enabled_for_creation: boolean },
670-
ref
671-
) => {
672-
const stripe = useStripe()
673-
const elements = useElements()
674-
675-
const createPaymentMethod = async () => {
676-
if (!stripe || !elements) return
677-
await elements.submit()
678-
679-
if (pending_subscription_flow_enabled_for_creation) {
680-
// To avoid double 3DS confirmation, we just create the payment method here, as there might be a confirmation step while doing the actual payment
681-
const { error, paymentMethod } = await stripe.createPaymentMethod({
682-
elements,
683-
})
684-
if (error || paymentMethod == null) {
685-
toast.error(error?.message ?? ' Failed to process card details')
686-
return
687-
}
688-
return paymentMethod
689-
} else {
690-
const { error, setupIntent } = await stripe.confirmSetup({
691-
elements,
692-
redirect: 'if_required',
693-
confirmParams: {
694-
return_url: `${getURL()}/new`,
695-
expand: ['payment_method'],
696-
},
697-
})
698-
699-
if (error || !setupIntent.payment_method) {
700-
toast.error(error?.message ?? ' Failed to save card details')
701-
return
702-
}
703-
704-
return setupIntent.payment_method as PaymentMethod
705-
}
706-
}
707-
708-
useImperativeHandle(ref, () => ({
709-
createPaymentMethod,
710-
}))
711-
712-
return (
713-
<Panel.Content>
714-
<PaymentElement />
715-
</Panel.Content>
716-
)
717-
}
718-
)
719-
720-
Payment.displayName = 'Payment'

0 commit comments

Comments
 (0)