SubscriptionPlanUpdateDialog.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. import { Elements } from '@stripe/react-stripe-js'
  2. import { loadStripe, PaymentIntentResult, StripeElementsOptions } from '@stripe/stripe-js'
  3. import { useParams } from 'common'
  4. import { Check, InfoIcon } from 'lucide-react'
  5. import { useTheme } from 'next-themes'
  6. import Link from 'next/link'
  7. import { useMemo, useRef, useState } from 'react'
  8. import { plans as subscriptionsPlans } from 'shared-data/plans'
  9. import { toast } from 'sonner'
  10. import { Button, cn, Dialog, DialogContent } from 'ui'
  11. import { Admonition } from 'ui-patterns'
  12. import { InfoTooltip } from 'ui-patterns/info-tooltip'
  13. import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
  14. import { InvoiceEstimateTooltip } from './InvoiceEstimateTooltip'
  15. import PaymentMethodSelection from './PaymentMethodSelection'
  16. import { getStripeElementsAppearanceOptions } from '@/components/interfaces/Billing/Payment/Payment.utils'
  17. import { PaymentConfirmation } from '@/components/interfaces/Billing/Payment/PaymentConfirmation'
  18. import type { PaymentMethodElementRef } from '@/components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement'
  19. import {
  20. billingPartnerLabel,
  21. getPlanChangeType,
  22. } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
  23. import { type OrganizationBillingSubscriptionPreviewQueryResult } from '@/data/organizations/organization-billing-subscription-preview'
  24. import type { CustomerAddress, CustomerTaxId } from '@/data/organizations/types'
  25. import { OrgProject } from '@/data/projects/org-projects-infinite-query'
  26. import { useConfirmPendingSubscriptionChangeMutation } from '@/data/subscriptions/org-subscription-confirm-pending-change'
  27. import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
  28. import { useOrgSubscriptionUpdateMutation } from '@/data/subscriptions/org-subscription-update-mutation'
  29. import { OrgPlan, SubscriptionTier } from '@/data/subscriptions/types'
  30. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  31. import {
  32. DOCS_URL,
  33. PRICING_TIER_PRODUCT_IDS,
  34. PROJECT_STATUS,
  35. STRIPE_PUBLIC_KEY,
  36. } from '@/lib/constants'
  37. import { formatCurrency } from '@/lib/helpers'
  38. const stripePromise = loadStripe(STRIPE_PUBLIC_KEY)
  39. const PLAN_HEADINGS = {
  40. tier_pro:
  41. 'the Pro plan to unlock more compute resources, daily backups, no project pausing, and email support whenever you need it',
  42. tier_team: 'the Team plan for SOC2, SSO, priority support and greater data and log retention',
  43. default: 'to a new plan',
  44. } as const
  45. type PlanHeadingKey = keyof typeof PLAN_HEADINGS
  46. // Add downgrade headings
  47. const DOWNGRADE_PLAN_HEADINGS = {
  48. tier_free: 'the Free plan with limited resources and active projects',
  49. tier_pro: 'the Pro plan',
  50. default: 'to a lower plan',
  51. } as const
  52. type DowngradePlanHeadingKey = keyof typeof DOWNGRADE_PLAN_HEADINGS
  53. type BreakdownItem =
  54. | { type: 'amount'; label: string; amount: number; tooltip?: string }
  55. | { type: 'notice'; label: string }
  56. interface Props {
  57. selectedTier: 'tier_free' | 'tier_pro' | 'tier_team' | undefined
  58. onClose: () => void
  59. planMeta?: OrgPlan | null
  60. currentPlanMeta?: Partial<OrgPlan> & { features: (string | string[])[] }
  61. subscriptionPreviewQueryResult: OrganizationBillingSubscriptionPreviewQueryResult
  62. projects: OrgProject[]
  63. onAddressChange?: (address: CustomerAddress) => void
  64. onTaxIdChange?: (taxId: CustomerTaxId | null) => void
  65. useAsDefaultBillingAddress: boolean
  66. onUseAsDefaultBillingAddressChange: (useAsDefault: boolean) => void
  67. }
  68. export const SubscriptionPlanUpdateDialog = ({
  69. selectedTier,
  70. onClose,
  71. planMeta,
  72. subscriptionPreviewQueryResult,
  73. currentPlanMeta,
  74. projects,
  75. onAddressChange,
  76. onTaxIdChange,
  77. useAsDefaultBillingAddress,
  78. onUseAsDefaultBillingAddressChange,
  79. }: Props) => {
  80. const { slug } = useParams()
  81. const { resolvedTheme } = useTheme()
  82. const { data: selectedOrganization } = useSelectedOrganizationQuery()
  83. const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>()
  84. const [paymentIntentSecret, setPaymentIntentSecret] = useState<string | null>(null)
  85. const [paymentConfirmationLoading, setPaymentConfirmationLoading] = useState(false)
  86. const paymentMethodSelectionRef = useRef<{
  87. createPaymentMethod: PaymentMethodElementRef['createPaymentMethod']
  88. validateBillingProfile: () => Promise<boolean>
  89. }>(null)
  90. const {
  91. data: subscriptionPreview,
  92. isPending: subscriptionPreviewIsLoading,
  93. isFetching: subscriptionPreviewIsFetching,
  94. isSuccess: subscriptionPreviewInitialized,
  95. } = subscriptionPreviewQueryResult
  96. const { data: subscription } = useOrgSubscriptionQuery({
  97. orgSlug: slug,
  98. })
  99. const billingViaPartner = subscription?.billing_via_partner === true
  100. const billingPartner = subscription?.billing_partner
  101. const stripeOptionsConfirm = useMemo(() => {
  102. return {
  103. clientSecret: paymentIntentSecret,
  104. appearance: getStripeElementsAppearanceOptions(resolvedTheme),
  105. } as StripeElementsOptions
  106. }, [paymentIntentSecret, resolvedTheme])
  107. const changeType = useMemo(() => {
  108. return getPlanChangeType(subscription?.plan?.id, planMeta?.id)
  109. }, [planMeta, subscription])
  110. const subscriptionPlanMeta = useMemo(
  111. () => subscriptionsPlans.find((tier) => tier.id === selectedTier),
  112. [selectedTier]
  113. )
  114. const onSuccessfulPlanChange = () => {
  115. setPaymentConfirmationLoading(false)
  116. toast.success(
  117. `Successfully ${changeType === 'downgrade' ? 'downgraded' : 'upgraded'} subscription to ${subscriptionPlanMeta?.name}!`
  118. )
  119. onClose()
  120. window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
  121. }
  122. const { mutate: updateOrgSubscription, isPending: isUpdating } = useOrgSubscriptionUpdateMutation(
  123. {
  124. onSuccess: (data) => {
  125. if (data.pending_payment_intent_secret) {
  126. setPaymentIntentSecret(data.pending_payment_intent_secret)
  127. return
  128. }
  129. onSuccessfulPlanChange()
  130. },
  131. onError: (error) => {
  132. setPaymentConfirmationLoading(false)
  133. toast.error(`Unable to update subscription: ${error.message}`)
  134. },
  135. }
  136. )
  137. const { mutate: confirmPendingSubscriptionChange, isPending: isConfirming } =
  138. useConfirmPendingSubscriptionChangeMutation({
  139. onSuccess: () => {
  140. onSuccessfulPlanChange()
  141. },
  142. onError: (error) => {
  143. toast.error(`Unable to update subscription: ${error.message}`)
  144. },
  145. })
  146. const paymentIntentConfirmed = async (paymentIntentConfirmation: PaymentIntentResult) => {
  147. // Reset payment intent secret to ensure another attempt works as expected
  148. setPaymentIntentSecret('')
  149. if (paymentIntentConfirmation.paymentIntent?.status === 'succeeded') {
  150. await confirmPendingSubscriptionChange({
  151. slug: selectedOrganization?.slug,
  152. payment_intent_id: paymentIntentConfirmation.paymentIntent.id,
  153. })
  154. } else {
  155. setPaymentConfirmationLoading(false)
  156. // If the payment intent is not successful, we reset the payment method and show an error
  157. toast.error(`Could not confirm payment. Please try again or use a different card.`)
  158. }
  159. }
  160. const onUpdateSubscription = async () => {
  161. if (!selectedOrganization?.slug) return console.error('org slug is required')
  162. if (!selectedTier) return console.error('Selected plan is required')
  163. setPaymentConfirmationLoading(true)
  164. if (paymentMethodSelectionRef.current) {
  165. const isValid = await paymentMethodSelectionRef.current.validateBillingProfile()
  166. if (!isValid) {
  167. setPaymentConfirmationLoading(false)
  168. return
  169. }
  170. }
  171. const result = await paymentMethodSelectionRef.current?.createPaymentMethod()
  172. if (result) {
  173. setSelectedPaymentMethod(result.paymentMethod.id)
  174. } else {
  175. setPaymentConfirmationLoading(false)
  176. }
  177. if (!result && subscription?.payment_method_type !== 'invoice' && changeType === 'upgrade') {
  178. return
  179. }
  180. // If the user is downgrading from team, should have spend cap disabled by default
  181. const tier =
  182. subscription?.plan?.id === 'team' && selectedTier === PRICING_TIER_PRODUCT_IDS.PRO
  183. ? (PRICING_TIER_PRODUCT_IDS.PAYG as SubscriptionTier)
  184. : selectedTier
  185. updateOrgSubscription({
  186. slug: selectedOrganization?.slug,
  187. tier,
  188. paymentMethod: result?.paymentMethod?.id,
  189. address: result?.address,
  190. tax_id: result?.taxId ?? undefined,
  191. billing_name: result?.customerName ?? undefined,
  192. })
  193. }
  194. const features = subscriptionPlanMeta?.features || []
  195. const topFeatures = features
  196. // Get current plan features for downgrade comparison
  197. const currentPlanFeatures = currentPlanMeta?.features || []
  198. // Features that will be lost when downgrading
  199. const featuresToLose =
  200. changeType === 'downgrade'
  201. ? currentPlanFeatures.filter((feature) => {
  202. const featureStr = typeof feature === 'string' ? feature : feature[0]
  203. // Check if this feature exists in the new plan
  204. return !topFeatures.some((newFeature: string | string[]) => {
  205. const newFeatureStr = typeof newFeature === 'string' ? newFeature : newFeature[0]
  206. return newFeatureStr === featureStr
  207. })
  208. })
  209. : []
  210. const upfrontCharge = subscriptionPreview?.upfront_charge
  211. const proratedCredit = upfrontCharge?.prorated_credit ?? 0
  212. const customerBalance = upfrontCharge?.customer_balance ?? 0
  213. const totalCharge = upfrontCharge?.total ?? 0
  214. const tax = upfrontCharge?.tax
  215. const taxableAmount = upfrontCharge?.taxable_amount
  216. const taxStatus = upfrontCharge?.tax_status
  217. const hasTax = taxStatus === 'calculated' && (tax?.tax_amount ?? 0) > 0
  218. const taxFailed = taxStatus === 'failed'
  219. const newPlanCost = Number(subscriptionPlanMeta?.priceMonthly) || 0
  220. const currentPlanId = subscription?.plan?.id
  221. const currentPlanName = subscription?.plan?.name
  222. // Derives the itemized charge breakdown rows shown above "Charge today".
  223. // Example: Pro -> Team upgrade with proration, tax, and credits:
  224. // Team Plan $25.00
  225. // Tax (10%) $1.67
  226. // Subtotal $26.67
  227. // Unused Time on Pro -$8.33
  228. // Credits -$5.00
  229. // ─────────────────────────────
  230. // Charge today $13.34
  231. const breakdownItems = useMemo(() => {
  232. const items: BreakdownItem[] = []
  233. if (hasTax && tax) {
  234. items.push({
  235. type: 'amount',
  236. label: `Tax (${tax.tax_rate_percentage}%)`,
  237. amount: tax.tax_amount,
  238. })
  239. if (taxableAmount !== newPlanCost) {
  240. items.push({ type: 'amount', label: 'Subtotal', amount: taxableAmount! })
  241. }
  242. }
  243. if (taxFailed) {
  244. items.push({
  245. type: 'notice',
  246. label: 'Tax could not be estimated and may be applied separately',
  247. })
  248. }
  249. if (currentPlanId !== 'free' && proratedCredit > 0) {
  250. items.push({
  251. type: 'amount',
  252. label: `Unused Time on ${currentPlanName} Plan`,
  253. amount: -proratedCredit,
  254. tooltip:
  255. 'Your previous plan was charged upfront, so a plan change will prorate any unused time in credits. If the prorated credits exceed the new plan charge, the excessive credits are added to your organization for future use.' +
  256. (hasTax ? ' Includes proportional tax if applicable.' : ''),
  257. })
  258. }
  259. if (customerBalance > 0) {
  260. items.push({
  261. type: 'amount',
  262. label: 'Credits',
  263. amount: -customerBalance,
  264. tooltip: 'Credits will be used first before charging your card.',
  265. })
  266. }
  267. // Prepend the plan cost row when there are adjustment items to show
  268. if (items.length > 0) {
  269. items.unshift({
  270. type: 'amount',
  271. label: `${subscriptionPlanMeta?.name} Plan`,
  272. amount: newPlanCost,
  273. })
  274. }
  275. return items
  276. }, [
  277. currentPlanId,
  278. currentPlanName,
  279. proratedCredit,
  280. hasTax,
  281. tax,
  282. taxableAmount,
  283. newPlanCost,
  284. taxFailed,
  285. customerBalance,
  286. subscriptionPlanMeta?.name,
  287. ])
  288. return (
  289. <Dialog
  290. open={selectedTier !== undefined && selectedTier !== 'tier_free'}
  291. onOpenChange={(open) => {
  292. // Do not allow closing mid-change
  293. if (isUpdating || paymentConfirmationLoading || isConfirming) {
  294. return
  295. }
  296. if (!open) onClose()
  297. }}
  298. >
  299. <DialogContent
  300. onOpenAutoFocus={(event) => event.preventDefault()}
  301. size="xlarge"
  302. className="p-0"
  303. >
  304. <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-5 h-full items-stretch">
  305. {/* Left Column */}
  306. <div className="p-8 pb-8 flex flex-col xl:col-span-3">
  307. <div className="flex-1">
  308. <div>
  309. {!billingViaPartner &&
  310. subscriptionPreviewInitialized &&
  311. changeType === 'upgrade' && (
  312. <div className="space-y-2 mb-4">
  313. <PaymentMethodSelection
  314. ref={paymentMethodSelectionRef}
  315. selectedPaymentMethod={selectedPaymentMethod}
  316. onSelectPaymentMethod={(pm) => setSelectedPaymentMethod(pm)}
  317. readOnly={paymentConfirmationLoading || isConfirming || isUpdating}
  318. onAddressChange={onAddressChange}
  319. onTaxIdChange={onTaxIdChange}
  320. useAsDefaultBillingAddress={useAsDefaultBillingAddress}
  321. onUseAsDefaultBillingAddressChange={onUseAsDefaultBillingAddressChange}
  322. />
  323. </div>
  324. )}
  325. {billingViaPartner && (
  326. <div className="mb-4">
  327. <p className="text-sm">
  328. This organization is billed through our partner{' '}
  329. {billingPartnerLabel(billingPartner)}.{' '}
  330. {/* @ts-ignore [Joshen] Might be API types issue */}
  331. {billingPartner === 'aws' ? (
  332. <>The organization's credit balance will be decreased accordingly.</>
  333. ) : (
  334. <>You will be charged by them directly.</>
  335. )}
  336. </p>
  337. {billingViaPartner &&
  338. billingPartner === 'fly' &&
  339. subscriptionPreview?.plan_change_type === 'downgrade' && (
  340. <p className="text-sm">
  341. Your organization will be downgraded at the end of your current billing
  342. cycle.
  343. </p>
  344. )}
  345. </div>
  346. )}
  347. </div>
  348. {subscriptionPreviewIsLoading && (
  349. <div className="space-y-2 mb-4 mt-2">
  350. <ShimmeringLoader />
  351. <ShimmeringLoader className="w-3/4" />
  352. <ShimmeringLoader className="w-1/2" />
  353. </div>
  354. )}
  355. {subscriptionPreviewInitialized && (
  356. <>
  357. <div
  358. className={cn(
  359. 'mt-2 mb-4 text-foreground-light text-sm transition-opacity',
  360. subscriptionPreviewIsFetching && 'opacity-50'
  361. )}
  362. >
  363. {breakdownItems.map((item, i) =>
  364. item.type === 'amount' ? (
  365. <div
  366. key={i}
  367. className="flex items-center justify-between gap-2 border-b border-muted text-xs"
  368. >
  369. <div className="py-2 pl-0 flex items-center gap-1">
  370. <span>{item.label}</span>
  371. {item.tooltip && (
  372. <InfoTooltip className="max-w-sm">{item.tooltip}</InfoTooltip>
  373. )}
  374. </div>
  375. <div className="py-2 pr-0 text-right tabular-nums" translate="no">
  376. {formatCurrency(item.amount)}
  377. </div>
  378. </div>
  379. ) : (
  380. <div
  381. key={i}
  382. className="flex items-center justify-between gap-2 border-b border-muted text-xs"
  383. >
  384. <div className="py-2 pl-0 text-foreground-lighter">{item.label}</div>
  385. </div>
  386. )
  387. )}
  388. <div className="flex items-center justify-between gap-2 border-b border-muted text-foreground">
  389. <div className="py-2 pl-0">Charge today</div>
  390. <div className="py-2 pr-0 text-right tabular-nums" translate="no">
  391. {formatCurrency(totalCharge)}
  392. {currentPlanId !== 'free' && (
  393. <>
  394. {' '}
  395. <Link
  396. href={`/org/${selectedOrganization?.slug}/billing#breakdown`}
  397. className="text-sm text-brand hover:text-brand-600 transition"
  398. target="_blank"
  399. >
  400. + current spend
  401. </Link>
  402. </>
  403. )}
  404. </div>
  405. </div>
  406. <div className="flex items-center justify-between gap-2 text-foreground-lighter text-xs mt-4">
  407. <div className="py-2 pl-0 flex items-center gap-1">
  408. <span>Monthly invoice estimate</span>
  409. <InvoiceEstimateTooltip
  410. subscriptionPreviewQueryResult={subscriptionPreviewQueryResult}
  411. />
  412. </div>
  413. <div className="py-2 pr-0 text-right tabular-nums" translate="no">
  414. {formatCurrency(
  415. subscriptionPreview?.breakdown.reduce(
  416. (prev: number, cur) => prev + cur.total_price,
  417. 0
  418. ) ?? 0
  419. )}
  420. </div>
  421. </div>
  422. </div>
  423. </>
  424. )}
  425. </div>
  426. <div className="pt-4">
  427. {projects.filter(
  428. (it) =>
  429. it.status === PROJECT_STATUS.ACTIVE_HEALTHY ||
  430. it.status === PROJECT_STATUS.COMING_UP
  431. ).length === 0 &&
  432. subscriptionPreview?.plan_change_type !== 'downgrade' && (
  433. <div className="pb-2">
  434. <Admonition title="Empty organization" type="warning">
  435. This organization has no active projects. Did you select the correct
  436. organization?
  437. </Admonition>
  438. </div>
  439. )}
  440. {projects.filter(
  441. (it) =>
  442. it.status === PROJECT_STATUS.ACTIVE_HEALTHY ||
  443. it.status === PROJECT_STATUS.COMING_UP
  444. ).length === 1 &&
  445. subscriptionPlanMeta?.planId === 'pro' &&
  446. changeType === 'upgrade' && (
  447. <div className="pb-2">
  448. <Admonition type="note">
  449. <div className="text-sm prose">
  450. First project included. Additional projects cost{' '}
  451. <span translate="no">$10</span>+/month regardless of activity.{' '}
  452. <Link
  453. href={`${DOCS_URL}/guides/platform/manage-your-usage/compute`}
  454. target="_blank"
  455. className="underline"
  456. >
  457. Learn more
  458. </Link>
  459. </div>
  460. </Admonition>
  461. </div>
  462. )}
  463. <div className="flex space-x-2">
  464. <Button
  465. loading={isUpdating || paymentConfirmationLoading || isConfirming}
  466. disabled={subscriptionPreviewIsLoading || subscriptionPreviewIsFetching}
  467. type="primary"
  468. onClick={onUpdateSubscription}
  469. className="flex-1"
  470. size="small"
  471. >
  472. Confirm {changeType === 'downgrade' ? 'downgrade' : 'upgrade'}
  473. </Button>
  474. </div>
  475. </div>
  476. </div>
  477. {/* Right Column */}
  478. <div className="bg-surface-100 p-8 flex flex-col border-l xl:col-span-2">
  479. <h3 className="mb-8">
  480. {changeType === 'downgrade' ? 'Downgrade' : 'Upgrade'}{' '}
  481. <span className="font-bold">{selectedOrganization?.name}</span> to{' '}
  482. {changeType === 'downgrade'
  483. ? DOWNGRADE_PLAN_HEADINGS[(selectedTier as DowngradePlanHeadingKey) || 'default']
  484. : PLAN_HEADINGS[(selectedTier as PlanHeadingKey) || 'default']}
  485. </h3>
  486. {changeType === 'downgrade'
  487. ? featuresToLose.length > 0 && (
  488. <div className="mb-4">
  489. <h3 className="text-sm mb-1">Features you'll lose</h3>
  490. <p className="text-xs text-foreground-light mb-4">
  491. Please review carefully before downgrading.
  492. </p>
  493. <div className="space-y-2 mb-4 text-foreground-light">
  494. {featuresToLose.map((feature) => (
  495. <div
  496. key={typeof feature === 'string' ? feature : feature[0]}
  497. className="flex items-center gap-2"
  498. >
  499. <div className="w-4">
  500. <InfoIcon className="h-3 w-3 text-amber-900" strokeWidth={3} />
  501. </div>
  502. <p className="text-sm">
  503. {typeof feature === 'string' ? feature : feature[0]}
  504. </p>
  505. </div>
  506. ))}
  507. </div>
  508. </div>
  509. )
  510. : topFeatures.length > 0 && (
  511. <div className="mb-4">
  512. <h3 className="text-sm mb-4">Upgrade features</h3>
  513. <div className="space-y-2 mb-4 text-foreground-light">
  514. {topFeatures.map((feature: string | string[]) => (
  515. <div
  516. key={typeof feature === 'string' ? feature : feature[0]}
  517. className="flex items-center gap-2"
  518. >
  519. <div className="w-4">
  520. <Check className="h-3 w-3 text-brand" strokeWidth={3} />
  521. </div>
  522. <div className="text-sm">
  523. <p>{typeof feature === 'string' ? feature : feature[0]}</p>
  524. {Array.isArray(feature) && feature.length > 1 && (
  525. <p className="text-foreground-lighter text-xs">{feature[1]}</p>
  526. )}
  527. </div>
  528. </div>
  529. ))}
  530. </div>
  531. </div>
  532. )}
  533. </div>
  534. </div>
  535. {stripePromise && paymentIntentSecret && (
  536. <Elements stripe={stripePromise} options={stripeOptionsConfirm}>
  537. <PaymentConfirmation
  538. paymentIntentSecret={paymentIntentSecret}
  539. onPaymentIntentConfirm={(paymentIntentConfirmation) =>
  540. paymentIntentConfirmed(paymentIntentConfirmation)
  541. }
  542. onLoadingChange={(loading) => setPaymentConfirmationLoading(loading)}
  543. />
  544. </Elements>
  545. )}
  546. </DialogContent>
  547. </Dialog>
  548. )
  549. }